1. 测试数据准备
sql
-- 可重复执行:先删旧表
DROP TABLE TEST_EMPLOYEE_SKILLS PURGE;
CREATE TABLE TEST_EMPLOYEE_SKILLS (
EMP_ID VARCHAR2(10) PRIMARY KEY,
EMP_NAME VARCHAR2(50),
DEPT_NAME VARCHAR2(50),
SKILL_LIST VARCHAR2(200),
PROJECT_LIST VARCHAR2(500)
);
INSERT INTO TEST_EMPLOYEE_SKILLS VALUES ('E001', '张三', '技术部', 'Java,Python,SQL', '项目A,项目B');
INSERT INTO TEST_EMPLOYEE_SKILLS VALUES ('E002', '李四', '技术部', 'Python,JavaScript,HTML', '项目C');
INSERT INTO TEST_EMPLOYEE_SKILLS VALUES ('E003', '王五', '产品部', 'Axure,SQL,Python', '项目A,项目D,项目E');
INSERT INTO TEST_EMPLOYEE_SKILLS VALUES ('E004', '赵六', '设计部', 'Photoshop,Illustrator', '项目F');
INSERT INTO TEST_EMPLOYEE_SKILLS VALUES ('E005', '孙七', '技术部', 'Java,Spring,MySQL,Redis', '项目B,项目G,项目H');
INSERT INTO TEST_EMPLOYEE_SKILLS VALUES ('E006', '周八', '产品部', 'SQL,Tableau', '项目A,项目I');
INSERT INTO TEST_EMPLOYEE_SKILLS VALUES ('E007', '吴九', '技术部', NULL, '项目J');
INSERT INTO TEST_EMPLOYEE_SKILLS VALUES ('E008', '郑十', '设计部', 'Photoshop', '');
注意 :E008 的 PROJECT_LIST 写的是空字符串,但 Oracle 中 '' 等价于 NULL(零长度字符即 NULL,这是 Oracle 与 MySQL/SQL Server 的关键差异)。因此它实际存的是 NULL:WHERE PROJECT_LIST = '' 查不到它,判空必须用 IS NULL。
测试数据预览:
| EMP_ID | EMP_NAME | DEPT_NAME | SKILL_LIST | PROJECT_LIST |
|---|---|---|---|---|
| E001 | 张三 | 技术部 | Java,Python,SQL | 项目A,项目B |
| E002 | 李四 | 技术部 | Python,JavaScript,HTML | 项目C |
| E003 | 王五 | 产品部 | Axure,SQL,Python | 项目A,项目D,项目E |
| E004 | 赵六 | 设计部 | Photoshop,Illustrator | 项目F |
| E005 | 孙七 | 技术部 | Java,Spring,MySQL,Redis | 项目B,项目G,项目H |
| E006 | 周八 | 产品部 | SQL,Tableau | 项目A,项目I |
| E007 | 吴九 | 技术部 | NULL | 项目J |
| E008 | 郑十 | 设计部 | Photoshop | NULL('' 即 NULL) |
2. 判断是否包含
需求:查 SKILL_LIST 是否包含某个技能。核心思路是"前后补逗号后精确匹配",避免把 Java 误配成 JavaScript。
2.1 INSTR(推荐,性能最好)
sql
SELECT EMP_ID, EMP_NAME, DEPT_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE INSTR(',' || SKILL_LIST || ',', ',Python,') > 0;
执行结果:
| EMP_ID | EMP_NAME | DEPT_NAME | SKILL_LIST |
|---|---|---|---|
| E001 | 张三 | 技术部 | Java,Python,SQL |
| E002 | 李四 | 技术部 | Python,JavaScript,HTML |
| E003 | 王五 | 产品部 | Axure,SQL,Python |
原理 :字段前后各加逗号得到 ,Java,Python,SQL,,目标值也加逗号 ,Python,,再用 INSTR 找位置。NULL 拼接后仍为 NULL,INSTR 返回 NULL 不匹配------三个内建函数方案对 NULL 都天然安全,无需额外过滤。
优点:性能最好------纯字符串函数、无正则解析;NULL 天然安全,无需过滤。
缺点:需手工前后补逗号,写法略啰嗦;仅支持单值判断,多值需自行组合(见 2.5)。
适用版本 :广泛兼容 | 性能:⭐⭐⭐⭐⭐
2.2 LIKE
sql
-- 前后加逗号匹配(推荐,避免误匹配)
SELECT EMP_ID, EMP_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE ',' || SKILL_LIST || ',' LIKE '%,Java,%';
-- 直接使用通配符(可能误匹配,不推荐)
SELECT EMP_ID, EMP_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE SKILL_LIST LIKE '%Java%'; -- 会误匹配 'JavaScript'
注意 :%Java% 会把 'JavaScript' 也匹配进来;必须用第一种前后补逗号的写法。
优点:写法简单易读;同为纯字符串操作,性能接近 INSTR。
缺点 :容易漏掉前后补逗号导致误匹配(%Java% 会命中 JavaScript);语义不如 INSTR 直观。
适用版本 :广泛兼容 | 性能:⭐⭐⭐⭐
2.3 REGEXP_LIKE
sql
-- 精确匹配(边界控制)
SELECT EMP_ID, EMP_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE REGEXP_LIKE(SKILL_LIST, '(^|,)SQL(,|$)');
-- 匹配多个值
SELECT EMP_ID, EMP_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE REGEXP_LIKE(SKILL_LIST, '(^|,)(Java|Python)(,|$)');
正则说明 :(^|,) 匹配开头或逗号,(,|$) 匹配逗号或结尾,精确圈定独立技能项。多值 (Java|Python) 为 OR(任一匹配)语义;AND 写法见 2.5。
注意 :技能名含正则元字符(如 .、()时需转义;10g 起可用。
优点 :一个表达式同时支持边界控制与多值((^|,)(Java|Python)(,|$)),最灵活。
缺点:正则开销最大(官方博客:正则比其他字符串函数慢);技能名含元字符时需转义。
适用版本 :10g+ | 性能:⭐⭐⭐
2.4 自定义函数(复用)
sql
CREATE OR REPLACE FUNCTION FIND_IN_SET(
p_value VARCHAR2,
p_str VARCHAR2,
p_delim VARCHAR2 DEFAULT ','
) RETURN NUMBER AS
BEGIN
IF p_str IS NULL OR p_value IS NULL THEN
RETURN 0;
END IF;
RETURN INSTR(p_delim || p_str || p_delim, p_delim || p_value || p_delim);
END;
/
-- 用法一:判断是否包含
SELECT EMP_ID, EMP_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE FIND_IN_SET('Python', SKILL_LIST) > 0;
-- 用法二:查看位置(字符位置,非元素序号)
SELECT EMP_ID, EMP_NAME, SKILL_LIST,
FIND_IN_SET('Python', SKILL_LIST) AS CHAR_POS
FROM TEST_EMPLOYEE_SKILLS;
语义说明 :本函数借用 MySQL 的 FIND_IN_SET 命名以方便记忆,但语义不同------MySQL 版本返回 1 基的元素序号 ,本函数返回的是字符位置 (INSTR 语义)。做判断时只看 > 0 即可,不要当作序号使用。
注意:p_value 若含分隔符会误判;数据量大时每行都调用函数,性能一般。
优点:一处定义、处处复用,业务 SQL 最简洁;支持自定义分隔符。
缺点:需先创建函数;返回字符位置(非序号),易与 MySQL FIND_IN_SET 混淆;每行调用函数,大表性能一般。
适用版本 :广泛兼容(需先创建) | 性能:⭐⭐
2.5 多值匹配
objectivec
-- OR:掌握 'Java' 或 'Python'
SELECT EMP_ID, EMP_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE INSTR(',' || SKILL_LIST || ',', ',Java,') > 0
OR INSTR(',' || SKILL_LIST || ',', ',Python,') > 0;
-- AND:同时掌握 'Java' 和 'SQL'
SELECT EMP_ID, EMP_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE INSTR(',' || SKILL_LIST || ',', ',Java,') > 0
AND INSTR(',' || SKILL_LIST || ',', ',SQL,') > 0;
说明:把 2.1 的 INSTR 条件用 OR/AND 组合即可;REGEXP_LIKE 的多值写法见 2.3 第二个示例。
优点:INSTR 组合,语义清晰,无正则开销。
缺点:每多一个值就多一条 INSTR,条件多时冗长;性能随条件数线性下降。
适用版本 :广泛兼容 | 性能:⭐⭐⭐⭐⭐
2.6 方案对比
| 方案 | 适用版本 | 性能 | 推荐指数 |
|---|---|---|---|
| INSTR | 广泛兼容 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| LIKE | 广泛兼容 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| REGEXP_LIKE | 10g+ | ⭐⭐⭐ | ⭐⭐⭐ |
| 自定义函数 | 广泛兼容(需先创建) | ⭐⭐ | ⭐⭐⭐ |
性能说明:拼接列无法走索引,以上均为全表扫描下的相对开销。数据量大、查询频繁时应考虑规范化表设计,而不是依赖 SQL 技巧。
3. 一行转多行
需求:把每行 SKILL_LIST 拆成多行,每行一个技能。以下方案均假设值中不含分隔符本身------含逗号的值在 CSV 语义下天然有歧义,任何方案都无解。
3.1 JSON_TABLE(12c+,推荐)
sql
SELECT T.EMP_ID, T.EMP_NAME, T.SKILL_LIST, J.SKILL
FROM TEST_EMPLOYEE_SKILLS T,
JSON_TABLE(
'["' || REPLACE(T.SKILL_LIST, ',', '","') || '"]',
'$[*]'
COLUMNS SKILL VARCHAR2(50) PATH '$'
) J
WHERE T.SKILL_LIST IS NOT NULL;
转换过程 :'Java,Python,SQL' → '["Java","Python","SQL"]' → 输出 3 行。
执行结果(部分):
| EMP_ID | EMP_NAME | SKILL_LIST | SKILL |
|---|---|---|---|
| E001 | 张三 | Java,Python,SQL | Java |
| E001 | 张三 | Java,Python,SQL | Python |
| E001 | 张三 | Java,Python,SQL | SQL |
优点:性能最优;无层级、递归 hack,写法直观。
缺点 :仅 12c+;手工拼接字符串遇 "/\ 报 ORA-40441;值含逗号无解(所有方案通病)。
注意(重要) :这里是手工拼接 JSON 字符串,值中若含双引号或反斜杠会生成非法 JSON,报 ORA-40441 (JSON syntax error)。测试数据不含这些字符所以正常;真实数据若可能包含,需先 REPLACE 转义(如 " → ")。
适用版本 :12c+(12.1.0.2 起) | 性能:⭐⭐⭐⭐⭐
3.2 XMLTABLE(10g+)
sql
-- 标准写法
SELECT T.EMP_ID, T.EMP_NAME, T.SKILL_LIST, X.SKILL
FROM TEST_EMPLOYEE_SKILLS T,
XMLTABLE(
'/ROWSET/ROW'
PASSING XMLTYPE('<ROWSET><ROW>' || REPLACE(T.SKILL_LIST, ',', '</ROW><ROW>') || '</ROW></ROWSET>')
COLUMNS SKILL VARCHAR2(50) PATH '.'
) X
WHERE T.SKILL_LIST IS NOT NULL;
-- 简写(12c+ 隐式转换,字符串表达式直接作 XQuery)
SELECT T.EMP_ID, T.EMP_NAME, T.SKILL_LIST, X.SKILL
FROM TEST_EMPLOYEE_SKILLS T,
XMLTABLE(
('"' || REPLACE(T.SKILL_LIST, ',', '","') || '"')
COLUMNS SKILL VARCHAR2(50) PATH '.'
) X
WHERE T.SKILL_LIST IS NOT NULL;
注意 :两种写法都要处理 XML 特殊字符------值中含 &、< 等会报 ORA-19112 (XQuery 求值错误),需先转义(如 & → &)。简写只是语法糖,并不比标准写法更安全,值中同样不能含双引号。
优点:10g 起可用,兼容旧库;标准写法直观。
缺点 :特殊字符需手工转义(&、< 等,否则 ORA-19112);实测性能明显更慢(约 10 倍);简写仅 12c+ 且难读。
适用版本 :标准写法 10g+;简写 12c+ | 性能:⭐⭐⭐
3.3 REGEXP_SUBSTR + CONNECT BY(10g+)
sql
SELECT T.EMP_ID, T.EMP_NAME, T.SKILL_LIST,
TRIM(REGEXP_SUBSTR(T.SKILL_LIST, '[^,]+', 1, LEVEL)) AS SKILL
FROM TEST_EMPLOYEE_SKILLS T
WHERE T.SKILL_LIST IS NOT NULL
CONNECT BY LEVEL <= REGEXP_COUNT(T.SKILL_LIST, ',') + 1
AND PRIOR T.EMP_ID = T.EMP_ID
AND PRIOR SYS_GUID() IS NOT NULL;
关键点:
REGEXP_SUBSTR(字段, '[^,]+', 1, LEVEL):取第 LEVEL 个逗号分隔值,LEVEL 即元素序号PRIOR T.EMP_ID = T.EMP_ID:把层级树限制在每行内部,避免行间笛卡尔爆炸PRIOR SYS_GUID() IS NOT NULL:官方推荐做法(Oracle 官方 SQL 博客)------每行 GUID 唯一,可避开 CONNECT BY 的循环检测;旧资料常见的DBMS_RANDOM.VALUE亦可,但官方用 SYS_GUID- 连续逗号(空元素)会被跳过:
'a,,b'只出 2 行,不会报错
注意:层级机制本身较重,大表不推荐(见 3.5 性能对比)。
优点:写法简洁;10g 起可用。
缺点:PRIOR+SYS_GUID hack 难懂;层级机制重,大表慢(实测约为行生成器 5 倍耗时);REGEXP_COUNT 仅 11g+。
适用版本 :10g+(示例用 REGEXP_COUNT 需 11g+;10g 改用 LENGTH-REPLACE 计数) | 性能:⭐⭐⭐
3.4 LATERAL(12c+)
sql
SELECT T.EMP_ID, T.EMP_NAME, T.SKILL_LIST, L.LVL, L.SKILL
FROM TEST_EMPLOYEE_SKILLS T,
LATERAL (
SELECT LEVEL AS LVL,
TRIM(REGEXP_SUBSTR(T.SKILL_LIST, '[^,]+', 1, LEVEL)) AS SKILL
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT(T.SKILL_LIST, ',') + 1
) L
WHERE T.SKILL_LIST IS NOT NULL;
说明:把 3.3 的层级逻辑搬进 DUAL 上的子查询,用 LATERAL 逐行关联。无需 PRIOR 技巧、无 JSON/XML 转义问题,语法清晰;Oracle 官方博客处理"列中分隔值转行"用的就是这条路线。
优点:语法清晰、官方博客同款路线;无 PRIOR hack、无转义问题;性能良好(行生成器)。
缺点:仅 12c+;嵌套子查询逐行执行,极端大表需实测。
适用版本 :12c+ | 性能:⭐⭐⭐⭐
3.5 方案对比
| 方案 | 适用版本 | 语法简洁度 | 性能 | 推荐指数 |
|---|---|---|---|---|
| JSON_TABLE | 12c+ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| LATERAL | 12c+ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| XMLTABLE | 10g+(简写 12c+) | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| REGEXP+CONNECT BY | 10g+ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
性能说明:行生成器(LATERAL 内部)与 JSON_TABLE 均为高效路线;XML 与 CONNECT_BY_ROOT 层级变体明显更慢(第三方实测 XML 约慢 10 倍、CONNECT_BY_ROOT 约慢 40 倍以上)。星级为相对开销,拼接列本就无法走索引。
4. 选型总结
- 12c+ 性能优先:JSON_TABLE------最快,但注意 3.1 的特殊字符前提
- 12c+ 可读性优先:LATERAL------无转义问题、无 hack,官方同款路线,团队易维护
- 10g/11g 兼容:XMLTABLE(记得转义)或 REGEXP+CONNECT BY(数据量小可接受)
- 需要复用:把逻辑封装成自定义函数(参考 2.4 的 FIND_IN_SET)
- 根本建议:数据量大、查询频繁时,应把拼接列规范化为子表,而非在 SQL 里反复拆分