深入理解数据库索引:为什么加了索引,查询速度就变快了?
一、前言
在日常开发中,数据库查询几乎是所有业务系统都绕不开的环节。
当数据量只有几百条时,即使没有任何优化,查询速度通常也很快。但随着业务发展,数据量从几万条增长到几百万条,甚至上千万条,原本简单的 SQL 查询可能开始变得缓慢。
例如:
SELECT * FROM users WHERE username = 'zhangsan';
如果 users 表中有 1000 万条记录,数据库如何快速找到目标用户?
一种最直接的方式是从第一条记录开始,逐条检查 username 是否匹配,直到找到目标数据。
这种方式称为全表扫描(Full Table Scan)。
显然,当数据量越来越大时,这种查询方式的成本也会越来越高。
而数据库索引,就是解决这一问题的重要技术。
二、索引到底是什么?
可以把数据库索引理解成一本书的目录。
假设你需要在一本 1000 页的技术书籍中查找某个知识点。
如果没有目录,你只能从第一页开始逐页翻阅。
如果有目录,你可以先找到知识点对应的章节和页码,然后直接跳转到目标位置。
数据库索引的作用与此类似。
它通过额外的数据结构,保存数据字段与数据存储位置之间的关联,使数据库不必每次都扫描整张表。
例如:
CREATE INDEX idx_username
ON users(username);
这条 SQL 创建了一个基于 username 字段的索引。
之后执行:
SELECT * FROM users
WHERE username = 'zhangsan';
数据库就可以利用索引快速定位符合条件的记录。
三、为什么 MySQL 喜欢使用 B+Tree?
在 MySQL 的 InnoDB 存储引擎中,默认使用 B+Tree 作为索引的数据结构。
那么,为什么不是普通数组、链表或者二叉搜索树?
主要原因在于数据库不仅需要考虑查询效率,还需要考虑磁盘 I/O 成本。
1. 二叉搜索树的问题
二叉搜索树虽然可以实现较快的查询,但当数据分布不均匀时,树结构可能退化成链表。
查询复杂度最坏会从 \(O(\log n)\) 退化为 \(O(n)\)。
2. 平衡二叉树的问题
AVL 树能够保持平衡,但每个节点最多只有两个子节点。
当数据量非常大时,树的高度仍然可能比较高,需要访问多个节点。
对于数据库来说,每次访问磁盘页面都有一定成本。
3. B+Tree 的优势
B+Tree 的一个重要特点是拥有较高的分支因子。
一个节点可以存储大量索引键值,因此整棵树的高度通常比较低。
其结构可以简化理解为:
[30 | 60]
/ | \
/ | \
[10|20] [40|50] [70|80]
| | |
数据 数据 数据
实际的 InnoDB B+Tree 结构更加复杂:
-
非叶子节点保存索引键和子节点指针。
-
叶子节点保存索引记录。
-
聚簇索引的叶子节点保存完整的行数据。
-
二级索引的叶子节点保存索引字段和主键值。
-
叶子节点之间通过链表连接,方便范围查询。
正是这些特性,使 B+Tree 非常适合数据库索引场景。
四、聚簇索引与二级索引
理解 MySQL 索引,还需要掌握两个重要概念:聚簇索引和二级索引。
1. 聚簇索引
InnoDB 中,聚簇索引的叶子节点直接保存完整的数据行。
通常情况下,主键索引就是聚簇索引。
例如:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(100),
age INT
);
执行:
SELECT * FROM users WHERE id = 100;
数据库可以通过主键索引直接定位到完整的数据记录。
2. 二级索引
如果我们给 age 字段创建索引:
CREATE INDEX idx_age ON users(age);
那么这个索引的叶子节点保存的是 age 值以及对应记录的主键值,而不是完整的数据行。
例如:
SELECT * FROM users WHERE age = 25;
执行过程可以简化为:
-
通过
idx_age找到所有符合条件的索引记录。 -
获取对应的主键 ID。
-
根据主键 ID 回到聚簇索引。
-
获取完整的数据行。
这个过程通常被称为回表(Bookmark Lookup)。
如果查询需要的字段已经全部包含在二级索引中,就可能直接通过索引返回结果,而不需要回表。这种情况称为覆盖索引(Covering Index)。
例如:
SELECT id, age
FROM users
WHERE age = 25;
因为二级索引中已经包含 age 和主键 id,所以可能直接完成查询。
五、联合索引与最左前缀原则
实际开发中,我们经常需要根据多个字段进行查询。
例如:
SELECT *
FROM orders
WHERE user_id = 100
AND status = 1;
这时候可以考虑创建联合索引:
CREATE INDEX idx_user_status
ON orders(user_id, status);
联合索引并不是简单地创建两个独立索引,而是按照字段顺序组合形成一个索引结构。
对于 (user_id, status) 这个索引,通常可以有效支持:
WHERE user_id = 100
以及:
WHERE user_id = 100 AND status = 1
但下面这个查询:
WHERE status = 1
通常无法有效利用该联合索引进行完整的定位。
这就是最左前缀原则。
需要注意的是,MySQL 优化器在某些情况下可能通过索引跳跃扫描等方式利用非最左前缀字段,但不能因此认为字段顺序不重要。
设计联合索引时,应该结合实际查询频率、字段区分度、排序需求以及范围条件综合考虑。
六、为什么索引并不是越多越好?
既然索引能够提高查询速度,为什么不直接给每个字段都创建索引?
因为索引本身也是需要维护的数据结构。
当执行以下操作时:
INSERT INTO users
VALUES (100, 'zhangsan', 25);
数据库不仅需要写入数据,还需要更新相关索引。
同样,执行 UPDATE 和 DELETE 时,也可能需要维护索引结构。
索引过多可能产生以下问题:
-
增加磁盘空间占用。
-
降低 INSERT、UPDATE、DELETE 的性能。
-
增加数据库维护成本。
-
让优化器面对更多执行计划选择。
因此,索引设计的核心不是数量,而是收益与成本之间的平衡。
七、如何排查慢 SQL?
在 MySQL 中,可以使用 EXPLAIN 分析 SQL 的执行计划。
例如:
EXPLAIN
SELECT *
FROM users
WHERE username = 'zhangsan';
重点关注以下字段:
|
字段 |
含义 |
|---|---|
|
type |
访问类型 |
|
possible_keys |
可能使用的索引 |
|
key |
实际使用的索引 |
|
rows |
预计扫描的行数 |
|
Extra |
额外执行信息 |
其中,type 是一个值得重点关注的字段。
常见访问类型包括:
-
const:通过主键或唯一索引进行常量匹配。 -
eq_ref:关联查询中通过唯一索引匹配一条记录。 -
ref:通过非唯一索引进行匹配。 -
range:范围扫描。 -
index:扫描整个索引。 -
ALL:全表扫描。
不过,不能简单地认为 ALL 一定不好,或者 index 一定比 ALL 快。对于小表、低选择性查询,优化器可能主动选择全表扫描。
真正的优化应该结合实际数据量、执行耗时、扫描行数和执行计划进行判断。
八、总结
数据库索引是关系型数据库性能优化中最基础、也最重要的技术之一。
理解索引,不能只停留在创建索引的 SQL 语句层面,还需要理解底层数据结构、磁盘 I/O、聚簇索引、二级索引以及查询执行计划。
在实际开发中,建议遵循以下原则:
-
优先为高频查询条件设计索引。
-
合理设计联合索引的字段顺序。
-
尽可能利用覆盖索引减少回表。
-
避免创建大量低收益索引。
-
使用 EXPLAIN 分析真实执行计划。
-
定期检查长期未使用或重复的索引。
索引优化的本质,不是让数据库拥有更多索引,而是让数据库用更少的资源找到需要的数据。
- 点赞
- 收藏
- 关注作者
评论(0)