Oracle 逗号拼接字段处理

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

优点: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 里反复拆分
相关推荐
Sirens.15 分钟前
MySQL数据库:视图
数据库·mysql
geovindu27 分钟前
CSharp: State Pattern
开发语言·后端·c#·.net·状态模式·行为模式
山岚的运维笔记30 分钟前
mysql 专业笔记 -- 第 1 章:MySQL 入门
运维·数据库·笔记·后端·学习·mysql·dba
IT小白杨31 分钟前
TikTok小店防封号浏览器实战:2026年多店铺环境隔离与账号安全运营技术解析
开发语言·数据库·安全·php·chrome devtools·指纹浏览器
祢真伟大35 分钟前
Oracle含LOB大字段表的存储占用统计
数据库·oracle
fliter1 小时前
我们如何通过优化 1.1.1.1 的 DNS 缓存节省 100 TB 内存
后端
Inhand-Victor1 小时前
从 CAN 总线到 AWS IoT:EC312 边缘计算网关实现 CAN 数据云端接入
数据库
一开1 小时前
一个自己开发的 Agent Harness-子 Agent篇
后端
KoPa1 小时前
HeySmart:事件总线——异步解耦的艺术
前端·后端