在 2025 年 2 月 21 日,Robert Haas 提交了一个补丁:
允许 EXPLAIN 显示分数行数
当 nloops > 1 时,我们现在显示两位小数,而不是没有小数。这很重要,因为我们显示的实际上是 planstate->instrument->ntuples / nloops
,而有时你真正想知道的是 planstate->instrument->ntuples
。你可以通过将显示的行数乘以显示的 nloops 值来估算它,但由于显示的值被四舍五入,这使得估算不精确。即使我们显示两位额外的小数,它仍然是不精确的,但不精确的程度降低了。也许我们会在以后找到一种方法来进一步改进这种输出,但目前看来,这比什么都不做要好。
讨论链接: http://postgr.es/m/603c8f070905281830g2e5419c4xad2946d149e21f9d%40mail.gmail.com
然后,6 天后,他又提交了:
EXPLAIN:始终为行数显示两位小数
提交 ddb17e387aa28d61521227377b00f997756b8a27 试图避免在 nloops > 1 时才显示小数点后的数字,因为单次迭代后不可能有分数行数。然而,这使得回归测试变得不稳定,因为并行查询在正常情况下,Gather 或 Gather Merge 以下的所有节点的 nloops 都会 > 1,但如果工作进程没有及时启动,而主进程完成了所有工作,它们的 nloops 就会突然变成 1,这使得小数点后的数字是否显示变得不可预测。虽然 44cbba9a7f51a3888d5087fc94b23614ba2b81f2 似乎修复了即时失败,但可能仍存在较低概率的回归测试失败。
这里可以采用多种修复方法。例如,有人曾提议,如果 rows/nloops 是整数,则只显示小数点后的数字,但目前 rows 是以浮点数存储的,因此它在理论上不是一个精确的量——在极端情况下可能会丢失精度。还有人提议,如果我们在某种可能引起循环的结构下(无论它是否实际发生),则只显示小数点后的数字。尽管这些想法并非毫无道理,但这个补丁采用了更简单的解决方案,即始终显示两位小数。如果这种方法能够经受住构建农场和人类用户的审查,它可以为我们省去做更复杂事情的麻烦;如果不行,我们可以重新评估。
此提交偶然地撤销了 44cbba9a7f51a3888d5087fc94b23614ba2b81f2,该提交不再需要。
讨论链接: http://postgr.es/m/CA+TgmoazzVHn8sFOMFAEwoqBTDxKT45D7mvkyeHgqtoD2cn58Q@mail.gmail.com
以前,对于非常简单的查询,explain analyze 的输出看起来像这样:
=# explain analyze select 1; QUERY PLAN ------------------------------------------------------------------------------------ Result (cost=0.00..0.01 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=1) Planning Time: 4.648 ms Execution Time: 0.354 ms (3 rows)
复制
现在,它看起来像这样:
QUERY PLAN ─────────────────────────────────────────────────────────────────────────────────────── Result (cost=0.00..0.01 rows=1 width=4) (actual time=0.002..0.002 rows=1.00 loops=1) Planning: Buffers: shared hit=3 Planning Time: 0.113 ms Execution Time: 0.041 ms (5 rows)
复制
区别在于它在“actual”部分显示 rows=1.00
,而以前是 rows=1
。
为什么这很重要呢?对于一行的情况,这并不重要,但让我给你展示一些有趣的东西。假设我们有一个包含 5 行的表,如下所示:
=$ select * from data; v ─── 0 0 0 0 0 1 (6 rows)
复制
现在,假设我们想从表中选择所有 v 为 1、2、3 和 4 的行。显然,只有 v == 1 的一行。为了使事情按我需要的方式工作,我必须强制 PostgreSQL 使用嵌套循环,但这都是可能的。经过一些调整后,这是 PostgreSQL 17 的 explain analyze 输出:
=$ explain analyze SELECT w.*, x.* FROM unnest('{1,2,3,4}'::INT4[]) with ordinality as w (q, idx), lateral ( SELECT * FROM data WHERE v = q ORDER BY v desc LIMIT 1 ) x; QUERY PLAN -------------------------------------------------------------------------------------------------------------- Nested Loop (cost=0.00..12.97 rows=4 width=16) (actual time=0.014..0.021 rows=1 loops=1) -> Function Scan on unnest w (cost=0.00..0.04 rows=4 width=12) (actual time=0.006..0.007 rows=4 loops=1) -> Limit (cost=0.00..3.22 rows=1 width=4) (actual time=0.003..0.003 rows=0 loops=4) -> Seq Scan on data (cost=0.00..41.88 rows=13 width=4) (actual time=0.002..0.002 rows=0 loops=4) Filter: (v = w.q) Rows Removed by Filter: 6 Planning Time: 1.449 ms Execution Time: 0.051 ms (8 rows)
复制
请注意,Seq Scan on data 显示为重复 4 次(loops=4),因为展开的数组有 4 个元素。它显示返回了 0 行。
这是因为实际的行数实际上是一个平均值。但由于 PostgreSQL < 18 不能显示分数,所以它显示为 0。这可能会导致行数计算出现问题。
在 PostgreSQL 18 中,explain 的输出看起来不同:
Nested Loop (cost=0.00..12.97 rows=4 width=16) (actual time=0.017..0.025 rows=1.00 loops=1) Buffers: shared hit=4 -> Function Scan on unnest w (cost=0.00..0.04 rows=4 width=12) (actual time=0.006..0.007 rows=4.00 loops=1) -> Limit (cost=0.00..3.22 rows=1 width=4) (actual time=0.003..0.004 rows=0.25 loops=4) Buffers: shared hit=4 -> Seq Scan on data (cost=0.00..41.88 rows=13 width=4) (actual time=0.003..0.003 rows=0.25 loops=4) Filter: (v = w.q) Rows Removed by Filter: 6 Buffers: shared hit=4 Planning: Buffers: shared hit=4 Planning Time: 0.142 ms Execution Time: 0.044 ms (13 rows)
复制
请注意,它显示rows=0.25 - 乘以 4 后(因为 loops=4)得出该表所有循环中返回的行数正确为“1”。
这很棒。需要对explain.depesz.com进行小修复才能正确处理,但这绝对是一件好事。
原文地址:https://www.depesz.com/2025/02/28/waiting-for-postgresql-18-allow-explain-to-indicate-fractional-rows/