mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
5341 字
14 分钟
索引原理:加速查询的核心数据结构
2024-05-26

上一章中,我们剖析了数据如何被组织并写入磁盘——B 树页的分裂与合并、LSM 树的分层压缩、WAL 的崩溃恢复机制。存储引擎回答了”数据怎么存”的问题,但数据库的另一个核心问题是:数据怎么找?

一张表可能有上亿行数据,如果每次查询都从头扫到尾,性能将不可接受。索引正是为解决这一问题而生——它用额外的空间换取查询时间的急剧缩短。本章将从索引的根本动机出发,逐一剖析 B+ 树、哈希、位图、空间与全文索引的原理,再深入覆盖索引、索引下推、最左前缀等优化技巧,最后系统分析索引失效的根因与规避方法。

前置知识#

  • Ch02 存储引擎:B 树页结构是 B+ 树索引的物理基础
  • 数据结构基础:B+ 树、哈希表

一、为什么需要索引#

1.1 全表扫描的代价#

假设一张 orders 表有 1 亿行数据,每行约 200 字节,数据文件约 20 GB。现在执行以下查询:

SELECT * FROM orders WHERE user_id = 42;

如果没有索引,数据库只能从数据文件的第一页开始,逐行检查 user_id 是否等于 42。这就是全表扫描(Full Table Scan)——时间复杂度 O(n),需要读取全部数据页。

Note

全表扫描并非总是最差选择。当表很小、或查询需要返回大部分行时,全表扫描反而比索引查找更快——因为索引查找需要额外的随机 I/O。优化器会根据统计信息自动选择,详见查询处理与优化

1.2 索引加速的原理#

索引的本质是一种空间换时间的数据结构:在原始数据之外,维护一份有序的、更小的副本,使得查找不必遍历全部数据。

维度全表扫描索引查找
时间复杂度O(n)O(log n) 或 O(1)
I/O 模式顺序读(大量页)随机读(少量页)
1 亿行约需读取~20 GB 数据34 次磁盘 I/O(B+ 树 3 层)
适用场景返回大量行、小表精确匹配、范围查询、排序
额外开销占用存储空间、写入时需维护

以 B+ 树为例,1 亿行数据只需 34 层即可容纳,每次查找最多 34 次磁盘 I/O——从 20 GB 的顺序扫描骤降到个位数的随机读取,这就是索引的威力。

1.3 索引的分类体系#

不同查询场景需要不同类型的索引。以下是数据库索引的完整分类:

graph TB INDEX["数据库索引"] INDEX --> BY_STRUCTURE["按数据结构"] INDEX --> BY_PHYSICAL["按物理存储"] INDEX --> BY_LOGIC["按逻辑语义"] BY_STRUCTURE --> BPTREE["B+ 树索引"] BY_STRUCTURE --> HASH["哈希索引"] BY_STRUCTURE --> BITMAP["位图索引"] BY_STRUCTURE --> RTREE["空间索引(R 树)"] BY_STRUCTURE --> FTS["全文索引(倒排)"] BY_PHYSICAL --> CLUSTERED["聚簇索引"] BY_PHYSICAL --> NON_CLUSTERED["二级索引(非聚簇)"] BY_LOGIC --> PRIMARY["主键索引"] BY_LOGIC --> UNIQUE["唯一索引"] BY_LOGIC --> COMPOSITE["联合索引"] BY_LOGIC --> COVERING["覆盖索引"] BY_LOGIC --> PREFIX["前缀索引"] BY_LOGIC --> PARTIAL["部分索引"] style INDEX fill:#e3f2fd,stroke:#1565c0 style BY_STRUCTURE fill:#e8f5e9,stroke:#2e7d32 style BY_PHYSICAL fill:#fff3e0,stroke:#e65100 style BY_LOGIC fill:#fce4ec,stroke:#c62828

接下来,将按数据结构逐一深入。

二、B+ 树索引#

B+ 树是关系型数据库中最广泛使用的索引结构。MySQL InnoDB、PostgreSQL(默认 B-tree)、Oracle、SQL Server 的默认索引均基于 B+ 树或其变体。

2.1 B+ 树的结构#

B+ 树是一种多路平衡搜索树,其核心特征是:所有数据都存储在叶子节点,内部节点只存键值用于路由。叶子节点通过双向链表串联,支持高效的范围扫描。

graph TB subgraph 内部节点["内部节点(只存键值,用于路由)"] ROOT["[30 | 60]"] end subgraph 叶子层["叶子节点(存完整数据,双向链表串联)"] L1["[10|20|30] →"] L2["[35|45|60] →"] L3["[70|85|99]"] end ROOT -->|"key < 30"| L1 ROOT -->|"30 ≤ key < 60"| L2 ROOT -->|"key ≥ 60"| L3 L1 <-->|"双向链表"| L2 L2 <-->|"双向链表"| L3 style ROOT fill:#bbdefb,stroke:#1565c0 style L1 fill:#c8e6c9,stroke:#2e7d32 style L2 fill:#c8e6c9,stroke:#2e7d32 style L3 fill:#c8e6c9,stroke:#2e7d32

B+ 树的关键参数:

参数含义典型值
阶(Order)每个节点最多拥有的子节点数InnoDB 约 1200(16 KB 页 / 12 字节键)
层数根到叶子的路径长度3~4 层可容纳数亿行
叶子链表叶子节点间的双向链表支持范围扫描的 O(k) 遍历
填充因子节点实际使用率通常 50%~100%,影响分裂频率
Tip

B+ 树的层数与数据量呈对数关系。假设每个内部节点有 1000 个子节点指针,3 层 B+ 树可容纳 10^9(10 亿)行,4 层可容纳 10^12(万亿)行。这意味着即使数据量从百万增长到十亿,查找开销只增加一次 I/O。

2.2 聚簇索引 vs 二级索引#

**聚簇索引(Clustered Index)**将索引与数据合二为一——叶子节点直接存储完整的行数据。一张表只能有一个聚簇索引,因为数据只能按一种方式物理排列。

二级索引(Secondary Index)的叶子节点不存储行数据,而是存储主键值。通过二级索引查找数据时,需要先在二级索引中找到主键,再回到聚簇索引中查找完整行——这个过程称为回表

-- InnoDB 中,主键就是聚簇索引
CREATE TABLE users (
id BIGINT PRIMARY KEY, -- 聚簇索引
email VARCHAR(255),
name VARCHAR(100),
age INT,
INDEX idx_email (email) -- 二级索引,叶子存 id 值
);
-- 通过二级索引查找需要回表
SELECT * FROM users WHERE email = 'alice@example.com';
-- 步骤:idx_email 找到 id=5 → 聚簇索引找 id=5 的完整行
维度聚簇索引二级索引
叶子节点存储完整行数据主键值
每表数量1 个多个
范围查询高效(数据物理有序)需回表,可能大量随机 I/O
插入顺序按主键有序插入最优随机插入,可能页分裂
存储开销无额外索引空间需额外存储索引 + 主键
Warning

如果二级索引的回表操作量很大(例如范围查询返回大量行),优化器可能放弃索引而选择全表扫描。这就是为什么”索引存在但不被使用”的常见原因之一——回表成本超过了全表扫描成本。

2.3 联合索引与最左前缀#

联合索引是在多个列上建立的 B+ 树索引,其排序规则为:先按第一列排序,第一列相同则按第二列排序,依此类推。这决定了联合索引的最左前缀原则——查询条件必须从索引的最左列开始,才能有效利用索引。

-- 联合索引
CREATE INDEX idx_status_created ON orders (status, created_at);
-- 可以使用索引(最左前缀匹配)
SELECT * FROM orders WHERE status = 'shipped';
SELECT * FROM orders WHERE status = 'shipped' AND created_at > '2026-01-01';
-- 无法使用索引(跳过了最左列 status)
SELECT * FROM orders WHERE created_at > '2026-01-01';
-- 部分使用索引(仅使用 status 列,created_at 无法用于过滤)
SELECT * FROM orders WHERE status = 'shipped' AND amount > 100;

联合索引的列顺序至关重要。一般原则是:高选择性(基数大)的列放前面,等值查询的列放前面,范围查询的列放后面

2.4 索引选择性与基数#

**选择性(Selectivity)**衡量索引的区分度,定义为:

选择性 = DISTINCT(col) / COUNT(*)

选择性越高,索引过滤效果越好。选择性为 1(如主键、唯一列)时索引效率最高;选择性接近 0(如性别列只有 2 个值)时,索引几乎无法过滤数据。

-- 查看列的基数(不同值的数量)
-- MySQL
SHOW INDEX FROM orders;
-- PostgreSQL
SELECT attname, n_distinct FROM pg_stats WHERE tablename = 'orders';
选择性范围示例索引效果
≈ 1.0主键、唯一 ID极佳,一次定位
0.1 ~ 0.9用户名、邮箱良好,大幅过滤
< 0.1性别、状态差,可能不如全表扫描
≈ 0常量列无效,索引无意义

三、哈希索引#

3.1 精确匹配的 O(1) 利器#

哈希索引使用哈希表实现,对等值查询具有 O(1) 的时间复杂度。其原理是:对索引列的值计算哈希,将哈希值映射到哈希槽,槽内存储指向数据行的指针。

-- PostgreSQL 哈希索引
CREATE INDEX idx_user_email_hash ON users USING hash (email);
-- 哈希索引擅长:精确匹配
SELECT * FROM users WHERE email = 'alice@example.com';
-- 哈希索引不支持:范围查询
SELECT * FROM users WHERE email > 'alice@example.com'; -- 无法使用索引

3.2 哈希索引的局限性#

能力B+ 树索引哈希索引
等值查询O(log n)O(1)
范围查询O(log n + k)不支持
排序天然有序无序
最左前缀支持不支持
模糊匹配支持前缀 LIKE不支持
哈希冲突需处理

哈希索引的核心问题是数据无序——哈希函数将相邻的键值映射到完全不同的槽位,因此无法支持范围查询、排序和前缀匹配。

3.3 MySQL 自适应哈希索引#

InnoDB 的**自适应哈希索引(Adaptive Hash Index, AHI)**是一种自动优化机制:当 InnoDB 监控到某些 B+ 树索引页被频繁访问时,会在内存中自动为这些页构建哈希索引,将 O(log n) 的 B+ 树查找加速为 O(1) 的哈希查找。

-- 查看自适应哈希索引状态
SHOW ENGINE INNODB STATUS\G
-- 开启/关闭自适应哈希索引
SET GLOBAL innodb_adaptive_hash_index = ON;

AHI 的适用场景与限制:

  • 适用:高并发等值查询、B+ 树非叶子页被反复访问
  • 不适用:范围查询为主、查询模式频繁变化
  • 风险:AHI 的维护需要加锁,在高并发写入场景下可能成为争用热点

3.4 PostgreSQL 哈希索引#

PostgreSQL 从 10 版本开始大幅改进了哈希索引(此前因 WAL 支持不完善而不推荐使用)。现在的 PG 哈希索引:

  • 完整支持 WAL 日志,崩溃安全
  • 支持多并发扫描
  • 适用于等值查询密集且不需要范围查询的场景
-- PG 哈希索引适用于等值查询为主的场景
CREATE INDEX idx_session_token ON sessions USING hash (token);
-- 对比 B-tree 索引,哈希索引在等值查询上更紧凑
-- 但 B-tree 仍然是默认选择,因为其通用性更强

四、位图索引#

4.1 位图索引的原理#

位图索引为索引列的每个不同值创建一个位图(Bitmap),位图的每一位对应表中的一行——如果该行的值等于该不同值,则位为 1,否则为 0。

graph TB subgraph 数据表["员工表(8 行)"] direction LR R1["1: 研发"] R2["2: 市场"] R3["3: 研发"] R4["4: 财务"] R5["5: 研发"] R6["6: 市场"] R7["7: 财务"] R8["8: 研发"] end subgraph 位图索引["部门列的位图索引"] BM_RD["研发: 1 0 1 0 1 0 0 1"] BM_MK["市场: 0 1 0 0 0 1 0 0"] BM_FN["财务: 0 0 0 1 0 0 1 0"] end R1 --> BM_RD R2 --> BM_MK R3 --> BM_RD R4 --> BM_FN style 数据表 fill:#e3f2fd,stroke:#1565c0 style 位图索引 fill:#e8f5e9,stroke:#2e7d32

位图索引的核心优势在于多条件 AND/OR 查询:只需对多个位图进行位运算(AND/OR),即可快速得到结果集。

-- Oracle 位图索引
CREATE BITMAP INDEX idx_emp_dept ON employees (department);
-- 多条件查询:位图 AND 运算
-- 查找"研发部门 AND 资深"的员工
SELECT /*+ INDEX(employees idx_emp_dept idx_emp_level) */
* FROM employees
WHERE department = '研发' AND level = '资深';
-- 执行:研发位图 AND 资深位图 → 结果位图 → 行号

4.2 低基数列的优势#

位图索引在低基数列(不同值很少,如性别、状态、部门)上效果最佳。对比 B+ 树索引:

维度B+ 树索引位图索引
低基数列效果差(选择性低,大量重复值)优(位图紧凑,位运算高效)
高基数列效果优(选择性高,精确过滤)差(位图过多,空间膨胀)
多条件 AND/OR需合并多个索引扫描位运算极快
并发写入行级锁,冲突少段级锁,冲突多
存储空间与数据量成正比与 基数×行数 成正比
Warning

位图索引在 OLTP 场景中需谨慎使用。由于位图索引的锁粒度较粗(一个位图段覆盖多行),高并发写入时容易产生锁冲突。位图索引更适合 OLAP 场景——大量读取、少量写入、多条件组合过滤。

4.3 Oracle 位图索引与 PostgreSQL 部分索引#

Oracle 位图索引是生产环境中最成熟的位图索引实现,广泛应用于数据仓库:

-- Oracle:为低基数的每个列建位图索引
CREATE BITMAP INDEX idx_sales_region ON sales (region);
CREATE BITMAP INDEX idx_sales_channel ON sales (channel);
CREATE BITMAP INDEX idx_sales_quarter ON sales (quarter);
-- 星型查询:位图索引的杀手级场景
SELECT region, channel, SUM(amount)
FROM sales
WHERE region = '华东' AND channel = '线上' AND quarter = 'Q1'
GROUP BY region, channel;
-- Oracle 自动进行位图 AND → 行号 → 事实表查找

**PostgreSQL 部分索引(Partial Index)**是一种不同的思路——不是对列的所有值建索引,而是只对满足条件的子集建索引:

-- PostgreSQL:只对活跃订单建索引
CREATE INDEX idx_orders_active ON orders (user_id)
WHERE status IN ('pending', 'processing', 'shipped');
-- 查询自动使用部分索引
SELECT * FROM orders
WHERE user_id = 42 AND status = 'pending';

部分索引的优势:索引更小、维护成本更低、缓存命中率更高。适用于”只关心数据的某个子集”的场景。

五、空间索引与全文索引#

5.1 空间索引:R 树与 GiST#

空间数据(地理坐标、几何图形)的查询模式与普通数据截然不同——“查找距离我 3 公里内的所有餐厅”涉及二维范围搜索,B+ 树无法高效处理。

R 树(R-Tree)是空间索引的经典数据结构,其核心思想是最小外接矩形(MBR):将空间对象用矩形包围,内部节点存储子矩形的 MBR,查询时通过矩形相交判断快速剪枝。

-- MySQL 空间索引
CREATE TABLE locations (
id INT PRIMARY KEY,
name VARCHAR(100),
point GEOMETRY NOT NULL SRID 4326,
SPATIAL INDEX idx_point (point)
);
-- 空间范围查询
SELECT name, ST_Distance_Sphere(point, ST_GeomFromText('POINT(121.47 31.23)', 4326)) AS dist
FROM locations
WHERE ST_Contains(
ST_Buffer(ST_GeomFromText('POINT(121.47 31.23)', 4326), 0.03),
point
);

**PostgreSQL GiST(Generalized Search Tree)**是一种更通用的索引框架,R 树只是 GiST 的一种实现。GiST 允许自定义数据类型的索引策略,PostGIS 就基于 GiST 实现空间索引:

-- PostgreSQL + PostGIS 空间索引
CREATE INDEX idx_locations_geom ON locations USING gist (geom);
-- KNN 查询(最近邻,GiST 原生支持排序)
SELECT name, geom <-> ST_MakePoint(121.47, 31.23) AS dist
FROM locations
ORDER BY dist
LIMIT 10;

5.2 全文索引:倒排原理#

全文索引的核心是倒排索引(Inverted Index)——不是”文档 → 词”的正向映射,而是”词 → 文档列表”的反向映射。

倒排索引的构建过程:

  1. 分词:将文档拆分为词元(Token)
  2. 归一化:词干提取、大小写统一、同义词映射
  3. 构建倒排表:每个词指向包含该词的文档列表(Posting List)
  4. 记录位置:可选地记录词在文档中的位置,支持短语查询
-- MySQL 全文索引
CREATE TABLE articles (
id INT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
FULLTEXT INDEX ft_content (title, content) WITH PARSER ngram
);
-- 全文搜索
SELECT title,
MATCH(title, content) AGAINST('数据库 索引' IN NATURAL LANGUAGE MODE) AS score
FROM articles
WHERE MATCH(title, content) AGAINST('数据库 索引' IN NATURAL LANGUAGE MODE)
ORDER BY score DESC;

5.3 PostgreSQL GiST 与 GIN 简介#

PostgreSQL 提供两种全文索引实现,各有侧重:

维度GiSTGIN(Generalized Inverted Index)
构建速度慢(但可并发构建)
查询速度较慢
更新成本高(需更新倒排列表)
适用场景数据频繁更新数据相对静态、查询密集
全文搜索支持推荐(默认选择)
-- PostgreSQL GIN 全文索引(推荐)
CREATE INDEX idx_articles_fts ON articles USING gin (to_tsvector('simple', title || ' ' || content));
-- 全文搜索
SELECT title, ts_rank(to_tsvector('simple', title || ' ' || content), query) AS rank
FROM articles, to_tsquery('simple', '数据库 & 索引') query
WHERE to_tsvector('simple', title || ' ' || content) @@ query
ORDER BY rank DESC;
Note

PostgreSQL 的 GIN 索引本质上就是倒排索引的通用实现——不仅用于全文搜索,还支持 JSONB、数组等数据类型的索引。更多细节见PostgreSQL 深入

六、索引优化技巧#

6.1 覆盖索引:避免回表#

当查询所需的所有列都包含在索引中时,数据库可以直接从索引返回结果,无需回表——这就是覆盖索引

-- 联合索引
CREATE INDEX idx_user_email_name ON users (email, name);
-- 覆盖索引:查询只需要 email 和 name,索引中都有
SELECT email, name FROM users WHERE email = 'alice@example.com';
-- 非覆盖索引:还需要 age,必须回表
SELECT email, name, age FROM users WHERE email = 'alice@example.com';

覆盖索引的判断方法:在 EXPLAIN 输出中,Extra 列出现 Using index 即表示使用了覆盖索引。

-- MySQL EXPLAIN 验证覆盖索引
EXPLAIN SELECT email, name FROM users WHERE email = 'alice@example.com';
-- Extra: Using index ← 覆盖索引,无需回表
-- PostgreSQL EXPLAIN 验证
EXPLAIN SELECT email, name FROM users WHERE email = 'alice@example.com';
-- Index Only Scan using idx_user_email_name ← 仅索引扫描

6.2 索引下推(Index Condition Pushdown, ICP)#

索引下推是 MySQL 5.6 引入的优化:将部分 WHERE 条件在索引遍历时就进行过滤,而非等到回表后再过滤。

-- 联合索引 (email, name)
-- 查询条件使用了 email(索引第一列)和 name(索引第二列)
SELECT * FROM users WHERE email LIKE 'ali%' AND name LIKE '%ce';

无 ICP:先通过 email LIKE 'ali%' 从索引找到所有匹配行 → 回表获取完整行 → 再过滤 name LIKE '%ce'

有 ICP:在索引遍历时就检查 name LIKE '%ce' → 只对同时满足两个条件的行回表

-- 查看 ICP 是否生效
EXPLAIN SELECT * FROM users WHERE email LIKE 'ali%' AND name LIKE '%ce';
-- Extra: Using index condition ← 索引下推生效

6.3 前缀索引#

对于长字符串列(如 VARCHAR(500) 的 URL),完整列作为索引键会占用大量空间。前缀索引只取列的前 N 个字符作为索引键:

-- 前缀索引:只取 URL 前 20 个字符
CREATE INDEX idx_url_prefix ON pages (url(20));
-- 选择前缀长度的原则:选择性接近完整列即可
SELECT
COUNT(DISTINCT url) / COUNT(*) AS full_selectivity,
COUNT(DISTINCT LEFT(url, 10)) / COUNT(*) AS prefix_10,
COUNT(DISTINCT LEFT(url, 20)) / COUNT(*) AS prefix_20,
COUNT(DISTINCT LEFT(url, 30)) / COUNT(*) AS prefix_30
FROM pages;
-- 选择使选择性接近 full_selectivity 的最短前缀

前缀索引的局限:无法用于覆盖索引(因为索引中不存完整值)、无法用于 ORDER BY / GROUP BY。

6.4 索引列顺序选择#

联合索引的列顺序直接影响索引的可用性和效率。核心原则:

  1. 等值条件列在前,范围条件列在后
  2. 高选择性列在前(在等值条件列之间)
  3. 考虑查询频率:最常查询的列组合优先
-- 场景:订单表有 status、user_id、created_at 三列
-- 查询模式 1:WHERE user_id = ? AND status = ?(高频)
-- 查询模式 2:WHERE user_id = ? AND created_at > ?(中频)
-- 查询模式 3:WHERE status = ? AND created_at > ?(低频)
-- 最优索引:user_id 在前(等值+高频),status 在中(等值),created_at 在后(范围)
CREATE INDEX idx_uid_status_created ON orders (user_id, status, created_at);
-- 这个索引可以覆盖查询模式 1 和 2
-- 查询模式 3 需要单独的索引
CREATE INDEX idx_status_created ON orders (status, created_at);

6.5 多列索引策略#

面对多列查询,是建一个联合索引还是多个单列索引?答案取决于查询模式:

flowchart TD START["多列查询场景"] --> Q1{"查询模式是否固定?"} Q1 -->|"是:总是按相同列组合查询"| UNION["建一个联合索引<br/>(最左前缀覆盖多种查询)"] Q1 -->|"否:列组合多变"| Q2{"是否有高频组合?"} Q2 -->|"是"| BOTH["联合索引覆盖高频组合<br/>+ 单列索引覆盖低频查询"] Q2 -->|"否"| MULTI["多个单列索引<br/>让优化器做索引合并"] MULTI --> WARN[" 索引合并效率通常<br/>不如联合索引"] BOTH --> CHECK["验证:EXPLAIN 确认<br/>索引被正确使用"] style START fill:#e3f2fd,stroke:#1565c0 style UNION fill:#c8e6c9,stroke:#2e7d32 style BOTH fill:#fff3e0,stroke:#e65100 style MULTI fill:#fce4ec,stroke:#c62828 style WARN fill:#ffcdd2,stroke:#c62828

七、索引失效分析#

索引存在但未被使用,是数据库性能问题中最常见的陷阱之一。以下是索引失效的六大场景,每种给出错误写法和正确写法。

7.1 函数/运算导致失效#

对索引列使用函数或算术运算,会导致优化器无法使用索引。

-- 错误:对 created_at 使用函数
SELECT * FROM orders WHERE YEAR(created_at) = 2026;
-- 正确:使用范围查询
SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
-- 错误:对 user_id 进行算术运算
SELECT * FROM orders WHERE user_id + 1 = 43;
-- 正确:将运算移到等号另一侧
SELECT * FROM orders WHERE user_id = 42;

7.2 隐式类型转换#

当查询条件的值类型与列类型不匹配时,数据库可能进行隐式类型转换,导致索引失效。

-- 假设 user_id 是 VARCHAR 类型
-- 错误:传入整数,数据库将 user_id 隐式转为数字
SELECT * FROM users WHERE user_id = 123;
-- 正确:传入字符串
SELECT * FROM users WHERE user_id = '123';
-- 假设 phone 是 VARCHAR 类型
-- 错误:传入数字
SELECT * FROM users WHERE phone = 13800138000;
-- 正确:传入字符串
SELECT * FROM users WHERE phone = '13800138000';
Note

MySQL 的隐式转换规则是:将字符串转为数字进行比较。这意味着对 VARCHAR 列传入整数时,MySQL 会对列值做类型转换(而非对常量做转换),从而导致索引失效。PostgreSQL 的行为不同——它通常会对常量做转换,索引可能仍然有效。

7.3 LIKE 前缀通配符#

LIKE 查询以通配符 % 开头时,B+ 树无法利用有序性进行定位。

-- 错误:前缀通配符,无法使用索引
SELECT * FROM users WHERE name LIKE '%lice';
-- 正确:前缀匹配,可以使用索引
SELECT * FROM users WHERE name LIKE 'ali%';
-- 如需全文搜索,使用全文索引
SELECT * FROM users WHERE MATCH(name) AGAINST('lice');

7.4 OR 条件#

OR 连接的条件中,如果有一个条件列没有索引,整个 OR 子句都无法使用索引。

-- 假设 email 有索引,name 没有索引
-- 错误:name 无索引导致整个 OR 无法使用索引
SELECT * FROM users WHERE email = 'alice@example.com' OR name = 'Alice';
-- 正确方案 1:为 name 也建索引
CREATE INDEX idx_name ON users (name);
SELECT * FROM users WHERE email = 'alice@example.com' OR name = 'Alice';
-- 优化器可能使用索引合并(Index Merge)
-- 正确方案 2:用 UNION 替代 OR
SELECT * FROM users WHERE email = 'alice@example.com'
UNION
SELECT * FROM users WHERE name = 'Alice';

7.5 违反最左前缀#

联合索引的最左前缀原则要求查询条件必须从索引的最左列开始。

-- 索引:idx_status_created (status, created_at)
-- 错误:跳过 status,直接查 created_at
SELECT * FROM orders WHERE created_at > '2026-01-01';
-- 正确:包含最左列 status
SELECT * FROM orders WHERE status = 'shipped' AND created_at > '2026-01-01';
-- 错误:status 用范围查询,created_at 无法利用索引排序
SELECT * FROM orders WHERE status > 'pending' ORDER BY created_at;
-- 正确:status 用等值查询,created_at 可利用索引排序
SELECT * FROM orders WHERE status = 'shipped' ORDER BY created_at;

7.6 索引失效决策树#

遇到”索引存在但不被使用”的问题时,按以下决策树逐项排查:

flowchart TD START["索引未被使用"] --> FUNC{"索引列是否<br/>使用了函数/运算?"} FUNC -->|"是"| FIX1["将运算移到等号另一侧<br/>或使用范围查询"] FUNC -->|"否"| TYPE{"是否存在<br/>隐式类型转换?"} TYPE -->|"是"| FIX2["确保查询值类型<br/>与列类型一致"] TYPE -->|"否"| LIKE{"LIKE 是否<br/>以 % 开头?"} LIKE -->|"是"| FIX3["改用前缀匹配<br/>或全文索引"] LIKE -->|"否"| OR{"OR 中是否有<br/>无索引列?"} OR -->|"是"| FIX4["为 OR 列建索引<br/>或改用 UNION"] OR -->|"否"| LEFT{"是否违反<br/>最左前缀?"} LEFT -->|"是"| FIX5["调整查询条件<br/>或索引列顺序"] LEFT -->|"否"| COST{"优化器判断<br/>回表成本过高?"} COST -->|"是"| FIX6["考虑覆盖索引<br/>或强制索引提示"] COST -->|"否"| OTHER["检查统计信息<br/>是否过期"] style START fill:#ffcdd2,stroke:#c62828 style FIX1 fill:#c8e6c9,stroke:#2e7d32 style FIX2 fill:#c8e6c9,stroke:#2e7d32 style FIX3 fill:#c8e6c9,stroke:#2e7d32 style FIX4 fill:#c8e6c9,stroke:#2e7d32 style FIX5 fill:#c8e6c9,stroke:#2e7d32 style FIX6 fill:#c8e6c9,stroke:#2e7d32 style OTHER fill:#fff9c4,stroke:#f9a825

八、跨数据库索引对比#

不同数据库的索引实现存在显著差异。以下对比 MySQL、PostgreSQL 和 Redis 三种代表性数据库的索引机制:

维度MySQL (InnoDB)PostgreSQLRedis
默认索引结构B+ 树B-tree(Lehman-Yao 变体)无默认索引
聚簇索引有(主键即聚簇索引)无(堆表组织)
哈希索引自适应哈希索引(内存中)支持(PG 10+)原生哈希表
全文索引FULLTEXT + ngram 分词GIN + tsvector
空间索引R-Tree(SPATIAL)GiST(PostGIS)GEOADD + GeoHash
位图索引不支持不支持(有部分索引)
部分索引不支持支持(WHERE 子句)
表达式索引不支持(8.0 支持函数索引)支持
索引覆盖Using indexIndex Only Scan
索引下推支持(ICP)不需要(堆表无回表概念)
BRIN 索引不支持支持(块范围索引)
并发索引构建Online DDLCONCURRENTLY 选项
Note

PostgreSQL 没有”聚簇索引”的概念——它的表是堆表(Heap Table),数据按插入顺序存储,所有索引都是二级索引,叶子节点存储的是 CTID(行标识符)而非主键。这意味着 PG 的索引查找总是需要一次额外的堆表访问,但也避免了聚簇索引的页分裂问题。详见MySQL 深入PostgreSQL 深入

Redis 的索引机制#

Redis 作为内存数据库,其”索引”概念与传统数据库不同:

  • 原生数据结构即索引:Sorted Set 的跳表、Hash 的哈希表、List 的压缩列表/快速列表——每种数据结构本身就是一种索引
  • Geo 索引:基于 Sorted Set + GeoHash 实现,支持范围查询和距离计算
  • 无 B+ 树:内存中随机访问代价极低,不需要磁盘优化的 B+ 树结构
# Redis Geo 索引示例
GEOADD locations 121.47 31.23 "上海站" 121.48 31.24 "南京路"
GEORADIUS locations 121.47 31.23 3 km COUNT 10

·附、实践:PostgreSQL 索引类型对比#

本节在 PostgreSQL 中创建不同类型的索引,对比它们的大小和适用场景。需要 PostgreSQL 环境。

1. 创建测试表和索引#

CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
price NUMERIC,
tags TEXT[],
description TEXT
);
INSERT INTO products (name, price, tags, description) VALUES
('Laptop', 999.99, ARRAY['electronics', 'computer'], 'A powerful laptop'),
('Phone', 699.99, ARRAY['electronics', 'mobile'], 'A smart phone'),
('Tablet', 399.99, ARRAY['electronics', 'mobile'], 'A tablet device'),
('Book', 29.99, ARRAY['education', 'reading'], 'A good book'),
('Pen', 1.99, ARRAY['education', 'writing'], 'A simple pen');
-- B-tree 索引(默认,适合范围查询和排序)
CREATE INDEX idx_products_price ON products USING btree (price);
-- Hash 索引(只适合等值查询)
CREATE INDEX idx_products_name ON products USING hash (name);
-- GIN 索引(适合数组/全文搜索)
CREATE INDEX idx_products_tags ON products USING gin (tags);
-- BRIN 索引(适合大表的范围块索引)
CREATE INDEX idx_products_price_brin ON products USING brin (price);

2. 查看索引类型和大小#

SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'products';
indexname | indexdef
-------------------------+----------------------------------------------------------------------------
products_pkey | CREATE UNIQUE INDEX products_pkey ON public.products USING btree (id)
idx_products_price | CREATE INDEX idx_products_price ON public.products USING btree (price)
idx_products_name | CREATE INDEX idx_products_name ON public.products USING hash (name)
idx_products_tags | CREATE INDEX idx_products_tags ON public.products USING gin (tags)
idx_products_price_brin | CREATE INDEX idx_products_price_brin ON public.products USING brin (price)
SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) as size
FROM pg_indexes WHERE tablename = 'products';
indexname | size
-------------------------+-------
products_pkey | 16 kB
idx_products_price | 16 kB
idx_products_name | 32 kB
idx_products_tags | 16 kB
idx_products_price_brin | 24 kB

关键观察

  • Hash 索引最大(32 kB):Hash 索引的桶数量是固定的,即使数据量很小也占用较多空间
  • BRIN 索引最小(24 kB):BRIN 只存储每个数据块的统计信息(最小值/最大值),空间效率极高
  • B-tree 索引适中(16 kB):B-tree 是最通用的选择,支持等值、范围、排序

注意:在小数据集上索引大小差异不明显。当数据量达到百万级时,BRIN 索引可能只有 B-tree 索引的 1/100 大小,但查询精度也相应降低。

九、总结#

索引是数据库性能优化的基石。本章从”为什么需要索引”出发,系统梳理了索引的核心知识:

  1. B+ 树索引是关系型数据库的默认选择,3~4 层即可容纳数亿数据,支持等值查询、范围查询和排序。理解聚簇索引与二级索引的区别、联合索引的最左前缀原则、索引选择性的计算,是索引设计的基础。

  2. 哈希索引在等值查询上具有 O(1) 优势,但不支持范围查询和排序。MySQL 的自适应哈希索引是自动优化,PostgreSQL 的哈希索引适合特定场景。

  3. 位图索引是 OLAP 场景的利器,低基数列上的位图运算极快,但高并发写入时锁冲突严重。PostgreSQL 的部分索引提供了另一种思路——只索引需要的子集。

  4. 空间索引与全文索引解决了特殊数据类型的查询需求。R 树/GiST 处理空间数据,倒排索引处理全文搜索,PostgreSQL 的 GIN 是通用倒排索引框架。

  5. 索引优化的核心技巧:覆盖索引避免回表、索引下推提前过滤、前缀索引节省空间、合理选择联合索引列顺序。

  6. 索引失效是生产环境最常见的性能陷阱——函数运算、隐式转换、LIKE 通配符、OR 条件、违反最左前缀,每种都有明确的规避方法。

索引设计没有银弹——它始终在查询性能写入开销之间权衡。下一章事务与并发控制将从另一个维度审视数据库:如何在并发访问下保证数据的正确性。而关于优化器如何选择执行计划,将在查询处理与优化中深入探讨。MySQL 和 PostgreSQL 各自的索引实现细节,分别在MySQL 深入PostgreSQL 深入中展开。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

索引原理:加速查询的核心数据结构
https://blog.souloss.com/posts/database/indexing-principles/
作者
Souloss
发布于
2024-05-26
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时