一、索引的本质与数据结构

为什么需要索引?

先问大家一个问题:一本500页的技术书,你想找「B+树」这个知识点,会怎么做?

正常人不会从第1页翻到第500页。你会翻目录,找到对应的页码,直接翻过去。索引,就是数据库的目录。

没有索引的时候,数据库只能做全表扫描。我见过一个真实案例:某电商平台的订单表有2000万行,每次查用户最近订单都要扫全表,耗时8秒多。加了个合适的索引后,降到20毫秒。差了400倍。

说白了,索引的核心价值就两点:

  • 快速定位:把O(n)的全表扫描,变成O(log n)的树查找
  • 避免排序:索引本身是有序的,order by可以直接用索引

核心观点:索引是空间换时间的典型。它占用额外的磁盘空间,但能大幅提升查询速度。没有索引的数据库,就像没有目录的百科全书——能用,但效率极低。

B+树 vs LSM树

聊索引,绕不开数据结构。目前主流数据库的索引底层,基本就两派:B+树和LSM树。

B+树:关系型数据库的标配

MySQL的InnoDB、PostgreSQL、Oracle,用的都是B+树。为什么大家都选它?

我个人的理解是:B+树太适合磁盘IO了。你想想看,磁盘随机读写很慢,但顺序读写快。B+树的叶子节点是双向链表,范围查询时只需要沿着链表走,顺序读就行。

B+树有几个关键特征:

  • 非叶子节点只存键,不存数据。这样每个节点能存更多键,树的高度更低
  • 叶子节点存数据,并且用链表串联起来
  • 所有数据都在叶子层,查询任何一条数据的IO次数基本一致

举个例子,假设每个节点能存100个键,三层B+树就能存100万条数据。查任何一条数据,最多3次磁盘IO。我在项目中遇到过一张10亿级的订单表,B+树深度也就4层,查询依然很快。

小技巧:B+树的扇出(每个节点能存的键数量)取决于键的大小和页大小。MySQL默认16KB一页,如果键是bigint(8字节),加上指针,一个节点能存上千个键。这就是为什么B+树能撑住海量数据。

LSM树:写优化的利器

LSM树(Log-Structured Merge-Tree)是另一条路。它牺牲了点读性能,换来了极致的写性能。

LSM树的核心思想是:先把数据写到内存里,攒够了再批量刷到磁盘。磁盘写入是顺序的,所以很快。

它的工作流程大致是:

  1. 写入先到内存的MemTable(一般用跳表实现)
  2. MemTable满了,冻结成不可变的MemTable,后台刷到磁盘变成SSTable
  3. 磁盘上的SSTable越来越多,后台线程做合并(Compaction)

Cassandra、HBase、LevelDB、RocksDB都用LSM树。我曾在日志系统里用过RocksDB,每秒写入几十万条毫无压力。但读的时候,如果数据在多个SSTable里,需要合并查询,性能就不如B+树了。

特性 B+树 LSM树
读性能 稳定,O(log n) 可能较差,需要合并多层
写性能 随机写,较慢 顺序写,极快
空间放大 较高(重复数据多)
写放大 较高(Compaction导致)
典型代表 MySQL InnoDB, PostgreSQL Cassandra, HBase, RocksDB

避坑指南:我曾经帮一个团队排查问题,他们用Cassandra做订单查询,结果读延迟经常飙到几秒。原因就是LSM树的读放大——数据分散在多个SSTable里,查询需要合并。后来加了布隆过滤器,才把无效的SSTable扫描过滤掉。所以选型时一定要想清楚:你的场景是读多还是写多?

聚簇索引与非聚簇索引

这个知识点,我见过太多人搞混了。其实一句话就能说清楚:聚簇索引的叶子节点存的是整行数据,非聚簇索引的叶子节点存的是主键值

聚簇索引

InnoDB的表,一定会有一个聚簇索引。如果你定义了主键,主键就是聚簇索引。如果你没定义主键,InnoDB会选一个唯一非空索引,或者自己生成一个隐藏的ROW_ID。

聚簇索引的特点:

  • 数据即索引,索引即数据。叶子节点就是数据页
  • 物理存储顺序和索引顺序一致。按主键范围查询时,数据是连续存放的,IO效率高
  • 二级索引(非聚簇索引)的叶子节点存的是主键值,不是数据地址

举个例子:

-- 假设有一张用户表
CREATE TABLE users (
    id INT PRIMARY KEY,      -- 聚簇索引
    name VARCHAR(50),
    age INT,
    INDEX idx_name (name)    -- 二级索引,非聚簇
);

-- 查询走聚簇索引
SELECT * FROM users WHERE id = 100;
-- 直接通过B+树找到id=100的叶子节点,数据就在那里

-- 查询走二级索引
SELECT * FROM users WHERE name = '张三';
-- 先通过idx_name找到name='张三'对应的主键id
-- 再通过主键id回表查询完整数据
-- 这就是所谓的"回表查询"

重要:回表查询意味着两次B+树查找。如果二级索引能覆盖所有需要的字段,就不需要回表。这就是「覆盖索引」的优化思路。

非聚簇索引

MyISAM引擎用的就是非聚簇索引。它的索引和数据是分开存储的:

  • .MYI文件存索引,叶子节点存的是数据行的磁盘地址
  • .MYD文件存数据,按插入顺序存放

非聚簇索引的叶子节点不存数据,只存一个指针。查询时先找到指针,再根据指针去数据文件里取数据。这其实也是一种「回表」,只不过回的是数据文件。

我个人习惯:生产环境几乎只用InnoDB。MyISAM不支持事务,不支持行锁,崩溃恢复也慢。除非是只读的报表场景,否则别碰它。

对比项 聚簇索引(InnoDB) 非聚簇索引(MyISAM)
数据存储 索引和数据在一起 索引和数据分开
二级索引 存主键值,需回表 存数据地址,需回数据文件
主键顺序 物理顺序和主键一致 物理顺序和插入顺序一致
范围查询 快,数据连续 较慢,数据分散
插入性能 主键乱序时可能页分裂 直接追加,较快

我的建议:设计表时,尽量用自增ID做主键。为什么?因为InnoDB的聚簇索引是按主键顺序存储的。如果用UUID这种随机值做主键,每次插入都可能触发页分裂,性能会急剧下降。我曾经优化过一个表,把UUID主键改成自增ID后,写入性能提升了3倍。

核心知识体系

下面这张图,是我梳理的本章知识脉络。建议你保存下来,后面几章都会用到。

索引的本质与数据结构 为什么需要索引? 核心数据结构 B+树 • 非叶子节点只存键 • 叶子节点存数据+链表 • 代表:MySQL InnoDB LSM树 • 内存MemTable+磁盘SSTable • 顺序写,写性能极强 • 代表:Cassandra, RocksDB 聚簇索引 vs 非聚簇索引 叶子节点存整行数据 叶子节点存主键值/指针

嗯,这一章的内容就到这里。记住一句话:理解索引的本质,就是理解数据是怎么存、怎么找的。B+树和LSM树各有千秋,聚簇和非聚簇各有适用场景。后面几章,我们会深入聊索引的具体使用策略和优化技巧。

本章要点回顾

  • 索引是数据库的目录,核心作用是快速定位和避免排序
  • B+树适合读多写少的场景,LSM树适合写多读少的场景
  • 聚簇索引的叶子节点存数据,非聚簇索引的叶子节点存主键值或指针
  • InnoDB必须有聚簇索引,建议用自增ID做主键

公众号:蓝海资料掘金营,微信deep3321