在报表开发或数据清洗任务中,经常遇到某个字段存放的是逗号分隔的多个值,例如订单表的标签列保存了 1,3,7 这样的内容。为了与标签表做关联查询,需要把 1,3,7 拆成三行记录。SQL 标准并没有定义通用的 SPLIT 函数,各个数据库的实现方式差异较大,有的提供原生函数,有的则需要借助递归或 JSON 能力完成拆分。理解这些方案的区别后,才能根据当前数据库版本和业务场景选择最合适的写法。

下面分别从 MySQL、SQL Server、PostgreSQL、Oracle 和 SQLite 几个主流数据库出发,介绍字符串拆分的典型实现,并讨论性能与通用性之间的权衡。
一、MySQL:从 SUBSTRING_INDEX 到 JSON_TABLE 的多种拆法
MySQL 8.0 之前没有原生的 SPLIT 函数,最常用的技巧是借助 SUBSTRING_INDEX 和辅助数字表。SUBSTRING_INDEX 函数接收三个参数:原始字符串、分隔符和序号。当序号为正数时,返回从左侧开始第几个分隔符之前的子串;当序号为负数时,返回从右侧开始数第几个分隔符之后的子串。例如拆分 a,b,c 时,可以通过嵌套调用取出中间部分。
SELECT SUBSTRING_INDEX('a,b,c', ',', 1) AS part1,
SUBSTRING_INDEX(SUBSTRING_INDEX('a,b,c', ',', 2), ',', -1) AS part2,
SUBSTRING_INDEX('a,b,c', ',', -1) AS part3;实际问题中,字段里分隔符的数量并不固定,因此需要动态生成序号。可以通过关联一个数字表或系统表来产生序号,再用序号控制 SUBSTRING_INDEX 的截取位置。下面的示例使用数字序列把订单中的标签字段拆成多行,其中分隔符个数通过字符串长度差计算得到。
SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(t.tags, ',', n.num), ',', -1) AS tag_id FROM orders t JOIN ( SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 ) n ON n.num <= LENGTH(t.tags) - LENGTH(REPLACE(t.tags, ',', '')) + 1 WHERE t.id = 100;
这种写法在拆分固定少量元素时比较直观,但如果元素数量超过数字表的行数,就需要扩大数字表范围。另一个问题是每次取子串都要重复扫描整个字符串,数据量较大时性能偏差。因此在实际项目中,它更适合小批量数据或临时查询。
MySQL 8.0 引入了 JSON_TABLE,可以先通过 REPLACE 把逗号分隔字符串包装成 JSON 数组,再使用 JSON_TABLE 展开为行。这种写法非常简洁,特别适合数字或简单标识符列表。
SELECT jt.*
FROM orders t,
JSON_TABLE(
CONCAT('["', REPLACE(t.tags, ',', '","'), '"]'),
'$[*]' COLUMNS (tag_id VARCHAR(20) PATH '$')
) AS jt;如果拆分的值中可能包含双引号或反斜杠,需要先对内容做转义,否则构造出来的 JSON 可能不合法。除了 JSON_TABLE,MySQL 8 也支持递归公用表表达式 CTE 来逐个截取逗号分隔值。递归 CTE 的可读性较高,但在数据行数很多时容易触发递归深度限制,同样需要谨慎使用。
WITH RECURSIVE split_cte AS (
SELECT id, tags,
SUBSTRING_INDEX(tags, ',', 1) AS part,
IF(LOCATE(',', tags) != 0, SUBSTRING(tags, LOCATE(',', tags) + 1), '') AS rest
FROM orders
UNION ALL
SELECT id, tags,
SUBSTRING_INDEX(rest, ',', 1),
IF(LOCATE(',', rest) != 0, SUBSTRING(rest, LOCATE(',', rest) + 1), '')
FROM split_cte
WHERE rest != ''
)
SELECT id, part FROM split_cte;二、SQL Server 与 PostgreSQL:原生函数与数组展开
SQL Server 从 2016 版本开始提供 STRING_SPLIT 函数,可以直接按分隔符把字符串拆成行,返回列名 value。例如拆分 a,b,c 只需要一个简单查询。
SELECT value
FROM STRING_SPLIT('a,b,c', ',');不过 STRING_SPLIT 有两个明显限制:一是不保证返回顺序,即使输入是 a,b,c,输出顺序也可能是 c,a,b;二是分隔符只支持单个字符,无法直接按逗号加空格这种多字符规则拆分。如果业务必须保留原有顺序,可以考虑使用 OPENJSON,先把字符串构造成 JSON 数组再展开,并通过 key 列获取下标。
SELECT j.[key], j.value
FROM OPENJSON('["a","b","c"]') j;SQL Server 2022 对 STRING_SPLIT 增加了 ordinal 参数,可以在拆分时返回序号。不过很多生产环境并未升级到该版本,因此旧库中仍然需要借助 JSON 或自定义函数。如果只是临时拆分少量记录,也可以在应用层把值传成表变量再关联,避免在 SQL 内处理顺序问题。
PostgreSQL 的字符串拆分能力则灵活得多。它没有单独的 split 函数,但可以通过 string_to_array 把字符串转成数组,再配合 unnest 展开为多行,一行代码即可完成拆分。
SELECT unnest(string_to_array('a,b,c', ',')) AS part;如果需要知道每个拆分值在原字符串中的位置,可以使用 WITH ORDINALITY 语法把数组下标一并返回。PostgreSQL 还提供了 regexp_split_to_table 函数,支持正则表达式作为分隔规则,因此可以轻松处理多字符分隔符或者包含空格的复杂格式。
SELECT elem, ord
FROM unnest(string_to_array('a,b,c', ',')) WITH ORDINALITY AS t(elem, ord);
SELECT regexp_split_to_table('a||b||c', '\|\|');三、Oracle 与 SQLite:正则和递归路线的代表
Oracle 没有内置的 SPLIT 函数,但可以利用 CONNECT BY 层级查询配合 REGEXP_SUBSTR 实现拆分。REGEXP_SUBSTR 可以按正则表达式提取第 n 个子串,CONNECT BY 则负责生成连续序号。下面的示例按逗号拆分 a,b,c,并从 DUAL 表返回三行记录。
SELECT REGEXP_SUBSTR('a,b,c', '[^,]+', 1, LEVEL) AS part
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT('a,b,c', ',') + 1;这种写法在执行单条拆分时非常简洁,但当源表包含多行数据时,直接把 CONNECT BY 放在同一查询里容易产生笛卡尔积,导致行数爆炸。更稳健的做法是用横向派生表或自定义 PL/SQL 函数来控制层级条件。Oracle 12c 以后还可以结合 LATERAL 视图来展开每一行,把层级生成限制在对应行的范围内。
SQLite 同样没有原生 split 函数,通常使用递归 CTE 模拟拆分过程。其思路是先给原字符串末尾补一个分隔符,然后用 instr 定位分隔符位置,逐次用 substr 截取第一个元素,同时截掉已经处理过的部分,直到找不到分隔符为止。
WITH RECURSIVE split(id, part, rest) AS (
SELECT id, '', tags || ','
FROM orders
UNION ALL
SELECT id,
substr(rest, 1, instr(rest, ',') - 1),
substr(rest, instr(rest, ',') + 1)
FROM split
WHERE instr(rest, ',') != 0
)
SELECT id, part FROM split WHERE part != '';SQLite 的递归 CTE 默认有递归深度限制,如果一条记录包含大量分隔符,可能会触发限制。对于移动端或嵌入式场景,SQLite 中的字符串拆分通常数量不大,这种写法已经足够。若数据量较大,建议先在应用层完成拆分,再批量写入临时表。
四、跨数据库通用思路与性能注意事项
如果项目需要兼容多种数据库,最好避免在 SQL 内做过于复杂的字符串拆分,而将拆分工作放在应用层完成。比如 Python、Java 等语言可以轻松把逗号分隔字符串转成集合,再通过临时表或参数化查询把结果传回数据库。这样做的优点是逻辑清晰、便于单元测试,也能减少数据库 CPU 压力,缺点是会增加应用代码量和网络传输成本。
对于必须在数据库内拆分的场景,优先选择原生能力:PostgreSQL 的 string_to_array + unnest 最灵活;SQL Server 2016 以上可以直接用 STRING_SPLIT,但要注意顺序和多字符分隔符限制;MySQL 8 使用 JSON_TABLE 时性能通常优于辅助数字表;Oracle 的 CONNECT BY 方案要防止层级查询扩散;SQLite 递归 CTE 适合小数据量。判断性能时,可以使用 EXPLAIN 或执行计划检查拆分函数是否被重复调用,以及中间结果是否被过度放大。
综合来看,字符串拆分没有一种方案能在所有数据库中既高效又通用。理解不同数据库的执行机制后,再结合数据量、版本和业务对顺序的要求,才能选择最稳妥的实现。如果只是临时处理少量记录,最简单的原生函数即可;如果是高频任务,则建议把拆分结果落成明细表,避免每次查询都重复计算。