Oracle 逗号拼接字段处理

0 阅读9分钟

1. 测试数据准备

-- 可重复执行:先删旧表
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_IDEMP_NAMEDEPT_NAMESKILL_LISTPROJECT_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郑十设计部PhotoshopNULL('' 即 NULL)

2. 判断是否包含

需求:查 SKILL_LIST 是否包含某个技能。核心思路是"前后补逗号后精确匹配",避免把 Java 误配成 JavaScript。

2.1 INSTR(推荐,性能最好)

SELECT EMP_ID, EMP_NAME, DEPT_NAME, SKILL_LIST
FROM TEST_EMPLOYEE_SKILLS
WHERE INSTR(',' || SKILL_LIST || ',', ',Python,') > 0;

执行结果

EMP_IDEMP_NAMEDEPT_NAMESKILL_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

-- 前后加逗号匹配(推荐,避免误匹配)
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

-- 精确匹配(边界控制)
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 自定义函数(复用)

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 多值匹配

-- 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_LIKE10g+⭐⭐⭐⭐⭐⭐
自定义函数广泛兼容(需先创建)⭐⭐⭐⭐⭐

性能说明:拼接列无法走索引,以上均为全表扫描下的相对开销。数据量大、查询频繁时应考虑规范化表设计,而不是依赖 SQL 技巧。

3. 一行转多行

需求:把每行 SKILL_LIST 拆成多行,每行一个技能。以下方案均假设值中不含分隔符本身——含逗号的值在 CSV 语义下天然有歧义,任何方案都无解。

3.1 JSON_TABLE(12c+,推荐)

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_IDEMP_NAMESKILL_LISTSKILL
E001张三Java,Python,SQLJava
E001张三Java,Python,SQLPython
E001张三Java,Python,SQLSQL

优点:性能最优;无层级、递归 hack,写法直观。

缺点:仅 12c+;手工拼接字符串遇 "​/\​ 报 ORA-40441;值含逗号无解(所有方案通病)。

注意(重要) :这里是手工拼接 JSON 字符串,值中若含双引号或反斜杠会生成非法 JSON,报 ORA-40441(JSON syntax error)。测试数据不含这些字符所以正常;真实数据若可能包含,需先 REPLACE 转义(如 "​ → "​)。

适用版本:12c+(12.1.0.2 起) | 性能:⭐⭐⭐⭐⭐

3.2 XMLTABLE(10g+)

-- 标准写法
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 求值错误),需先转义(如 &​ → &amp;​)。简写只是语法糖,并不比标准写法更安全,值中同样不能含双引号。

优点:10g 起可用,兼容旧库;标准写法直观。

缺点:特殊字符需手工转义(&​、<​ 等,否则 ORA-19112);实测性能明显更慢(约 10 倍);简写仅 12c+ 且难读。

适用版本:标准写法 10g+;简写 12c+ | 性能:⭐⭐⭐

3.3 REGEXP_SUBSTR + CONNECT BY(10g+)

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+)

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_TABLE12c+⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
LATERAL12c+⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
XMLTABLE10g+(简写 12c+)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
REGEXP+CONNECT BY10g+⭐⭐⭐⭐⭐⭐⭐⭐⭐

性能说明:行生成器(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 里反复拆分