一则复杂 SQL 改写后有感

抱歉很久没写技术博客。

自从LLM爆发之后,写概念堆叠的所谓"博客"已经没有意义了,那么我会思考我的博客还有什么作用。

得出的结论是:具体业务的讨论仍然是有价值的

所以之后会随缘更新一些强业务相关的博客

为什么说随缘呢?因为这段时间经历了一些事情,让我感觉比起死磕技术(工作),体验生活才是我的首要目标

别说什么裁员的,真裁员我还来不及高兴呢

以上,2025.06.12

背景:维护中遇到一段"屎山 SQL"

近期在优化一个业务接口性能时,发现其背后依赖了一段极为复杂的 SQL 查询,执行时间长达 20 多秒 。这段 SQL 看似在统计一些业务数据,但结构混乱、嵌套严重、含义难懂,最严重的是,其中一段使用了过度复杂的窗口函数嵌套,成为性能瓶颈

本文记录我分析、重构并优化该 SQL 的过程,并结合实际经验,聊聊 SQL 优化中"理解数据关系 vs 语法技巧"的重要性。


原始 SQL 功能分析

该 SQL 由三部分构成,通过 UNION ALL 拼接。表结构如下:

  • sdd_t_ace: 主表,记录交易信息。字段有 scan_seq_no, tr_date, status, b_code 等。
  • aaa_t_tr_over_log: 记录交易"处理完成"的日志。字段有 scan_seq_no, finished_time
  • aaa_t_tr_obtain_log: 记录交易"领取处理"的日志。字段有 ace_id, tr_date

SQL 三段查询的业务目标(推测):

太难看了只能靠猜

序号 SQL 功能 推测业务目标
查询今日内所有交易记录数量 统计交易总量
查询今日内未完成的交易记录 统计待处理数量
查询今日内已完成但处理时间晚于某时间点 ,且已被领取的任务 检查延迟处理的任务

第三段 SQL(屎山核心)原样示例(已简化):

sql 复制代码
select scan_seq_no, finished_time, obtain_time
from (
    select scan_seq_no, finished_time
    from (
        select t1.scan_seq_no, t2.finished_time,
            row_number() over(partition by t2.scan_seq_no order by t2.finished_time desc) as rank1
        from sdd_t_ace t1
        left join aaa_t_tr_over_log t2 on t1.scan_seq_no = t2.scan_seq_no
        where t1.status != 0
          and t1.tr_date between curdate() and '2025-06-06 16:30'
    ) k1
    where finished_time >= '2025-06-06 16:30' and rank1 = 1
) m1
left join aaa_t_tr_obtain_log m2 on m1.scan_seq_no = m2.scan_seq_no
where m2.tr_date <= '2025-06-06 16:30'

存在的问题分析

问题点 描述 技术影响
多层嵌套 三层 SELECT 嵌套,逻辑难以理解 执行计划难优化,难排查
重复排序 两次 row_number() 排序 触发多次 filesort,严重拖慢性能
LEFT JOIN + WHERE 使用 LEFT JOIN 却又筛选右表字段 实际等同 INNER JOIN,但影响优化器判断
字段使用混乱 m1.scan_seq_no = m2.scan_seq_no 实为业务等价字段,但未解释 增加阅读成本

重写目标:保证结果一致,逻辑清晰,性能提升

优化后的 SQL(最终版本),如下:

sql 复制代码
select substring(b_code, 1, 3) as b_code_per3, count(*) as dealTask_cnt
from (
  select distinct sdd.scan_seq_no, sdd.b_code
  from sdd_t_ace sdd
  inner join aaa_t_tr_over_log log on sdd.scan_seq_no = log.scan_seq_no
  inner join aaa_t_tr_obtain_log m2 on sdd.scan_seq_no = m2.ace_id
  where sdd.status != 0
    and log.finished_time >= '2025-06-06 16:30'
    and sdd.tr_date between curdate() and '2025-06-06 16:30'
    and m2.tr_date <= '2025-06-06 16:30'
) grouped
group by substring(b_code, 1, 3)

优化思路解析

1. 用 distinct 替代 row_number()

原意是"每个 scan_seq_no 取最新的一条",但如果我们能提前确认一条记录只会对应唯一一条日志,则无需 row_number,使用 distinct 更高效。

2. 使用 INNER JOIN 替代 LEFT JOIN + WHERE

原始代码中 LEFT JOIN ... WHERE m2.tr_date <= ... 实际相当于 INNER JOIN,直接明确意图。

3. 消除嵌套子查询

去掉所有 SELECT FROM (SELECT FROM (SELECT ...)) 的结构,提升可读性,也让优化器更好地生成执行计划。

4. 预判数据范围 + 合理过滤条件

结合业务逻辑判断只需要查 today目标时间 的数据,避免全表扫描。

实际执行效果对比

比较项 原始 SQL 优化后 SQL
查询耗时 约 20 秒 < 1 秒
可读性 嵌套复杂,字段混乱 简洁清晰,一目了然
可维护性 差,修改易出错 高,字段含义明确

技术反思:SQL 优化靠什么?

错误理解:"优化 SQL 就是写法要帅,函数要高级"

这种想法容易导致用 row_number、CTE、嵌套写法堆叠逻辑,表面花哨,实则低效

正确理解:SQL 优化 80% 是理解数据,20% 是写法技巧

能力 描述
理解表关系 哪些字段是一对一?一对多?是外键?
理解字段语义 字段如 status, finished_time,分别代表什么状态?何时更新?
理解数据分布 status=0 的比例?b_code 的稀疏度?
清楚业务目标 查询是统计量?查异常?查明细?

一些思考

SQL 优化怎么问、怎么做?

面对陌生 SQL 的优化流程:

  1. 理解业务意图:这条 SQL 是做什么的?统计?监控?导出?
  2. 理清字段含义:问清楚字段和表的关系,不怕问错,只怕想当然。
  3. 观察数据量和索引EXPLAIN + SHOW INDEX 是必须工具。
  4. 写小查询试水:验证表连接关系和过滤逻辑,减少误解。
  5. 一步步重构:每改一段先测一次,再合并。

总结

真正复杂的 SQL 往往不是难在语法,而是难在"它试图一口气表达太多复杂业务意图"。

而优化 SQL 的关键,并不是你能写多炫技的语句,而是你是否能------

  • 看懂它背后真正想干什么;
  • 拆解出最小单元;
  • 找到正确字段连接路径;
  • 用最朴素的方式表达业务意图。
相关推荐
gsfl2 小时前
Redis 缓存
数据库·redis·缓存
月夕·花晨3 小时前
Gateway-过滤器
java·分布式·spring·spring cloud·微服务·gateway·sentinel
hssfscv4 小时前
JAVA学习笔记——9道综合练习习题+二维数组
java·笔记·学习
初听于你6 小时前
缓存技术揭秘
java·运维·服务器·开发语言·spring·缓存
小蒜学长7 小时前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
恒悦sunsite8 小时前
Ubuntu之apt安装ClickHouse数据库
数据库·clickhouse·ubuntu·列式存储·8123
奥尔特星云大使9 小时前
MySQL 慢查询日志slow query log
android·数据库·mysql·adb·慢日志·slow query log
来自宇宙的曹先生9 小时前
MySQL 存储引擎 API
数据库·mysql
间彧9 小时前
MySQL Performance Schema详解与实战应用
数据库
间彧9 小时前
MySQL Exporter采集的关键指标有哪些,如何解读这些指标?
数据库