一月 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
列上的索引会是一个好主意。在本文中,我们的目的是了解没有该索引时的行为。 - 所有数据都是从共享缓冲区访问的,这是通过添加
BUFFERS
到EXPLAIN (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” 如何只在添加
ANALYZE
到EXPLAIN
时才显示。 - “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” 的数字时,请检查计划节点是否具有多次循环。在这种情况下,删除行数是所有循环的一个平均值,四舍五入到最接近的整数。
- 对于性能方面,筛选掉的行占比高,表示存在优化的机会。添加一个索引可能会大大减少对如此多行的筛选,从而减少存储访问,并加快查询速度。