Mat*_*sen 6 postgresql count window-functions postgresql-performance postgresql-13
在我的应用程序服务器中,我想使用LIMIT
和对数据集进行分页OFFSET
,并另外将数据集的总数返回给用户。
而不是对数据库进行两次远程调用:
select count(1) as total_count from foo;
select c1 from foo;
Run Code Online (Sandbox Code Playgroud)
我认为在单个数据库调用中完成此操作会更明智:
select c1, count(1) over (partition by null) from foo;
Run Code Online (Sandbox Code Playgroud)
但是,与不使用窗口函数相比,添加此窗口函数会导致执行时间长一个数量级。
我觉得这很令人惊讶,因为类似的时间select count(1) from foo
只需要两倍的时间select c1 from foo
。然而,将其转换为窗口函数会导致性能下降。
此外,使用以下使用子查询的替代方案非常快:
select c1, (select count(1) from foo) as total_count from foo;
Run Code Online (Sandbox Code Playgroud)
我本来期望 postgresql 能够优化partition by null
我在 Oracle 中尝试过这一点,发现了类似的性能损失。
如何解释为什么这里会出现性能损失?对于核心 postgresql 开发人员来说,进行更改以优化这一点是否相对容易,甚至值得,例如通过将 PARTITION BY NULL 的窗口函数转换为子查询?
设置:
drop table foo;
create table foo (c1 int);
insert into foo
select i from generate_series(0, 100000) i;
analyze foo;
Run Code Online (Sandbox Code Playgroud)
常规的SELECT
:
=> explain analyze select c1 from foo;
QUERY PLAN
----------------------------------------------------------------------------------------------------------
Seq Scan on foo (cost=0.00..1443.01 rows=100001 width=4) (actual time=0.021..6.848 rows=100001 loops=1)
Planning Time: 0.045 ms
Execution Time: 10.021 ms
Run Code Online (Sandbox Code Playgroud)
不使用窗口函数会导致执行时间约为 10 毫秒。
具有COUNT(1) OVER (PARTITION BY NULL)
窗口函数:
=> explain analyze select c1, count(1) over (partition by null) as total_count from foo;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
WindowAgg (cost=0.00..2943.03 rows=100001 width=44) (actual time=63.828..100.321 rows=100001 loops=1)
-> Seq Scan on foo (cost=0.00..1443.01 rows=100001 width=36) (actual time=0.025..17.727 rows=100001 loops=1)
Planning Time: 0.071 ms
Execution Time: 106.386 ms
Run Code Online (Sandbox Code Playgroud)
使用窗口函数的执行时间为 100 ms。这比没有此窗口函数的相同查询要昂贵一个数量级。
与常规的SELECT COUNT(1)
=> explain analyze select count(1) from foo;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------
Aggregate (cost=1693.01..1693.02 rows=1 width=8) (actual time=19.876..19.876 rows=1 loops=1)
-> Seq Scan on foo (cost=0.00..1443.01 rows=100001 width=0) (actual time=0.026..9.238 rows=100001 loops=1)
Planning Time: 0.066 ms
Execution Time: 19.908 ms
Run Code Online (Sandbox Code Playgroud)
常规SELECT COUNT(1)
需要大约20ms。
并使用更优化的子查询形式:
=> explain analyze select c1, (select count(1) from foo) as total_count from foo;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
Seq Scan on foo (cost=1693.02..3136.03 rows=100001 width=12) (actual time=18.554..30.492 rows=100001 loops=1)
InitPlan 1 (returns $0)
-> Aggregate (cost=1693.01..1693.02 rows=1 width=8) (actual time=18.533..18.534 rows=1 loops=1)
-> Seq Scan on foo foo_1 (cost=0.00..1443.01 rows=100001 width=0) (actual time=0.010..8.438 rows=100001 loops=1)
Planning Time: 0.074 ms
Execution Time: 33.696 ms
Run Code Online (Sandbox Code Playgroud)
这大约需要 30 毫秒,这是我所期望的(select count(1)
需要 20 毫秒,select c1
需要 10 毫秒)。
首先,OFFSET
/LIMIT
是用于分页的原始工具,扩展性不佳,并且在并发写入负载下无法正常工作。根据您的用例,有更多更智能的解决方案。看:
OVER (PARTITION BY NULL)
是一种毫无意义的浪费。使用等效的、更快的OVER ()
代替。
count(1)
是另一种(轻微的)无意义的浪费。count(*)
代替使用。
尽管如此,观察结果还是可靠的,我可以按预期重现它。
然而,它仍然具有误导性。测试表是不现实的,只有一个整数列。通常您会添加WHERE
子句和/或涉及索引。所以你原来的测试有效性是有限的。
一个更现实的测试用例(至少)有一些基本的有效负载列和索引显示了不同的见解:
CREATE TABLE foo (
id int PRIMARY KEY
, payload text -- mininal payload of 32 byte text ...
, dt timestamptz DEFAULT now()); -- ... and a timestamp column
INSERT INTO foo (id, payload)
SELECT i, md5(i::text)::text
FROM generate_series(1, 100000) i;
Run Code Online (Sandbox Code Playgroud)
考虑 fiddle 中的各种测试(使用 Postgres 14;Postgres 13 非常相似,您只需切换引擎并重新运行即可):
db<>fiddle here
count(*) OVER ()
比 快约 10% count(1) OVER (PARTITION BY NULL)
。特别注意使用热缓存的更可靠的测试(“使用热缓存重复主测试 x”。
EXPLAIN
甚至期望它3185
比3435
.
对于没有WHERE
.
但添加选择性WHERE
条款会改变游戏规则。这是更常见的用例。
如果查询没有(完美的)索引支持,情况会再次发生变化。现在,子查询的速度要慢得多。两次连续扫描超过了窗口函数所增加的开销。
现实生活中的示例通常更混乱,表/索引膨胀、更多列、连接、计算等。然后,对子查询中的计数进行单独扫描通常会变得更加昂贵。
有关的:
归档时间: |
|
查看次数: |
4772 次 |
最近记录: |