Rows Removed by Filter 是 PostgreSQL 10+ 中 EXPLAIN ANALYZE 输出的统计项,表示该节点扫描后被 WHERE、JOIN 或 HAVING 等条件实际丢弃的行数,反映“扫描了但没要”的数据量,其值高常提示索引缺失、函数包裹或类型转换等问题。

Rows Removed by Filter 是什么?
这是 EXPLAIN ANALYZE 输出中出现在 Extra 或节点详情里的统计项(PostgreSQL 10+),表示**该执行节点在完成扫描后,被 WHERE、JOIN 条件或 HAVING 等过滤逻辑实际丢弃的行数**。它不等于“没用上索引”,而是反映「扫描了但没要」的数据量。
- 例如:全表扫描读了 100 万行,其中 99.9 万行不满足
status = 'active'→ 就会显示Rows Removed by Filter: 999000 - 它和
rows(预估返回行)与actual rows(实际返回行)共同构成三元关系:actual rows + Rows Removed by Filter ≈ total rows scanned - 这个值高本身不直接等于慢,但若远高于
actual rows,说明过滤效率低,很可能存在索引缺失、函数包裹、类型隐式转换等问题
为什么索引没被用上?常见触发场景
最典型的矛盾现象:你建了索引,EXPLAIN ANALYZE 却显示大量 Rows Removed by Filter,且执行计划是 Seq Scan 或 Index Scan 后跟巨量过滤 —— 这往往意味着索引无法用于条件判断。
-
WHERE jsonb_column -> 'tags' @> '["vip"]':即使对jsonb_column建了 GIN 索引,若查询用了->提取后再运算,优化器无法下推,只能先扫再滤 -
WHERE UPPER(name) = 'ALICE':函数包裹导致普通 B-tree 索引失效;需建函数索引CREATE INDEX idx_users_upper_name ON users (UPPER(name)) -
WHERE created_at::date = '2025-01-01':类型强制转换打断索引使用;应改写为created_at >= '2025-01-01' AND created_at - 多列索引顺序错位:比如索引是
(a, b),但查询只用了WHERE b = 123→ 无法利用该索引做索引查找,只能 Index Scan + Filter
如何快速验证和修复?
别猜,用 EXPLAIN (ANALYZE, VERBOSE, BUFFERS) 对比改写前后的执行计划变化,重点关注 Index Cond 是否出现、Rows Removed by Filter 是否大幅下降。
- 先看原计划里有没有
Index Cond:有 → 索引被用于定位;没有但有Filter→ 索引仅用于读取,过滤在内存中做 - 对 JSONB 字段,优先用支持下推的操作符:比如用
jsonb_path_exists()替代->> ... =,并配合jsonb_path_ops索引 - 对时间范围查询,避免用
date()、to_char()包裹字段;用范围写法,并确保字段上有 B-tree 索引 - 检查
pg_stats中对应列的n_distinct和most_common_vals是否过期,过时的统计信息会导致优化器误判选择性,从而放弃索引
容易被忽略的细节
很多人盯着 Rows Removed by Filter 想加索引,却忘了 PostgreSQL 的“索引扫描成本”模型:如果估算出的过滤后结果集占全表比例过高(比如 >10%),优化器宁可走顺序扫描 —— 因为随机 IO 成本可能高于顺序读。
- 此时加索引未必提速,反而增加写开销;应考虑分区裁剪、物化视图预聚合,或从业务侧限制查询范围(如强制带
tenant_id) -
Rows Removed by Filter在嵌套循环 JOIN 中可能出现在内表节点,这时要检查驱动表输出行数是否爆炸(actual rows过大),而非只盯过滤率 - 使用
pg_stat_statements查看该 SQL 的平均shared_blks_read和blk_read_time,比单次EXPLAIN ANALYZE更能反映真实 I/O 压力










