PostgreSQL 教程: EXPLAIN 输出中的 'Rows Removed by Filter'

一月 6, 2025

摘要:在本教程中,您将学习关于 EXPLAIN 输出中的 ‘Rows Removed by Filter’ 的内容。

目录

示例

我们使用的是一个users表,其中包含了 20,000+ 行,该表有一个name_code列,其值由随机字符串填充。

-- Sample rows, note name_code "BA492"
SELECT
id,
first_name,
last_name,
name_code
FROM users
ORDER BY id DESC
LIMIT 3;
  id   | first_name | last_name | name_code
-------+------------+-----------+-----------
 20210 | Brooks     | Aufderhar | BA492
 20209 | Nereida    | Goodwin   | NG8292
 20208 | Dillon     | Rodriguez | DR9151
-- total rows, and all rows with name_code "BA492"
SELECT
COUNT(*) AS total_count,
COUNT(*) FILTER (WHERE "users"."name_code" = 'BA492') AS with_code
FROM users;
 total_count | with_code
-------------+-----------
       20210 |         1
-- Why is Rows Removed by Filter "20208" and not "20209"?
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT * FROM "users" WHERE
"users"."name_code" = 'BA492' LIMIT 1;
                           QUERY PLAN
------------------------------------------------------------------
 Limit (actual time=13.334..13.338 rows=1 loops=1)
   Buffers: shared hit=684
   -> Seq Scan on users (actual time=13.328..13.329 rows=1 loops=1)
        Filter: ((name_code)::text = 'BA492'::text)
        Rows Removed by Filter: 20208
        Buffers: shared hit=684
 Planning Time: 0.322 ms
 Execution Time: 13.645 ms

深入探究 “Rows Removed by Filter”

让我们讨论一下表和查询的详细信息,以及其他情况。

  • 我们查询了一个总共有 20210 行的 “users” 表。该表使用了一个从 1 开始的整数序列。
  • 没有插入新行。我们使用的是一组静态的行。
  • 还没有在此表上运行VACUUM
  • 我们在查询中使用了相同的WHERE子句,其中的name_code字符串列,为每行保存了一个基本唯一(但是并没有约束来强制保证)的 “code” 值。
  • 我们在查询上设置了一个LIMIT为 1,但没有ORDER BY,也没有OFFSET。在实践中,一个LIMIT通常会伴随着一个ORDER BY,但这里并非如此。
  • 使用了顺序扫描来访问行数据,因为没有索引。在生产系统中,一个name_code列上的索引会是一个好主意。在本文中,我们的目的是了解没有该索引时的行为。
  • 所有数据都是从共享缓冲区访问的,这是通过添加BUFFERSEXPLAIN (ANALYZE)来确认的。

发生了什么情况?

我们可能预期 “Rows Removed by Filter” 会比总行数少 1 行,但有很多原因导致情况并非如此。

在查询中带了LIMIT 1后,一旦 PostgreSQL 找到任何匹配行,就会出现 “提前返回”。无需再访问页面中的其他行。

在从更早时候插入的行中提供name_code值时,这些行出现在前面的页面中,我们会看到 “Rows Removed by Filter” 的值较小。

对于插入顺序中靠后的name_code值,在找到一个匹配行之前会删除更多行。

通过观察访问更多的缓冲页,我们可以在规划器的输出中确认这一点。

性能详细信息

如前所述,在查询语句前面添加EXPLAIN (ANALYZE, BUFFERS)时,我们看到这些 “早期匹配的情况” 访问的缓冲页较少。

访问的缓冲页越少,延迟就越短,执行时间也越短。但是,性能并不是该测试的目的。如果是这样,那么添加一个覆盖name_code列的索引会更有意义。

有关 “Rows Removed by Filter” 的其他信息

实际上,“Rows Removed by Filter” 是每次循环的一个平均值,四舍五入到最接近的整数。

这是什么意思呢?当计划节点使用到多次循环(例如loops=2或更大)时,“Rows Removed by Filter” 是每次循环的 “Rows Removed by Filter” 的一个平均值。

例如,如果有一次循环删除了 10 行,另一次循环删除了 30 行,我们预计会看到两者的平均值 20。

在只有一次循环(loops=1)时,该数字是处理和删除的实际行数。

使用 OFFSET 的实验

使用没有排序的OFFSET时,会与 “Rows Removed by Filter” 相关联。

首先,当我们使用LIMIT 1从第一行中获取到name_code时,我们根本没有看到 “Rows Removed by Filter”。在默认的规划器输出格式中,当过滤了零行时,不会显示该消息。

当我们基于默认排序选择第二行时,我们会看到共享缓冲页命中数为 1,并且我们看到删除行数为 1。

接下来,我们尝试转到这 20,000 行的 “中间”,使用的OFFSET为 10000,并将该偏移位置第一行的name_code值提供给查询,同样不指定对查询进行排序。

使用了这个name_code值之后,我们看到 “Rows Removed by Filter: 10000”,它与偏移量完全匹配。

其他知识点

  • PostgreSQL EXPLAIN 文档介绍了,“Rows Removed by Filter” 如何只在添加ANALYZEEXPLAIN时才显示。
  • “Rows Removed by Filter” 适用于比如WHERE子句上的筛选条件,也适用于JOIN节点上的条件。
  • 只有扫描了至少一行进行评估时,才显示 “Rows Removed by Filter”,或者对于连接节点,当有行被筛选条件丢弃时,会显示一条 “potential join pair”。
  • ORDER BY子句通常会与LIMIT一起使用,这样可以产生更可预测的规划器结果。

要点

  • 如果没有显式的排序(ORDER BY),并且在使用了LIMIT时,“Rows Removed by Filter” 的结果可能会令人惊讶。
  • 使用LIMIT 1时,PostgreSQL 查找到第一个匹配行就会返回。默认顺序可能就是行的插入顺序。
  • 在分析 “Rows Removed by Filter” 的数字时,请检查计划节点是否具有多次循环。在这种情况下,删除行数是所有循环的一个平均值,四舍五入到最接近的整数。
  • 对于性能方面,筛选掉的行占比高,表示存在优化的机会。添加一个索引可能会大大减少对如此多行的筛选,从而减少存储访问,并加快查询速度。

了解更多

PostgreSQL 优化