文章总结: 本文总结了提升SQL性能的四个核心写法:避免SELECT*以利用覆盖索引减少回表;JOIN前用CTE下推谓词缩小数据量;用CTE替代重复子查询避免逐行计算;确保过滤条件命中索引列,避开函数和左模糊等导致索引失效的写法。建议通过EXPLAIN执行计划验证优化效果。 综合评分: 86 文章分类: 安全开发,实战经验
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 性能坑。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:哪吒网络安全 钟智强 钟智强《SQL 查询优化实战:4 个让查询快 10 倍的写法》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论