索引的价值在于改变真实查询需要完成的工作,索引了多少列并不是有效的成功指标。一个能直接找到某位客户最新三笔订单的窄 B-tree,可能胜过无法提供所需顺序的宽索引;读取几乎全部行的查询,则可能正确地忽略两者。评估时要把访问方式、排序和返回数据量放在一起,而不是看到索引名称就宣布优化完成。
从需要的结果出发
假设应用要查询某位客户最新三笔订单。customer 的等值条件缩小候选范围,placed_at 降序选择该客户最新记录,再用 id 作为最后决胜键,处理时间戳相同的情况。(customer, placed_at DESC, id DESC) 就表达了这条查询需要的键顺序。每个键都应对应可解释的条件或排序要求,而不是根据列名凭直觉添加。
收益不只是避免全表扫描。有序索引路径可以取得所需的有限结果后就停止,不必先收集该客户全部订单再排序。去掉 LIMIT,或改成返回大部分行后,成本平衡可能变化。因此同一个索引必须结合完整 SQL 和常用参数评价,不能只盯 WHERE 的一个片段,更不能把少量结果的计划当成大规模导出的保证。
WHERE 条件的文本顺序不会定义索引顺序。PostgreSQL 规划逻辑表达式,把 customer 等值写在 AND 前面或后面,不会重排 B-tree。真正相关的是索引方向、空值顺序、前导键限制和最终 ORDER BY。修改查询时应明确核对这些结构属性,而不是套用“列写在前面就能用索引”的笼统规则。PostgreSQL 18 还能在缺失前导键的不同值较少时,对某些后续键条件采用 B-tree skip scan;没有前导等值条件,并非绝对禁止使用索引。
如实读取执行计划
EXPLAIN 展示计划工作,EXPLAIN ANALYZE 会真正执行并报告实际行数和循环次数。查询返回表中大部分数据,尤其需要索引中没有的宽列时,顺序扫描可能是正确选择。这不证明列顺序错误,也不证明索引创建失败。应把它看成优化器在当前条件下选择的访问成本,而不是必须消除的红色标记。
反过来,出现索引名也不证明查询便宜。索引扫描仍可能访问大量条目和堆元组,bitmap 路径可能还要排序,index-only 计划也可能检查堆可见性。结合 actual rows、适用时的 Heap Fetches、缓冲区活动和 Sort 阅读。顶层有 LIMIT 只有在下层也避免了大量多余工作时才真正省事,不能忽略 LIMIT 下面的完整树。
坏掉的样子
生产参数可能比开发参数多选出几千倍的行。小客户上表现优秀的索引,对拥有大量订单的大客户未必同样合算。数据分布、统计信息、缓存和并发都会影响计划与延迟,验证变更时应保留多个代表性参数。只测试一个常见编号,很容易遗漏真正拖慢系统的极端但合法客户。
索引还会持续产生存储、缓存、写入维护和构建成本。仅因为一次诊断查询返回 note,就把宽 note 加入热索引,可能让日常写入更贵。应该区分窄的高频应用查找和偶尔的大范围报表,要求每个附加键或 INCLUDE 列都有反复出现的真实用途。不能用读路径的一次局部收益掩盖整个系统长期承担的维护工作。
还有一种问题是分页依赖未定义的顺序。时间戳相同或执行计划改变时,如果 ORDER BY 没有给出确定选择,前几条记录可能变化。索引本身不会独立建立 API 的排序契约。顺序应明确写在 SQL 中,测试也应检查具体标识,而不只是检查“返回了三行”,否则拿错三行仍可能通过表面的数量校验。
快速验证
保存为 topn.sql,运行 psql -X -v ON_ERROR_STOP=1 -f topn.sql,连接可丢弃的 PostgreSQL 18 数据库。夹具创建两万笔订单,分布在一百位客户中,并加入一个较宽的 note 列用于比较全量查询。表和索引都是临时对象,最后回滚清理,不依赖业务数据。
\set ON_ERROR_STOP on
BEGIN;
CREATE TEMP TABLE orders (id integer, customer integer, placed_at integer, note text);
INSERT INTO orders SELECT g, g%100, g, repeat('x',100) FROM generate_series(1,20000) AS g;
CREATE INDEX orders_customer_recent ON orders (customer, placed_at DESC, id DESC);
ANALYZE orders;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT id,placed_at FROM orders WHERE customer=7 ORDER BY placed_at DESC,id DESC LIMIT 3;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT * FROM orders WHERE customer>=0;
SELECT array_agg(id ORDER BY placed_at DESC,id DESC) AS newest
FROM (SELECT id,placed_at FROM orders WHERE customer=7
ORDER BY placed_at DESC,id DESC LIMIT 3) s;
DO $$ BEGIN
IF (SELECT array_agg(id ORDER BY placed_at DESC,id DESC)
FROM (SELECT id,placed_at FROM orders WHERE customer=7
ORDER BY placed_at DESC,id DESC LIMIT 3) s)
IS DISTINCT FROM ARRAY[19907,19807,19707] THEN
RAISE EXCEPTION 'wrong newest orders';
END IF;
END $$;
ROLLBACK;
客户七的结果按顺序是 19907,19807,19707。PostgreSQL 18.6 选择 Limit 加 index-only scan,找到三条匹配行后停止;因为新表尚未 VACUUM,仍需要堆可见性检查。查询所有客户并返回 note 时,计划选择顺序扫描,实际返回两万行。两种选择分别对应各自的任务,并不互相矛盾,也不说明某个索引突然失效。
断言检查确定的前三条结果,耗时和缓冲区数字不被写成固定预期。这个实验展示按工作负载使用索引,不提供跨机器速度倍数,也不保证生产数据与需求变化后同一计划永远最优。若未来增加全局排序、额外过滤或更多返回列,就应重新检查完整语句的访问路径和成本。
来源
- PostgreSQL 多列索引,键条件与扫描范围,已核验 2026-10-15。
- PostgreSQL 索引与 ORDER BY,有序读取与 LIMIT,已核验 2026-10-15。
- PostgreSQL 使用 EXPLAIN,理解计划中的实际工作,已核验 2026-10-15。