大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
聊到索引优化,很多人知道“建索引”,但不知道“怎么建才能让查询最快”。今天讲一个进阶却非常实用的概念:覆盖索引。用好它,可以让你的查询直接从索引拿到数据,连表都不需要回,性能直接起飞。
上周讲了窗口函数和CTE的性能对比,有读者留言说那些优化确实好,但自己遇到的慢查询大多还是索引没用对。没错,索引是SQL性能的基石。覆盖索引是索引优化里“性价比”最高的技巧之一——不增加额外存储成本,不改SQL逻辑,只调整索引字段顺序,就能减少一半的I/O。下面我们从原理开始,一步步说清楚。
覆盖索引是什么?
先回顾一下InnoDB索引的结构:InnoDB使用B+树索引,对于二级索引(非主键索引),叶子节点存储的是索引列的值 + 主键值。当执行一个查询时,如果索引里已经包含了查询需要的所有列,InnoDB就可以直接从索引树的叶子节点拿到数据,不需要再根据主键回主键索引查整行。这就叫覆盖索引。
举个具体例子:表orders有列order_id(主键)、user_id、order_amount、order_date。执行查询SELECT user_id, order_amount FROM orders WHERE user_id = 12345。
- 如果只在
user_id上建索引:InnoDB先在user_id索引上找到所有匹配的记录,得到主键order_id列表;然后根据每个order_id去主键索引回表,取出order_amount。这需要「索引扫描行数 + 回表次数」次I/O。 - 如果建复合索引
(user_id, order_amount):InnoDB在复合索引上找到user_id=12345的叶子节点,叶子节点里已经包含了user_id和order_amount,直接返回,不需要回表。只需要「索引扫描行数」次I/O。
性能提升通常在2~5倍,回表成本越高,提升越明显。
如何判断你的查询是否用上了覆盖索引?
用EXPLAIN看执行计划,Extra列出现Using index时,说明用上了覆盖索引。注意:Using index不是索引类型,而是一种执行方式,表示“不需要回表”。如果出现Using index condition,说明用了索引下推(ICP),但仍需回表。如果出现Using where,说明没有用覆盖索引。
掌握这个判断方法之后,你就可以主动检查自己的慢查询,看看有没有机会通过调整索引来消除回表。
深分页场景下的覆盖索引应用
日常开发中经常遇到这样的分页查询:SELECT id, name, age FROM users ORDER BY id LIMIT 100000, 10。直接这样写,MySQL会先扫描前100010行(包括回表),然后丢弃前100000行,只返回最后10行。当偏移量很大时,这个操作非常慢。
优化的思路是:先用覆盖索引快速定位到要取的行的主键范围,再回表取完整数据。具体写法:
SELECT u.id, u.name, u.age FROM users u
JOIN (SELECT id FROM users ORDER BY id LIMIT 100000, 10) tmp ON u.id = tmp.id;
内层子查询SELECT id FROM users ORDER BY id LIMIT 100000, 10只用到了主键id,主键索引天然就是覆盖索引,执行非常快。外层再根据这10个id回表取完整数据,只需要10次回表。这种方法称为“延迟关联”,性能提升非常明显。
使用覆盖索引需要注意什么
覆盖索引虽然好用,但也不是万能的,有几个地方要留心:
- 索引不是越宽越好:覆盖索引需要把查询中用到的列都放进索引,这会增加索引的存储空间。而且索引越“胖”,写入(INSERT/UPDATE/DELETE)时维护索引的成本就越高。只把查询中频繁出现的列放进去,不要贪多。
- 尽量避免
SELECT *:SELECT *基本不可能被覆盖,因为索引一般不会包含所有列(那样索引会巨大)。尽量只查询需要的字段,这不仅有利于覆盖索引,也能减少网络传输。 - 字符串列要小心:如果查询中包含了长文本字段(如
VARCHAR(255)),覆盖索引会变得很大,可能得不偿失。这种情况下可以权衡是否值得做覆盖。
总结
覆盖索引是SQL优化工具箱中最趁手的工具之一。它不改变业务逻辑,不增加额外的系统复杂度,只是让索引设计得更“聪明”。下次写查询时,先问问自己:这个查询需要的所有列,有没有可能全部放进一个索引里?如果能,性能提升会非常直观。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~