
PostgreSQL 部分索引(Partial Index)详解
一、什么是部分索引?
普通索引会对表中所有行 建立索引条目;而部分索引只对满足特定条件的行建立索引。
sql
-- 普通索引:对全表 10000 万行建索引
CREATE INDEX idx_normal ON mage_op_order_full (version_number, svc_tag);
-- 部分索引:只对"待处理的 ESS 行"建索引(可能只有几万行)
CREATE INDEX idx_partial ON mage_op_order_full (version_number, svc_tag)
WHERE ess_no IS NULL
AND source_parent_id IS NOT NULL;
一句话理解:普通索引是给全班同学建花名册,部分索引是只给"还没交作业的同学"建花名册。
二、真实案例背景
在 Magellan ST 数据集成链路中,有一个 flush2 SQL 专门用于补填 ESS 产品编号:
sql
-- flush2:找出 ESS 行中 ess_no 为空的记录,从父记录中回填 product_no
UPDATE mage_op_order_full a
SET ess_no = b.product_no
FROM mage_op_order_full b
WHERE a.version_number = #{versionNumber} -- 当前批次
AND a.svc_tag = 'ESS' -- 只处理 ESS 类型
AND a.ess_no IS NULL -- 尚未填充
AND a.source_parent_id IS NOT NULL -- 有父记录
AND a.source_parent_id = b.sell_through_record_id
表数据特征:
mage_op_order_full总数据量:数千万行- 其中
svc_tag = 'ESS'且ess_no IS NULL的行:极少数(只有新批次刚写入的数据) - 随着
flush2执行完毕,这些行的ess_product_no被填充后,自动退出部分索引范围
三、普通索引 vs 部分索引
3.1 索引大小对比
表总行数:5000 万行
其中 ess_no IS NULL 的行:约 50 万行(仅占 1%)
普通索引大小:~2GB(覆盖 5000 万行)
部分索引大小:~20MB(只覆盖 50 万行)
索引体积缩小 100 倍 → 全部装入内存 → 查询速度极快
3.2 写入维护成本对比
| 操作 | 普通索引 | 部分索引 |
|---|---|---|
| INSERT 新行(ess_no IS NULL) | 必须写入索引 | 必须写入索引 |
| INSERT 新行(ess_no 有值) | 必须写入索引 | ✅ 不需要维护 |
| UPDATE 填充 ess_product_no(IS NULL → 有值) | 索引条目需要删除+更新 | ✅ 行退出索引范围,自动失效 |
| UPDATE 与索引无关的字段 | 所有索引都要检查 | ✅ 不在条件范围内的行直接跳过 |
核心优势 :只有 1% 的行满足部分索引条件,但 99% 的 INSERT/UPDATE 操作都不需要维护这个索引,写入成本降低 99%。
3.3 查询命中条件对比
sql
-- ✅ 能命中部分索引:查询条件包含部分索引的 WHERE 子句
UPDATE ... WHERE version_number = ?
AND svc_tag = 'ESS'
AND ess_no IS NULL -- ← 包含此条件
AND source_parent_id IS NOT NULL -- ← 包含此条件
-- ❌ 不能命中部分索引:查询条件不满足部分索引的 WHERE 子句
SELECT * FROM mage_op_order_full WHERE version_number = ?
-- 这个查询没有 ess_no IS NULL 条件,走普通索引或全表扫描
四、建立部分索引
针对 flush2 的查询模式,建议创建如下部分索引:
sql
-- 针对 flush2 的部分索引
CREATE INDEX idx_st_full_ess_flush
ON mage_op_order_full (version_number, svc_tag)
WHERE ess_no IS NULL
AND source_parent_id IS NOT NULL;
索引字段解释:
version_number:WHERE 中的等值过滤,放第一位(选择性高)svc_tag:WHERE 中的等值过滤= 'ESS',放第二位WHERE ess_no IS NULL AND source_parent_id IS NOT NULL:部分索引条件,只索引待处理的行
五、部分索引的"自动缩减"特性
这是部分索引最精妙的地方,结合本案例理解:
时间线:
T1: 新批次写入 50 万条 ESS 行,ess_no = NULL
→ 这 50 万行进入部分索引
→ 索引大小:50 万条目
T2: flush2 执行,回填 ess_product_no
→ 每填充一行,ess_no IS NULL 不再成立
→ 该行自动退出部分索引范围
→ 索引大小:逐渐缩减至 0
T3: flush2 执行完毕
→ 部分索引几乎为空(只剩真正没有父记录的极少数行)
→ 索引几乎不占空间,也不影响后续写入性能
这就像"待办清单",完成一项划掉一项,清单越来越短,查找速度始终很快。
六、验证部分索引是否生效
sql
-- 用 EXPLAIN ANALYZE 查看执行计划
EXPLAIN ANALYZE
UPDATE mage_op_order_full a
SET ess_no = b.product_no
FROM mage_op_order_full b
WHERE a.version_number = '20260522000707011'
AND a.svc_tag = 'ESS'
AND a.ess_no IS NULL
AND a.source_parent_id IS NOT NULL
AND a.source_parent_id = b.record_id;
执行计划中出现以下内容,说明部分索引已命中:
Index Scan using idx_st_full_ess_flush on mage_op_order_full a
Index Cond: ((version_number = '20260522000707011') AND (svc_tag = 'ESS'))
Filter: (ess_no IS NULL AND source_parent_id IS NOT NULL)
七、什么时候适合用部分索引?
| 适用场景 | 说明 | 示例 |
|---|---|---|
| 只处理少数状态的数据 | 大表中只有少数行处于"待处理"状态 | status = 'PENDING',处理完变为 DONE |
| 软删除过滤 | 绝大多数行都是有效数据 | WHERE deleted_at IS NULL |
| 特定类型数据加速 | 只有某个枚举值的子集需要高频查询 | WHERE svc_tag = 'ESS' |
| NULL 值过滤 | 表中大量字段为 NULL,只查非 NULL | WHERE ess_no IS NULL |
| 时间窗口查询 | 只查最近 N 天的数据 | WHERE created_at > NOW() - INTERVAL '7 days' |
八、部分索引 vs 普通索引 总结
| 对比维度 | 普通索引 | 部分索引 |
|---|---|---|
| 索引覆盖范围 | 全表所有行 | 只覆盖满足 WHERE 条件的行 |
| 索引体积 | 大(与表成比例) | 小(只有目标行) |
| 写入维护成本 | 每次 INSERT/UPDATE 都要维护 | 只有满足条件的行才维护 |
| 查询命中条件 | 只需查询字段匹配 | 查询条件必须包含索引的 WHERE 子句 |
| 适用场景 | 通用查询 | 特定状态/条件的高频查询 |
| MySQL 支持 | ✅ | ❌(MySQL 不支持) |
| PostgreSQL 支持 | ✅ | ✅ |
九、注意事项
1. 查询条件必须"覆盖"部分索引条件
sql
-- ✅ 命中:查询条件包含了部分索引的 WHERE 子句
WHERE version_number = ? AND svc_tag = 'ESS' AND ess_no IS NULL
-- ❌ 不命中:缺少 ess_no IS NULL 条件
WHERE version_number = ? AND svc_tag = 'ESS'
2. 新建部分索引需要 REINDEX 或等待 autovacuum
sql
-- 建好后立即生效,不需要重建
-- 但如果表已有大量数据,建索引过程会扫描全表一次(只扫满足条件的行)
CREATE INDEX CONCURRENTLY idx_st_full_ess_flush -- 加 CONCURRENTLY 不锁表
ON mage_op_order_full (version_number, svc_tag)
WHERE ess_no IS NULL AND source_parent_id IS NOT NULL;
3. 部分索引不适合频繁变化的条件列
如果 WHERE 条件中的列频繁被 UPDATE ,会导致行不断进出索引,反而增加维护开销。
本案例中 ess_no 只会从 NULL → 有值(单向变化),非常适合部分索引。
十、一句话总结
部分索引是"给少数特殊行开的快速通道",它比普通索引体积更小、维护成本更低、查询更快------前提是你的查询条件能精确描述这批"特殊行"是谁。