PostgreSQL18允许Grouping Sets的HAVING子句下推
虽然离PostgreSQL18发版还很久远,但是许多已经提交到git上的新特性,我们可以慢慢的揭开它的面纱,看看是否有我们感兴趣的一些新特性。今天我们来看看在PostgreSQL17之前(包括PG17)的版本中Grouping Sets的HAVING子句未下推的问题。
存在问题
PostgreSQL17之前版本的版本中Grouping Sets的HAVING子句未下推的问题?
PostgreSQL17的执行计划
在PostgreSQL17的客户端中执行sql语句
demo=# EXPLAIN (costs off)
demo-# SELECT aircraft_code, status, COUNT(*)
demo-# FROM flights
demo-# GROUP BY GROUPING SETS ((aircraft_code, status), (status))
demo-# HAVING status = 'Arrived';
QUERY PLAN
----------------------------------------------
HashAggregate
Hash Key: status, aircraft_code
Hash Key: status
Filter: ((status)::text = 'Arrived'::text)
-> Seq Scan on flights
(5 rows)
Time: 19.832 ms
从PostgreSQL17的执行计划,可以看到执行计划中的Filter行,它位于HashAggregate节点下,但是又在Seq Scan之前,这就表示在聚合的时候才检查HAVING条件,而不是我们希望的在对表扫描的时候过滤数据。
准备测试用例
在PostgreSQL17和PostgreSQL18DEV版本数据库导入demo测试数据,demo的数据库可以从下面的地址自行下载。
https://postgrespro.com/community/demodb
利用在flights表来验证Grouping Sets的having子句的是否下推
SELECT aircraft_code, status, COUNT(*)
FROM flights
GROUP BY GROUPING SETS ((aircraft_code, status), (status))
HAVING status = 'Arrived';
问题原因
这里的主要原因是如果存在任何非空的分组集,我们就不能把HAVING条件下推到where中,因为我们没有办法判断HAVING子句是否引用了分组集可以为空的任何列,并且将这样的子句移动到WHERE中可能会改变结果。
为了方便大家快速理解,使用上述的测试用例l来讲述,在执行的SQL中的分组结果集有两个(aircraft_code, status), (status),having条件为status = 'Arrived',当然原来的SQL的having条件列都包括在分组集的列中,因此在这里可以是可以把having条件下推到where中,但是如果我们在添加一个条件aircraft_code in ('319','733','773','321')。这个时候测试的SQL语句就成为了下面的语句。
EXPLAIN (costs off)
SELECT aircraft_code, status, COUNT(*)
FROM flights
GROUP BY GROUPING SETS ((aircraft_code, status), (status))
HAVING aircraft_code in ('319','733','773','321') and status = 'Arrived';
在这个语句中,HAVING子句的新增的aircraft_code in ('319','733','773','321') 条件
demo=# SELECT aircraft_code, status, COUNT(*)
demo-# FROM flights
demo-# GROUP BY GROUPING SETS ((aircraft_code, status), (status))
demo-# HAVING aircraft_code in ('319','733','773','321') and status = 'Arrived';
aircraft_code | status | count
---------------+---------+-------
733 | Arrived | 7635
319 | Arrived | 7417
321 | Arrived | 11702
773 | Arrived | 3657
(4 rows)
假如可以把having条件下推,SQL就等价于把查询条件放到了where条件中,如下面的SQL:
demo=# SELECT aircraft_code, status, COUNT(*)
demo-# FROM flights
demo-# where aircraft_code in ('319','733','773','321') and status = 'Arrived'
demo-# GROUP BY GROUPING SETS ((aircraft_code, status), (status));
aircraft_code | status | count
---------------+---------+-------
733 | Arrived | 7635
319 | Arrived | 7417
321 | Arrived | 11702
773 | Arrived | 3657
| Arrived | 30411
(5 rows)
从查询的结果的行数,也可以看出两个SQL是不等价的,主要是因为aircraft_code在分组集(status)中没有包括aircraft_code条件,因此如果把aircraft_code下推到where条件会导致查询的结果不一致。
解决方案
在PostgreSQL18通过使用RTE_GROUP RTE的RT索引对grouping sets标记为可空的表达式,识别那些引用"可空grouping-sets列"的子句变得容易得多:我们只需要检查RTE_GROUP RTE的RT索引是否存在于子句中。对于其他HAVING子句,则可以安全的下推。下面我们在PG18中来看一下执行计划。
PostgreSQL18的执行计划
在PostgreSQL18的客户端中执行sql语句
demo=# EXPLAIN (costs off)
demo-# SELECT aircraft_code, status, COUNT(*)
demo-# FROM flights
demo-# GROUP BY GROUPING SETS ((aircraft_code, status), (status))
demo-# HAVING status = 'Arrived';
QUERY PLAN
----------------------------------------------------
HashAggregate
Hash Key: status, aircraft_code
Hash Key: status
-> Seq Scan on flights
Filter: ((status)::text = 'Arrived'::text)
(5 rows)
通过上面的执行计划,可以看出Filter行,现在位于Seq Scan节点下,而过去位于HashAggregate节点下,这就意味着在扫描表行时就检查HAVING条件,也就是在聚合前进行过滤。当然,对于上面的SQL,其实是可以进行查询重写的,将条件从HAVING移到WHERE中,但是也很高兴看到开发者找到更有效的解决方案。
总结
最后,我们也来总结一下,通过这篇文章,你至少可以了解如下:
PG18允许Grouping Sets的HAVING子句下推,该功能是优化器自动处理
理解了之前的版本Grouping SetsHAVING子句没有办法判断HAVING子句是否引用了分组集可以为空的任何列,并且将这样的子句移动到WHERE中可能会改变结果。
过滤条件尽可能的放到where条件中,而不是HAVING子句中,这其实也是之前一些优化经验。
参考
https://postgrespro.com/blog/pgsql/5971769#commit_67a54b9e8
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=67a54b9e8
– / END / –
可以通过下面的方式联系我
如果这篇文章为你带来了灵感或启发,就请帮忙点赞、收藏、转发;如果文章中不严谨或者错漏之处,请及时评论指正。非常感谢!




