1. 什么是索引?索引有何用?
索引是数据库中一种特殊的数据结构,旨在帮助数据库系统高效地检索数据。它类似于书籍的目录,通过预先组织和排序特定列的数据,并存储指向实际数据行的物理位置信息,使得数据库能够快速定位到所需的数据,而无需扫描整个数据表。
索引的主要作用体现在以下几个方面:
- 加速数据检索:这是索引最核心的功能。通过索引,数据库可以将原本可能需要全表扫描(线性时间复杂度)的操作,优化为对数时间复杂度的查找,显著减少磁盘 I/O,从而大幅提升查询速度。
- 避免服务器内部排序:索引通常会对其包含的列进行排序存储。当查询语句中包含
ORDER BY或GROUP BY子句时,如果相关列上存在索引,数据库可以直接利用索引的有序性,避免在内存中进行额外的排序操作,从而提高效率。 - 将随机 I/O 变为顺序 I/O:对于某些索引结构(如 B+ 树),其叶子节点之间通过链表连接,使得范围查询可以进行顺序扫描,减少了随机磁盘寻道的时间。
- 保证数据唯一性:通过创建唯一索引(包括主键索引),数据库可以强制确保特定列或列组合的值是唯一的,从而维护数据完整性。
2. 索引如何存储?
数据库索引通常存储在磁盘上,以文件形式存在。例如,在 MySQL 的 InnoDB 存储引擎中,表数据和索引数据通常存储在 .ibd 文件中。索引的存储并非简单地将数据复制一份,而是以一种高效的数据结构组织起来,最常见的就是 B+ 树。
索引的存储单元是页(Page),这是磁盘和内存之间数据传输的最小单位。在 MySQL 中,默认的页大小通常是 16 KB。一个索引结构由多个这样的页组成,通过指针相互连接,形成一个树状结构。
3. 什么是 B+ 树?
**B+ 树(B+ Tree)**是数据库索引最常用的数据结构,它是一种多路平衡查找树,专为磁盘存储优化而设计。
B+ 树具有以下核心特点:
- 多路平衡:与二叉树不同,B+ 树的每个节点可以拥有多个子节点(通常称为“阶”或“度”)。这种设计使得树的高度相对较低,从而减少了磁盘 I/O 的次数,因为每次磁盘读取通常会加载一个页(节点)的数据。
- 非叶子节点仅存储键值和指针:B+ 树的非叶子节点(内部节点)只存储索引的键值(Key)和指向子节点的指针,而不存储实际的数据行。这样做的好处是,每个非叶子节点可以容纳更多的索引项,进一步降低树的高度,减少查找路径。
- 所有数据存储在叶子节点:B+ 树的所有实际数据(或指向数据行的指针)都存储在叶子节点中。这意味着任何查询都需要从根节点遍历到叶子节点才能找到数据,保证了查询效率的稳定性。
- 叶子节点之间通过链表连接:B+ 树的所有叶子节点之间构成一个有序的双向链表。这一特性使得范围查询(如
WHERE id BETWEEN 100 AND 200)变得非常高效,只需找到范围的起始点,然后沿着链表进行顺序扫描即可,避免了重复的树遍历。
4. 什么是回表读?
在数据库中,当使用**非聚簇索引(也称为二级索引或辅助索引)进行查询时,可能会发生回表读(Lookup by Primary Key / Secondary Index Lookup)**操作。
其过程如下:
- 第一次查找(索引查找):数据库首先在二级索引的 B+ 树中根据查询条件进行查找,找到满足条件的索引项。二级索引的叶子节点通常存储的是索引列的值和对应数据行的主键值(而不是完整的行数据)。
- 第二次查找(回表):数据库获取到主键值后,会拿着这个主键值,再次回到**聚簇索引(主键索引)**的 B+ 树中进行查找,以获取该数据行的所有列信息 。
显然,回表读增加了额外的磁盘 I/O 和查找开销,降低了查询效率。为了避免回表读,可以采用**覆盖索引(Covering Index)**的优化策略。如果一个二级索引包含了查询语句中 SELECT 列表所需的所有列,那么数据库在遍历二级索引时就能直接获取所有需要的数据,无需再回到聚簇索引查找,从而避免了回表读 。
5. 什么是最左前缀原则?
**最左前缀原则(Leftmost Prefix Rule)是针对联合索引(Composite Index)**而言的,它规定了联合索引的生效条件 。
假设我们为一个表创建了一个联合索引 INDEX idx_abc (a, b, c),这意味着索引的键值是 (a, b, c) 的组合,并且按照 a、b、c 的顺序进行排序。
最左前缀原则指出,只有当查询条件从联合索引的最左侧列开始,并且是连续匹配时,该索引才能被数据库优化器有效利用。
-
索引生效的情况:
WHERE a = 1:只使用列a,符合最左前缀。WHERE a = 1 AND b = 2:使用列a和b,符合最左前缀。WHERE a = 1 AND b = 2 AND c = 3:使用列a、b和c,符合最左前缀。WHERE a = 1 AND c = 3:只使用列a,列c不会使用索引,因为中间的b列被跳过了。WHERE b = 2 AND a = 1:虽然b在前,但优化器会重写条件为a = 1 AND b = 2,仍然可以使用a和b索引。
-
索引失效的情况:
WHERE b = 2:查询条件没有从最左侧的a列开始,索引失效。WHERE c = 3:同上,索引失效。WHERE b = 2 AND c = 3:同上,索引失效。
理解最左前缀原则对于设计高效的联合索引至关重要,它指导我们如何排列联合索引中的列顺序,以最大化索引的利用率 [7]。
6. 什么时候走索引?什么时候不走索引?
数据库优化器会根据查询语句、表数据量、索引统计信息等多种因素来决定是否使用索引。以下是一些常见的使用索引和不使用索引的场景:
6.1 走索引的情况
- 全值匹配查询:
WHERE col1 = 'value'。 - 匹配左前缀查询:
WHERE col1 LIKE 'prefix%'。 - 范围查询:
WHERE col1 > 100、WHERE col1 BETWEEN 10 AND 20。 - 覆盖索引查询:查询所需的所有列都在索引中,无需回表。
- 排序和分组:
ORDER BY indexed_col或GROUP BY indexed_col。 - 连接操作:
JOIN条件中的列有索引。
6.2 不走索引(索引失效)的情况
- 对索引列进行函数或表达式操作:
WHERE ABS(col1) = 10、WHERE col1 + 1 = 10。数据库需要先计算函数或表达式的结果,导致无法直接使用索引树进行查找。 - 隐式类型转换:如果索引列是字符串类型,但在查询时使用了数字类型且未加引号,数据库可能会进行隐式类型转换,导致索引失效。例如,
WHERE phone_number = 13812345678(phone_number 是 VARCHAR 类型) 。 - 模糊查询开头使用
%:WHERE col1 LIKE '%suffix'或WHERE col1 LIKE '%middle%'。由于无法确定前缀,索引无法进行有序查找。 - 使用
OR连接没有索引的列:WHERE indexed_col = 1 OR non_indexed_col = 2。如果OR条件中有一个列没有索引,为了保证结果的正确性,数据库通常会放弃使用索引而进行全表扫描 。 - 使用
NOT、!=、<>等负向查询:这些操作通常会导致索引失效,因为它们需要查找不匹配的值,可能涉及大部分数据。 - 索引列参与计算:
WHERE col1 / 2 = 5。 - 优化器判断全表扫描更快:当表数据量非常小,或者查询条件的选择性(过滤性)非常低(即索引能过滤掉的数据很少,需要扫描大部分数据)时,数据库优化器可能会认为全表扫描的成本低于使用索引,从而放弃使用索引。
7. 结论
数据库索引是提升数据库性能的基石,但其背后涉及复杂的数据结构和优化原理。深入理解 B+ 树的存储机制、回表读的开销、最左前缀原则的约束,以及索引失效的各种场景,对于数据库开发者和管理员而言至关重要。只有掌握这些底层知识,才能更有效地设计、创建和优化索引,从而构建出高效、稳定的数据库系统。