覆盖索引要求查询所需所有字段均被同一索引完全包含,否则即使where条件命中索引,select * 或含text/blob等非索引列仍会强制回表,导致extra变为using where或using index condition,丧失覆盖优势。

子查询里 SELECT * 直接废掉覆盖索引
子查询本身不是“不能走索引”,而是它内部的 SELECT * 会把所有字段都拉出来,只要其中有一个字段不在索引里,整个子查询就无法享受覆盖索引——哪怕 WHERE 条件完全命中索引,也得回表。比如 SELECT * FROM inventory WHERE product_id = ? AND stock > 0,即使有联合索引 (product_id, stock),* 会强制读取 id、updated_at 等未建在索引中的列,Extra 就从 Using index 变成 Using where。
EXISTS 子查询比 IN 更容易触发覆盖扫描
EXISTS 只关心“是否存在匹配行”,优化器通常不会去取被驱动表的非条件字段,所以更容易命中覆盖索引。而 IN (SELECT product_id FROM inventory WHERE stock > 0) 中,子查询若没加 SELECT product_id 而是写成 SELECT *,就会拖累主表索引选择。
- ✅ 推荐写法:
SELECT * FROM products WHERE EXISTS (SELECT 1 FROM inventory WHERE inventory.product_id = products.id AND inventory.stock > 0)—— 子查询只返回常量1,不涉及任何字段提取 - ❌ 风险写法:
SELECT * FROM products WHERE id IN (SELECT id FROM inventory WHERE stock > 0)—— 如果inventory.id不在索引中(比如只有(stock)单列索引),子查询就得全表扫 - ⚠️ 注意:
EXISTS的子查询里也别写SELECT *,哪怕只是语法上合法,也可能干扰优化器对覆盖的判断
JOIN 场景下被驱动表字段要单独检查覆盖
多表 JOIN 时,覆盖索引是否生效,得按表逐个看 EXPLAIN 输出。驱动表(如 products)的索引是否覆盖,取决于它的 SELECT 字段 + WHERE 条件;被驱动表(如 inventory)是否覆盖,则只看它自己那行的 key 和 Extra —— 和主表选了哪些字段无关,但和它自己被 ON 或 WHERE 引用的字段强相关。
- 查
EXPLAIN FORMAT=TREE时,重点找-> Filtered: 100.00%下面有没有attached_condition涉及非索引列 - 用
SHOW INDEX FROM inventory确认索引列顺序,比如想让ON i.product_id = p.id AND i.stock > 0走覆盖,索引至少得是(product_id, stock),且SELECT里不能出现i.updated_at这类额外字段 - 如果被驱动表只需要一个字段(比如只取
i.stock),但索引里没有它,Using index condition可能出现,但依然不算真正覆盖——它只是用了 ICP,仍需回表取值
TEXT/BLOB 字段是覆盖索引的硬性终结者
只要子查询或 JOIN 表的 SELECT 列里出现 TEXT、BLOB 或 JSON 类型字段,MySQL 会直接放弃覆盖索引,因为这类字段不存于索引 B+ 树叶子节点,必须回表。这个限制不因索引是否包含该列而改变——它们根本就不能被索引存储。
- 常见陷阱:
SELECT p.*, i.description FROM products p JOIN inventory i ON ...,哪怕i.description是TEXT且你根本没在逻辑里用它,只要 SELECT 出来,覆盖就失效 - 临时排查法:把疑似字段注释掉,再跑一次
EXPLAIN,对比 Extra 是否重新出现Using index - 替代方案:业务层拆查,或用
SUBSTR(description, 1, 200)截取后建前缀索引(但注意前缀索引无法支持模糊匹配开头以外的场景)
覆盖索引是否生效,不看“有没有索引”,而看“SELECT 的每一个字段是否都在某一个索引里”。子查询和 JOIN 让这件事变得更隐蔽——你得分别盯住每张表的 SELECT 列、WHERE 条件、索引定义三者是否严丝合缝,漏掉任何一个字段,就等于主动放弃覆盖。










