GaussDB(DWS)性能调优系列实战篇四:十八般武艺之SQL改写

         数据库的应用中,充斥着坏味道的SQL,非常影响查询的性能。坏味道SQL,即由于开发者写的随意,导致执行性能较差,需要通过优化SQL语句进行调优的SQL。在GaussDB(DWS)分布式场景下,相对于单机环境,将出现更多的坏味道SQL语句。本文将系统介绍在GaussDB(DWS)系统中影响性能的坏味道SQLSQL模式,帮助大家能够从原理层面尽快识别这些坏味道SQL,在调优过程中及时发现问题,进行整改。从大的方面来看,主要包含以下几类:a. 不支持下推导致的坏味道;b.不支持重分布导致的坏味道;c.数据类型转换导致的坏味道;d.全局性操作导致的坏味道;e.NestLoop类低效运算导致的坏味道;f.冗余操作导致的坏味道。本文将介绍每一类坏味道的原因,以及如何进行SQL改写及调优。


一.不支持下推导致的坏味道

GaussDB(DWS)分布式场景下,数据运算应该全部下推到DN上执行,才能获得比较好的性能收益。但对于某些场景,数据必须在CN上执行,导致语句无法全部下推到DN运算,会导致两个主要的瓶颈点:

(1)       只有基表扫描在DN执行,需要将大量数据传输到CN上,网络开销增大。

(2)       原先可以在DN上分布式执行的数据,均由CN单个执行,瓶颈加大。

通常情况下,我们不支持不下推函数、复合类型、复杂语法及组合(例如:某些场景的with recursive语法,rollup函数+count(distinct)语法)的下推,所以应该尽量避免在语句中使用以上元素。在客户场景中,经常遇到函数不能下推导致的问题,本篇博文重点以函数下推为例,讲述如何解决类似的问题。

如下图计划所示,在语句中包含了不支持下推的函数unship_func(),导致整个计划不能下推,计划中出现“_REMOTE_TABLE_QUERY_”的字样,即会出现上述的瓶颈问题。遇到类似问题,需要根据具体应用场景,为函数设置合理的下推属性,使其可以下推。

通常来说,函数可以通过可变性和下推维度进行划分,主要包含以下函数属性:

划分维度

函数属性

说明

示例

可变性维度

immutable

函数在每次调用时返回值都是恒定的,例如:一些数学函数等

abs()

add_month()

stable

函数在一个语句中反复调用时,返回值都是恒定的,但在不同环境下返回值可能不同,例如:与环境设置,语句运行上下文相关的函数

statement_timestamp()(返回语句开始时间)

字符串类函数(结果与encoding设置相关)

volatile

语句在一个语句中反复调用时,返回值都可能会变化,例如:随机类函数,与表的数据相关的函数(表随时可能会被更新)

clock_timestamp()(返回当前时间)

random()

下推维度

unshippable

不能下推到DN执行的函数,通常是因为CN/DN执行行为不一样


shippable

可以下推到DN执行的函数,要求CN/DN执行行为必须一样,对于immutable属性的函数,在CN/DN执行的结果必定相同,因此immutable函数必定是shippable


 

以上两个属性可以通过系统表pg_procprovalitileproshippable字段查询。目前GaussDBCN/DN行为是否一致作为下推标准,支持大部分immutablestable函数的下推,以及特定场景少量volatile函数的下推。对于用户自定义函数,由于数据库无法知晓函数的行为,因为不知道函数的属性,因为默认是volatileunshippable的。包含对应函数的语句将无法下推到DN执行。用户可以根据函数的行为,判断返回结果是否恒定,以及是否可以下推,设置对应的属性。具体的设置方法为:

(1)       如果函数的返回结果是恒定的,比如数字计算函数,日期计算函数,则可以为其设置immutable属性。

(2)       如果函数中使用了数据表,且数据表均是复制表的只读操作且不涉及事务操作(所以DN数据均相同,可以下推到一个DN上执行),则可以为其设置shippable属性。

其余情况则还是不能下推,如果错误设置,会引发不可预知错误,因此需要慎重设置。

 

如果无法使函数下推,可以对语句进行改写,使不涉及函数的部分能够部分下推到DN执行。例如,对于以下SQL语句:

select unship_func() from t1 join t2 on t1.a=t2.a;

可以改写为:select unship_func() from (select * from t1 join t2 on t1.a=t2.a);

这样,t1t2 join的部分可以推到DN执行,只有unship_func()的计算是逐行在CN执行的,流程变化如下图所示,在join结果集较小的情况下,性能也可以得到明显的提升。


二.不支持重分布导致的坏味道

share-nothing架构的分布式场景下,数据使用哈希分布在不同DN上,并且通过数据重分布使得中间结果均匀分布在各个DN上进行并行计算,进行执行查询的加速。所以在执行过程中,一直保持数据能够均匀分布在DN上,是保证性能的关键。通常情况下,我们需要进行表的关联(Join)和聚集(Agg)操作,这就需要关联和聚集列能够支持重分布,从而进行灵活的重分布操作。在GaussDB(DWS)中,不支持重分布的类型主要有realdouble类型,因此使用这两种类型进行操作时,将导致无法生成重分布的计划,在实际使用时要尽量避免。首先,在进行表定义时,要尽量避免使用这两种类型,使用numeric类型进行替代。同时,还要避免使用返回值为这两种类型的函数,进行关联和聚集运算,例如:ceil, floor, pow, sqrt, 以及一些分析类聚集函数如stddev_samp等。如果必须使用此类函数进行相关运算,需要在计算完对这些类型进行类型转换,转换成numeric类型。例如下面的语句:select count(distinct ceil(a)) from t1;由于ceil()返回值为double,原始生成的计划是不能下推的:

如果我们显式将ceil()转换成numeric类型,得到的下推计划如下:

 

三.数据类型转换导致的坏味道

数据库在进行不同列的比较、计算运算时,如果类型不同,需要进行类型转换。通常情况下,由优化先低的类型往优先级高的类型转换,字符串的优先级较数字较低,同时数字类型,精度低的会向精度高的转换。在应用中,经常遇到的是,字符串和数字进行比较,导致字符串需要转成数字进行比较操作。由于数据库的基本调优都是基于基表列的,数据转换后就会带来以下性能问题:

(1)       无法使用索引。由于索引是基于列的排序构造的,字符串转换成数字后的排序性与字符串不一致(’2’’12’,字符串’2’大,数字12大),故无法使用索引,对于返回数据量少的场景,使用全表扫描带来性能问题。

(2)       无法进行分区剪枝。GaussDB(DWS)支持range分区,即根据分区键值的范围创建不同的分区。当需要进行分区键上的范围操作时,进行分区剪枝。而字符串转换成数字后,道理与(1)类似,打破排序性,导致分区剪枝失效。

(3)       需要进行网络重分布操作。在进行表的关联,聚集操作时,如果涉及表分布键的操作,可以在本地并行进行。但如果涉及到类型转换,则hash值发生变化,需要进行网络重分布,增加了网络开销。

(4)       估算不够准确,可能造成计划的性能问题。我们收集的统计信息都是基于基表列的,如果进行类型转换,则缺少转换后的统计信息,同样可能造成计划不准。

因此,在表设计之初就要把数据类型定义好,数字类型尽量使用整型或numeric(浮点型),尽量少使用字符串数据类型,除了与数字比较产生上述开销外,变长的字符串在处理时还产生了额外的空间申请释放,内存拷贝的开销,都是无形中性能的损耗。

 

四.全局性操作导致的坏味道

前面提到,GaussDB(DWS)分布式数据库的优势,就是利用多DN的资源进行并行计算,提高吞吐量。但有些SQL在这些方面不够注意,导致执行过程中由于全局性操作仅能在一个DNCN上执行,造成了性能瓶颈。不能下推即属于这一类型问题。除了不能下推,本章节主要讨论在DN上进行全局操作导致的问题。客户场景遇到的主要问题,是需要对全量大数据量进行排序的问题,比如:windowagg函数没有partition by,但包含order by的问题。例如如下语句:

select * from (select ss_sold_date_sk, sum(ss_sales_price) over (order by ss_sold_date_sk rows 2 preceding) sum_2, sum(ss_sales_price) over (order by ss_sold_date_sk rows 5 preceding) sum_5 from store_sales), date_dim where ss_sold_date_sk=d_date_sk and d_year=2000 and d_moy=5 order by 1;

执行计划如下:

         计划中第4层将所有DN的数据广播到一个DN上进行全局排序,并计算sum窗口函数的值,导致性能瓶颈。对于类似情况,在语义允许的情况下,尽量给窗口函数增加partition by的字段,这样分组计算时,GaussDB(DWS)可以将计算分配到不同的DN上进行,提高执行效率。

 

五.NestLoop类低效运算导致的坏味道

NestLoop是最简单的表关联手段,当然也是最低效的,每条元组之间都要进行匹配,数据量大的时候经常执行不出来。所以在GaussDB(DWS)中,我们经常使用HashJoin进行表的关联。但有些SQL语句从语义上决定,只能使用NestLoop的方式执行,导致性能问题。总结起来,有以下几方面:

(1)       无等值关联条件场景

GaussDB(DWS)中,我们推荐使用等值关联,这样可以使用HashJoin进行执行加速。但对于非等值关联条件,只能使用NestLoop的方式进行连接,同时需要将其中一个表进行Broadcast广播到所有DN进行。如果两个表都比较大,将导致性能瓶颈。例如:select * from t1 join t2 on t1.a<t2.a;计划如下图所示:

         类似的场景还有:<1> select * from t1 join t2 on 1=1;这个语句无关联条件,我们称为笛卡尔积关联,返回结果行数为t1t2行数的乘积。<2> select * from t1 join t2 on t1.a=t2.a and t1.a=5;这个语句虽然有等值关联条件,但关联条件还有个过滤条件t1.a=5,所以实际上t1.a=t2.a=5,是在a=5过滤基础上的笛卡尔积。这类语句都会导致性能瓶颈。

       (2)       相关子查询场景

        相关子查询,即子查询中需要依赖父查询的列值进行迭代计算的场景。例如如下语句:

        select (select ss_item_sk from store_sales where ss_item_sk<i_item_sk limit 1) from item;

   计划如下:

在分布式框架下,为了保证每一行父查询元组均能够在子查询中迭代计算出结果,需要所有DN均维护一份子查询表的全局数据,在上面的计划中,子查询中的store_sales表进行了广播和物化,将数据存在所有DN上,见8层算子。第3层算子每一行数据均需要通过迭代子查询计算(第4层及之下的SubPlan 1)获得是否匹配的信息。

      这个计划执行非常慢且耗费资源,原因是:

      a)   DN节点非常多的分布式环境下,将数据广播到所有DN上进行物化,将导致网络资源和IO资源耗费巨大,同时大数据量的广播耗时也很长。

      b)   对于父查询的每一行元组,均需要迭代计算子查询的值,类似于NestLoop的执行方式,效率极低。

以上两个问题带来的语句性能问题在各个客户现场均被识别,因此需要进行通用子链接提升转成join的查询重写来解决该问题。

在之前的版本中,我们已经支持了如下场景的子链接提升,即将子链接转化为join获得性能提升,这些子链接均出现在where条件里,包括:

a)    IN/NOT IN的非相关子查询。

b)   EXISTS/NOT EXISTS的等值相关子查询。

c)    包含Agg表达式的等值相关子查询。

d)   以上场景子查询的OR场景。

当然,目前我们还不支持目标列上出现相关子查询的场景,以及相关子查询中出现不等值比较的场景,需要首先识别出坏味道,进行语义等价的整改。

 

    (3)       Not IN场景

当查询语句中包含NOT IN谓词时,如语句:select * from t t1 where t1.a not in (select b from t);其计划如下图所示:

         我们发现其走了NestLoop计划,原因是NOT IN的特殊语义,NULL值和任意值的比较结果是NULL,不是true,所以需要单独在Join条件上加上IS NULL的条件,导致等值关联变成非等值关联。一般客户场景下,使用NOT IN并不是为了处理NULL值,而是对NOT IN语义的误解,因此需要将两侧的NULL值去除(验证无NULL值,或建立临时表)后,使用下面两种方法解决性能问题:

        a)   如果确认关联列没有NULL值,需要在关联列上建立NOT NULL约束,比如示例SQL语句,要在t表的a列和b列上均建立NOT NULL约束。

        b)   可以将语句改写成NOT EXISTS,例如示例语句可以改写为:select * from t t1 where not exists (select 1 from t where t.b=t1.a);

 

    (4)       IN list场景

IN list场景是指语句中包含大量常数的IN条件,例如下例:select count(*) from customer where c_customer_sk in (1, 101, 201, 1001, 2001, 10001, 20001, 100001, 200001, 500001, 800001);通常情况下,会生成基表扫描的查询计划,即对于customer表的每一条,需要检查c_customer_sk列是否会和IN list中的某个值匹配,匹配即返回该条元组。当IN list中的条件比较多时,匹配近似于NestLoop的操作。

针对这种场景,GaussDB(DWS)实现了In list to Join的查询优化规则,根据代价估算,针对In list中的值较多的场景,生成Hash Join的计划,极大提升性能,如下图所示:

         但代价估算存在估算不准的情况,对于列存表有min/max过滤,有时转成Join并不一定性能最好,因此我们增加了GUC参数:qrw_inlist2join_optmode,用户可以手动设置该参数的值进行调优,其值说明如下:

        a)   cost_base,即根据代价估算,默认值。

        b)   rule_base,强制使用转换成Join的优化规则。

        c)   不小于2的整数,表示当In list中的常量个数不小于N时,使用转换成Join的优化规则。

 

六.冗余操作导致的坏味道

GaussDB(DWS)场景中,经常会遇到一类SQL,其中存在大量的冗余操作,导致执行时进行了大量无效计算。这类语句的场景很多,需要根据performance数据详细分析,以下举几个例子简单看一下。

(1)       语句中存在大量冗余case when语句的场景

例如如下语句,语句中有大量case when语句,大多数条件都是一样的,只是default值存在不同,但执行时,每个分支的case when均需要执行,导致时间成倍增加。

         这种情况下需要对语句进行深入分析,根据语义进行等价改写。通常我们可以把case when加到过滤条件,分成多个子查询分别求值,或提取公共部分进行改写。经过优化后,消除了case when,性能得到了提升,修改后的语句如下所示。

         再看一个例子,下图的例子中,会进行聚集函数的计算,但在下方的case when中频繁引用这些聚集函数,导致聚集函数计算多遍。

         经过等价优化后,在子查询中仅计算一遍聚集函数,在父查询的case when中,直接使用计算好的聚集函数的值,避免多次计算,优化后的语句如下图所示。

    (2)       排序仅取部分数据的场景

在应用中,经常出现提取前若干条数据的场景。例如如下语句:

select a from (select a, row_number() over (order by a desc) rid from t1) where rid between <start> and <end>;

即取按a排序后的从startend的行数,对应的执行计划如下所示:

         我们发现对所有的数据进行了排序,然后返回了前10条数据,数据量较大时,所有数据量全排将大量耗费时间。这种情况下,我们可以在子查询中加入limit语句,这样排序变为top N排序,减少了排序的时间,修改后的语句为:

        select a from (select a, row_number() over (order by a desc) rid from t1 limit <end>-<start>+1 offset <start>-1) where rid between 1 and 10;

  对应的计划变为:

    (3)       聚集操作顺序出现问题的场景

GaussDB(DWS)场景中,通常为分析类应用,最终均需要进行聚集操作。通常聚集都是在语句最后进行,起到去重统计的效果。但是,如果去重前的重复值较多,但会显著影响关联的性能,如SQL语句:select t1.c1, count(*) from t1 join t2 on t1.c1=t2.c1 group by t1.c1;其计划如下图所示:

         t1.c1t2.c1有大量重复值,导致Join完之后行数激增,Join性能较差,因此需要将Agg下推到Join之前进行,通过提前的Agg操作减少Join结果的行数,修改后的语句为:select t1.a, c1*c2 from (select t1.a, count(*) c1 from t1 group by t1.a) t1 join (select t2.a, count(*) c2 from t2 group by t2.a) t2 on t1.a=t2.a;,我们称这个改写规则为Eager Agg

         相反,如果改写后的语句,在子查询中的Agg去重效果不明显,但耗时较长,则可以做反向改写,去除冗余的Agg操作,我们称之为Lazy Agg

 

         以上只列出了客户场景经常出现的需要改写的语句,当然,要想深得SQL改写精髓,还是要深入了解GaussDB(DWS)实现原理,找到性能瓶颈,才能进行针对性的改写,起到事半功倍的效果。


合智互联客户成功服务热线:400-1565-661

admin
admin管理员

上一篇:tensorflow张量的简单用法
下一篇:从嵌套结构中取值时如何编写兜底逻辑

留言评论

暂无留言
取消
扫码支持