开发者生态
morning
您的计数中的独特之处
摘要
Here is a query that shows up in every analytics workload: SELECT count ( DISTINCT user_id) FROM events; It looks like the cheapest possible thing: count the distinct users. On a machine with cores to...
the
events
count
loops
actual
rows
SELECT
user_id
FROM
and
2026-08-07
1 阅读
约8分钟阅读
gmcabrita
字号:
以下是每个分析工作负载中都会出现的查询: SELECT count (DISTINCT user_id) FROM events;这看起来是最便宜的事情:计算不同的用户。在一台有空闲核心的机器上,你会期望 Postgres 向它投入一些并行工作人员,就像它对几乎所有大型扫描所做的那样。事实并非如此。 DISTINCT 这个关键字会关闭整个语句的并行查询,表越大,成本就越高。没有任何设置或索引会改变这一点;原因在于聚合必须如何执行。该架构 1000 万个事件、大约 5 万个不同的用户、几个国家/地区。没什么不寻常的。 CREATE TABLE 事件 ( id bigint GENERATED ALWAYS AS IDENTITY , user_id int NOT NULL , Country text NOT NULL , amount numeric ( 10 , 2 ) NOT NULL ); INSERT INTO events (user_id, country, amount) SELECT (random() * 50000 ):: int + 1 , ( ARRAY ['US','DE','GB','FR','JP','BR','IN','CA'])[(random()*7)::int + 1], (random() * 500 ):: numeric ( 10 , 2) FROMgenerate_series(1, 10000000);分析事件; max_parallel_workers_per_gather 在新集群上的默认值为 2。对于这些示例,我将其提高到 4,将 work_mem 提高到 64MB,因此下面的计划不会归咎于资源匮乏。两个计数,两个不同的计划 从一个普通的 count(*) 开始,它没有任何可重复数据删除的内容: EXPLAIN (ANALYZE, COSTS OFF ) SELECT count ( * ) FROM events;完成聚合(实际行=1.00循环=1)->收集(实际行=5.00循环=1)工作人员计划:4工作人员启动:4->部分聚合(实际行=1.00循环=5)->并行序列扫描事件(实际行=2000000.00循环=5)四个工作人员加上领导者(循环=5)每个扫描他们的切片并保持连续计数,领导者在最后将五个部分计数加在一起。现在添加一个词:EXPLAIN (ANALYZE, COSTS OFF, BUFFERS) SELECT count (DISTINCT user_id) FROM events;聚合(实际行=1.00个循环=1)缓冲区:共享命中=15915读取=47783,临时读取=14681写入=14684 - >排序(实际行=10000000.00个循环=1)排序键:user_id排序方法:外部合并磁盘:117448kB缓冲区:共享命中=15915读取=47783,临时读取=14681 写入=14684 -> 对事件进行顺序扫描(实际行=10000000.00 循环=1)缓冲区:共享命中=15912 读取=47783 没有收集。无部分聚合。无并行扫描。单个进程读取所有 1000 万行,按 user_id 对每一行进行排序,以便重复项彼此相邻,然后遍历排序的输出,对运行进行计数。该排序不适合 64MB 的 work_mem ,因此它会将 115MB 溢出到磁盘上的临时文件中。一个核心,整个表,加上并行计数(*)从未触及的磁盘IO。为什么规划器无法拆分它排序是 Postgres 在聚合内计算 DISTINCT 的方式:对值进行排序,并将相邻的相等值折叠起来。哈希表是另一种选择,但经典的 DISTINCT 聚合路径进行排序。无论哪种方式,它都必须在一个地方看到每个值,这就是整个问题。 Postgres 中的并行聚合分为两部分。每个工作线程都运行一个构建转换状态的部分聚合,这是它所看到的行的一个小的运行摘要。对于计数来说,状态只是一个数字。然后,领导者运行一个 Finalize Aggregate,将这些部分状态与聚合的组合函数合并,该函数知道如何将两个部分状态合并为一个。 count 的组合函数添加部分计数。 sum 、 avg 、 min 、 max 都有一个。这种拆分、并行扫描、最后合并,就是聚合并行查询的全部基础。 count(DISTINCT user_id) 没有可用的组合步骤,并不是因为没有人编写一个。想想工人可以返还什么。要将两个工作人员的结果合并为正确的全局非重复计数,领导者需要知道每个工作人员看到了哪些用户,因为出现在工作人员 1 的切片中并再次出现在工作人员 2 的切片中的用户必须被计数一次,而不是两次。不同值的部分计数无法合并;你必须向每个工人传达整套独特的价值观,并将他们联合起来。无论如何,此时您已将所有数据移至一处,这正是并行聚合要避免的情况。因此,携带 DISTINCT (或内部 ORDER BY )的聚合无法在部分模式下运行,规划器无法将部分聚合置于 Gather 下,并且由于没有部分聚合可供馈送,并行扫描什么也买不到。整个计划崩溃了。我针对 PostgreSQL 17.10、18.4 和 19beta1 检查了这一点:部分聚合仍然不涵盖其中任何一个上的不同且有序的聚合。 debug_parallel_query 是一种检查这是否是有利于串行执行的成本估算的方法。设置为 on ,它使规划器在任何合法的地方都采用并行计划,即使优化器认为串行更便宜: SET debug_parallel_query = on ; EXPLAIN (COSTS OFF) SELECT count (DISTINCT user_id) FROM events;计划收集工人:1 个单个副本:true -> 聚合 -> 排序排序 K
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱