部分索引可以把高频访问的少量行放在更小的结构中。假如 open 任务很少,而请求多数只找 open 任务,在 id 上建立 WHERE status = 'open' 的索引就值得评估。但它是否可用,取决于规划器能为当前查询证明什么,不只是应用在某次执行时碰巧传入了 open。准备语句的计划方式会直接影响这份证据。
证明不需要被排除的行
索引里没有 closed 任务,因此不能独立回答可能需要 closed 行的查询。PostgreSQL 必须在规划时识别查询条件蕴含索引谓词。显式写出的 status = 'open' 提供了这个事实;通用计划中的 status = $1 则没有,因为同一份计划还必须在参数为 closed 时保持正确。规划器不能依靠应用的口头约定丢弃可能需要的数据。
蕴含关系不总是逐字相同。PostgreSQL 能识别一些简单逻辑关系,但不是任意表达式的定理证明器。统一真实需要的谓词,有助于避免重构把原本可用的索引条件藏起来。仅过滤索引键 id 也不能证明状态为 open,即使当前数据或业务习惯让它经常如此。性能优化必须保持查询语义,不能把偶然数据状态当成永久约束。
准备计划改变可用信息
准备语句可以使用根据具体参数规划的 custom plan,也可以使用跨参数复用的 generic plan。状态参数为 open 时,定制计划可能知道所需谓词并考虑部分索引;通用计划面对未知状态,就不能把该索引当成完整答案路径。SQL 文本没有变化,改变的是规划阶段掌握的信息,这也是同一语句不同执行阶段出现不同计划的原因之一。
这不代表参数化查询都无法使用部分索引。如果 status 始终显式为 open,只有 id 是参数,通用计划仍保留必要谓词。也不应为了追求索引把参数绑定换成字符串拼接;不可信输入仍应作为参数传递。正确做法是让查询表达真实不变量,而不是牺牲数据处理安全来获得一次漂亮的计划截图。
默认策略会依据成本考虑选择定制或通用计划。诊断时可以在会话中用 plan_cache_mode 强制两种行为进行比较,但这只是实验,不是要求全局关闭通用计划。重新规划也有成本,参数分布与执行频率都会影响取舍。应观察真实连接与实际重复执行,而不是把某次本地设置直接推广到整个系统。
坏掉的样子
一条查询可能在多次准备执行后变慢,因为可复用计划取代了依赖参数的计划,现象看起来像索引随机消失。应捕获应用真正发送的准备语句,并用代表性参数检查 EXPLAIN EXECUTE。临时把参数写成字面量的查询,不能复现所有准备计划条件。把谓词、计划模式、估计行数和实际工作量放在一起,才能定位差异来自哪里。
子集本身也可能不再小。任务积压使大多数记录都处于 open 时,原来的尺寸优势就会下降,而状态转换仍需要维护索引。不能仅根据建索引时的数据比例,就承诺以后写入更快、缓存更热。统计信息和接近生产的数据分布是评估前提,部分索引是一项工作负载选择,不会保证未来数据一直满足最初假设。
谓词限制的是索引条目,不是表里允许保存的行,也不是读取权限。closed 任务仍在表中,可以通过其他访问路径读取。若使用部分唯一索引,它也只约束满足谓词的子集,不能代替全局唯一性规则。审阅模型时,应把查询加速、数据完整性和访问权限分开,否则容易把物理结构误认为业务规则已经得到保证。
快速验证
把 SQL 保存为 partial-plan.sql,运行 psql -X -v ON_ERROR_STOP=1 -f partial-plan.sql,连接可丢弃的 PostgreSQL 18 数据库。临时表一万行中只有一百条 open,索引只保存这一子集;两条准备语句分别演示状态也是参数和状态固定两种真实不同的契约。
\set ON_ERROR_STOP on
BEGIN;
CREATE TEMP TABLE jobs (id integer, status text NOT NULL);
INSERT INTO jobs SELECT g, CASE WHEN g<=100 THEN 'open' ELSE 'closed' END
FROM generate_series(1,10000) AS g;
CREATE INDEX jobs_open_id ON jobs (id) WHERE status='open';
ANALYZE jobs;
PREPARE chosen(text,integer) AS SELECT id FROM jobs WHERE status=$1 AND id=$2;
SET LOCAL plan_cache_mode=force_generic_plan;
EXPLAIN (COSTS OFF) EXECUTE chosen('open',7);
EXECUTE chosen('open',7);
SET LOCAL plan_cache_mode=force_custom_plan;
EXPLAIN (COSTS OFF) EXECUTE chosen('open',7);
EXECUTE chosen('open',7);
PREPARE always_open(integer) AS SELECT id FROM jobs WHERE status='open' AND id=$1;
SET LOCAL plan_cache_mode=force_generic_plan;
EXPLAIN (COSTS OFF) EXECUTE always_open(7);
EXECUTE always_open(7);
DEALLOCATE chosen;
DEALLOCATE always_open;
ROLLBACK;
本轮 PostgreSQL 18.6 单用户 SQL 重跑中,状态未知的通用计划使用顺序扫描;未执行 psql 客户端与连接池行为。同一准备语句在强制定制计划、参数为 open 时使用 jobs_open_id。第二条语句把状态固定为字面量,只参数化 id,即使强制通用计划也使用该部分索引。三次执行都返回编号七,结果语义相同,访问路径却不同,体现的是规划信息差异而不是数据错误。
这些计划说明已知谓词的重要性,不承诺每台机器都选择完全相同的访问路径。SET LOCAL 的改变在回滚后结束,DEALLOCATE 清理准备语句,临时表也随事务撤销。实验并没有复现真实连接池中的自动计划切换;那需要记录应用会话、参数分布与重复执行历史,不能用强制模式的结果冒充已验证线上触发条件。
来源
- PostgreSQL 部分索引,规划阶段的谓词蕴含关系,已核验 2026-10-15。
- PostgreSQL PREPARE,定制与通用执行计划,已核验 2026-10-15。