暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

聚合分组等价变换大法之分组下压

图片

引言

年少不知优化苦,遇坑方知优化难。


不少人可能都经历过这样的场景,

在刚开始写程序的时候,我们往往只在意返回结果正确即可。

然而,系统运行一段时间后,我们会渐渐感到力不从心,

产生一堆疑问,

我写的程序为什么那么慢?

程序为什么卡住不动了?

究竟是哪个程序造成了路阻?


这时候如果我们通过各种手段去监控慢 SQL,被动应对优化,很可能事倍功半。

如果我们置之不理,很有可能导致程序瘫痪、假死、崩溃。


怎么办呢?OceanBase 教你主动出击!

工欲性能调优,必先 get SQL 改写大法!

要想 SQL 跑得快,改写文章看起来!


在前几期查询改写文章中介绍了几种子查询相关改写策略,本期我们聚焦在分组聚合(Group By)的优化上,探讨分组聚合存在的等价变化可能性。在偏分析型的场景中,分组聚合主要用于满足业务的统计分析需求,例如:统计不同年龄段的人数;统计不同年龄段的平均工资等。典型的统计分析类 SQL 会首先连接(Join)多表中的数据,然后将连接结果按照某些维度分组,最后在每个分组进行求和、计数、算极值等聚合运算。在这类请求中,通常原始表中的数据量比较庞大,连接操作本身需要处理大量数据,并且多表连接可能进一步放大数据的规模,导致分组操作也需要处理庞大的数据。


针对这类统计分析类的查询,一种有效的优化策略是调节连接和分组操作之间的执行次序,先对原始表的数据进行一定的预聚合,然后再进行多表连接和最终的分组聚合,这种优化策略称之为分组下压


图片分组下压

考虑下面这个统计分析类型的查询 Q1,该查询统计了某个影院中每部电影上映以来的总票房。不妨假定,该影院开放以来,总共为 100 部影片放映了 3,000 次排片(即 PLAY 表中共有 3,000 条记录,movie_name 有 100 个不同值);售出了 200,000 张电影票(即 TICKETS 表中共有 200,000 条记录)


-- 排片表PLAY(play_id, movie_name, time, price, seats)-- 售票表TICKETS(play_id, real_price, sale_date);

Q1:
SELECT P.movie_name, SUM(T.real_price)FROM PLAY P,
TICKETS T
WHERE P.play_id = T.play_id
GROUP BY P.movie_name;
复制


图 (一)展示了 Q1 的逻辑执行计划树。首先,我们需要将 P 和 T 连接在一起,每一张电影票都唯一对应了一次排片,所以 P 和 T 连接会产生 200,000 条记录,最后需要对连接结果再次进行分组。可以看到,这里连接和分组都需要处理 200,000 条记录。整体的代价较高。


图片


OceanBase 会对 Q1 使用分组下压优化,将分组操作提前至连接运算前进行,这时可以构造得到相同语义的查询 Q2。


Q2:
SELECT P.movie_name,
SUM(V.total)FROM PLAY P,
(SELECT T.play_id,
SUM(T.real_price) total FROM TICKETS T GROUP BY T.play_id) V
WHERE P.play_id = V.play_idGROUP BY P.movie_id;
复制


以上改写的一个重要观察是:在 TICKETS 表中存在大量 play_id 取值相同的记录,它们会被划分到相同的影片分组中,我们可以对相同 play_id 的数据做一次预聚合得到每场排片的票房,然后根据每场排票的票房来统计每部影片的总票房。


可以看到,在 Q2 中,视图 V 按照 play_id 进行分组聚合,可以得到每一场排片的票房,这轮分组聚合可以大大减少数据量(从 200,000 减少到 3,000),然后将分组结果和 PLAY 表中的排片记录按照 P.play_id = T.play_id 进行连接,最后对连接结果按照 movie_name 进行分组操作,得到每部电影的总票房。


图(二)展示了 Q2 的逻辑执行计划树。可以看到,经过分组下压之后,连接和分组操作运算的数据规模显著降低至 3,000。这种方式大大提升了查询的执行性能。


图片

性能分析

在上文分组下压的例子中,不难发现下压分组操作可以显著减少后续的连接和分组操作需要运算的数据量,那么是否分组下压总是会产生更好的执行性能呢?答案是否定的,在不同的场景下,Q1 和 Q2 孰优孰劣并不确定,需要根据实际的数据规模进一步判断。这里,不妨考虑两个场景。


场景1: 影院有大量巨幕放映室并且观众基本满座,此时表 TICKETS 中存在大量 play_id 相同的购票信息。在这种情况下,Q1 和 Q2 的行为类似于上文给定的例子。Q2 会有更好的表现。这里不再赘述。

场景2: 影院多为小型放映室,受到疫情的影响,售票情况不理想,大量座位空置,甚至有些排片空场放映。此时 TICKETS 表的规模比较小,并且 play_id 相同的购票信息数量也相对较少。不妨假定,这种情况下,TICKETS 表的规模为 1000。图(三)和图(四)展示了这种数据分布下,Q1 和 Q2 的逻辑执行计划树。


图片


在图(四)中,预聚合无法显著地减少 TICKETS 表的数据规模。这使得,后续的连接和分组操作在数据处理规模上也不会有显著的变化。相对图(三)而言,图(四)额外增加了一轮预聚合的时耗。整体上看,进行分组下压反而会导致整个查询的执行性能变差。


可以看到,在以上两种场景中,Q2 性能有时并不优于 Q1。因此,为了保证改写后查询性能不会变差,分组下压是一种基于代价的查询改写策略:在完成改写后,通过“询问”物理优化器,根据实际数据分布获得改写前后执行计划的代价,并仅在改写后代价降低时才选择触发改写。经过代价评估触发的分组下压改写,能够有效地减少连接操作处理的数据规模,在多表连接后计算分组的场景中,可以有效地提升查询效率。


图片

本文主要介绍了分组下压的查询改写策略,通过将一些分组运算先于连接执行,下压到单个表中去缩小数据集,减少参与连接运算的数据规模,提升查询性能。由于某些场景分组下压后会导致查询性能变差,该策略是一个基于代价的改写,只有评估改写后代价降低才会触发。本文介绍的分组下压主要是将分组操作调整到连接操作之前。在上文的讨论中也可以看到,在一些场景中,我们也会希望先执行连接操作,后执行分组操作。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论