索引简介
索引(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 或其变体)保存索引键,并让键保持有序。一次典型的索引查找可以分为:
- 从索引根节点开始,逐层缩小键的范围。
- 在叶子节点找到目标键或目标范围。
- 根据叶子节点中的行定位信息读取表数据。
因此,索引特别适合等值查找和有序范围查找:
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 不承诺结果顺序。即使某次查询恰好按索引顺序返回,也不能把这种偶然行为当作排序保证。
一套实用流程
- 找到真实的慢查询和高频查询。
- 明确筛选、连接、排序和返回列。
- 设计最小且有针对性的索引,优先考虑复合索引的列顺序。
- 在接近生产的数据上使用
EXPLAIN验证。 - 对比查询耗时、读写吞吐、锁和存储开销。
- 上线后持续监控,定期清理重复或长期未使用的索引。
记住一句话:索引是用空间和写入成本换取读取速度的工具;好的索引来自真实工作负载和执行计划,而不是固定的“每列一个索引”规则。