一句话结论

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 核心字段

字段

含义

关注

type

访问类型

最优: const > eq_ref > ref > range > index > ALL(全表)

key

使用的索引

空 = 没走索引

rows

扫描行数(估算)

越小越好

Extra

额外信息

Using index=覆盖 ✅ / Using filesort=排序未用索引 ❌ / Using temporary=临时表 ❌

索引失效场景

场景

原因

WHERE func(col) = val

对索引列用函数 → 无法使用索引

WHERE col LIKE '%abc'

左模糊 → 无法使用 B+ 树有序性

WHERE col != val

不等号 → 可能全表

WHERE a = 1 OR b = 2

不同列 OR → 可能走不了联合索引

隐式类型转换

WHERE phone = 13800000000 而 phone 是 VARCHAR

项目中的应用

在 项目三-分布式电商交易系统 中,订单查询的索引设计:

-- 高频查询:用户查自己订单
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: 游标分页的缺点是什么?

  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 多什么信息?

  1. 详细代价估计(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 显示用了一个"看起来不是最好"的索引?

  1. Cardinality 不准确 → 优化器误判过滤性;2) 覆盖索引 > 过滤性——如果一个索引能覆盖查询(Using index),优化器可能选它即使过滤性不如另一个;3) 排序优先级——ORDER BY 的列如果在一个索引中天然有序,优化器可能选它避免 filesort;4) 代价模型的限制——优化器的代价模型是估算,不可能 100% 准确。


索引优化速查卡

┌─────────────────────────────────────────────────────────────┐
│                    索引优化核心口诀                           │
│                                                              │
│  建索引: WHERE 最左列, ORDER BY 中间列, SELECT 覆盖列         │
│  看计划: type 从好到差, Extra 避免 filesort 和 temporary      │
│  慢查询: 定位 → EXPLAIN → 索引/SQL改写 → 验证                 │
│  深分页: 游标 > 延迟关联 > 限制深度                           │
│  JOIN:   小表驱大表, 被驱动表必建索引                         │
│                                                              │
└─────────────────────────────────────────────────────────────┘