索引介绍
基本介绍
MySQL 官方定义索引为一种帮助数据库高效获取数据的结构,其本质是排好序的快速查找数据结构。数据库除了存储表中的数据,还维护一些额外的满足特定查找算法的数据结构,这些数据结构以某种方式指向实际的数据,从而加快查询的速度。这种额外维护的数据结构就是索引。
由于索引是在存储引擎层实现的,不同的存储引擎对索引的实现方式可能不尽相同,因此没有统一的索引标准。
索引的使用
数据库中的索引可以类比为书籍的目录,它指向实际的数据,帮助快速定位。下图展示了索引的工作原理:
在图中,左边是一张数据表,包含两列和七条记录。最左边的是每条记录的物理地址(需要注意的是,数据逻辑上相邻并不意味着它们在物理磁盘上也是相邻的)。为了提高基于 Col2 列的查询效率,可以维护一个二叉查找树(右图所示)。每个节点包含一个索引键值和一个指向对应数据记录的物理地址的指针。通过二叉查找树,数据库可以快速找到相应的数据记录。
索引的优缺点
优点:
- 提高查询效率:索引通过类似书籍目录的方式,快速定位数据,降低了数据库的 I/O 成本。
- 排序优化:索引可以根据索引列对数据进行排序,降低排序的成本,并减少 CPU 的消耗。
缺点:
- 占用存储:由于索引文件通常较大,不能全部存储在内存中,索引文件往往被存储在磁盘上,增加了存储开销。
- 降低更新速度:在对表执行 INSERT、UPDATE 或 DELETE 操作时,除了需要更新数据外,还需要维护索引文件。因此,索引虽然提高了查询速度,但同时也增加了数据更新的开销。然而,由于数据更新也需要先通过索引查询,索引的加速效果在一定程度上抵消了这一缺点。
- 影响查询与排序:索引会对 WHERE 查询条件和 ORDER BY 排序操作产生直接影响。
索引分类
索引在 MySQL 中根据其功能和结构可分为以下几类:
1. 功能分类
- 主键索引:一种特殊的唯一索引,用于确保表中每条记录的唯一性,不允许有空值,通常在创建表时设置。
- 单列索引:仅对单个列创建索引,一个表可以拥有多个单列索引(即普通索引)。
- 联合索引:将多个单列索引组合在一起,形成的多列索引。可以提高多条件查询的效率。
- 唯一索引:要求索引列的值必须唯一,但允许有空值。对于联合唯一索引,要求列值的组合唯一。
NULL
值可以出现多次,因为在 SQL 中,NULL
与NULL
之间的比较结果为“未知”,既不是相等,也不是不等。- 可以声明不允许存储
NULL
值的非空唯一索引。
- 外键索引:主要用于 InnoDB 存储引擎,确保数据一致性、完整性,支持级联操作。
2. 结构分类
- BTree 索引:最常用的索引结构。InnoDB 和 MyISAM 存储引擎都默认使用基于 B+Tree 的索引结构,适用于大多数查询场景。
- Hash 索引:Memory 存储引擎默认使用的索引结构。基于哈希表实现,适用于精确查询,但不支持范围查询。
- R-tree 索引(空间索引):MyISAM 引擎中的一种特殊索引,主要用于处理地理空间数据类型,如 GIS 应用。
- Full-text 索引(全文索引):用于全文检索,快速匹配大文本的内容。MyISAM 引擎原生支持 InnoDB 在 5.6 版本后也开始支持此类型的索引,主要用于文本字段中的关键字搜索。
不同存储引擎的索引支持情况
索引 | InnoDB | MyISAM | Memory |
---|---|---|---|
BTREE | 支持 | 支持 | 支持 |
HASH | 不支持 | 不支持 | 支持 |
R-tree | 不支持 | 支持 | 不支持 |
Full-text | 5.6 版本之后支持 | 支持 | 不支持 |
索引操作
在 MySQL 中,索引可以在创建表时同时创建,也可以在表创建后随时添加新的索引。以下是常见的索引操作:
1. 创建索引:如果表中的某列是主键,MySQL 会自动为该列创建主键索引,不需要手动创建。除此之外,还可以手动创建其他类型的索引。
CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...);
-- 默认情况下,索引类型是 B+TREE
- UNIQUE:创建唯一索引,确保列中的值是唯一的。
- FULLTEXT:创建全文索引,用于全文检索。
2. 查看索引:使用以下命令可以查看表中已经存在的索引。
SHOW INDEX FROM 表名;
3. 添加索引:可以使用 ALTER TABLE 命令在表中添加不同类型的索引。
-- 添加单列索引
ALTER TABLE 表名 ADD INDEX 索引名称(列名);
-- 添加组合索引(联合索引)
ALTER TABLE 表名 ADD INDEX 索引名称(列名1, 列名2,...);
-- 添加主键索引
ALTER TABLE 表名 ADD PRIMARY KEY(主键列名);
-- 添加外键索引(通过添加外键约束来实现)
ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名);
-- 添加唯一索引
ALTER TABLE 表名 ADD UNIQUE 索引名称(列名);
-- 添加全文索引(MySQL 只支持文本类型列的全文索引)
ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名);
4. 删除索引:可以使用 DROP INDEX 命令删除索引。
DROP INDEX 索引名称 ON 表名;
注意:删除主键索引时,需要确保没有外键引用该主键,否则会报错。
聚簇索引
索引对比
聚簇索引(Clustered Index)是一种数据存储方式,而不是独立的索引类型。它与非聚簇索引(Non-clustered Index)在数据存储方式和访问效率上有很大的区别:
- 聚簇索引的叶子节点存放的是主键值和数据行本身。
- 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定)。
在 InnoDB 存储引擎中,主键索引是聚簇索引;在 MyISAM 存储引擎中,主键索引是非聚簇索引。
InnoDB
聚簇索引
InnoDB 存储引擎中的 B+ 树索引可以分为聚簇索引和辅助索引。聚簇索引是根据表的主键构建的 B+ 树,叶子节点保存的是完整的数据行。由于聚簇索引将数据与索引结合,因此每张表只能有一个聚簇索引。
聚簇索引的优点:
- 数据访问更快:聚簇索引将数据行和索引保存在同一棵 B+ 树中,所以查询效率高。
- 主键排序和范围查找速度快。
聚簇索引的缺点:
- 插入性能受顺序影响:按照主键顺序(如递增 ID)插入速度最快,乱序插入会导致页面分裂,影响性能。
- 更新主键代价高:更新主键时,数据行会被移动,影响效率。
- 辅助索引访问需要两次索引查找:先通过辅助索引找到主键,再通过聚簇索引找到数据。
辅助索引
InnoDB 中的辅助索引是聚簇索引之外的非聚簇索引,例如复合索引、前缀索引等。叶子节点存储的是主键值,而不是数据行本身。因此,需要二次查找才能获取数据。
检索过程:
- 辅助索引找到主键值。
- 使用主键值通过聚簇索引找到对应的数据页。
- 在数据页中通过二分法查找到具体的数据行。
MyISAM
非聚簇索引
MyISAM 存储引擎中的索引文件和数据文件是分离的。主键索引和辅助索引都是非聚簇索引,索引叶子节点存储的是数据的物理地址。
- 主键索引的 B+ 树存储主键,叶子节点存储数据行的地址。
- 辅助索引的 B+ 树存储辅助键,叶子节点同样存储数据行的地址。
由于索引树是独立的,辅助索引不需要经过主键索引树,可以直接通过物理地址访问数据行。
索引实现对比
存储引擎 | 主键索引 | 辅助索引 | 叶子节点存储 | 查找方式 |
---|---|---|---|---|
InnoDB | 聚簇索引 | 非聚簇索引 | 主键值或数据行 | 需要二次查找 |
MyISAM | 非聚簇索引 | 非聚簇索引 | 数据行地址 | 无需二次查找 |
InnoDB 通过主键进行数据聚集,可以提供非常快速的主键查找,但辅助索引访问效率稍低;MyISAM 主索引与辅助索引在结构上没有本质区别,访问数据都依赖地址指针,查询效率较为均衡。
索引结构
数据页
在文件系统中,最小的存储单元是块(block),每个块的大小通常为4KB。系统从磁盘读取数据到内存时,是以磁盘块为单位的,所在的磁盘块会整体读取到内存中,而不是按需读取。
InnoDB 存储引擎中采用了页(Page)的概念,页是 MySQL 磁盘管理的最小单元,默认情况下每个页的大小为 16KB。在索引结构中,一个节点对应一个数据页,因此每次会一次性将 16KB 的数据读入内存。
页结构:InnoDB 引擎将多个地址连接到磁盘块上,合并达到页的大小为 16KB。
查询效率:在进行查询时,如果一个页中的数据都能帮助定位到记录,则可以减少磁盘 I/O 次数,提高查询效率。
对于超过 16KB 的记录,主键索引页只存储部分数据,并且会有一个指向溢出页的指针,其余的数据会被分散存储到溢出页中。
数据页的物理结构
数据页的物理结构从上到下包括:
- File Header:包含指向上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、校验和、LSN(上次修改该页时的日志序列号)。
- Page Header:存储页的状态信息。
- Infimum + Supremum:该页的最小记录和最大记录,作为边界标记。Infimum 所在的组仅有一条记录,Supremum 所在的组可以有 1 到 8 条记录,其他组有 4 到 8 条记录。
- User Records:实际存储的数据记录。
- Free Space:未使用的存储空间。
- Page Directory:记录分组的目录,允许快速通过二分法定位到数据所在的分组。
- File Trailer:包含校验和字段,用于在写入磁盘时验证数据的完整性。
BTree
BTree 是一种多路平衡搜索树,广泛应用于数据库索引和操作系统的文件系统中。其特性是能够保持数据的稳定有序性,使得数据查找、插入和删除操作更加高效。B+Tree 是 BTree 的一种变种,广泛用于数据库系统中。BTree 的主要特性包括:
- 树中的每个节点最多可以有 m 个子节点。
- 除根节点和叶子节点外,每个节点至少有 [ceil(m/2)] 个子节点。
- 如果根节点不是叶子节点,则至少有 两个子节点。
- 所有的叶子节点都位于同一层,保持结构的平衡。
- 每个非叶子节点包含 n 个 key 和 n+1 个指针,其中 [ceil(m/2)-1] ≤ n ≤ m-1。
例如,5 叉 BTree 中,key 的数量范围为 2 ≤ n ≤ 4。当一个节点中 key 的数量超过最大值时(n>4),节点将会发生分裂,并将中间元素上升到父节点。
插入示例
以下是插入一系列字母的过程及其 BTree 构建过程:
插入 C N G A:按顺序插入,节点未满。
插入 H:n>4,节点分裂,G 上升到父节点。
插入 E K Q:不需要分裂,继续插入。
插入 M:节点满,中间元素 M 上升到父节点。
插入 F W L T:无需分裂,继续插入。
插入 Z:节点满,T 上升到父节点。
插入 D P R X Y:无需分裂,继续插入。
插入 S:多个节点满,Q 上升,随后 M 也向上分裂。
BTree 优势
与二叉树相比,BTree 的层次结构较少,在相同数据量下,BTree 的搜索路径更短,因而搜索效率更高。通过维护 key 的顺序,BTree 结构能够有效定位数据所在的磁盘块,降低磁盘 I/O 操作次数。
BTree 缺点
BTree 的缺点是当进行范围查找时,可能会产生回旋查找,即查找过程可能不如 B+Tree 那样高效,尤其是在处理较大范围的数据时。
B+Tree
数据结构
B+Tree 是 BTree 的改进版,常用于数据库索引和文件系统中,目的是提高查询效率,减少磁盘 I/O 次数。与 BTree 相比,B+Tree 具有以下特点:
- 非叶子节点只存储键值 key,不存储具体的数据,只作为索引使用,这样能在同样大小的节点中存储更多的 key。
- 所有数据都存储在叶子节点,这意味着每次查找操作最终都定位到叶子节点,确保查询的次数一致。
- 叶子节点按 key 大小顺序排列,并通过指针相互连接形成链表,便于范围查询。
- 允许 key 重复:B+Tree 的非叶子节点和叶子节点都可以包含相同的 key 值,BTree 不允许不同节点出现重复的 key。
在 B+Tree 的基础上,还可以构建 B*Tree,它增加了非根和非叶子节点之间的兄弟指针,使得数据更加紧密存储,进一步提高了查询效率。
优化结构
MySQL 采用的 B+Tree 索引结构在经典 B+Tree 基础上进行了优化,增加了顺序指针,即相邻叶子节点之间通过指针相互连接,形成链表。这种结构提高了区间访问的性能,避免了回旋查找。
B+Tree 的叶子节点是数据页,每个数据页可以存储多条记录。对于主键为整数型(如 INT 或 BIGINT)的表,指针大小为 6 字节,这样一个页可以存储约 1000 个键值(key)。假设 B+Tree 的深度为 3,则可以维护约 10 亿条记录。
在实际应用中,B+Tree 的高度通常在 2 到 4 层之间。由于 MySQL 的 InnoDB 存储引擎将根节点常驻内存,查找一条记录的键值时,最多只需要 1 到 3 次磁盘 I/O 操作。
B+Tree 的优势
- 查询效率高:由于所有数据都在叶子节点,且叶子节点通过顺序指针连接,查找和范围查询效率很高。
- 减少磁盘 I/O:B+Tree 的多路平衡结构使得树的高度较低,查找操作通常只需要少量磁盘 I/O。
- 顺序访问:通过叶子节点的链表结构,B+Tree 可以高效地进行区间查询或顺序查找。
索引维护
为了保持索引的有序性,B+Tree 需要在插入或删除数据时进行维护,可能涉及以下操作:
- 页分裂:当数据页已满时,需要申请一个新的页,并将部分数据移动到新的页。这一过程称为页分裂,尽管增加了空间,但可以维持树的平衡。
- 页合并:当两个相邻的数据页利用率很低时,可以将它们合并为一个页,以提高空间利用率。合并的过程与分裂相反。
为了减少页分裂,通常建议选择较小的数据字段作为索引。此外,自增主键的设计可以避免频繁的页分裂,因为数据会顺序插入,从而减少数据的移动。
总之,B+Tree 通过其高效的树形结构和链表顺序连接,在大规模数据存储和查询中表现出色,尤其适合范围查询和排序操作。
设计原则
在设计数据库索引时,遵循一些基本原则可以显著提升索引的使用效率,
创建索引的原则
- 查询频次与数据量:对于查询频次较高且数据量较大的表,建立索引可以显著提高查询性能。
- 唯一索引:使用唯一索引(Unique Index)可以提高区分度,区分度越高,使用索引的效率越高,能更快找到目标数据。
- 最佳候选列:应从 WHERE 子句的条件中提取最佳候选列,并考虑使用覆盖索引。覆盖索引可以避免回表操作,从而提高查询效率。
- 短索引:尽量创建短索引,因为索引占用硬盘空间,较短的索引可以在相同的存储块中存储更多的索引值,提高访问效率。
- 适度索引:虽然索引可以提升查询效率,但过多的索引会增加维护成本,尤其是在频繁进行插入、更新和删除的表中。过多的索引可能导致性能下降并增加 DML 操作的时间消耗。
- 最左前缀匹配原则:在建立联合索引时,MySQL 会遵循最左前缀匹配原则。查询条件应使用联合索引中的最左侧列,只有这样才能有效利用该索引。
例如,创建一个联合索引 (name, address, phone),查询时可以用 (name, address), (name, phone), (name) 来利用索引,但如果只使用 (address, phone),则索引不会命中。
-- 创建联合索引
ALTER TABLE user ADD INDEX index_three(name, address, phone);
-- 查询示例
SELECT * FROM user WHERE name = '张三'; -- 可用索引
SELECT * FROM user WHERE address = '北京' AND phone = '12345'; -- 不命中索引
不适合建立索引的情况
- 记录较少的表:在记录数量很少的表中,索引可能不会带来明显的性能提升。
- 频繁增删改的表:在数据频繁变化的表中,维护索引的成本可能高于索引带来的查询性能提升。
- 频繁更新的字段:如果某个字段频繁更新,创建索引会导致额外的维护开销,不适合建立索引。
- 未使用的字段:在 WHERE 条件中未被使用的字段不应创建索引,以免浪费存储空间和增加维护成本。
索引优化
在数据库中,优化索引设计可以显著提高查询效率。以下是几种索引优化的方法和原则。
覆盖索引
覆盖索引是指一个索引包含了查询所需的所有字段,因此可以直接通过该索引返回结果,而不必查找表中的数据。这可以有效减少回表查询的次数。
回表查询:如果查询字段不在索引中,数据库会通过叶子节点的主键值查找主键索引,从而获取完整的行数据。
示例:
查询没有使用覆盖索引:
SELECT * FROM user WHERE age = 30;
这个查询会通过索引 age=30 查找,然后再回表通过主键 id 查找完整记录。
使用覆盖索引:
DROP INDEX idx_age ON user;
CREATE INDEX idx_age_name ON user(age, name);
SELECT id, age FROM user WHERE age = 30;
这个查询可以直接从索引中获取 id 和 age,避免了回表的步骤。
注意事项:使用覆盖索引时,尽量只选取需要的列,避免使用 SELECT *,因为索引中字段过多会导致索引文件变大,从而影响查询性能。
索引下推
索引条件下推(Index Condition Pushdown,ICP)是 MySQL 5.6 引入的优化特性,能够在索引遍历过程中,提前判断索引中的字段,过滤掉不满足条件的记录,从而减少回表的次数。
不使用索引下推:存储引擎先检索索引,再通过主键回表查询。
使用索引下推:存储引擎在索引遍历时先判断条件,只有满足条件的数据才进行回表查询。
适用条件:
- 需要将判断条件的列包含在同一个索引中。
- 适用于 InnoDB 和 MyISAM 存储引擎。
- InnoDB 聚簇索引不适用此优化,因为它已将整行数据加载到缓冲区中。
前缀索引
前缀索引是指只索引字段的前部分字符,以降低索引大小和提高索引效率。这在处理字符较长的字段时尤其有用。
示例:
假设地区表的 area 字段包含许多以 "china" 开头的记录,可以选择对 area 字段的前 6 位字符建立索引:
CREATE INDEX idx_area ON table_name(area(6));
优化原则:
- 降低重复的索引值。
- 由于使用前缀索引会导致覆盖索引对查询性能优化的效果降低,因此要慎重考虑。
索引合并
索引合并是使用多个索引来完成一次查询的执行方法,分为以下几种情况:
Intersection 索引合并:查询使用 AND 连接的条件。
SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b';
通过取不同索引的交集来获取结果。
Union 索引合并:查询使用 OR 连接的条件。
SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b';
通过取不同索引的并集来获取结果。
Sort-Union 索引合并:先对各个索引结果进行排序,再进行合并。
SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b';
注意:索引合并算法的效率较低,通常通过将其中一个索引改成联合索引来优化查询性能。
示例
-- 创建用户表
CREATE TABLE user
(
id INT PRIMARY KEY, -- 主键
name VARCHAR(50), -- 姓名
age INT, -- 年龄
email VARCHAR(255), -- 邮箱
address VARCHAR(100), -- 地址
phone VARCHAR(20) -- 电话
);
-- 插入一些示例数据
INSERT INTO user (id, name, age, email, address, phone)
VALUES (1, '张三', 30, 'zhangsan.longemailaddress@example.com', '北京', '123456789'),
(2, '李四', 25, 'lisi.longemailaddress@example.com', '上海', '987654321'),
(3, '王五', 40, 'wangwu.longemailaddress@example.com', '广州', '456123789'),
(4, 'Alice', 30, 'alice.longemailaddress@example.com', 'New York', '987123456'),
(5, 'Bob', 25, 'bob.longemailaddress@example.com', 'Los Angeles', '321987654');
-- 1. 创建覆盖索引:包含 age 和 name 字段
CREATE INDEX idx_age_name ON user (age, name);
-- 使用覆盖索引的查询
SELECT id, age
FROM user
WHERE age = 30;
-- 2. 创建联合索引和索引下推优化示例
-- 联合索引 (name, age)
CREATE INDEX idx_name_age ON user (name, age);
-- 使用索引下推优化查询
SELECT *
FROM user
WHERE name LIKE '张%'
AND age = 30;
-- 3. 创建前缀索引:email 列前 10 个字符的索引
CREATE INDEX idx_email_prefix ON user (email(10));
-- 使用前缀索引的查询
SELECT *
FROM user
WHERE email LIKE 'alice%';
-- 4. 创建索引合并示例
-- 使用单列索引 key1 和 key3 的示例表
CREATE TABLE table_test
(
id INT PRIMARY KEY, -- 主键
key1 VARCHAR(50), -- 索引键1
key2 VARCHAR(50), -- 索引键2
key3 VARCHAR(50) -- 索引键3
);
-- 插入示例数据
INSERT INTO table_test (id, key1, key2, key3)
VALUES (1, 'a', 'x', 'b'),
(2, 'a', 'y', 'c'),
(3, 'd', 'z', 'b');
-- 创建单列索引
CREATE INDEX idx_key1 ON table_test (key1);
CREATE INDEX idx_key3 ON table_test (key3);
-- 使用索引合并查询
SELECT *
FROM table_test
WHERE key1 = 'a'
OR key3 = 'b';
-- 5. 联合索引和最左前缀匹配示例
-- 联合索引 (name, address, phone)
CREATE INDEX idx_user ON user (name, address, phone);
-- 使用最左前缀匹配查询
SELECT *
FROM user
WHERE name = '张三'
AND address = '北京';
-- 不符合最左前缀匹配原则的查询
SELECT *
FROM user
WHERE address = '北京'
AND phone = '123456789';