数据库查询过程中,磁盘I/O的效率直接影响整体性能,随机I/O和顺序I/O的性能差距可达数十倍,而索引的核心作用之一就是将查询过程中的随机I/O转换为顺序I/O,从而大幅提升查询效率。

磁盘I/O的基本类型
要理解索引的作用,首先需要明确两种I/O类型的区别:
- 随机I/O:指磁盘读写头需要频繁移动到不同的物理位置读取数据,每次移动都需要消耗寻道时间和旋转延迟,性能极低。比如查询用户表中ID为100、200、300的三条不连续数据,磁盘需要三次移动到不同位置读取。
- 顺序I/O:指磁盘读写头连续读取相邻的物理扇区数据,不需要频繁移动,只需要少量寻道时间,后续读取可以连续进行,性能远高于随机I/O。比如读取一个连续存储的文件,磁盘可以一次性连续读完。
无索引时的查询I/O情况
当表中没有索引时,数据库执行查询会进行全表扫描,也就是从表的第一行开始逐行读取数据,判断是否符合查询条件。如果查询条件对应的数据在表中分布非常分散,那么每找到一条符合条件的数据,都需要进行一次随机读取,整个过程会包含大量的随机I/O。
举个例子,我们有一个用户表user,结构如下:
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
city VARCHAR(50)
);
如果我们要查询所有年龄大于30岁的用户,且没有对age字段建立索引,数据库会扫描全表,假设符合条件的用户ID分别是10、55、120、300,那么磁盘需要分别移动到这四个ID对应的物理存储位置读取数据,产生四次随机I/O。
索引如何将随机I/O转换为顺序I/O
常见的数据库索引比如B+树索引,本身是有序存储的,这个特性是转换I/O类型的关键,具体过程可以分为以下几步:
1. 索引的有序存储特性
B+树索引的叶子节点是按照索引键的大小顺序存储的,并且叶子节点之间通过双向指针连接,形成一个有序的链表。比如我们对user表的age字段建立索引,那么索引的叶子节点会按照age从小到大排列,相同age的节点再按照主键顺序排列。
2. 范围查询的I/O优化
当执行范围查询时,比如查询age BETWEEN 30 AND 40,数据库首先会在索引的非叶子节点中找到第一个age大于等于30的叶子节点,然后沿着叶子节点的链表顺序向后读取,直到遇到age大于40的节点为止。这个过程读取的索引节点是连续的,属于顺序I/O。
读取完符合条件的索引节点后,索引中存储了对应数据行的物理地址(或者是主键ID,取决于索引类型),如果是聚簇索引,叶子节点直接存储数据行,那么此时已经完成了数据读取;如果是非聚簇索引,还需要根据主键ID回表查询数据行。
但是如果符合条件的数据行在物理存储上也是连续的,那么回表的过程也会是顺序I/O;即使物理存储不连续,由于索引已经过滤掉了大量不符合条件的数据,需要回表的次数会大幅减少,整体的随机I/O次数也会远低于全表扫描的情况。
3. 单点查询的I/O优化
即使是单点查询,比如查询age = 35的用户,索引也可以减少I/O次数。数据库只需要从索引的根节点开始,逐层向下查找,最终定位到age=35的叶子节点,这个过程读取的索引节点数量很少,且索引节点通常连续存储,属于少量顺序I/O,远优于全表扫描的大量随机I/O。
代码示例验证
我们可以通过MySQL的执行计划来观察索引使用前后的I/O变化,首先创建测试表和索引:
-- 创建测试表
CREATE TABLE test_io (
id INT PRIMARY KEY AUTO_INCREMENT,
val INT,
data VARCHAR(100)
);
-- 插入10万条测试数据
DELIMITER //
CREATE PROCEDURE insert_test_data()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 100000 DO
INSERT INTO test_io (val, data) VALUES (FLOOR(RAND() * 1000), CONCAT('test_data_', i));
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
CALL insert_test_data();
-- 未建立索引时查询val在500到550之间的数据
EXPLAIN SELECT * FROM test_io WHERE val BETWEEN 500 AND 550;
-- 对val字段建立索引
CREATE INDEX idx_val ON test_io(val);
-- 建立索引后再次查询
EXPLAIN SELECT * FROM test_io WHERE val BETWEEN 500 AND 550;
第一次执行EXPLAIN时,结果中的type字段会显示ALL,代表全表扫描,会产生大量随机I/O;第二次执行EXPLAIN时,type字段会显示range,代表使用了范围索引扫描,此时读取索引的过程是顺序I/O,I/O效率会大幅提升。
注意事项
- 不是所有索引使用场景都能完全消除随机I/O,如果回表的数据行物理存储非常分散,还是会产生一定的随机I/O,但整体次数会远少于无索引的情况。
- 索引本身也会占用存储空间,并且会增加写入操作的开销,因为每次写入数据都需要同步更新索引,所以需要根据实际查询需求合理建立索引,避免过度建索引。
- 对于写入非常频繁的表,过多的索引会导致写入时的I/O次数增加,反而可能降低整体性能,需要平衡查询和写入的需求。