索引保存额外的数据组织方式,使数据库更快找到候选行;它同时增加空间和写入维护成本。优化的目标是完成具体查询所需的工作量,而不是让所有 SQL 都出现 Index Scan。小表全扫、大范围聚合走顺序扫描,都可能比随机访问索引更便宜。
建一个能观察的样本
以下 PostgreSQL 实验只创建练习表。使用独立数据库,执行计划和耗时随机器、缓存与统计信息变化,不预设固定数字。
CREATE TABLE query_lab (
id bigint PRIMARY KEY,
owner_id integer NOT NULL,
state text NOT NULL,
created_at timestamptz NOT NULL
);
INSERT INTO query_lab
SELECT g, g % 1000, CASE WHEN g % 10 = 0 THEN 'open' ELSE 'done' END,
timestamptz '2026-01-01 00:00:00+00' + g * interval '1 second'
FROM generate_series(1, 200000) AS g;
ANALYZE query_lab;
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, created_at FROM query_lab
WHERE owner_id = 42 ORDER BY created_at DESC, id DESC LIMIT 20;
CREATE INDEX query_lab_owner_time
ON query_lab(owner_id, created_at DESC, id DESC);
ANALYZE query_lab;
-- 再运行相同 EXPLAIN,对比扫描行数、排序节点和缓冲区访问。
复合索引先按 owner_id 缩小范围,再匹配排序,让数据库更有机会取得前 20 条就停止。是否这样执行仍由实际计划决定。仅为低基数 state 建一个普通索引,查询覆盖大部分行时可能没有收益;若固定业务只查很少的 open 记录,可以评估带 WHERE 条件的部分索引。
先分清估计与实测
EXPLAIN 展示估计计划;EXPLAIN ANALYZE 真正执行语句。后者用于 UPDATE、DELETE、带副作用的函数时会产生实际效果,不能随意在生产运行。cost 是规划器的相对成本单位,不是毫秒。
看计划时先找估计行数与实际行数差距,再看过滤掉多少行、循环次数、排序是否落盘和缓冲区读取。一条内层查询每次只花一点时间,乘上十万次 loops 后也可能成为主要瓶颈。统计信息过旧、列之间相关性、隐式类型转换、对索引列套函数,都可能导致计划偏离预期。
Index Only Scan 不保证完全不访问表,PostgreSQL 还需检查可见性信息。为了“覆盖所有查询”把很多大字段塞进索引,会推高写放大和缓存压力。先量化最重要的查询,再增加索引。
深分页需要改变读取方式
OFFSET 100000 LIMIT 20 仍需跳过很多候选行。若界面只需连续向后阅读,可把上一页最后一项作为游标:
SELECT id, created_at FROM query_lab
WHERE owner_id = 42
AND (created_at, id) < (timestamptz '2026-01-03 00:00:00+00', 172800)
ORDER BY created_at DESC, id DESC
LIMIT 20;
比较按元组字典序进行,两个排序项方向必须与条件一致。游标包含时间和唯一 ID,避免同一时间多条记录被遗漏。它让续查更便宜,但不会自动提供“阅读期间所有页面同一快照”,也不适合任意跳到第 5000 页。
练习与面试自查
分别尝试删去 owner 条件、改变排序方向、查询所有 owner。保留修改前后的计划,并解释复合索引为什么不一定适合每一种查询。补一个会显著影响写入的批量插入测量,才能评价索引的完整代价。
- 为什么“索引失效”不是足够精确的诊断?应查看哪些证据?
- 为什么数据库认为顺序扫描更便宜时,强行使用索引可能变慢?
- 索引能改善查询,为什么还可能加重锁等待和写入延迟?