一则复杂 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(屎山核心)原样示例(已简化):
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(最终版本),如下:
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 的优化流程:
- 理解业务意图:这条 SQL 是做什么的?统计?监控?导出?
- 理清字段含义:问清楚字段和表的关系,不怕问错,只怕想当然。
- 观察数据量和索引:
EXPLAIN
+SHOW INDEX
是必须工具。 - 写小查询试水:验证表连接关系和过滤逻辑,减少误解。
- 一步步重构:每改一段先测一次,再合并。
总结
真正复杂的 SQL 往往不是难在语法,而是难在“它试图一口气表达太多复杂业务意图”。
而优化 SQL 的关键,并不是你能写多炫技的语句,而是你是否能——
- 看懂它背后真正想干什么;
- 拆解出最小单元;
- 找到正确字段连接路径;
- 用最朴素的方式表达业务意图。
一则复杂 SQL 改写后有感的更多相关文章
- 标量子查询SQL改写
一网友说下面sql跑的好慢,让我看看 sql代码: select er, cid, pid, tbl, zs, sy, (select count(sr.mobile_tele_no) from tb ...
- 十八般武艺玩转GaussDB(DWS)性能调优:SQL改写
摘要:本文将系统介绍在GaussDB(DWS)系统中影响性能的坏味道SQL及SQL模式,帮助大家能够从原理层面尽快识别这些坏味道SQL,在调优过程中及时发现问题,进行整改. 数据库的应用中,充斥着坏味 ...
- sql改写优化:简单规则重组实现
我们知道sql执行是一个复杂的过程,从sql到逻辑计划,到物理计划,规则重组,优化,执行引擎,都是很复杂的.尤其是优化一节,更是内容繁多.那么,是否我们本篇要来讨论这个问题呢?答案是否定的,我们只特定 ...
- 安装完sql server 后修改计算机名后不能进行发布的订阅的解决办法
由于需要需要配置一个发布订阅,可是一直报告:" sql server 复制需要有实际的服务器名称才能连接到服务器,不支持通过别名.ip地址或其他任何备用名称进行连接.请指定实际的服务器名称“ ...
- spring 默认情况下事务是惟一的 同一个方法里面第一个sql开启后 在执行完 将事务传递给下一个sql
spring 默认情况下事务是惟一的 同一个方法里面第一个sql开启后 在执行完 将事务传递给下一个sql
- 系统安装SQL Sever2000后1433端口未开放,如何打开1433端口的解决方法
这篇文章主要针对Win2003系统安装SQL Sever2000后1433端口未开放,如何打开1433端口的解决方法. 用了几年的Windows2003和SQL Server2000了,不过这个问题倒 ...
- sql 分组后显示每组的前几条记录
sql 分组后显示每组的前几条记录 如 表中记录是 code serialno A1 1 ...
- Python学习---Django误删除sql表后,如何创建数据
误删除sql表后,怎么创建数据? 仅仅适合单表,多表因为涉及约束, python mangage.py makemigrations --> 生成migrations目录和根数据库对应的sql ...
- 新生 & 语不惊人死不休 —— 《无限恐怖》读后有感
开篇声明,我博客中“小心情”这一系列,全都是日记啊随笔啊什么乱七八糟的.如果一不小心点进来了,不妨直接关掉.我自己曾经写过一段时间的日记,常常翻看,毫无疑问我的文笔是很差的,而且心情也是瞬息万变的.因 ...
- sql 分组后按时间降序排列再取出每组的第一条记录
原文:sql 分组后按时间降序排列再取出每组的第一条记录 竞价记录表: Aid 为竞拍车辆ID,uid为参与竞价人员ID,BidTime为参与竞拍时间 查询出表中某人参与的所有车辆的最新的一条的竞价记 ...
随机推荐
- idea 登录提示Server's certificate is not trusted
原因:你本地的idea证书不可以 解决方式1: 你去安装一个正版的: 解决方式2: 设置接受不受信任证书即可. AS:File - Settings - Tools - Server Certific ...
- python 更新pip镜像源
前言 默认情况下 pip 使用的是国外的镜像,在下载的时候速度非常慢,下载速度是几kb或者几十kb,花费的时间比较长. 解决办法 国内目前有些机构或者公司整理了对应的镜像源,使得通过内网就能访问即可, ...
- Can't find PHP headers in /usr/include/php The php-devel package is required for use of this command.
报错 phpize 编译扩展时,报错:Can't find PHP headers in /usr/include/php The php-devel package is required for ...
- PIL或Pillow学习2
接着学习下Pillow常用方法: PIL_test1.py : ''' 9, Pillow图像降噪处理 由于成像设备.传输媒介等因素的影响,图像总会或多或少的存在一些不必要的干扰信息,我们将这些干扰信 ...
- Flask快速入门3
十一,Flask Cookies Cookie以文本文件的形式存储在客户端的计算机上.其目的是记住和跟踪与客户使用相关的数据,以获得更好的访问者体验和网站统计信息. Request对象包含Cookie ...
- delphi 让执行程序不在任务栏显示
Application.MainFormOnTaskbar := False; procedure TForm1.FormShow(Sender: TObject); begin ShowWindow ...
- 认识知识库与知识图谱:从CDSS的前世今生聊聊模型幻觉问题
提供AI咨询+AI项目陪跑服务,有需要回复1 今年很多医院已经部署上了DeepSeek,甚至有医生真的使用它对患者进行诊断,但马上就出问题了:AI 误诊,上海患者获赔 127 万. 不过,我去搜索详情 ...
- frameset frame 实例和用法
转
看这个比较好
- 【FAQ】HarmonyOS SDK 闭源开放能力 — IAP Kit(6)
1.问题描述: 支付场景,表现是在沙盒情况下所有商品都可以正常跑通,但是在非沙盒情况下,线上购买年包1800大额支付华为的 iap.createPurchas 在输完密码就会报 1001860001 ...
- java——文件的上传、下载、删除操作DEMO
记录一下java实现文件的上传.下载.删除的功能demo: /** * Controller */ @Slf4j @RestController @RequestMapping public clas ...