折腾侠
技术教程

数据库索引深度解析:从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分析方法、遵循索引设计原则,能让你在面对慢查询时胸有成竹。同时记住:索引不是越多越好,每个索引都有维护成本,要根据实际查询模式做出合理权衡。

分享到:

如果这篇文章对你有帮助,欢迎请作者喝杯咖啡 ☕

加载评论中...