优先用 join 替代三层以上子查询,避免 in+子查询,改用 inner join 或 exists;join 字段须加索引,组合索引按驱动表顺序设计;慎用函数、隐式转换和跨表 group by/order by;务必用 explain analyze 验证真实执行性能。

用 JOIN 替代多层 SELECT 嵌套,尤其是三层以上
深层子查询(比如 SELECT * FROM t1 WHERE id IN (SELECT id FROM (SELECT id FROM t2 WHERE ...)))会让优化器难以生成高效执行计划,MySQL 5.7+、PostgreSQL 12+ 都容易退化成临时表 + 文件排序。实际查 10 万行数据时,嵌套三层的子查询可能比等价 JOIN 慢 5–8 倍。
- 优先把
WHERE ... IN (SELECT ...)改成INNER JOIN或EXISTS(后者在子查询结果集小时更稳) - 避免在
ON条件里写函数,比如ON UPPER(t1.name) = UPPER(t2.name)——会跳过索引 - 如果必须用子查询,确保内层有明确
WHERE过滤且返回字段尽量少,别写SELECT *
给 JOIN 字段加索引,但注意组合索引顺序
没索引的 JOIN 字段(如 t1.user_id = t2.id)会导致全表扫描,尤其当 t2 是大表时,t1 每扫一行都触发一次 t2 全扫——这就是“嵌套循环爆炸”。但光加单列索引不够,组合条件得看驱动表顺序。
- 如果执行计划显示
t1是驱动表,t2是被驱动表,那t2上要建(id, status, created_at)这类覆盖索引,而非只建id -
EXPLAIN中type是ALL或index就得警惕;理想是ref或eq_ref - PostgreSQL 要留意
JOIN字段的数据类型是否严格一致,int4和int8隐式转换会丢索引
GROUP BY 和 ORDER BY 涉及多表时,避免跨表字段混用
比如 SELECT t1.name, COUNT(*) FROM t1 JOIN t2 ON t1.id = t2.t1_id GROUP BY t2.category ORDER BY t1.created_at DESC,这个 ORDER BY 用了非 GROUP BY 字段,MySQL 5.7 strict 模式直接报错,即使不报错也会强制使用临时表 + filesort。
- 要么把
t1.created_at加进GROUP BY(但语义可能变),要么改用窗口函数(如ROW_NUMBER() OVER (PARTITION BY t2.category ORDER BY t1.created_at DESC)) - PostgreSQL 对
SELECT列是否在GROUP BY中检查更松,但性能隐患一样存在——它可能选错分组算法 - 如果只是想取每组最新一条,别用
MAX(created_at)再连表查,改用DISTINCT ON(PG)或ROW_NUMBER()(通用)
用 EXPLAIN ANALYZE 看真实执行路径,别信 EXPLAIN 的预估
EXPLAIN 只做估算,而 EXPLAIN ANALYZE(PostgreSQL)或 EXPLAIN FORMAT=JSON + 执行后查 information_schema.PROFILING(MySQL)才能看到真实耗时分布。常看到“预估 100 行,实际扫描 20 万行”,问题就出在统计信息陈旧或隐式类型转换上。
- MySQL 执行完后立刻查
SHOW PROFILE FOR QUERY N,重点关注Copying to tmp table和Sorting result时间占比 - PostgreSQL 中如果
Actual Rows远大于Rows Removed by Filter,说明WHERE条件没走索引,或者索引选择性太差 - 更新统计信息:MySQL 用
ANALYZE TABLE,PG 用VACUUM ANALYZE,别等自动触发
复杂点在于,不同数据库对相同 SQL 的优化策略差异很大——MySQL 更依赖驱动表顺序,PostgreSQL 更吃统计信息质量,而 SQLite 根本不重排 JOIN 顺序。最容易被忽略的是:你以为的“小表驱动大表”,在真实数据分布下可能完全相反。










