南大通用GBase 8a之listagg/string_agg 函数的反函数实现
GBase8a数据库中 listagg/string_agg 函数的反函数实现
一、业务场景背景
在日常数据开发中,我们经常会遇到这种场景:某张表的字段里存储了用逗号(或其他分隔符)拼接的多个值,比如商品分类、标签、关联系统名称等,业务上需要把这些逗号分隔的字符串拆分成多行数据,方便后续的统计分析、关联查询或ETL处理。
这就是我们常说的“字符串拆分”,也被称为listagg/string_agg的“反函数”操作。
不同数据库生态下,实现方案差异较大:
•Oracle 中常用 CONNECT BY 层级查询配合正则实现拆分;
•GBase 8a MPP 这类国产数据库也支持类似的层级查询语法;
•但在部分环境中,层级查询语法存在兼容性限制,或在数据量较大时性能不够稳定,这时候就需要更通用、可移植的实现方案。
二、问题分析与通用方案选型
1. 原方案(层级查询法)的局限性
你最开始使用的 Oracle/GBase 层级查询方案,核心逻辑是通过 CONNECT BY LEVEL 生成序列,再用 regexp_substr 按位置截取字符串:
SELECT
id,
regexp_substr(names, '[^,]+', 1, LEVEL) AS name
FROM t_demo
WHERE names IS NOT NULL
CONNECT BY PRIOR id = id
AND PRIOR rand() IS NOT null
AND LEVEL <= length(names) - length(replace(names, ',', '')) + 1;
这种方案在小数据量下能快速实现需求,但存在几个明显的短板:
•兼容性差:依赖数据库的层级查询和正则扩展函数,无法直接迁移到不支持该语法的数据库;
•性能瓶颈:当单条记录的分隔项数量极多(比如上千个)时,层级查询会产生大量递归操作,执行效率下降明显;
•特殊字符风险:原方案对中文逗号、全角符号等分隔符的处理不够友好,容易出现拆分失败的情况。
2. 通用方案:数字辅助表法
我们最终选择的“数字辅助表+关联拆分”方案,是一种几乎兼容所有关系型数据库的通用实现,核心思路是:
•先创建一个存储连续整数的“数字辅助表”(numbers),生成从1到N的序列;
•用辅助表的数字,和原表做关联,关联条件为“数字 ≤ 分隔符数量+1”(也就是拆分后的行数);
•通过两次 SUBSTRING_INDEX 函数,按位置截取第N个分隔项;
•统一分隔符格式,兼容全角逗号、半角逗号等不同写法。
这种方案的优势在于:
•兼容性极强:不依赖任何数据库的特殊语法,MySQL、GBase、PostgreSQL 等主流数据库都支持;
•性能稳定:基于普通的表关联实现,执行计划清晰,大数据量下的表现远优于层级递归;
•扩展性强:可以轻松适配不同分隔符(逗号、分号、竖线等),也支持处理全角/半角混合的场景。
三、完整实现步骤
下面我们一步步实现这个通用的字符串拆分方案。
步骤1:创建测试表与初始化数据
先创建一张测试表,模拟业务中常见的场景,同时包含半角逗号、全角逗号两种分隔符:
SQL
-- 先删除旧表(避免报错)
DROP TABLE IF EXISTS t_demo;
CREATE TABLE t_demo (
id INT ,
names VARCHAR(255)
);
-- 插入测试数据,包含全角逗号、半角逗号两种分隔符
INSERT INTO t_demo VALUES
(101, '苹果,香蕉,橙子'), -- 全角逗号分隔
(102, '电脑,手机'), -- 全角逗号分隔
(103, 'GBase,MySQL,Oracle,SQLServer'); -- 半角逗号分隔
步骤2:创建数字辅助表并初始化数据
数字辅助表是整个方案的核心,我们通过存储过程批量生成连续整数,方便后续使用:
SQL
-- 删除旧的辅助表
DROP TABLE IF EXISTS numbers;
-- 创建数字辅助表,存储从1开始的连续整数
CREATE TABLE numbers (
n INT
);
-- 如果存储过程已存在,先删除
DROP PROCEDURE IF EXISTS InsertNumbers;
-- 修改语句结束符,避免存储过程内部的分号和外部冲突
DELIMITER $$
CREATE PROCEDURE InsertNumbers(IN max_num INT)
BEGIN
DECLARE i INT DEFAULT 1;
-- 清空辅助表数据
TRUNCATE TABLE numbers;
-- 处理异常参数:如果传入的max_num为空或小于1,默认生成100条数据
IF max_num IS NULL OR max_num < 1 THEN
SET max_num = 100;
END IF;
-- 循环插入从1到max_num的连续整数
WHILE i <= max_num DO
INSERT INTO numbers (n) VALUES (i);
SET i = i + 1;
END WHILE;
END$$
DELIMITER ;
-- 调用存储过程,生成1-100的数字序列
CALL InsertNumbers(100);
说明:max_num 的取值建议设置为业务中“单条记录最大分隔项数量”的上限,比如你的业务中最多单条记录有50个标签,设置为100就完全够用了。
步骤3:核心拆分SQL实现
通过子查询统一分隔符,再关联数字辅助表,完成字符串拆分:
SQL
SELECT
t.id,
-- 两次SUBSTRING_INDEX截取第n个分隔项,再用TRIM去除首尾空格
TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(unified_names, ',', n.n), ',', -1)) AS name
FROM (
-- 子查询:先把所有全角逗号替换为半角逗号,统一分隔符格式
SELECT
id,
REPLACE(names, ',', ',') AS unified_names
FROM t_demo
WHERE names IS NOT NULL
) t
-- 关联数字辅助表:关联条件为n不超过拆分后的总行数
JOIN numbers n
ON n.n <= LENGTH(t.unified_names) - LENGTH(REPLACE(t.unified_names, ',', '')) + 1
-- 按原表id和拆分顺序排序,结果更易读
ORDER BY t.id, n.n;
gbase> SELECT
-> t.id,
-> TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(unified_names, ',', n.n), ',', -1)) AS name
-> FROM (
-> SELECT
-> id,
-> REPLACE(names, ',', ',') AS unified_names
-> FROM t_demo
-> WHERE names IS NOT NULL
-> ) t
-> JOIN numbers n
-> ON n.n <= LENGTH(t.unified_names) - LENGTH(REPLACE(t.unified_names, ',', '')) + 1
-> ORDER BY t.id, n.n;
+------+-----------+
| id | name |
+------+-----------+
| 101 | 苹果 |
| 101 | 香蕉 |
| 101 | 橙子 |
| 102 | 电脑 |
| 102 | 手机 |
| 103 | GBase |
| 103 | MySQL |
| 103 | Oracle |
| 103 | SQLServer |
+------+-----------+
9 rows in set (Elapsed: 00:00:00.12)
可以看到,所有逗号分隔的字符串都被拆分成了独立的行,全角/半角逗号也被统一处理,拆分结果完全符合预期。
四、核心原理详解
1. 分隔符统一处理
REPLACE(names, ',', ',') 这一步是为了处理全角逗号和半角逗号混用的场景,把所有全角逗号替换为半角逗号,避免因为分隔符格式不统一导致拆分失败。
2. 计算拆分后的总行数
LENGTH(t.unified_names) - LENGTH(REPLACE(t.unified_names, ',', '')) + 1 这个公式,是计算单条记录需要拆分成多少行:
•原字符串长度:LENGTH(unified_names)
•去掉所有逗号后的字符串长度:LENGTH(REPLACE(unified_names, ',', ''))
•两者的差值,就是字符串中逗号的数量;
•逗号数量 + 1,就是拆分后的总行数(比如a,b,c有2个逗号,拆分后是3行)。
3. 两次 SUBSTRING_INDEX 截取
SUBSTRING_INDEX(str, delim, count) 函数的作用是按分隔符截取字符串:
•SUBSTRING_INDEX(unified_names, ',', n.n):截取字符串的前n.n个分隔项,比如n=2时,GBase,MySQL,Oracle会被截取为GBase,MySQL;
•再用 SUBSTRING_INDEX(..., ',', -1):从截取后的字符串中,取最后一个分隔项,也就是MySQL;
•最后用 TRIM() 去除可能存在的首尾空格,得到干净的结果。
评论
热门帖子
- 12025-12-01浏览数:182764
- 22023-05-09浏览数:25062
- 42023-09-25浏览数:18526
- 52020-05-11浏览数:17529