写给所有开发:不要在分页查询里做字符串拆分聚合;
数据库不是翻译器:列表接口必须回归"只查数据"原则;
分页查询里的 LISTAGG:看似优雅,实则致命;
REGEXP_SUBSTR 在列表接口中出现,就是隐患;
这不是优化技巧问题,这是架构纪律问题。
🚫 列表接口严禁嵌套 LISTAGG + REGEXP 拆分:一次 mission_label 性能事故复盘
1. 背景:一个看似正常的字段翻译需求
在飞行计划历史放飞列表接口中,我们需要返回任务类型字段:
-
mission:存储的是 ID 列表,例如
"1,3,7" -
mission_label:希望返回可读名称,例如
"航拍,巡检,物流"
于是 SQL 中写了如下逻辑:
(
SELECT LISTAGG(uft.remark, ',') WITHIN GROUP (ORDER BY uft.id)
FROM uav_bas_flight_type uft
WHERE uft.id IN (
SELECT REGEXP_SUBSTR(up.mission, '[^,]+', 1, LEVEL)
FROM dual
CONNECT BY LEVEL <= REGEXP_COUNT(up.mission, ',') + 1
)
) AS mission_label
看起来很合理:
-
字段拆分
-
查字典表
-
LISTAGG 拼接成字符串
但是------
这是列表接口,属于绝对禁止写法。
2. 问题本质:这是一个典型的"行级子查询灾难"
列表接口意味着:
-
一次查询返回 20 条
-
下一页 20 条
-
可能全表数万条
而这种写法的执行方式是:
每返回一行,就执行一次子查询
所以复杂度变成:
列表行数 N × 子查询开销
假设:
-
每页 50 条
-
每条 mission 平均 5 个 ID
那么数据库执行的工作量是:
-
50 次 REGEXP_SUBSTR 拆分
-
50 次 CONNECT BY 递归
-
50 次 LISTAGG 聚合
-
50 次字典表扫描
这不是查询,这是 CPU 消耗器。
3. 更严重的问题:REGEXP_SUBSTR + CONNECT BY 是性能杀手
在 Oracle/达梦体系里:
REGEXP_SUBSTR(...)
CONNECT BY LEVEL <= ...
属于典型的:
-
无法走索引
-
必须逐字符解析
-
必须递归生成行
-
优化器无法提前裁剪
这种写法在单条数据里还能接受。
但在列表接口里出现,就是生产环境严重事故(已经踩坑一次了,排查了三天各种深层次优化,把DBA专家都找来分析了说是CPU跑爆了90%+,最后团队又自己排查了一遍发现是这里一个不起眼的细节搞得鬼,真的是害惨了)。
4. mission_label 的正确处理方式是什么?
✅ 原则:列表接口 SQL 只负责返回原始 ID,不负责翻译
mission 字段:
"mission": "1,3,7"
直接返回即可。
翻译工作必须放到:
-
应用层一次性批量翻译
-
后端不想做的话,甚至可以让前端通过字典接口取匹配
-
或缓存字典表
-
或结构设计重构
5. 推荐方案对比
❌ 错误方案(禁止)
SQL 行级子查询:
SELECT
...,
(SELECT LISTAGG(...) FROM dict WHERE id IN (...REGEXP...)) AS mission_label
FROM plan
问题:
-
每行执行一次
-
正则拆分极慢
-
无法索引优化
-
分页越大越灾难
✅ 正确方案 1:应用层字典翻译(推荐)
SQL 只查 mission:
SELECT id, mission FROM uav_plan;
Java 层:
Map<String,String> dict = loadMissionDict();
for (Plan p : list) {
p.setMissionLabel(
translate(p.getMission(), dict)
);
}
优势:
-
字典只查一次
-
翻译是内存操作
-
接口性能稳定
✅ 正确方案 2:字典缓存(最优)
系统启动时加载:
@Cacheable("missionDict")
public Map<String,String> getMissionDict() {}
列表接口翻译:
-
O(N)
-
无数据库额外压力
✅ 正确方案 3:结构设计修复(根治)
mission 不应该存 "1,3,7" 字符串。
应该拆表:
plan_mission_rel(
plan_id,
mission_id
)
查询时 join:
SELECT plan_id,
LISTAGG(m.name, ',')
FROM rel r
JOIN mission m ON r.mission_id=m.id
GROUP BY plan_id;
优势:
-
正常索引
-
正常 join
-
数据库设计合理
✅ 正确方案 4:SQL层设计修复(LEFT JOIN)
sql
SELECT
th.id,
tp.mission,
ml.mission_label
FROM th_takeoff th
LEFT JOIN th_plan tp ON tp.id = th.plan_id
LEFT JOIN (
SELECT
tp2.id AS plan_id,
LISTAGG(tpt.plan_type, ',') WITHIN GROUP (ORDER BY tpt.id) AS mission_label
FROM th_plan tp2
LEFT JOIN th_bas_plan_type tpt
ON INSTR(',' || tp2.mission || ',', ',' || tpt.id || ',') > 0
GROUP BY tp2.id
) ml ON ml.plan_id = tp.id
6. 团队规范:必须杜绝这种情况发生
🚨 列表接口 SQL 禁止出现:
-
行级子查询
-
REGEXP_SUBSTR 拆分字符串
-
CONNECT BY 递归拆字段
-
LISTAGG 聚合字典翻译
列表接口 SQL 只能做:
-
主表查询
-
必要 join
-
必要过滤
-
原始字段返回
翻译必须:
-
应用层批量做
-
或缓存做
-
或结构设计修复
7. 结论:这不是优化,是纪律
mission_label 的问题不是 SQL 写得不好。
而是:
列表接口承担了它不该承担的工作。
数据库负责:
-
数据筛选
-
数据分页
应用负责:
-
字典翻译
-
展示拼接
任何把翻译逻辑塞进 SQL 的行为,在列表接口里都是性能事故隐患。
✅ 最后一句话
LISTAGG + REGEXP_SUBSTR
在列表接口里出现一次,就必须重构一次。
不允许存在"先这样写着"。
-
SQL 完全移除 LISTAGG
-
接口返回 missionIds
-
Java 层一次性翻译并缓存
-
性能提升一个数量级