SQL改写实战:子查询、CTE、窗口函数性能对比

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

写SQL写久了就会发现,同样的业务需求,不同的人写出来,执行效率可能差几十倍。尤其是在复杂统计、排名分组这些场景,选错写法,查询能从毫秒级变成分钟级。以前做运营的时候,我只需要看懂Excel里的公式;转行后才明白,SQL优化的本质是"用更少的扫描、更少的临时表完成同样的事"。今天就把子查询、CTE、窗口函数这三者的性能差异彻底讲透。

这两年MySQL 8.0普及之后,CTE和窗口函数不再是"新特性",而是每个写SQL的人必须掌握的技能。很多以前只能用子查询或临时表硬扛的需求,现在有了更优雅高效的解法。理解这三种写法的性能特性,能让你在面对复杂报表时少走弯路。

先花一分钟搞懂三个概念

  • 子查询:把一个查询的结果作为另一个查询的条件或数据源。优点是直观,缺点是相关子查询(外层每行执行一次内层)性能极差。
  • **CTE(公共表表达式)**:可以理解为一个命名的临时结果集,只在当前查询中有效。提升可读性,还能支持递归查询(比如组织架构树)。
  • 窗口函数:在保留原行数据的基础上,对一组行进行聚合或排名计算。不会像GROUP BY那样压缩行数,非常适合"每行后面加一个汇总值"的场景。

实测对比:每种写法最适合什么场景

场景 推荐写法 原因
简单过滤(查某个用户的最新订单) 窗口函数 或 CTE+JOIN 一次扫描完成,效率高
多步复杂逻辑,需要分步写 CTE 可读性最好,便于调试
每行后面跟一个汇总值 窗口函数 不改变行数,一次扫描
递归查询(树形、BOM) 递归CTE 唯一可行的标准写法
EXISTS / IN 半连接子查询 优化器能处理好
相关子查询 改写成JOIN或窗口函数 相关子查询通常较慢

真实数据:1000万行表,查每个客户的最新订单

环境:MySQL 8.0.32,8C32G,表orders有索引(order_date, customer_id)

❌ ​相关子查询​:耗时12.5秒

sql 复制代码
SELECT * FROM orders o1 
WHERE order_date = (SELECT MAX(order_date) FROM orders o2 WHERE o2.customer_id = o1.customer_id);

✅ ​窗口函数​:耗时3.8秒

sql 复制代码
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) as rn
    FROM orders
) t WHERE rn = 1;

✅ ​CTE+JOIN​:耗时4.2秒

vbnet 复制代码
WITH latest AS (
    SELECT customer_id, MAX(order_date) as max_date
    FROM orders GROUP BY customer_id
)
SELECT o.* FROM orders o JOIN latest l ON o.customer_id = l.customer_id AND o.order_date = l.max_date;

结论:相关子查询最慢,窗口函数和CTE+JOIN都在4秒左右。以后遇到"每组取最值"的需求,优先用窗口函数。

排名统计场景:按部门计算员工工资排名

10万行员工表,按部门内工资排名。

窗口函数(RANK):0.9秒,代码一行

sql 复制代码
SELECT *, RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) as rnk FROM emp;

自连接旧写法:7.2秒,SQL复杂难懂

vbnet 复制代码
SELECT e1.*, COUNT(DISTINCT e2.salary) as rnk
FROM emp e1 LEFT JOIN emp e2 ON e1.dept_id = e2.dept_id AND e1.salary <= e2.salary
GROUP BY e1.id;

窗口函数在排名、累计、移动平均等场景下,性能和简洁度都是碾压级别。

递归CTE实际运用:查询组织架构树

需求:部门表dept(id, parent_id, name),查询id=1及其所有下级。

sql 复制代码
WITH RECURSIVE dept_tree AS (
    SELECT id, parent_id, name FROM dept WHERE id = 1
    UNION ALL
    SELECT d.id, d.parent_id, d.name FROM dept d
    INNER JOIN dept_tree dt ON d.parent_id = dt.id
)
SELECT * FROM dept_tree;

一次查询搞定,这是MySQL 8.0之前很难优雅实现的。

一点体会

从运营转行做DBA之后,我最深的感受是:写SQL和做运营数据分析本质上都是"从数据里找答案",但SQL优化要求你更懂底层机制。窗口函数和CTE不是新语法,它们是工具包里最趁手的两把扳手。遇到复杂统计,第一反应应该是"能不能用窗口函数",而不是"先写个子查询凑合用"。掌握这三者,不是为了炫技,而是让查询跑得更快、代码更容易维护。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~

参考文献

  1. MySQL官方文档:《Window Functions》,《Common Table Expressions》
  2. 《高性能MySQL》第4版,第8章
相关推荐
@我漫长的孤独流浪4 小时前
数据库完整性约束全解析:从理论到实践
数据库
小江的记录本4 小时前
【Java基础】反射与注解:核心原理、自定义注解、注解解析方式(附《思维导图》+《面试高频考点清单》)
java·数据结构·python·mysql·spring·面试·maven
l1t5 小时前
DeepSeek总结的 DuckDB 1.5.3:并非普通的补丁版本
数据库·duckdb
Java成神之路-5 小时前
深入拆解 MySQL InnoDB 隔离级别:从 MVCC 到临键锁
mysql
云商直通车5 小时前
华为云ECS购买与配置超详细教程
服务器·数据库·华为云
hj10435 小时前
fastadmin对接人大金仓 kingbase问题记录&解决
数据库
罗超驿5 小时前
19.告别复杂SQL!用MySQL视图把逻辑拆成“变量”式操作
数据库·mysql·面试
技术钱5 小时前
向量数据库的介绍
数据库
Gauss松鼠会5 小时前
GaussDB(DWS) 日常维护命令
服务器·数据库·postgresql·性能优化·gaussdb·经验总结