【技术干货】MySQL联合索引底层原理 | 最左前缀原则详解

目录


一、前言:为什么你的SQL不走索引?

大家好,今天来聊一个面试高频问题:联合索引的底层实现原理。

很多同学遇到过这种情况:明明建了索引,SQL却不走索引;或者建立了idx(a,b,c)索引,查询WHERE a=1 AND b=2能用索引,但WHERE b=2就不行了。

要搞清楚这个问题,我们需要深入到B+Tree的底层结构,理解联合索引是如何存储和检索数据的。


二、联合索引到底是什么?

2.1 定义

联合索引 :指在多个列上同时创建的索引,本质上是对这些列的组合值建立索引。

sql 复制代码
-- 创建联合索引
CREATE INDEX idx_name_age_dept ON employee(name, age, dept_id);

这相当于对 (name, age, dept_id) 这个组合值建立B+Tree索引。

2.2 与单列索引的本质区别

特性 单列索引 联合索引
索引数量 每个字段独立创建 多字段组合成一个
存储方式 单独的B+Tree 拼接key的B+Tree
排序依据 单列值 多列值按顺序拼接

核心区别 :联合索引的key是由多个列的值拼接而成的,排序时按照创建顺序依次比较。

ini 复制代码
索引 key 构成:key = name + age + dept_id
例如:('张三', 25, 10) 存储为 key = '张三_25_10'

三、B+Tree与联合索引的深度原理

3.1 InnoDB B+Tree结构

ini 复制代码
                    [非叶子节点层]
                    ┌─────────────┐
                    │  name='D'   │
                    │  name='L'   │
                    │  name='W'   │
                    └─────────────┘
                    /      |      \
        ┌─────────┐   ┌─────────┐   ┌─────────┐
        │ D-H     │   │ L-Q     │   │ W-Z     │
        │ 索引页  │   │ 索引页  │   │ 索引页  │
        └─────────┘   └─────────┘   └─────────┘
              ↓            ↓            ↓
        ┌─────────────────────────────────────┐
        │              叶子节点(双向链表)      │
        │  (name,age,dept) → (name,age,dept) │
        │  ...            →            ...   │
        └─────────────────────────────────────┘

关键特性:

  • 非叶子节点:只存储索引列,不存储完整数据
  • 叶子节点:存储完整索引数据和指向主键的指针
  • 双向链表:叶子节点间用双向链表连接,支持高效范围查询

3.2 联合索引的排序逻辑

联合索引 (name, age, dept_id) 的排序规则:

  1. 先按 name 排序
  2. name 相同的情况下,按 age 排序
  3. name 和 age 都相同的情况下,按 dept_id 排序

示例数据排序:

name age dept_id 索引key
张三 20 10 '张三_20_10'
张三 25 5 '张三_25_5'
张三 25 10 '张三_25_10'
李四 22 8 '李四_22_8'
王五 30 15 '王五_30_15'

注意 :'张三_25_5' 排在 '张三_25_10' 前面,因为 dept_id=5 < 10


四、最左前缀原则详解

4.1 原则定义

联合索引要生效,必须从最左边的列开始使用 ,并且不能跳过中间列。

4.2 实战验证

sql 复制代码
CREATE TABLE employee (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    dept_id INT,
    INDEX idx_name_age_dept (name, age, dept_id)
);

-- ✅ 情况1:从最左列开始,使用索引
SELECT * FROM employee WHERE name = '张三';
SELECT * FROM employee WHERE name = '张三' AND age = 25;
SELECT * FROM employee WHERE name = '张三' AND age > 24 AND dept_id = 10;

-- ❌ 情况2:跳过最左列,不使用索引
SELECT * FROM employee WHERE age = 25;
SELECT * FROM employee WHERE dept_id = 10;

-- ⚠️ 情况3:跳过中间列,只使用部分索引
SELECT * FROM employee WHERE name = '张三' AND dept_id = 10;
-- 结果:只使用了 name 列的索引,age 和 dept_id 无法利用

4.3 执行计划分析

sql 复制代码
EXPLAIN SELECT * FROM employee WHERE name = '张三' AND age = 25;

-- 关键字段解读
+----+-------------+----------+------------+-------+------------------------+
| id | select_type| type     | key        | rows  | Extra                  |
+----+-------------+----------+------------+-------+------------------------+
| 1  | SIMPLE     | ref      | idx_name.. | 5     | Using index condition  |
+----+-------------+----------+------------+-------+------------------------+

-- 字段含义
key = idx_name_age_dept  -- 实际使用的索引
type = ref               -- 索引等值查询,性能优秀
rows = 5                 -- 预计扫描5行
Extra = Using index condition  -- 索引条件下推

4.4 范围查询导致索引断链

sql 复制代码
-- age 是范围查询,导致其后的 dept_id 无法使用索引
EXPLAIN SELECT * FROM employee 
WHERE name = '张三' AND age > 25 AND dept_id = 10;

-- 结果:name 使用索引,age 使用索引(范围),dept_id 不使用

优化建议 :范围查询列尽量放在联合索引的最后面。


五、索引下推优化(ICP)

5.1 什么是ICP?

索引下推(Index Condition Pushdown) :MySQL 5.6+引入的优化,在存储引擎层直接过滤数据,减少回表次数。

5.2 ICP vs 非ICP

sql 复制代码
SELECT * FROM employee WHERE name = '张三' AND age > 24 AND dept_id = 10;

关闭ICP的执行流程:

ini 复制代码
1. 存储引擎:根据 name='张三' 筛选,找到所有匹配行
2. 回表:每行都回表获取完整数据
3. Server层:过滤 age > 24 AND dept_id = 10

开启ICP的执行流程(默认):

ini 复制代码
1. 存储引擎:根据 name='张三' AND age > 24 筛选
2. 回表:只对 age > 24 的行回表
3. Server层:过滤 dept_id = 10

性能差异 :ICP显著减少了不必要的回表操作,特别是当 age > 24 条件能过滤掉大量数据时。

5.3 ICP适用场景

场景 ICP效果
联合索引中间列为范围查询 ✅ 有效减少回表
索引覆盖查询 ❌ 无需回表,ICP无效果
存储引擎层可下推的条件 ✅ 等值、范围、IN列表

六、实战代码演示

6.1 创建测试表

sql 复制代码
CREATE TABLE `orders` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `order_no` VARCHAR(32) NOT NULL,
    `user_id` BIGINT NOT NULL,
    `status` TINYINT DEFAULT 0,
    `amount` DECIMAL(10,2),
    `create_time` DATETIME,
    INDEX `idx_user_status_time` (`user_id`, `status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

6.2 索引使用分析

sql 复制代码
-- ✅ 使用完整索引
EXPLAIN SELECT * FROM orders 
WHERE user_id = 1001 AND status = 1 AND create_time > '2024-01-01';

-- ✅ 使用前两列索引
EXPLAIN SELECT * FROM orders 
WHERE user_id = 1001 AND status = 1;

-- ⚠️ 只使用第一列索引
EXPLAIN SELECT * FROM orders 
WHERE user_id = 1001 AND create_time > '2024-01-01';
-- 结果:只使用 user_id 索引,status 和 create_time 无法利用

6.3 覆盖索引优化

sql 复制代码
-- ❌ 需要回表:gender 不在索引中
SELECT user_id, status, create_time, gender 
FROM orders WHERE user_id = 1001;

-- ✅ 覆盖索引:无需回表
CREATE INDEX idx_covering ON orders(user_id, status, create_time);
SELECT user_id, status, create_time FROM orders WHERE user_id = 1001;

6.4 深度分页优化

sql 复制代码
-- ❌ 低效:OFFSET大时性能极差
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

-- ✅ 优化:书签式分页,利用主键索引
SELECT * FROM orders 
WHERE id > 1000000 ORDER BY id LIMIT 10;

-- ✅ 优化:子查询延迟关联
SELECT t.* FROM orders t
INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) AS tmp
ON t.id = tmp.id;

七、常见踩坑与优化建议

7.1 索引失效的常见场景

误区 说明 正确做法
跳过最左列 WHERE b = 2 从最左列开始使用
OR连接 WHERE a = 1 OR b = 2 改用 UNION 或 IN
函数运算 WHERE YEAR(create_time) = 2024 改写为范围查询
类型转换 WHERE name = 123 (name是VARCHAR) 使用正确类型
LIKE前缀通配 WHERE name LIKE '%三' 避免前缀通配符

7.2 联合索引设计原则

1. 区分度高的列放前面

sql 复制代码
-- ✅ 优化:区分度高的 user_id 在前
INDEX idx_1 (user_id, status, create_time)

-- ❌ 低效:区分度低的 status 在前
INDEX idx_2 (status, user_id, create_time)

2. 范围查询列放最后

sql 复制代码
-- ✅ 优化:范围查询列放最后
INDEX idx_range (user_id, status, create_time)

-- ❌ 低效:范围查询列在中间导致后续列无法使用
INDEX idx_bad (user_id, create_time, status)

3. 覆盖索引减少回表

sql 复制代码
-- 频繁查询字段
SELECT user_id, status, create_time FROM orders WHERE user_id = ?

-- 建立覆盖索引
INDEX idx_covering (user_id, status, create_time)

7.3 索引维护成本

操作 单列索引 联合索引
INSERT 维护1个索引 维护1个索引(但key更大)
UPDATE 维护1个索引 维护1个索引
DELETE 维护1个索引 维护1个索引
空间占用 分散 集中

注意:联合索引不是越多越好,过多索引会增加写入开销和存储空间。


八、总结

核心要点

  1. 联合索引本质:将多列值拼接为key,建立B+Tree索引
  2. 排序规则:从左到右依次比较,前面的列值相同才比较后面的列
  3. 最左前缀原则:必须从最左列开始且不能跳过中间列
  4. 索引下推(ICP):在存储引擎层过滤数据,减少回表
  5. 覆盖索引:查询字段全在索引中时,无需回表

面试高频回答

联合索引底层使用B+Tree结构存储,key由多个列的值按顺序拼接而成。查询必须从最左列开始且不跳过中间列才能使用索引。索引按创建顺序从左到右排序,前面的列值相同才比较后面的列。此外,MySQL 5.6+的索引下推优化会在存储引擎层直接过滤数据,减少回表次数。

相关推荐
kele_z2 小时前
手工修改数据库中的数据时,我会加的四道保险
mysql·postgresql
eleven_4u3 小时前
MySQL深分页如何处理
mysql
PBitW3 小时前
行内编辑,如何防止后端未返回,直接全部更新了?
前端·面试
零售校长3 小时前
二七区零售门店店长面试,重点在哪几处
面试·职场和发展·零售
考研保研资料分享3 小时前
农学跨保清华基础医学直博:四方联合与四校考核复盘(2024)
面试
川石课堂软件测试4 小时前
涨薪技术|0到1学会性能测试第8课-Analysis分析图
服务器·数据库·python·sql·jmeter·mysql·容器
Rain的Java大神之路4 小时前
百万并发直播弹幕系统设计全解:看懂了扇出,你就看懂了高并发的八成
java·后端·面试
太理摆烂哥5 小时前
MySQL表的约束
数据库·mysql
Thomas.Sir5 小时前
第35课:Spring Cloud 2025架构复盘、性能调优 & 面试核心总结
spring cloud·面试·架构