<p>SELECT 会让索引覆盖失效,因为二级索引不存储所有字段(如TEXT、BLOB或未包含列),必须回表查聚簇索引;即使有联合索引,只要SELECT 包含索引未涵盖的列,就无法满足覆盖条件。</p>

为什么 SELECT * 会让索引覆盖失效
索引覆盖(Covering Index)成立的前提是:查询所需的所有字段,都能从索引的 B+ 树叶子节点直接拿到,无需回表查主键聚簇索引。SELECT * 几乎总会导致回表——因为二级索引不存所有列,尤其不存 TEXT、BLOB 或未包含在索引定义里的字段。
- 即使你对
user_id建了联合索引(user_id, name, email),SELECT * FROM users WHERE user_id = 123仍会回表,因为没包含created_at、status等其他列 - MySQL 的
EXPLAIN中,Extra列出现Using index才表示命中覆盖;若出现Using where; Using index且没有Using filesort或Using temporary,才较理想 - PostgreSQL 对覆盖索引更敏感,必须显式用
INCLUDE把非键列加进索引,否则哪怕只多选一个不在索引键里的字段,就立刻失效
JOIN 字段怎么建索引才能让覆盖生效
连接字段本身不是“被覆盖”的目标,而是驱动索引扫描和避免全表 JOIN 的关键。覆盖是否成立,取决于整个查询最终要输出哪些列 + 这些列是否全在某个可用索引里。
-
ON条件中的字段必须有索引,否则驱动表可能走全表扫描,覆盖无从谈起。例如LEFT JOIN orders o ON u.id = o.user_id,orders.user_id必须有索引 - 如果想让
SELECT u.name, o.amount走覆盖,最优解是:在users上建(id, name),在orders上建(user_id, amount)—— 注意顺序:user_id必须是索引最左前缀,否则无法用于 JOIN 匹配 - 复合索引中字段顺序不能错:
(amount, user_id)对WHERE user_id = ?或ON user_id = ?完全无效,MySQL 无法跳过最左列做范围查找
MySQL 8.0 vs PostgreSQL 的覆盖行为差异
两者对“覆盖”的实现机制不同,导致同样 SQL 在不同数据库下是否走覆盖、是否需要额外配置,结果可能相反。
- MySQL 依赖 B+ 树二级索引天然存储主键值,所以只要
SELECT列全在索引定义中(含隐式主键),就能覆盖;但ORDER BY或GROUP BY涉及的列也得在索引里,否则仍会临时表或文件排序 - PostgreSQL 从 11 开始支持
INCLUDE,语法是CREATE INDEX idx ON orders (user_id) INCLUDE (amount);这里user_id是索引键,amount只存叶子页不参与搜索逻辑——少了它,SELECT amount就必须回表 - MySQL 不支持
INCLUDE,想“只存不搜”的字段只能靠冗余建联合索引,但会增大索引体积;PostgreSQL 的INCLUDE更省空间,但写法稍啰嗦
容易被忽略的三个破坏覆盖的细节
很多优化停在“建了索引”就以为完事,实际几个微小写法会让覆盖彻底失效,且 EXPLAIN 未必一眼报错。
- 使用函数或表达式:比如
SELECT UPPER(name) FROM users WHERE id = 123,即使(id, name)有索引,UPPER()也会阻止覆盖(MySQL 8.0+ 对部分函数支持函数索引,但需显式创建) - 隐式类型转换:
WHERE user_id = '123'(user_id是INT)会触发类型转换,导致索引失效,自然也谈不上覆盖 - NULL 值处理:如果
SELECT列允许 NULL,而索引定义里对应字段没明确声明NOT NULL,某些旧版本 MySQL 可能拒绝走覆盖(尤其配合IS NULL查询时)
覆盖不是建完索引就自动开启的开关,它是查询语句、索引结构、数据类型、甚至字符集排序规则共同作用的结果。少一个条件匹配,就退回磁盘随机 IO。

















