列表接口严禁嵌套 LISTAGG + REGEXP:一次 mission_label 性能事故复盘

写给所有开发:不要在分页查询里做字符串拆分聚合;

数据库不是翻译器:列表接口必须回归"只查数据"原则;

分页查询里的 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 层一次性翻译并缓存

  • 性能提升一个数量级

相关推荐
好好研究2 小时前
MyBatis - Plus(二)常见注解 + 常见配置
数据库·spring boot·mybatis·mybatis plus
m***06682 小时前
Java进阶(ElasticSearch的安装与使用)
java·elasticsearch·jenkins
PD我是你的真爱粉2 小时前
MySQL基础-DQL语句与多表查询
数据库·mysql
C#程序员一枚2 小时前
SqlServer如何创建全文索引
数据库·sqlserver
Anastasiozzzz2 小时前
Java异步编程:CompletableFuture从入门到底层实现
java·开发语言
DBA小马哥2 小时前
时序数据库迁移实践指南:面向业务连续性的技术演进路径
数据库·时序数据库·dba
xiaomin-Michael2 小时前
netty学习
java
生命因何探索2 小时前
Redis-持久化
数据库·redis·缓存
上海合宙LuatOS3 小时前
LuatOS核心库API——【fft 】 快速傅里叶变换
java·前端·人工智能·单片机·嵌入式硬件·物联网·机器学习