索引的理由可以来自访问需求,也可以来自完整性规则。前者例如取某个客户最近五笔订单,后者例如禁止重复订单编号。即使查询统计中只有前者表现为频繁读取,两者也都是真实用途。索引设计应先记录这些用途并测量代价,而不是收集看起来有趣的数据结构,也不是看到扫描次数很少就删除。约束承担的正确性职责,不能用某条读取查询是否选中它来代替判断。
描述完整查询
只知道过滤条件,还不足以描述一个负载。还应记录排序、数量限制、输出列、参数分布和执行频率。订单编号索引已经提供编号顺序,但为了找到某个客户的五条记录,可能先检查大量其他客户的订单。以等值过滤的客户列开头,再接所需顺序的复合 B-tree,可以先缩小搜索范围,再沿该客户的近期条目读取。
这与分别建立两个单列索引并假设效果相同不同。位图组合可能帮助过滤,却不会为最终输出保留索引中的顺序。覆盖索引只有在可以省去堆可见性检查时,才真正避免相应的堆访问;否则即便计划名为 index-only scan,仍会访问堆。条目变宽也会占用更多空间并增加维护。只有真实查询和测得的堆访问确实证明必要时,才考虑加入覆盖列,不能把“多包含一点将来可能有用”当成充分理由。
坏掉的样子
查询用了索引,仍然可能做很多无效工作。沿主键倒序读取,再丢弃成千上万条其他客户记录,当然属于索引扫描,但过滤发生在条目选择之后。应看 Index Cond、过滤掉的行数、循环次数和缓冲区访问,而不是把出现索引名字当成成功标准。节点名称只能告诉我们采用了哪条路径,不能单独证明路径足够精确。
为所有想象中的查询都建立索引,会增加存储和维护。具体写入成本取决于索引列、谓词与更新行为,不能说每次更新都以完全相同方式改写所有索引。不过,读取收益仍然需要与目标负载中的插入、更新、清理工作和缓存压力比较。不能只展示一条查询更快,就忽略它给高频写入带来的实际代价。
删除同样需要证据。idx_scan 只描述某个统计窗口中的观察,不证明索引没有用途。主键或唯一索引即使很少承担读取,也可能持续保护数据规则。低频报表、运维查询和引用关系,还可能有短时间采样看不到的需求。因此考虑移除前,应先检查约束与依赖,而不是对一个计数设定机械的删除阈值。
快速验证
在 PostgreSQL 18 的 psql 中执行下面受控例子。它建立五万条临时订单,均匀分配给二百五十个客户。主键从头到尾保留,最后回滚移除夹具。
\set ON_ERROR_STOP on
BEGIN;
CREATE TEMP TABLE orders (id integer PRIMARY KEY,customer_id integer,total integer);
INSERT INTO orders SELECT g,g%250,g*2 FROM generate_series(1,50000) AS g;
ANALYZE orders;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT id,total FROM orders WHERE customer_id=7 ORDER BY id DESC LIMIT 5;
CREATE INDEX orders_customer_recent ON orders(customer_id,id DESC);
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT id,total FROM orders WHERE customer_id=7 ORDER BY id DESC LIMIT 5;
SELECT id,total FROM orders WHERE customer_id=7 ORDER BY id DESC LIMIT 5;
DO $$ BEGIN
IF (SELECT array_agg(id ORDER BY id DESC) FROM (
SELECT id FROM orders WHERE customer_id=7 ORDER BY id DESC LIMIT 5
) s) IS DISTINCT FROM ARRAY[49757,49507,49257,49007,48757] THEN
RAISE EXCEPTION 'changed result ordering or membership';
END IF;
END $$;
DO $$ BEGIN
INSERT INTO orders VALUES (1,999,0);
RAISE EXCEPTION 'duplicate primary key accepted';
EXCEPTION WHEN unique_violation THEN RAISE NOTICE 'primary key still protects identity';
END $$;
ROLLBACK;
此前 PostgreSQL 18.6 实测记录中,初始计划倒序扫描 orders_pkey。它最终返回所需五行,但先因为客户条件过滤掉一千二百三十九行。主键确实提供了顺序,却没有直接定位目标客户的范围。因此,一个结果正确的计划,仍然可能执行本来可以避免的过滤。这也说明“没有额外排序”不足以证明完整访问已经最合适。
加入 (customer_id,id DESC) 后,计划通过 orders_customer_recent 把客户等值条件作为索引条件,直接走到该客户的近期记录。返回编号依次为四万九千七百五十七、四万九千五百零七、四万九千二百五十七、四万九千零七、四万八千七百五十七。断言检查精确成员和顺序,因此速度更快却改变分页答案的方案仍然会失败。
故意插入重复订单编号时,仍然触发唯一性错误。新增的访问索引没有取代主键的完整性职责。这项检查很重要,因为如果维护时只看最新查询选中了哪个索引,就可能把继续承担约束的主键误认为多余。访问优化和数据契约需要一起保留,不能互相抵消。
本轮通过 PostgreSQL 18.6 单用户后端复现了这些 SQL 结果,但没有重跑 psql 客户端命令与连接传输。具体计划与过滤计数来自本版本、本夹具的测量。其他数据分布或成本设置可能选择另一条路径。能够推广的是等值过滤、输出顺序与索引键顺序之间的对应关系,并且查询结果应独立于性能测量进行核对,而不是硬要求每个环境都打印完全一样的计划树。
保留负载记录
面向生产评估时,保留有代表性的参数,比较前后延迟、访问页面、结果身份、存储与写吞吐。覆盖偏斜客户和繁忙时段,不要只测平均客户。大索引构建还有运行开销与锁选项,临时表演示没有测量这些上线条件。保留由真实访问和完整性需求支持的最小集合,并在应用或数据变化时重新核对这些需求是否仍然成立。
来源
PostgreSQL 18:索引介绍,索引访问收益与维护成本,已核验 2026-10-15。
PostgreSQL 18:多列索引,复合索引前导等值键,已核验 2026-10-15。
PostgreSQL 18:检查索引使用,基于真实负载评估索引,已核验 2026-10-15。
PostgreSQL 18: Combining multiple indexes,位图组合不保留索引顺序,已核验 2026-10-15。
PostgreSQL 18: Index-only scans,可见性映射前提与覆盖索引代价,已核验 2026-10-15。