跳到主要内容

索引简介

索引(index)是数据库为加快数据查找而维护的一种辅助结构。它类似书籍末尾的目录:先通过索引定位可能的页码,再读取真正的数据,而不必逐行检查整本书。

索引不是“越多越好”。它通常能加速 SELECT、UPDATE 和 DELETE 的定位,却会增加插入、修改、删除时的维护成本,并占用额外存储空间。设计索引的核心,是让常见查询少读数据,同时接受合理的写入和维护开销。

本文以通用 SQL 介绍概念。CREATE INDEX 的选项、索引组织方式、优化器规则和在线创建能力都可能因数据库产品及版本而不同。

没有索引时会发生什么​

假设表中有一百万名用户,查询某个邮箱:

SELECT id, name
FROM users
WHERE email = 'alice@example.com';

如果 email 没有可用索引,数据库可能进行全表扫描(table scan):依次检查每一行的 email。数据量越大,读取和比较的成本通常越高。

如果为 email 建立索引,数据库可以先在索引中找到匹配值,再根据索引保存的行定位信息读取数据:

CREATE INDEX idx_users_email ON users (email);

实际是否使用索引由优化器决定。表很小、匹配行很多,或统计信息显示全表扫描更便宜时,即使存在索引也可能不使用。

索引大致如何工作​

多数关系型数据库使用树状结构(常见为 B-tree 或其变体)保存索引键,并让键保持有序。一次典型的索引查找可以分为:

  1. 从索引根节点开始,逐层缩小键的范围。
  2. 在叶子节点找到目标键或目标范围。
  3. 根据叶子节点中的行定位信息读取表数据。

因此,索引特别适合等值查找和有序范围查找:

WHERE user_id = 42
WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'
ORDER BY created_at DESC

索引并不保证查询一定更快。若条件会匹配表中很大比例的行,数据库可能认为顺序读取整张表更高效。

常见索引类型​

单列索引​

只包含一个列,适合经常按该列筛选、连接或排序的查询:

CREATE INDEX idx_orders_customer_id ON orders (customer_id);

复合索引​

包含多个列,列的顺序非常重要:

CREATE INDEX idx_orders_customer_status
ON orders (customer_id, status);

它通常最适合同时使用前缀列的查询,例如:

WHERE customer_id = 42
WHERE customer_id = 42 AND status = 'paid'

只按 status 查询时,数据库通常不能像使用以 status 开头的索引一样高效地利用这个索引。这称为最左前缀原则,但具体能力仍以目标数据库的优化器为准。

唯一索引​

唯一索引既用于查找,也用于保证索引列(或列组合)不重复:

CREATE UNIQUE INDEX uq_users_email ON users (email);

唯一性约束通常比手工检查更可靠,因为它由数据库在并发写入时统一保证。主键和 UNIQUE 约束通常会自动创建或依赖一个唯一索引,但实现细节因数据库而异。

覆盖索引​

如果索引已经包含查询所需的筛选列和返回列,数据库可能只读取索引而无需回表,这种情况常称为覆盖索引:

CREATE INDEX idx_orders_customer_created
ON orders (customer_id, created_at, id);

覆盖索引能减少数据页读取,但索引会更宽、占用更多空间,也会增加写入维护成本。不要为了覆盖少量低频查询而盲目把所有列塞入索引。

部分、表达式与全文索引​

部分索引只为满足条件的行建立索引,表达式索引为计算结果建立索引,全文索引用于自然语言文本搜索。这些能力通常是数据库方言,示意如下:

-- 具体语法和可索引表达式取决于数据库
CREATE INDEX idx_tasks_open ON tasks (priority) WHERE completed_at IS NULL;
CREATE INDEX idx_users_lower_email ON users (LOWER(email));

查询中的表达式需要与索引定义匹配;例如只给 email 建普通索引,通常不能直接高效支持 WHERE LOWER(email) = ...。

如何设计索引​

从真实查询出发​

先收集慢查询、访问频率和执行计划,再决定是否建索引。重点关注:

  • WHERE 中经常出现的筛选列;
  • JOIN 中用于匹配的列;
  • ORDER BY、GROUP BY 中反复使用的列组合;
  • 选择性较高的条件,即能排除大量行的条件。

不要只看列的基数。低基数列(例如只有“是/否”两种值)单独建索引常常收益有限,但和时间、租户等列组成复合索引后可能有价值。

确定复合索引的列顺序​

常见思路是先放等值筛选列,再放范围筛选列,最后考虑排序或覆盖所需的列:

-- 适合按租户和状态筛选,再按创建时间查看最近记录
CREATE INDEX idx_orders_tenant_status_created
ON orders (tenant_id, status, created_at DESC);

这不是绝对规则。列顺序还要结合查询比例、数据分布、排序方向和数据库优化器验证。对于经常变化的查询,不应仅凭经验创建大量组合。

保持条件可使用索引​

对索引列套函数、进行隐式类型转换或以通配符开头,可能使普通索引难以使用:

-- 可能无法直接使用普通 email 索引
WHERE LOWER(email) = 'alice@example.com'
WHERE CAST(user_id AS VARCHAR(20)) = '42'
WHERE name LIKE '%alice'

可选做法包括统一数据格式、使用正确的参数类型、建立匹配的表达式索引,或改用适合文本搜索的索引。是否能改写以及具体写法必须结合数据库验证。

不重复创建功能相同的索引​

以下索引可能存在明显重叠:

INDEX (customer_id)
INDEX (customer_id, status)

后者通常已经能覆盖以 customer_id 为前缀的一部分查询,但不能简单认为它永远替代前者。应根据实际执行计划、索引大小和查询负载判断,并在删除旧索引前确认没有约束或其他语句依赖它。

用执行计划验证​

索引设计不能只看 SQL 文本。使用数据库提供的 EXPLAIN 或等价工具检查实际计划:

EXPLAIN
SELECT id, status
FROM orders
WHERE customer_id = 42
AND status = 'paid'
ORDER BY created_at DESC;

通常需要关注:

  • 是否选择了预期索引;
  • 访问方式是索引查找、索引范围扫描还是全表扫描;
  • 估算行数与实际行数是否相差很大;
  • 是否出现额外排序、回表读取或大量随机 I/O;
  • 连接顺序和连接条件是否合理。

执行计划依赖统计信息、参数值和数据分布。同一条 SQL 在不同环境、不同参数下可能选择不同计划,因此应使用接近生产规模的数据验证。

索引的代价与维护​

每增加一个索引,写入一行数据时通常就多维护一份结构。因此索引可能带来:

  • INSERT、UPDATE、DELETE 变慢;
  • 额外磁盘空间、缓存占用和备份体积;
  • 创建、重建或更新统计信息时的资源消耗;
  • 长期不用的索引掩盖真正的性能问题。

更新索引列时,数据库可能需要移动索引记录;宽大的复合索引尤其要关注这一点。删除索引前先确认它不是主键、唯一约束或关键查询所必需的,并通过监控观察一段时间。

常见误区​

“有索引就一定快”​

优化器会比较成本。低选择性条件、很小的表、需要返回大多数行的查询,都可能更适合全表扫描。

“每个列都建一个索引”​

这会增加写入开销和索引管理复杂度,而且单列索引不一定能匹配真实查询的列组合。

“索引能解决所有慢查询”​

慢查询也可能源于返回数据过多、连接条件错误、排序和聚合开销、锁等待、网络传输或糟糕的 SQL 结构。应先用执行计划和监控定位瓶颈。

“索引会改变结果顺序”​

没有 ORDER BY 时,SQL 不承诺结果顺序。即使某次查询恰好按索引顺序返回,也不能把这种偶然行为当作排序保证。

一套实用流程​

  1. 找到真实的慢查询和高频查询。
  2. 明确筛选、连接、排序和返回列。
  3. 设计最小且有针对性的索引,优先考虑复合索引的列顺序。
  4. 在接近生产的数据上使用 EXPLAIN 验证。
  5. 对比查询耗时、读写吞吐、锁和存储开销。
  6. 上线后持续监控,定期清理重复或长期未使用的索引。

记住一句话:索引是用空间和写入成本换取读取速度的工具;好的索引来自真实工作负载和执行计划,而不是固定的“每列一个索引”规则。