SQL 查询优化实战:4 个让查询快 10 倍的写法
钟智强
钟智强
哪吒网络安全
2026年7月6日 13:13
马来西亚
在小说阅读器读本章
去阅读
—— 从「能跑」到「跑得快」,一线数据开发最常踩的 4 个坑
分类:数据库 / SQL 优化 标签:SQL、MySQL、CTE、索引、执行计划 阅读时长:约 10 分钟
前言
同样一句 SQL,返回的结果一模一样,但一条 50 毫秒返回,另一条要跑 30 秒 —— 这在生产环境中太常见了。正如那句被反复引用的话:Not all SQL queries are made equal(并非所有 SQL 查询都生而平等)。
很多同学写 SQL 只关心「结果对不对」,却忽略了「数据库到底做了多少无用功」。本文围绕四个最高频、也最容易被忽视的写法问题展开,每一条都给出反例、正例、原理图解和注意事项,帮助你把查询从「能跑」优化到「跑得快」。这四点分别是:
●列的选择:用 SELECT \* 还是只取需要的列;
●过滤时机:在 JOIN 之前过滤,还是 JOIN 之后过滤;
●重复计算:用相关子查询逐行计算,还是用 CTE 计算一次并复用;
●索引使用:过滤条件落在无索引列上,还是落在索引列上。
一、别再用 SELECT \*:只取你需要的列
问题场景
SELECT \* 写起来最省事,但它会让数据库读取并返回表中的每一列 —— 包括那些你根本用不到的大字段(如长文本、JSON、BLOB)。这带来三重成本:更大的磁盘 I/O、更多的网络传输、以及无法命中「覆盖索引」。
× 低效写法
| SELECT \* FROM table\_name; |
√ 推荐写法
| SELECT column1, column2, column3 FROM table\_name; |
图 1:SELECT \* 读取全部列,而按需选择只读取真正用到的列
为什么更快
覆盖索引(Covering Index)是关键收益点。如果一个索引已经包含了查询所需的全部列,数据库可以直接从索引里返回数据,完全不用回表(回到主键聚簇索引再取一次行)。一旦你写了 SELECT \*,只要表里有任何一列不在该索引中,覆盖索引就失效了,查询被迫回表,性能骤降。
此外,返回的列越少,结果集在内存、排序缓冲区、网络管道中占用的空间就越小,对高并发场景尤其明显。
| 注意事项 • 视图、ORM 或 BI 工具里隐藏的 SELECT \ 同样有代价,排查慢查询时别放过; • 表结构变更(新增大字段)会让原本没问题的 SELECT \ 突然变慢; • 只在临时排查、确实需要全部列时才用 \* ,生产代码应显式列出字段。 |
二、过滤时机:在 JOIN 之前过滤,而不是之后
问题场景
当你先把两张大表 JOIN 起来、再用 WHERE 过滤时,数据库往往要先生成一个庞大的中间结果集,然后才把绝大部分行丢掉。真正需要的可能只有几千行,却让上百万行参与了连接运算 —— 这是巨大的浪费。
× 低效写法:先 JOIN 再 WHERE
| SELECT t1.column\_1, t2.column\_2 FROM table1 t1 JOIN table2 t2 ON t1.foreign\_id = t2.primary\_id WHERE t2.filter\_column = 'some\_value'; |
√ 推荐写法:用 CTE 在 JOIN 之前过滤
| WITH cte AS ( SELECT \* FROM table2 WHERE filter\_column = 'some\_value' ) SELECT t1.column\_1, cte.column\_2 FROM table1 t1 JOIN cte ON t1.foreign\_id = cte.primary\_id; |
图 2:先在 CTE 中把 table2 过滤到很小,再参与 JOIN,中间结果大幅缩小
为什么更快
核心思想是「尽早缩小数据量」(predicate pushdown,谓词下推)。先把过滤条件应用到单表上,让参与连接的行数从百万级降到千级,连接的代价随之呈数量级下降,占用的内存和排序开销也更小。
| 注意事项 • 现代优化器(如 MySQL 8.0、PostgreSQL)在很多情况下会自动下推谓词,此时两种写法执行计划相同; • 但当涉及 OUTER JOIN、复杂子查询或视图时,优化器不一定能自动下推,显式用 CTE / 派生表更稳妥; • 别把过滤条件误写在 OUTER JOIN 的 ON 上,那会改变语义(左表不匹配行会被保留)。 |
三、用 CTE 替代重复的相关子查询
问题场景
在 SELECT 列表里塞入多个子查询(尤其是相关子查询)时,这些子查询可能会对外层结果的每一行都重新执行一次。下面的例子对同一份 JOIN 结果分别求 MAX 和 MIN,等于把相同的扫描做了两遍甚至更多遍。
× 低效写法:多个子查询重复扫描
| SELECT column\_1, (SELECT MAX(column2) FROM table1 t1 JOIN table2 t2 ON t1.column3 = t2.column3) AS max\_value, (SELECT MIN(column2) FROM table1 t1 JOIN table2 t2 ON t1.column3 = t2.column3) AS min\_value FROM table3; |
√ 推荐写法:CTE 只计算一次并复用
| WITH cte AS ( SELECT MAX(column2) AS max\_value, MIN(column2) AS min\_value FROM table1 t1 JOIN table2 t2 ON t1.column3 = t2.column3 ) SELECT column\_1, cte.max\_value, cte.min\_value FROM table3, cte; |
图 3:相关子查询逐行重复扫描;CTE 把聚合计算一次,然后被所有行复用
为什么更快
把公共的、重复的计算抽取到一个 CTE(Common Table Expression,公共表表达式)中,数据库只需扫描并聚合一次,得到 max\_value 和 min\_value 两个标量值,再让外层查询直接引用。原来「N 行 × 多次扫描」的开销被压缩为「一次扫描」,可读性也明显更好。
| 注意事项 • CTE 主要提升可读性与可维护性,是否物化(materialize)取决于数据库;例如 PostgreSQL 12+ 默认可内联,MySQL 会视情况物化; • 若确实需要缓存中间结果,可考虑临时表或数据库特定的物化提示; • 示例中 FROM table3, cte 是笛卡尔积写法:因为 cte 只有一行标量,等价于给每行附加两列,但业务中使用交叉连接务必确认行数。 |
四、让过滤条件落在索引列上
问题场景
WHERE 条件如果作用在没有索引的列上,数据库只能逐行扫描整张表(全表扫描),复杂度是 O(N)。把过滤条件放到已建索引的列上,数据库就能借助 B-Tree 索引快速定位,复杂度降到 O(log N),数据量越大,两者差距越夸张。
× 低效写法:过滤无索引列
| SELECT column\_1 FROM table\_name WHERE unindexed\_column = 'some\_value'; |
√ 推荐写法:过滤索引列
| SELECT column\_1 FROM table\_name WHERE indexed\_column = 'some\_value'; |
图 4:无索引 → 逐行全表扫描 O(N);有索引 → B-Tree 定位 O(log N)
为什么更快
B-Tree 索引是有序的多叉平衡树,查找时从根节点出发,每一层都能排除掉大部分数据,只需少数几跳就能定位到目标。这就是「逐行找」和「跳着找」的区别。
| 让索引失效的常见写法(务必避开) • 在索引列上套函数:WHERE DATE(created\_at) = '2026-07-06' → 改写成范围条件; • 前置通配符模糊匹配:WHERE name LIKE '%abc' → 无法利用 B-Tree,'abc%' 才可以; • 隐式类型转换:索引列是字符串却传入数字,会触发全表扫描; • 对索引列做运算:WHERE price \* 2 > 100 → 改成 WHERE price > 50; • 索引不是越多越好:写入时要维护索引,过多索引拖慢 INSERT / UPDATE。 |
五、四条原则速查
图 5:四条 SQL 优化原则一图速查
六、如何验证优化是否生效:EXPLAIN
优化不能靠感觉,要用执行计划说话。绝大多数数据库都提供 EXPLAIN(或 EXPLAIN ANALYZE)来查看查询的执行方式:
| -- MySQL / PostgreSQL EXPLAIN ANALYZE SELECT column1, column2 FROM table\_name WHERE indexed\_column = 'some\_value'; |
看执行计划时重点关注这几个信号:
●是否全表扫描:MySQL 里 type=ALL、PostgreSQL 里 Seq Scan 通常意味着没用上索引;
●是否命中索引:关注 key / Index Scan / Index Only Scan,后者代表命中了覆盖索引;
●预估与实际行数:rows 估计值和 actual rows 差距过大,往往意味着统计信息过期,需要 ANALYZE;
●是否有额外开销:Using filesort、Using temporary 提示存在排序或临时表,可能需要补索引。
结语
这四条原则本质上都指向同一个思想:让数据库少做无用功 —— 少读列、少连接行、少重复计算、少扫描。它们不依赖任何高级特性,却能在真实业务里带来数量级的性能提升。
当然,优化没有银弹。是否生效、生效多少,最终都要以你所用数据库的执行计划为准。建议养成写完 SQL 顺手 EXPLAIN 一下的习惯,用数据驱动优化,而不是凭直觉。
如果这篇文章对你有帮助,欢迎点赞、收藏、评论交流你踩过的 SQL 性能坑。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论