数据库索引深度解析:从B+树到查询优化器的完整指南
深入理解数据库索引的底层原理、各类索引的适用场景以及查询优化的实战技巧
折
折腾侠
2026/03/20 发布
19约 6 分钟1167 字 / 674 词00
引言
索引是数据库性能优化中最重要的工具。一个好的索引设计可以让查询速度提升千倍,而糟糕的索引设计则会让系统性能崩溃。本文将从底层原理出发,深入解析数据库索引的工作机制、不同索引类型的适用场景,以及实战中的查询优化技巧。
一、B+树索引原理
1.1 为什么是B+树?
数据库索引不使用普通二叉树的原因:
- 二叉树在最坏情况下退化为链表,O(n)查找
- 即使是平衡二叉树(AVL/红黑树),高度约为log₂(n),百万数据需要约20次磁盘IO
B+树的优势:
- 更矮:每个节点可以存储多个键值(由页大小决定,通常16KB),百万数据高度仅3-4层
- 更适合范围查询:叶节点以链表连接,范围查询只需找到起点然后顺序扫描
- 叶节点存数据:非叶节点只存键,可以存更多键,树更矮
1.2 InnoDB的聚簇索引
MySQL InnoDB中,主键索引就是数据本身——表数据按主键顺序存储在B+树的叶节点中。
聚簇索引(主键)
[30|50|80]
/ | \
[10|20|30] [40|50] [60|70|80]
↓每个叶节点存完整行数据
{id:10, name:Alice, age:25, ...}
{id:20, name:Bob, age:30, ...}
二级索引(非主键索引):叶节点存储的是主键值,需要再回表查询完整数据。
SQL
-- 这个查询需要回表(先查name索引得到id,再查聚簇索引得到完整行)
SELECT * FROM users WHERE name = 'Alice';
-- 这个查询不需要回表(覆盖索引,索引中已包含所需字段)
SELECT id, name FROM users WHERE name = 'Alice';
二、索引类型详解
2.1 联合索引与最左前缀原则
SQL
-- 创建联合索引
CREATE INDEX idx_user_search ON users(city, age, gender);
-- 可以使用索引(满足最左前缀)
SELECT * FROM users WHERE city = 'Beijing'; -- ✅
SELECT * FROM users WHERE city = 'Beijing' AND age = 25; -- ✅
SELECT * FROM users WHERE city = 'Beijing' AND age > 20; -- ✅ (city全匹配,age范围)
SELECT * FROM users WHERE city = 'Beijing' AND age = 25 AND gender = 'M'; -- ✅
-- 不能使用索引(跳过了最左列)
SELECT * FROM users WHERE age = 25; -- ❌
SELECT * FROM users WHERE age = 25 AND gender = 'M'; -- ❌
-- 只能用到city部分(age跳过后,gender无法用索引)
SELECT * FROM users WHERE city = 'Beijing' AND gender = 'M'; -- 只用city
索引设计口诀:把选择性高(唯一值多)的列放前面,常用于等值查询的列放前面,范围查询的列放最后。
2.2 覆盖索引
SQL
-- 慢:需要回表(查询列*不在索引中)
SELECT * FROM orders WHERE user_id = 123;
-- 快:覆盖索引(所需列都在索引中)
SELECT id, status, created_at FROM orders WHERE user_id = 123;
-- 前提:有索引 INDEX(user_id, status, created_at) 或 INDEX(user_id) 且只需id
-- 专为覆盖索引优化的查询
ALTER TABLE orders ADD INDEX idx_cover(user_id, status, created_at, id);
2.3 函数索引与表达式索引
SQL
-- 以下查询无法使用name列上的索引(函数破坏索引)
SELECT * FROM users WHERE LOWER(name) = 'alice';
SELECT * FROM users WHERE DATE(created_at) = '2024-01-01';
-- 解决方案1:应用层处理
SELECT * FROM users WHERE name = 'Alice'; -- 查询前统一大小写
-- 解决方案2:函数索引(MySQL 8.0+, PostgreSQL)
ALTER TABLE users ADD INDEX idx_name_lower ((LOWER(name)));
SELECT * FROM users WHERE LOWER(name) = 'alice'; -- 现在可以用索引
-- 解决方案3:生成列
ALTER TABLE users ADD COLUMN name_lower VARCHAR(100)
GENERATED ALWAYS AS (LOWER(name)) STORED;
ALTER TABLE users ADD INDEX idx_name_lower(name_lower);
2.4 部分索引(Partial Index)
SQL
-- PostgreSQL:只对未完成的订单建索引(大部分订单是已完成的,只需加速未完成订单查询)
CREATE INDEX idx_pending_orders ON orders(created_at)
WHERE status IN ('PENDING', 'PROCESSING');
-- MySQL的等价方案(用虚拟列)
ALTER TABLE orders ADD COLUMN is_active TINYINT(1)
GENERATED ALWAYS AS (status IN ('PENDING', 'PROCESSING'));
ALTER TABLE orders ADD INDEX idx_active(is_active, created_at);
三、EXPLAIN分析查询计划
3.1 EXPLAIN输出解读
SQL
EXPLAIN SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.city = 'Beijing'
GROUP BY u.id;
关键列说明:
- type(重要性从低到高):ALL(全表)→ index → range → ref → eq_ref → const
- key:实际使用的索引
- rows:预估扫描行数
- Extra:
- INLINE_CODE_0:覆盖索引(好)
- INLINE_CODE_1:需要额外排序(可优化)
- INLINE_CODE_2:用了临时表(警惕)
- INLINE_CODE_3:WHERE过滤(正常)
SQL
-- 更详细的分析(MySQL 8.0+)
EXPLAIN FORMAT=JSON SELECT ...\G
-- 实际执行计划(包含实际行数)
EXPLAIN ANALYZE SELECT ... -- PostgreSQL
3.2 慢查询优化案例
SQL
-- 原查询(慢):全表扫描,filesort
SELECT * FROM orders
WHERE user_id = 123
AND status = 'COMPLETED'
ORDER BY created_at DESC
LIMIT 20;
-- 添加复合索引后(快):
ALTER TABLE orders
ADD INDEX idx_user_status_time(user_id, status, created_at);
-- 优化分页(深分页问题)
-- 慢:OFFSET 10000 需要扫描10020行再丢弃前10000行
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 10000;
-- 快:游标分页,只需从cursor_id开始扫描
SELECT * FROM orders WHERE id > :last_id ORDER BY id LIMIT 20;
四、索引设计原则
4.1 选择性
索引选择性 = 唯一值数量 / 总行数。选择性越高,索引效果越好。
SQL
-- 计算列的选择性
SELECT
COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity, -- 约0.5,差
COUNT(DISTINCT city) / COUNT(*) AS city_selectivity, -- 可能0.01-0.1
COUNT(DISTINCT email) / COUNT(*) AS email_selectivity -- 接近1.0,极好
FROM users;
布尔列(is_active=true/false)选择性极低,单独建索引收效甚微,除非满足某一值的行只占少数(适合部分索引)。
4.2 索引维护成本
每个索引都会增加:
- 写入开销:INSERT/UPDATE/DELETE需要同步维护所有索引
- 存储空间:索引本身占用磁盘
- 内存压力:热点索引需要在Buffer Pool中缓存
原则:不建多余索引。定期用___INLINE_CODE_4___(MySQL)或___INLINE_CODE_5___(PostgreSQL)找出未使用的索引并删除。
4.3 索引设计checklist
- ✅ WHERE子句中的高选择性列建索引
- ✅ JOIN连接字段建索引
- ✅ ORDER BY / GROUP BY字段如果频繁使用,考虑加入索引
- ✅ 考虑覆盖索引,避免回表
- ✅ 联合索引注意最左前缀原则和列顺序
- ✅ 避免在索引列上使用函数或隐式类型转换
- ❌ 避免过多索引(每张表通常不超过5-6个)
- ❌ 低选择性列(性别、布尔值)不要单独建索引
结语
索引设计是数据库性能优化中效益最高的手段。深入理解B+树原理、掌握EXPLAIN分析方法、遵循索引设计原则,能让你在面对慢查询时胸有成竹。同时记住:索引不是越多越好,每个索引都有维护成本,要根据实际查询模式做出合理权衡。