对于一个小的Postgres 10数据仓库,我正在检查分析查询中的改进,发现了一个相当缓慢的查询,其中可能的改进基本上归结为该子查询(经典的每组最大n个问题):
SELECT s_postings.*
FROM dwh.s_postings
JOIN (SELECT s_postings.id,
max(s_postings.load_dts) AS load_dts
FROM dwh.s_postings
GROUP BY s_postings.id) AS current_postings
ON s_postings.id = current_postings.id AND s_postings.load_dts = current_postings.load_dts
具有以下执行计划:
"Gather (cost=23808.51..38602.59 rows=66 width=376) (actual time=1385.927..1810.844 rows=170847 loops=1)"
" Workers Planned: 2"
" Workers Launched: 2"
" -> Hash Join (cost=22808.51..37595.99 rows=28 width=376) (actual time=1199.647..1490.652 rows=56949 loops=3)"
" Hash Cond: (((s_postings.id)::text = (s_postings_1.id)::text) AND (s_postings.load_dts = (max(s_postings_1.load_dts))))"
" -> Parallel Seq Scan on s_postings (cost=0.00..14113.25 rows=128425 width=376) (actual time=0.016..73.604 rows=102723 loops=3)"
" -> Hash (cost=20513.00..20513.00 rows=153034 width=75) (actual time=1195.616..1195.616 rows=170847 loops=3)"
" Buckets: 262144 Batches: 1 Memory Usage: 20735kB"
" -> HashAggregate (cost=17452.32..18982.66 rows=153034 width=75) (actual time=836.694..1015.499 rows=170847 loops=3)"
" Group Key: s_postings_1.id"
" -> Seq Scan on s_postings s_postings_1 (cost=0.00..15911.21 rows=308221 width=75) (actual time=0.032..251.122 rows=308168 loops=3)"
"Planning time: 1.184 ms"
"Execution time: 1912.865 ms"
行估计是绝对错误的!对我来说,奇怪的是,如果我现在将联接更改为右联接:
SELECT s_postings.*
FROM dwh.s_postings
RIGHT JOIN (SELECT s_postings.id,
max(s_postings.load_dts) AS load_dts
FROM dwh.s_postings
GROUP BY s_postings.id) AS current_postings
ON s_postings.id = current_postings.id AND s_postings.load_dts = current_postings.load_dts
有执行计划:
"Hash Right Join (cost=22829.85..40375.62 rows=153177 width=376) (actual time=814.097..1399.673 rows=170848 loops=1)"
" Hash Cond: (((s_postings.id)::text = (s_postings_1.id)::text) AND (s_postings.load_dts = (max(s_postings_1.load_dts))))"
" -> Seq Scan on s_postings (cost=0.00..15926.10 rows=308510 width=376) (actual time=0.011..144.584 rows=308419 loops=1)"
" -> Hash (cost=20532.19..20532.19 rows=153177 width=75) (actual time=812.587..812.587 rows=170848 loops=1)"
" Buckets: 262144 Batches: 1 Memory Usage: 20735kB"
" -> HashAggregate (cost=17468.65..19000.42 rows=153177 width=75) (actual time=553.633..683.850 rows=170848 loops=1)"
" Group Key: s_postings_1.id"
" -> Seq Scan on s_postings s_postings_1 (cost=0.00..15926.10 rows=308510 width=75) (actual time=0.011..157.000 rows=308419 loops=1)"
"Planning time: 0.402 ms"
"Execution time: 1469.808 ms"
行估计要好得多!
我知道,例如,并行顺序扫描在某些情况下可能会降低性能,但它们不应更改行估计!!如果我没记错的话,汇总函数无论如何也会阻碍索引的正确使用,并且使用其他多变量统计信息(例如,元组id, load_dts
。数据库为VACUUM ANALYZE
d。
对我来说,查询在逻辑上是相同的。
是否有一种方法可以支持查询计划者对估计做出更好的假设或改善查询?也许有人知道这种差异存在的原因?
编辑:以前的加入条件为ON s_postings.id::text = current_postings.id::text
我将其更改为ON s_postings.id = current_postings.id
,以免混淆任何人。删除此转换不会更改查询计划。
Edit2:如以下建议,对于greatest-n-per-group
问题,有另一种解决方案:
SELECT p.*
FROM (SELECT p.*,
RANK() OVER (PARTITION BY p.id ORDER BY p.load_dts DESC) as seqnum
FROM dwh.s_postings p
) p
WHERE seqnum = 1;
一个非常好的解决方案,但可悲的是,查询计划程序还低估了行数:
"Subquery Scan on p (cost=44151.67..54199.31 rows=1546 width=384) (actual time=1742.902..2594.359 rows=171269 loops=1)"
" Filter: (p.seqnum = 1)"
" Rows Removed by Filter: 137803"
" -> WindowAgg (cost=44151.67..50334.83 rows=309158 width=384) (actual time=1742.899..2408.240 rows=309072 loops=1)"
" -> Sort (cost=44151.67..44924.57 rows=309158 width=376) (actual time=1742.887..1927.325 rows=309072 loops=1)"
" Sort Key: p_1.id, p_1.load_dts DESC"
" Sort Method: quicksort Memory: 172275kB"
" -> Seq Scan on s_postings p_1 (cost=0.00..15959.58 rows=309158 width=376) (actual time=0.007..221.240 rows=309072 loops=1)"
"Planning time: 0.149 ms"
"Execution time: 2666.645 ms"
时间上的差异不是很大。它很可能只是缓存效果。如果您在背对背之间反复反复交替,您是否还能得到区别?如果通过设置max_parallel_workers_per_gather = 0禁用并行执行,是否等于它们?
行估计绝对错误!
虽然这显然是对的,但我认为错误的估计不会导致发生特别糟糕的事情。
[我知道例如并行顺序扫描在某些情况下会降低性能,但它们不应更改行估计!!
正确。 JOIN类型的更改导致估计值更改,进而导致并行化更改。认为它必须将更多元组推向领导者(而不是使他们失去资格,而不是降低工人的资格),因为parallel_tuple_cost会阻止并行计划。
如果我没记错的话,聚合函数也会阻止索引的正确使用
不,(id, load_dts)
上的索引,甚至只是(id)
都可以用于聚合,但是由于您需要读取整个表,因此读取整个索引和整个表的速度可能会比读取慢就是将整个表读入HashAgg。您可以通过设置enable_seqscan = off来测试PostgreSQL是否认为它能够使用这样的索引。如果仍然进行seq扫描,则认为该索引不可用。否则,它只是认为使用索引适得其反。
是否有一种方法可以支持查询计划者对估计做出更好的假设或改善查询?也许有人知道这种差异存在的原因?
计划者缺乏洞察力,无法知道派生表中的每个id,max(load_dts)
必须都来自原始表中的至少一行。相反,它将ON中的两个条件作为独立变量应用,并且甚至不知道派生表最常见的值/直方图是什么,因此无法预测重叠程度。但是使用RIGHT JOIN,它知道无论是否在“其他”表中找到匹配项,都将返回派生表中的每一行。如果您通过派生的子查询创建一个临时表并对其进行分析,然后在联接中使用该表,则应该获得更好的估计,因为它至少知道每列中的分布有多少重叠。但是那些更好的估计不太可能加载到更好的计划中,因此我不会理会这种复杂性。
您可以通过将其重写为DISTINCT ON
查询来获得一定的边际速度,但这绝对不会更好。另请注意,这些并不等效。联接将返回给定ID中排在第一位的所有行,而DISTINCT ON将返回其中的任意一行(除非您向ORDER BY添加列以打破关系)
使用窗口功能:
SELECT p.*
FROM (SELECT p.*,
RANK() OVER (PARTITION BY p.id ORDER BY p.load_dts DESC) as seqnum
FROM dwh.s_postings p
) p
WHERE seqnum = 1;
或者,更好的是,如果您想每id
行一行,请使用DISTINCT ON
:
SELECT DISTINCT ON (p.id) p.*
FROM dwh.s_postings p
ORDER BY p.id, p.load_dts DESC;
[如果我不得不推测的话,id
的转换(这是完全不必要的)会抛出优化器。使用right join
,很明显所有行都保留在一个表中,这可能有助于统计信息的计算。