index merge避免回表

一、使用index Merge的企业级场景

1-1、先讲一个生活化的比喻

想象你在一家10万人的互联网大厂当 HR,老板让你找两类人:

"找出所有 2019年之前入职 且 职级是P8 的员工"

公司没有全能花名册,只有几本索引卡片:

  • 卡片A:按"入职年份"排序,记录了每个人对应的工号

  • 卡片B:按"职级"排序,记录了每个人对应的工号

没有 Index Merge 时(全表扫描)

你只能抱着全体员工档案柜(数据表),从第1个翻到第10万个,一个个看入职年份和职级。慢得要死,老板等得直跺脚。

有了 Index Merge 时(交集 Intersection)

MySQL 做了件聪明事:

  1. 卡片A,把"2019年前入职"的人的工号全部记下来 → 假设有 3 万人

  2. 卡片B,把"P8"的人的工号全部记下来 → 假设有 500 人

  3. 取交集:两份名单里都有的工号,才是最终目标 → 可能只有 80 人

  4. 最后只去档案柜里把这叫 80 个人的档案调出来(回表)

结果:翻了 3 万 + 500 个索引条目,但只回表 80 次。比翻 10 万个档案快多了!


1-2、企业级场景一:电商订单系统(AND + Intersection)

业务背景

你的电商平台有一张 orders 订单表,数据量 5000万条

sql 复制代码
CREATE TABLE orders (
    order_id BIGINT PRIMARY KEY,      -- 主键
    user_id BIGINT NOT NULL,          -- 用户ID
    status TINYINT NOT NULL,          -- 订单状态:0待支付 1已支付 2已发货 3已完成
    create_time DATETIME NOT NULL,
    INDEX idx_user_id (user_id),      -- 单列索引1
    INDEX idx_status (status)         -- 单列索引2
);

业务需求

客服系统要查:"用户 10086 的所有已支付订单"

sql 复制代码
SELECT * FROM orders 
WHERE user_id = 10086 AND status = 1;

没有 Index Merge 会怎样?

MySQL 只能选一个索引:

  • idx_user_id:先找到用户 10086 的 200 个订单,再一个个回表看 status 是不是 1。要回表 200 次。

  • idx_status:先找到所有"已支付"订单(可能有 1000 万条),再一个个看 user_id 是不是 10086。疯了。

或者更惨------直接全表扫描,5000万条全翻一遍。

有了 Index Merge(Intersection)

MySQL 同时查两个索引:

  1. idx_user_id 查到:用户 10086 的订单主键集合 → {101, 205, 308, ..., 共 200 个}

  2. idx_status 查到:状态=1 的订单主键集合 → {..., 205, 308, 412, ...}

  3. 取交集:两份名单里都有的主键 → 可能只有 50 个

  4. 只回表 50 次

执行计划长这样:

sql 复制代码
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1;
-- type: index_merge
-- key: idx_user_id, idx_status
-- Extra: Using intersect(idx_user_id, idx_status); Using where

核心收益:回表次数从 200 次降到 50 次,查询时间可能从 2 秒降到 50 毫秒。


1-3、企业级场景二:物流追踪系统(OR + Union)

业务背景

物流公司有张 packages 包裹表,数据量 1亿条

sql 复制代码
CREATE TABLE packages (
    package_id BIGINT PRIMARY KEY,
    tracking_no VARCHAR(50),          -- 运单号
    receiver_phone VARCHAR(20),       -- 收件人手机号
    sender_name VARCHAR(50),
    INDEX idx_tracking (tracking_no), -- 单列索引1
    INDEX idx_phone (receiver_phone)  -- 单列索引2
);

业务需求

客服接到投诉,客户只记得运单号收件人手机号其中一个,系统要同时查:

"运单号是 SF123456789 或者 收件人手机号是 13800138000 的包裹"

sql 复制代码
SELECT * FROM packages 
WHERE tracking_no = 'SF123456789' OR receiver_phone = '13800138000';

没有 Index Merge 会怎样?

OR 条件是个老大难问题。单个索引搞不定 OR

  • idx_tracking 只能查运单号那一半

  • idx_phone 只能查手机号那一半

MySQL 一看两个单列索引都搞不定 OR直接放弃,全表扫描 1 亿条记录。客服页面直接卡死。

有了 Index Merge(Union)

MySQL 做了件聪明事:

  1. idx_tracking:运单号 = 'SF123456789' 的主键 → {8888}

  2. idx_phone:手机号 = '13800138000' 的主键 → {6666, 8888, 9999}

  3. 取并集(去重):{6666, 8888, 9999} → 3 条

  4. 只回表 3 次

执行计划:

sql 复制代码
EXPLAIN SELECT * FROM packages WHERE tracking_no = 'SF123456789' OR receiver_phone = '13800138000';
-- type: index_merge
-- key: idx_tracking, idx_phone
-- Extra: Using union(idx_tracking, idx_phone); Using where

核心收益:从全表扫描 1 亿条 → 只回表 3 条。客服页面秒开。


1-4、企业级场景三:金融风控系统(OR + Sort-Union)

业务背景

银行有张 transactions 交易流水表,数据量 5亿条

sql 复制代码
CREATE TABLE transactions (
    trans_id BIGINT PRIMARY KEY,
    amount DECIMAL(18,2),             -- 交易金额
    trans_time DATETIME,              -- 交易时间
    risk_level TINYINT,
    INDEX idx_amount (amount),        -- 单列索引1
    INDEX idx_time (trans_time)       -- 单列索引2
);

业务需求

风控部门要筛查异常交易:

"交易金额 大于 10万 或者 交易时间在 凌晨 2:00~5:00 的交易"

sql 复制代码
SELECT * FROM transactions 
WHERE amount > 100000 OR trans_time BETWEEN '2024-01-01 02:00:00' AND '2024-01-01 05:00:00';

这里为什么用 Sort-Union 而不是普通 Union?

因为这两个条件都是范围查询>BETWEEN),不是等值查询。

普通 Union 要求索引扫描出来的主键是有序的,但范围查询扫出来的主键可能不是严格有序的。所以 MySQL 要先排序,再合并。

执行流程:

  1. idx_amount 扫出金额>10万的主键集合 → {100, 500, 200, 800, ...} (无序)

  2. idx_time 扫出凌晨交易的主键集合 → {300, 100, 900, ...} (无序)

  3. 分别排序 → {100, 200, 500, 800, ...} 和 {100, 300, 900, ...}

  4. 取并集去重 → {100, 200, 300, 500, 800, 900, ...}

  5. 按主键顺序回表(还能顺便利用 MRR 优化,把随机 I/O 变顺序 I/O)

执行计划:

sql 复制代码
EXPLAIN SELECT * FROM transactions WHERE amount > 100000 OR trans_time BETWEEN '...' AND '...';
-- type: index_merge
-- key: idx_amount, idx_time
-- Extra: Using sort_union(idx_amount, idx_time); Using where

1-5、但 Index Merge 不是万能的!(踩坑预警)

上面三个故事听起来很美好,但现实中 Index Merge 经常翻车,特别是 Intersection。

翻车案例:Mattermost 生产事故

Mattermost(一个开源协作平台)曾遇到过一个经典案例:

他们的 Posts 表有几百万条数据,查询:

sql 复制代码
SELECT * FROM Posts WHERE ChannelId = 'xxx' AND DeleteAt = 0 AND CreateAt < z;

MySQL 选择了 Index Merge Intersection ,把 ChannelIdDeleteAt 两个索引做交集。

但问题是:DeleteAt = 0 表示"未删除的帖子",几乎就是全表数据!MySQL 估算错了成本,实际上扫描了 1300 万行,查询卡死。

教训 :如果某个条件过滤性极差(比如 status = 1 占了 90% 的数据),Index Merge 反而比单索引或全表扫描更慢。

翻车案例:Percona 的性能测试

Percona 的测试也证明:

当两个列的数据高度相关时(比如 i1=50i2=50 总是同时出现),MySQL 假设它们是独立的,估算的交集行数远小于实际值,结果选了个最差的执行计划。


给你的实战建议

场景 建议
AND 条件 + 多个单列索引 优先建联合索引 (a, b),比 Index Merge 快 10 倍。Index Merge 只是"没有办法的办法"。
OR 条件 + 多个单列索引 Index Merge 是救星!因为 OR 没法走单个索引,有 Index Merge 至少比全表扫描强。
发现执行计划出现 index_merge 但查询很慢 EXPLAIN ANALYZE 看实际扫描行数。如果某个索引过滤性极差,用 USE INDEX(...) 强制走单索引,或关闭 index_merge_intersectionSET SESSION optimizer_switch='index_merge_intersection=off';
数据量小(< 10万行) 不用纠结,全表扫描可能更快,MySQL 自己会选择。

一句话总结

Index Merge 就像是"同时翻几本索引卡片,然后把名单合并"

对于 AND 条件它帮你减少回表次数,对于 OR 条件它帮你避免全表扫描。

但它不是银弹------当某个条件过滤性太差时,翻多本卡片的成本可能比直接翻档案柜还高。企业里最好的做法还是根据业务查询模式设计联合索引,让 Index Merge 只是兜底方案。

2、单个索引 联合索引 VS OR

假设你有一张用户表,有两个单列索引

sql 复制代码
CREATE TABLE users (
    id INT PRIMARY KEY,
    age INT,
    city VARCHAR(20),
    INDEX idx_age (age),      -- 单列索引1:只按 age 排序
    INDEX idx_city (city)     -- 单列索引2:只按 city 排序
);

现在执行:

sql 复制代码
SELECT * FROM users WHERE age = 20 OR city = '北京';

我问你:单独用 idx_age 这个索引,能不能查出所有满足条件的行?

不能。 因为 idx_age 这棵 B+Tree 里只存了 age 和主键 id 的对应关系,它根本不知道每行人的 city 是什么。

  • idx_age 能告诉你:age=20 的人是 id=1, 5, 9

  • city='北京' 的人呢?idx_age 完全无能为力,只能全表扫描去猜


同理,单独用 idx_city 呢?

也搞不定。它能找到 city='北京' 的人,但对 age=20 的人一无所知。


那联合索引能搞定吗?

你肯定会想:那我建个联合索引 (age, city) 总行了吧?

sql 复制代码
INDEX idx_age_city (age, city)

答案还是:搞不定这个 OR。


因为联合索引是先按 age 排序,再在相同 age 内按 city 排序

复制代码
age=18, city=上海 → id=2
age=20, city=北京 → id=1   ← 这里能找到 age=20 的
age=20, city=广州 → id=5
age=25, city=北京 → id=8   ← city=北京 的分散在不同地方!
  • age = 20 可以走联合索引(最左前缀)

  • city = '北京' 在整棵树上不是全局有序的(它只在 age 相同的小范围内有序)

  • 所以联合索引也无法一次性 覆盖 OR 的两边

联合索引能搞定的是 ANDWHERE age = 20 AND city = '北京',因为条件可以顺着树一路定位到 age=20, city=北京 那个分支。但 OR 不行。

3、index merge怎么用

Index Merge 是自动的,但你可以控制它

MySQL 的 Index Merge 默认就是开启的,不需要你手动"使用"。

优化器会自动判断:当查询满足条件时,就用;不满足时,就不用。

但你可以做三件事:观察它、控制它、优化它


一、先看:怎么知道有没有用到 Index Merge?

EXPLAIN 看执行计划:

sql 复制代码
EXPLAIN SELECT * FROM orders 
WHERE user_id = 10086 OR status = 1;

如果用了 Index Merge,你会看到:

字段 显示内容 含义
type index_merge ✅ 明确告诉你用了 Index Merge
key idx_user_id,idx_status 同时用了这两个索引
Extra Using union(idx_user_id,idx_status); Using where Union 合并方式

三种合并类型的 Extra 字样:

  • Intersection(交集,AND)Using intersect(idx_a,idx_b)

  • Union(并集,OR)Using union(idx_a,idx_b)

  • Sort-Union(排序并集,范围 OR)Using sort_union(idx_a,idx_b)


二、控制:手动开启/关闭 Index Merge

1. 全局开关(影响整个实例)

sql 复制代码
-- 查看当前设置
SHOW VARIABLES LIKE 'optimizer_switch';

-- 关闭 Index Merge(不建议生产环境用)
SET GLOBAL optimizer_switch='index_merge=off,index_merge_union=off,index_merge_sort_union=off,index_merge_intersection=off';

默认都是 on,一般不要关

2. 会话级开关(只影响当前连接)

sql 复制代码
-- 当前会话关闭 intersection(解决某个查询的误判)
SET SESSION optimizer_switch='index_merge_intersection=off';

-- 用完记得恢复,或退出会话自动失效

3. SQL Hint(只影响单条查询)⭐ 最常用

MySQL 8.0 提供了 Hint 语法,可以对单条 SQL 精准控制

sql 复制代码
-- 强制使用 Index Merge(当优化器没选时)
SELECT /*+ INDEX_MERGE(orders idx_age, idx_city) */ *
FROM orders 
WHERE age = 20 OR city = '北京';

-- 强制禁用 Index Merge(当优化器误选时)
SELECT /*+ NO_INDEX_MERGE(orders idx_age, idx_city) */ *
FROM orders 
WHERE age = 20 OR city = '北京';

💡 企业级场景 :当你发现某条 SQL 走了 Index Merge 但很慢时,不需要改全局配置,只需在这条 SQL 上加 NO_INDEX_MERGE 即可。


三、实战:什么时候该"用",什么时候该"禁"?

该让它自动用的情况

场景 示例 SQL 原因
OR + 多个单列索引 WHERE a = 1 OR b = 2 单个索引搞不定 OR,Index Merge 是唯一避免全表扫描的办法
AND + 单列索引 + 数据分布均匀 WHERE a = 1 AND b = 2 两个条件过滤性都好,交集后回表次数极少

❌ 该禁用的情况(加 NO_INDEX_MERGE)

场景 原因 解决方案
某个条件的过滤性极差 比如 status = 1 占了 90% 数据,Index Merge 要扫巨量行 NO_INDEX_MERGE 或改用联合索引
两个索引列高度相关 比如 province='广东'city='深圳' 几乎同时出现,MySQL 假设独立,估算错误 NO_INDEX_MERGE,建联合索引 (province, city)
索引返回行数估算偏差大 EXPLAIN 显示 rows 很小,实际扫了几百万行 ANALYZE TABLE 更新统计信息,或加 Hint

四、企业级最佳实践

1. 优先用联合索引,Index Merge 只是兜底

sql 复制代码
-- 不好的设计:两个单列索引
INDEX idx_age (age),
INDEX idx_city (city)

-- 好的设计:一个联合索引(如果 AND 查询多)
INDEX idx_age_city (age, city)

联合索引 (age, city) 比 Index Merge 快 5~10 倍,因为:

  • 只查一棵树

  • 不用合并两个索引的结果集

  • 天然有序,回表更可控

2. 如果必须用 Index Merge,确保回表次数可控

sql 复制代码
-- 先看两个条件各自过滤后有多少行
EXPLAIN SELECT id FROM orders WHERE age = 20;      -- 看 rows
EXPLAIN SELECT id FROM orders WHERE city = '北京';   -- 看 rows

如果两个条件的 rows 乘积(Intersection)或总和(Union)超过表总行数的 20%,Index Merge 大概率不如全表扫描。

3. 定期 ANALYZE TABLE 保持统计信息准确

sql 复制代码
ANALYZE TABLE orders;

MySQL 靠统计信息决定是否用 Index Merge。如果统计信息过时,优化器可能做出错误选择。


五、一句话总结

Index Merge 不用你"用",MySQL 会自动选。你要做的是:会看执行计划(type = index_merge)、会用 Hint 控制(INDEX_MERGE / NO_INDEX_MERGE)、会判断该不该让它用(看过滤性和数据分布)。

企业里的老手不会刻意追求 Index Merge,而是把联合索引设计好,让 Index Merge 只在"联合索引覆盖不了"的 OR 场景下默默兜底。

相关推荐
2501_937860943 小时前
MySQL CRUD 增删改查
数据库·mysql
竹枝溪3 小时前
MySQL数据库零基础入门
数据库·sql·mysql·navicat·ddl·dml·dql
lsylalalala5 小时前
mysql-索引1
数据库·mysql
2501_937860948 小时前
MySQL表的操作:创建、查看、修改、删除完整实战
android·数据库·mysql
奇特認12 小时前
mysql 集群技术 3.高可用之 MHA
数据库·mysql
Blossom i18 小时前
大数据预处理与采集实验一:使用Python操作MySQL数据库
数据库·mysql
InfinitePlus18 小时前
Docker MySQL搭建一主一从
mysql·docker
坐吃山猪20 小时前
Docker07-MySQL
mysql·docker·容器
少晓年21 小时前
从 MySQL 迁移到人大金仓 KingbaseES:完整指南与实践
数据库·mysql