一句话结论
MySQL 索引优化围绕一个核心目标:减少回表。手段是覆盖索引、联合索引、索引下推,工具是 EXPLAIN。
覆盖索引
一句话: 查询所需字段全部能从索引中获得,不需要回表查聚簇索引。
-- 索引: (user_id, status, created_at)
SELECT user_id, status FROM orders WHERE user_id = 100;
-- ✅ 覆盖索引:所有字段都在索引中,Extra: Using index
SELECT user_id, status, amount FROM orders WHERE user_id = 100;
-- ❌ 需要回表:amount 不在索引中
EXPLAIN Extra 字段显示
Using index表示覆盖索引。
联合索引与最左前缀
一句话: 联合索引 (a, b, c) 按 a → b → c 顺序排序。查询条件必须从最左边开始匹配,跳过 a 只用 b 不走索引。
INDEX idx_abc (a, b, c)
WHERE a = 1 -- ✅ 走索引(匹配 a)
WHERE a = 1 AND b = 2 -- ✅ 走索引(匹配 a, b)
WHERE a = 1 AND c = 3 -- ✅ 走索引(匹配 a,c 部分可用但 b 断了)
WHERE b = 2 -- ❌ 不走索引(跳过 a)
WHERE a = 1 AND b > 2 AND c = 3 -- ✅ 走 a,b(范围后的 c 不走)
索引下推(ICP)
一句话: MySQL 5.6+ 把 WHERE 条件下推到存储引擎层过滤,减少回表次数。
INDEX idx (name, age)
SELECT * FROM users WHERE name LIKE '张%' AND age = 20;
-- 无 ICP: 存储引擎返回所有 name LIKE '张%' 的行 → Server 层过滤 age=20
-- 有 ICP: 存储引擎直接过滤 name LIKE '张%' AND age=20 → 减少返回的行数
EXPLAIN 核心字段
索引失效场景
项目中的应用
在 项目三-分布式电商交易系统 中,订单查询的索引设计:
-- 高频查询:用户查自己订单
SELECT id, status, amount, created_at
FROM orders
WHERE user_id = ? AND status = ?
ORDER BY created_at DESC;
-- 索引: (user_id, status, created_at)
-- 覆盖索引 + 最左前缀 + 避免 filesort
速记
覆盖索引 = 不回表(Extra:Using index)。联合索引 = 最左前缀匹配。ICP = 存储引擎层过滤减少回表。EXPLAIN 看 type/key/rows/Extra。对索引列用函数/左模糊/OR 导致失效。
深入原理
一、覆盖索引深入
原理与 EXLPAIN 特征
覆盖索引不是一种新的索引类型,而是查询完全通过索引获取数据、不回表的一种优化状态。
-- 索引: INDEX idx_cover (user_id, status, created_at)
-- 查询 1: 覆盖索引
SELECT user_id, status, created_at FROM orders WHERE user_id = 100;
-- EXPLAIN Extra: Using index
-- 原理:user_id, status, created_at 都在 idx_cover 的叶子节点中
-- 存储引擎扫描 idx_cover 即可直接返回结果,不需要跳转到聚簇索引
-- 查询 2: 非覆盖索引(需要回表)
SELECT user_id, status, created_at, amount FROM orders WHERE user_id = 100;
-- EXPLAIN Extra: NULL 或 Using where
-- 原理:amount 不在 idx_cover 中,必须回表取
覆盖索引的代价
优点:
✅ 减少 I/O(不回表)
✅ 减少 Buffer Pool 污染(只读索引页,不污染数据页)
✅ 对分页优化友好
代价:
⚠️ 索引变大(更多列 → 更大的索引文件 → 更多磁盘空间)
⚠️ 写入变慢(每次 UPDATE/INSERT 需要维护更多索引列)
⚠️ 内存占用(Buffer Pool 中索引占更多空间 → 数据页缓存变少)
设计建议: 不要盲目追求覆盖索引。只在高频查询且返回列固定的场景使用。用 SHOW INDEX 或 information_schema.INNODB_SYS_INDEXES 监控索引大小。
二、联合索引与最左前缀(深入分析)
key_len 解读实战
-- 表 users: id INT PK, name VARCHAR(50), age INT, city VARCHAR(50)
-- 索引: INDEX idx_name_age_city (name, age, city)
-- 字符集 utf8mb4(VARCHAR 1字符=4字节,长度前缀=2字节,NULL标志=1字节)
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 25 AND city = '北京';
-- key_len: 203 + 5 + 203 = 411
-- 解读:name 占 50*4+2+1=203, age 占 4+1=5, city 占 50*4+2+1=203
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 25;
-- key_len: 208(203+5)
-- 解读:只有 name 和 age 被使用,city 未使用
EXPLAIN SELECT * FROM users WHERE name = '张三' AND city = '北京';
-- key_len: 203
-- 解读:只有 name 被使用——age 被跳过,city 不能走索引查找(只能走 ICP 过滤)
排序、分组与联合索引
-- 索引: INDEX idx_a_b (a, b)
-- ✅ 不需要 filesort 的查询:
WHERE a = 1 ORDER BY b; -- a 等值后,b 在索引中有序
ORDER BY a, b; -- 索引有序
-- ❌ 需要 filesort 的查询:
WHERE a = 1 ORDER BY b DESC; -- a 升序,b 降序——索引都是升序,不同向
WHERE a > 1 ORDER BY b; -- a 是范围,b 无序
ORDER BY b; -- 跳过 a,b 无序
-- GROUP BY 同理:索引有序 = 分组有序,不需要临时表(Using temporary)
索引条件下推(ICP)vs 索引覆盖(Covering)vs 范围优化(Range)
同一查询中可能同时出现三种优化:
SELECT user_id, status FROM orders
WHERE user_id > 100 AND status = 'paid';
-- 索引: INDEX idx_uid_status (user_id, status)
执行情况:
type: range(范围扫描)
Extra: Using index condition; Using where
1. range: 因为 user_id > 100 是范围 → type=range
2. ICP: status='paid' 在存储引擎层(索引内部)过滤 → Using index condition
3. 覆盖索引未完全生效(status 虽在索引中,但 user_id 是范围查询 →
部分需要 where 层过滤) → Using where
三、前缀索引
对于很长的 VARCHAR/TEXT 列,可以只索引前缀字符:
-- 对 email 列前 8 个字符建索引
CREATE INDEX idx_email_prefix ON users (email(8));
-- 前缀长短的选择:
SELECT
COUNT(DISTINCT LEFT(email, 4)) / COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(email, 6)) / COUNT(*) AS sel6,
COUNT(DISTINCT LEFT(email, 8)) / COUNT(*) AS sel8,
COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS sel10
FROM users;
-- 选择性越接近 1 越好(但前缀越长索引越大)
-- 通常取选择性达到 ~0.95 时即可
前缀索引的限制:
不能用于
ORDER BY和GROUP BY不能用于覆盖索引(需要完整值才能覆盖)
COUNT(DISTINCT)等依赖完整值
四、索引合并(Index Merge)
MySQL 5.6+ 支持索引合并——一条查询可以使用多个索引,然后合并结果:
-- 表 orders: INDEX idx_user_id (user_id), INDEX idx_status (status)
-- 查询使用两个索引
SELECT * FROM orders WHERE user_id = 100 OR status = 'paid';
-- Extra: Using union(idx_user_id, idx_status); Using where
-- 三种合并方式:
-- 1. Union(OR 条件):分别走两个索引,结果集做并集
-- 2. Intersection(AND 条件):分别走两个索引,结果集做交集
-- 3. Sort-Union(OR 条件但未排序):分别走索引 → 排序 → 合并
索引合并的陷阱: 如果触发 index_merge,说明单索引不够好——可能应该建联合索引。
五、索引统计与优化器陷阱
统计信息不准确导致的问题
-- 现象:某个查询平时走索引很快,某天突然变慢
-- 原因:统计数据变化 → 优化器换执行计划
-- 排查:
SHOW INDEX FROM orders WHERE Key_name = 'idx_user_status';
-- 对比 Cardinality 是否合理
-- 修复:
ANALYZE TABLE orders; -- 刷新统计信息
优化器有时会"犯错"
-- ORDER BY + LIMIT 场景:优化器可能误判
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- 优化器可能认为全表扫描 + 排序 优于 索引扫描(如果 created_at 有索引)
-- 但如果是覆盖索引,优化器一般会正确选择索引
-- 大表 JOIN 时:优化器可能选错驱动表
-- 用 STRAIGHT_JOIN 强制指定驱动表顺序
SELECT STRAIGHT_JOIN * FROM orders o
INNER JOIN order_items i ON o.id = i.order_id
WHERE o.user_id = 100;
六、索引设计实战案例集
案例 1:多条件搜索页面
-- 后台订单搜索:5 个可选筛选条件
SELECT * FROM orders
WHERE (user_id = ? OR ? IS NULL)
AND (status = ? OR ? IS NULL)
AND (created_at >= ? OR ? IS NULL)
AND (amount >= ? OR ? IS NULL)
ORDER BY created_at DESC
LIMIT ?, 20;
-- 问题:可选条件多,组合索引无法覆盖所有情况
-- 方案:
-- 1. 分析访问日志,找到最常见的条件组合
-- 2. 为 top-3 组合建联合索引
-- 3. 配合 ICP 和条件判断优化(动态 SQL 去掉 IS NULL 判断)
-- 更优的 SQL(应用层动态拼接):
SELECT * FROM orders
WHERE user_id = ?
AND status = ?
AND created_at >= ?
ORDER BY created_at DESC
LIMIT ?, 20;
-- 索引: INDEX idx_ust (user_id, status, created_at)
案例 2:排行榜/热门榜单
-- 需求:按金额排序 TOP 100
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
AND status = 1
GROUP BY user_id
ORDER BY total DESC
LIMIT 100;
-- 分析:
-- 1. WHERE 过滤:created_at(范围) + status(等值) → 约 10 万行
-- 2. GROUP BY user_id → 需要临时表 + 排序
-- 3. ORDER BY total DESC → 基于聚合结果排序,无法走索引
-- 索引策略:
-- INDEX idx_time_status (created_at, status) → 加速 WHERE 过滤
-- 但对于 GROUP BY + ORDER BY 聚合,纯 SQL 无法完全优化
-- 架构级优化:
-- 1. Redis ZSet 维护排行榜(异步更新)
-- 2. 定时任务预计算 → 汇总表
-- 3. Elasticsearch 做聚合分析
案例 3:模糊搜索
-- 电商商品搜索:按名称模糊匹配
SELECT * FROM products WHERE name LIKE '%手机%';
-- ❌ 全表扫描
-- 方案 1:MySQL 全文索引(FULLTEXT + ngram)
ALTER TABLE products ADD FULLTEXT INDEX ft_name (name) WITH PARSER ngram;
SELECT * FROM products WHERE MATCH(name) AGAINST('手机' IN BOOLEAN MODE);
-- 方案 2:Elasticsearch(推荐)
-- 支持分词、相关性排序、高亮、聚合
-- 通过 Canal/Binlog 同步数据
-- 方案 3:简单前缀搜索(右模糊)
SELECT * FROM products WHERE name LIKE '华为%';
-- ✅ 走 B+ 树索引
案例 4:UUID 主键的索引设计
-- 如果用 UUID 做主键
CREATE TABLE orders (
id VARCHAR(36) PRIMARY KEY, -- UUID
user_id BIGINT NOT NULL,
order_no VARCHAR(32) UNIQUE,
created_at DATETIME NOT NULL
);
-- 问题:
-- 1. 聚簇索引页分裂严重(UUID 随机插入)
-- 2. 二级索引叶子存 UUID(36B)不仅大而且乱序
-- 3. 所有通过二级索引的查询都经历 UUID 回表
-- 优化:
-- 1. 换自增主键(最优)
-- 2. 如果必须用 UUID:用有序 UUID(UUID v7、ULID)
-- 3. 用自增主键 + UUID 做业务唯一键
-- 如果同时需要全局唯一 + 有序:
-- Snowflake 雪花算法(64 位 Long)
-- 优势:数字比较快、趋势递增(减少页分裂)、全局唯一
七、索引监控与运维
-- 1. 查看未使用的索引(MySQL 8.0+)
SELECT * FROM sys.schema_unused_indexes;
-- 删除无用索引(减少写入开销)
-- 2. 查看冗余索引
SELECT * FROM sys.schema_redundant_indexes;
-- 例如:有 INDEX(a) 和 INDEX(a,b) → 前者可删除((a,b) 可替代 (a))
-- 3. 查看索引使用统计
SELECT
object_schema, object_name, index_name,
count_read, count_write,
rows_selected, rows_inserted, rows_updated, rows_deleted
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'ecommerce'
ORDER BY count_read DESC;
-- 4. 索引大小
SELECT
database_name, table_name, index_name,
ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) AS size_mb
FROM mysql.innodb_index_stats
WHERE database_name = 'ecommerce' AND stat_name = 'size'
ORDER BY stat_value DESC;
深度面试追问
Q1:MySQL 一张表的索引数量有没有上限?太多索引有什么问题?
30 秒回答: 技术上 InnoDB 表最多 64 个二级索引。但实际工程中建议控制在 5 个以内。太多索引的问题:1) 写入性能下降(INSERT/UPDATE/DELETE 需维护所有索引);2) 占用更多磁盘和 Buffer Pool;3) 优化器选择时开销增大。
深入回答:
索引过多的具体影响:
写入性能:
一次 INSERT 操作 = 1 次聚簇索引写入 + N 次二级索引写入
5 个索引 → INSERT 慢 5 倍
UPDATE 如果修改了索引列 → 同样慢
DELETE 需要标记删除所有索引中的记录
内存占用:
Buffer Pool 通常占内存的 75-80%
索引页占得越多 → 数据页缓存越少 → 回表命中磁盘更多
优化器开销:
5 个索引 → 优化器要评估 5 条执行路径
每个路径都需计算代价(统计信息采样、I/O 估算)
查询编译时间增加
追问 1: 怎么判断一个索引是否可以安全删除?
MySQL 8.0 用
sys.schema_unused_indexes查看从未被使用的索引(需要performance_schema开启)。注意:统计可能有误差(重启后清零)。确认无用后,在低峰期ALTER TABLE ... DROP INDEX。删除前建议先在从库确认。
追问 2: 什么场景确实需要很多索引?
OLAP 分析型场景(读写比高,数据批量导入后大量查询,很少更新)。或者分库分表后的单库表(查询模式固定,需要多维度索引)。但即使在这些场景,超过 10 个索引也要仔细审视必要性。
Q2:WHERE a = 1 OR b = 2 怎么优化?
30 秒回答: 1) 用 UNION ALL 改写(SELECT * WHERE a=1 UNION ALL SELECT * WHERE b=2 AND a<>1),让每半部分各自走自己的索引;2) 如果 MySQL 已走 index_merge,检查合并效率,必要时还是用 UNION;3) 建联合索引 (a, b),前提是 a 和 b 总是一起出现。
深入回答:
-- 原始 SQL(可能全表扫描)
SELECT * FROM orders WHERE user_id = 100 OR status = 'paid';
-- 方案 1: UNION ALL(推荐,稳定高效)
SELECT * FROM orders WHERE user_id = 100
UNION ALL
SELECT * FROM orders WHERE status = 'paid' AND user_id <> 100;
-- 优点:各自走各自的索引,无合并开销
-- 注意:UNION ALL 不去重(如不关心重复行),比 UNION 快
-- 方案 2: 依赖 index_merge(不太稳定)
SELECT * FROM orders WHERE user_id = 100 OR status = 'paid';
-- MySQL 5.6+ 可能自动走 index_merge
-- 但不保证(取决于统计信息、数据分布)
-- 方案 3: 联合索引(最高效,但需要预知查询模式)
CREATE INDEX idx_uid_status ON orders (user_id, status);
SELECT * FROM orders WHERE user_id = 100 OR (status = 'paid' AND user_id IN (...));
-- 注意:联合索引对跨列的 OR 效果有限
追问 1: 为什么 index_merge 不稳定?
index_merge 要求两个索引同时可用且合并开销小于全表扫描。当两个条件的命中行数差异大时(如 user_id=100 命中 10 行,status='paid' 命中 100 万行),合并效率并不好。优化器有时会选择放弃 index_merge 而走全表。
追问 2: UNION 和 UNION ALL 怎么选?
绝大多数场景用
UNION ALL(不去重,快)。只有需要去重时才用UNION。在 OR → UNION 的场景中,如果 WHERE 条件能保证两边的结果集不重叠(如 AND b<>1),直接 UNION ALL。
Q3:分页查询为什么越往后越慢?怎么优化?
30 秒回答: 分页靠后时(如 LIMIT 100000, 20),MySQL 需扫描并丢弃前 100000 行,扫描行数随偏移量线性增长。优化方案:游标分页(基于上一页最后记录的条件查询)、延迟关联(先在索引上分页再回表)、限制最大页码。
深入回答:
-- 原始分页
SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 100000, 20;
-- 扫描 100020 行,丢弃 100000 行
-- 优化 1: 游标分页(推荐)
-- 第一页
SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC, id DESC LIMIT 20;
-- 记住最后一行的 (created_at, id) = ('2026-05-15', 98765)
-- 第二页
SELECT * FROM orders
WHERE user_id = 100
AND (created_at < '2026-05-15' OR (created_at = '2026-05-15' AND id < 98765))
ORDER BY created_at DESC, id DESC LIMIT 20;
-- 需要索引 (user_id, created_at, id)
-- 优化 2: 延迟关联
SELECT * FROM orders o
INNER JOIN (
SELECT id FROM orders WHERE user_id = 100
ORDER BY created_at DESC LIMIT 100000, 20
) t ON o.id = t.id;
-- 子查询在覆盖索引 (user_id, created_at, id) 上执行,不回表
-- 拿到 20 个 id 后再 JOIN 回表取完整行
-- 优化 3: 限制最大页码(业务层)
-- 不允许翻到 1000 页之后,强制用户缩小筛选条件
追问 1: 游标分页的缺点是什么?
不支持跳页(不能直接跳到第 N 页);2) 如果游标列不是唯一值(如 created_at 可能重复),需要组合主键确保唯一性;3) 不支持并行分页(如同时加载第 1 页和第 5 页)。适合 C 端滚动加载(无限滚动),不适合 B 端需要跳页的报表。
追问 2: 延迟关联和游标分页哪个更好?
游标分页性能更好(每次都是固定范围查找),但灵活性差。延迟关联能兼容跳页,但
LIMIT N, M中的 N 仍然需要遍历 N 行(在覆盖索引上遍历比回表快)。如果 N > 10 万,游标分页仍是更优方案。
Q4:如何诊断慢查询并设计合适的索引?
30 秒回答: 1) 开慢查询日志 + long_query_time;2) EXPLAIN 分析 type/key/rows/Extra;3) 根据 WHERE + ORDER BY + GROUP BY 联合设计索引;4) 用 optimizer_trace 分析优化器决策过程。
深入回答(完整诊断流程):
Step 1: 开启慢查询监控
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.1; -- 100ms 以上的查询记录
SET GLOBAL log_queries_not_using_indexes = ON; -- 未走索引也记录
Step 2: 分析慢查询日志
-- 使用 pt-query-digest 分析
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
Step 3: 找到 TOP-N 慢查询,逐条 EXPLAIN
EXPLAIN FORMAT=JSON SELECT ...;
Step 4: 检查是否可以使用覆盖索引、联合索引优化
Step 5: 验证索引效果(EXPLAIN 对比)
项目三实践:
-- 慢查询:卖家后台订单列表
SELECT * FROM orders
WHERE shop_id = 12345
AND created_at >= '2026-06-01'
AND status IN (1, 2)
ORDER BY created_at DESC
LIMIT 0, 20;
-- 当前执行计划:type=ALL(全表扫描 5000 万行)
-- 问题:无联合索引
-- 添加索引:
ALTER TABLE orders ADD INDEX idx_shop_time_status (shop_id, created_at, status);
-- 添加后:type=range,rows≈500,查询从 3s 降至 50ms
追问 1: EXPLAIN FORMAT=JSON 比普通 EXPLAIN 多什么信息?
详细代价估计(
query_cost);2) 可能的执行路径及每条的代价;3)used_columns— 实际使用了哪些列;4)attached_condition— WHERE 条件在哪里被评估;5)optimizer_trace可以看到为什么优化器选了这条路径而非另一条。
追问 2: optimizer_trace 怎么用?
SET optimizer_trace='enabled=on'; SELECT ...; SELECT * FROM information_schema.OPTIMIZER_TRACE\G;输出 JSON 详述优化器每一步——索引选择、访问类型决策、代价计算、排序优化等。用于深度排查"为什么优化器不选这个索引"。
Q5: JOIN 优化实战 — 小表驱动大表 + 被驱动表索引
30 秒回答: JOIN 优化的核心原则:1) 小表驱动大表(优化器自动选择,但可用 STRAIGHT_JOIN 强制);2) 被驱动表的 JOIN 列必须建索引(否则全表扫描 N 次);3) 避免超过 3 表 JOIN(优化器决策空间爆炸)。
深入回答(实战案例分析):
-- 场景:查询用户及其订单数
-- users: 10 万行, orders: 500 万行
-- ❌ 差写法:
SELECT u.name, COUNT(o.id) AS order_cnt
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
-- 执行计划: 优化器选 users 驱动(10 万行)
-- users 每行去 orders 的 user_id 索引命中 50 行
-- 10 万 × 50 次索引查找 + GROUP BY(需要临时表)
-- ✅ 好写法:先聚合子查询减少 JOIN 数据量
SELECT u.name, IFNULL(t.cnt, 0) AS order_cnt
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
) t ON u.id = t.user_id;
-- 子查询在 orders.user_id 索引上聚合(500 万行 1 次)
-- JOIN 时只有去重后的 user_id(假设 30 万活跃用户)
-- 而不是原始 500 万行
-- ✅ 更好的写法:如果大多数用户都有订单
SELECT u.name, o.order_cnt
FROM users u
INNER JOIN (
SELECT user_id, COUNT(*) AS order_cnt
FROM orders
GROUP BY user_id
HAVING order_cnt > 0
) o ON u.id = o.user_id;
-- INNER JOIN 进一步减少结果集
JOIN 优化检查清单:
□ 被驱动表的 JOIN 列有索引 (type = ref 或 eq_ref)
□ 用小表驱动大表(能用 INNER JOIN 不用 LEFT JOIN)
□ 子查询替代多表 JOIN(减少中间结果集)
□ WHERE 条件在 JOIN 前过滤(写在子查询中而非外层)
□ 避免在 JOIN 的 ON 条件中使用函数
□ JOIN 不超过 3 表(超过则考虑反范式或应用层合并)
□ 大表 JOIN 先用覆盖索引取主键,再回表
追问 1:STRAIGHT_JOIN 和普通 JOIN 有什么区别?
STRAIGHT_JOIN强制按 SQL 中表出现的顺序作为驱动顺序(左边的表驱动右边的表)。普通 JOIN 由优化器自动选择驱动表。一般不推荐使用 STRAIGHT_JOIN,除非:1) 优化器明显选错了驱动表(统计信息过时);2) 压测确认手动指定比自动选择好。滥用 STRAIGHT_JOIN 会让优化器失去灵活性。
追问 2:MySQL 8.0 的 Hash Join 和 BNL 有什么区别?
BNL(Block Nested-Loop Join, MySQL 5.6/5.7):将驱动表数据分批读入 Join Buffer,每次和被驱动表全表匹配。O(N×M)。Hash Join(MySQL 8.0.18+):将驱动表数据构建 Hash 表,被驱动表每行探测 Hash 表。O(N+M)。Hash Join 比 BNL 快,但内存需求更大(join_buffer_size)。如果 Hash 表超过 join_buffer_size,会分批写入磁盘。
Q6: 索引条件下推(ICP)和覆盖索引可以同时生效吗?
30 秒回答: 可以!它们是不同层次的优化。覆盖索引是"查询列全在索引中,不回表"(Extra: Using index)。ICP 是"WHERE 条件在存储引擎层用索引列过滤"(Extra: Using index condition)。两者互补——ICP 减少回表次数,覆盖索引直接消除回表。
深入回答:
-- 索引: INDEX idx_a_b_c (a, b, c)
-- 场景 1: 覆盖索引 + ICP 同时生效
SELECT a, b, c FROM t WHERE a > 1 AND c = 3;
-- type: range (走 a)
-- Extra: Using index condition; Using where
-- 没有 Using index! 因为 a > 1 是范围,且 SELECT 列虽然全在索引中
-- 但范围查询后索引的有序性被打破,Server 层仍需二次过滤
-- 场景 2: 纯覆盖索引(最优)
SELECT a, b, c FROM t WHERE a = 1 AND b = 2 AND c = 3;
-- type: ref (走 a, b, c)
-- Extra: Using index
-- 查看 key_len 确认三列都用到了
-- 场景 3: ICP 但不覆盖
SELECT * FROM t WHERE a = 1 AND c = 3;
-- type: ref (走 a)
-- Extra: Using index condition
-- a = 1 走索引,c = 3 在 ICP 中过滤
-- 但 SELECT * 需要回表(Using index condition 不是 Using index)
三个 Extra 的关系:
Using index → 发生在索引树上,不需要回表(最优)
Using index condition → 发生在索引树上,减少了回表次数(好)
Using where → 发生在 Server 层,需要回表后才能判断(一般)
优先级理解:
Using index > Using index condition > Using where
但三者可以不同组合出现!
追问:Extra 中出现 "Using index; Using where" 是什么意思?
这意味着索引被用来查找数据(覆盖索引),但 WHERE 条件中有些列不完全被索引覆盖,需要在 Server 层二次过滤。例如:
INDEX (a, b),查询SELECT a, b FROM t WHERE a = 1 AND b > 2 AND c = 3。a 和 b 走覆盖索引(Using index),但 c = 3 的过滤在 Server 层完成(Using where)。
Q7: 索引统计信息(Cardinality)对优化器选择的影响
30 秒回答: Cardinality 是索引中唯一值的估算数量。优化器用它估算"走这个索引能过滤多少行"。Cardinality 不准 → 优化器选错索引 → 性能问题。用 ANALYZE TABLE 刷新统计信息。
深入回答:
Cardinality 的估算方式:
InnoDB 不是 COUNT DISTINCT(太慢!),而是随机采样:
随机选 N 个索引页 → 统计每页中不同值的个数 → 乘以总页数
采样数量由 innodb_stats_persistent_sample_pages 控制(默认 20)
如果采样不准:
- 太小(10 个页):估算误差大
- 太大(1000 个页):ANALYZE 本身变慢
建议:
数据量大且分布不均 → innodb_stats_persistent_sample_pages = 100-200
手动设置统计信息(极端情况):
SET PERSIST innodb_stats_persistent_sample_pages = 200;
ANALYZE TABLE orders;
追问:为什么有时 EXPLAIN 显示用了一个"看起来不是最好"的索引?
Cardinality 不准确 → 优化器误判过滤性;2) 覆盖索引 > 过滤性——如果一个索引能覆盖查询(Using index),优化器可能选它即使过滤性不如另一个;3) 排序优先级——ORDER BY 的列如果在一个索引中天然有序,优化器可能选它避免 filesort;4) 代价模型的限制——优化器的代价模型是估算,不可能 100% 准确。
索引优化速查卡
┌─────────────────────────────────────────────────────────────┐
│ 索引优化核心口诀 │
│ │
│ 建索引: WHERE 最左列, ORDER BY 中间列, SELECT 覆盖列 │
│ 看计划: type 从好到差, Extra 避免 filesort 和 temporary │
│ 慢查询: 定位 → EXPLAIN → 索引/SQL改写 → 验证 │
│ 深分页: 游标 > 延迟关联 > 限制深度 │
│ JOIN: 小表驱大表, 被驱动表必建索引 │
│ │
└─────────────────────────────────────────────────────────────┘