SQL语句优化是后端开发中无法绕开的话题。当业务数据量从几万行膨胀到上千万行,原本在测试环境秒回的查询可能在生产环境直接拖垮数据库连接池。优化的本质不是改写几句语法,而是理解数据库引擎如何存取数据、如何选择合适的访问路径。

从执行计划看数据库怎么跑你的SQL
很多开发者调优时习惯凭直觉改语句,但真正科学的起点是看执行计划。在MySQL中只需在查询前加上EXPLAIN关键字,就能得到优化器对这条语句的执行预估。计划中的type列显示了访问类型,从最优的system、const到最差的ALL(全表扫描),差距可达几个数量级。rows列代表引擎认为需要扫描的行数,如果这个值接近全表总量,基本可以确定没走索引。
举个例子,一张用户表有索引在age字段,但查询写成WHERE YEAR(create_time)=2023,由于对字段套了函数,优化器无法利用create_time上的索引,只能全表逐行计算。改用范围查询create_time >= '2023-01-01' AND create_time < '2024-01-01'后,执行计划的type会从ALL变为range,扫描行数骤减。
除了基础列,还要关注Extra里的Using filesort和Using temporary。前者意味着排序没用到索引,后者常出现在GROUP BY或DISTINCT缺乏合适索引时。一旦出现这两个提示,在大数据集上就会有明显的性能悬崖。通过联合索引把排序字段纳入,往往能直接消除文件排序。
索引设计中的常见误区与正确姿势
索引不是越多越好。每个索引都会占用存储空间,并在增删改时带来维护开销。最常见误区是为每列单独建索引,然后写WHERE a=1 AND b=2,以为两个单列索引能被同时使用。实际上多数存储引擎一次查询通常只选一个索引,这时复合索引(a,b)才是正解,且要遵循最左前缀原则:查询条件必须从索引第一列开始连续匹配。
另一个容易被忽视的点是索引覆盖。如果查询只需要id、status两列,而这两列恰好都在复合索引里,引擎无需回表取数据,直接在索引树读完就返回,执行计划Extra显示Using index。这比SELECT *再回主键查找要快得多。因此写查询时应明确列出所需字段,而不是图省事用星号。
对于文本字段的模糊查询,前置通配符LIKE '%abc'必然导致索引失效,因为B+树无法从中间匹配。若业务允许,尽量用LIKE 'abc%'保留左前缀;实在需要全文检索,应考虑倒排索引或专用搜索引擎。以下示例展示了一个合理的复合索引建立方式:
-- 订单表常按用户和状态筛选并按时间倒序 CREATE INDEX idx_user_status_time ON orders (user_id, status, create_time DESC); -- 可命中索引的查询 SELECT id, amount, create_time FROM orders WHERE user_id = 1001 AND status = 1 ORDER BY create_time DESC LIMIT 20;
改写语句结构带来的实质性提升
除了索引,语句自身的结构也大有可为。分页场景里LIMIT 100000, 20的写法会让引擎先排序再丢弃前十万行,越翻页越慢。改用游标分页,以最后一页的最大ID为基准:WHERE id > 上次最大ID ORDER BY id LIMIT 20,把全量排序变成索引范围扫描,性能直线上升。
子查询与连接的选择也需具体分析。早期MySQL版本对子查询优化较弱,IN (SELECT ...)可能被改写成多次查询;现代版本虽已改善,但在某些统计场景下,左连接配合聚合仍比嵌套子查询更可控。如下代码对比了两种写法:
-- 子查询写法 SELECT u.name, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS cnt FROM users u; -- 连接与聚合写法 SELECT u.name, COUNT(o.id) AS cnt FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.name;
此外,避免在WHERE中对字段做隐式类型转换,例如字段是字符串却用数字比较,也会让索引失效。养成用EXPLAIN验证的习惯,结合慢查询日志定位高频耗时语句,才能把优化工作做得精准而不是盲目。当数据量进一步膨胀,还可考虑分库分表、读写分离,但那属于架构层手段,语句与索引优化永远是性价比最高的第一道防线。