openGauss学习笔记-243 openGauss性能调优-SQL调优-典型SQL调优点-子查询调优

openGauss学习笔记-243 openGauss性能调优-SQL调优-典型SQL调优点-子查询调优243.1 子查询调优243.1.1 子查询背景介绍243.1.2 openGauss对SubLink的优化243.1.3 更多优化示例

openGauss学习笔记-243 openGauss性能调优-SQL调优-典型SQL调优点-子查询调优

SQL调优是一个不断分析与尝试的过程:试跑Query,判断性能是否满足要求;如果不满足要求,则通过查看执行计划分析原因并进行针对性优化;然后重新试跑和优化,直到满足性能目标。

243.1 子查询调优

243.1.1 子查询背景介绍

应用程序通过SQL语句来操作数据库时会使用大量的子查询,这种写法比直接对两个表做连接操作在结构上和思路上更清晰,尤其是在一些比较复杂的查询语句中,子查询有更完整、更独立的语义,会使SQL对业务逻辑的表达更清晰更容易理解,因此得到了广泛的应用。

openGauss根据子查询在SQL语句中的位置把子查询分成了子查询、子链接两种形式。

  • 子查询SubQuery:对应于查询解析树中的范围表RangeTblEntry,更通俗一些指的是出现在FROM语句后面的独立的SELECT语句。

  • 子链接SubLink:对应于查询解析树中的表达式,更通俗一些指的是出现在where/on子句、targetlist里面的语句。

    综上,对于查询解析树而言,SubQuery的本质是范围表,而SubLink的本质是表达式。针对SubLink场景而言,由于SubLink可以出现在约束条件、表达式中,按照openGauss对SubLink的实现,SubLink可以分为以下几类:

    • exist_sublink:对应EXIST、NOT EXIST语句

    • any_sublink:对应op ALL(select…)语句,其中OP可以是IN、<、>、=操作符

    • all_sublink:对应op ALL(select…)语句,其中OP可以是IN、<、>、=操作符

    • rowcompare_sublink:对应record op (select …)语句

    • expr_sublink:对应(SELECT with single targetlist item …)语句

    • array_sublink:对应ARRAY(select…)语句

    • cte_sublink:对应with query(…)语句

    其中OLAP、HTAP场景中常用的sublink为exist_sublink、any_sublink,在openGauss的优化引擎中对其应用场景做了优化(子链接提升),由于SQL语句中子查询的使用的灵活性,会带来SQL子查询过于复杂造成性能问题。子查询从大类上来看,分为非相关子查询和相关子查询:

    • 非相关子查询None-Correlated SubQuery

      子查询的执行不依赖于外层父查询的任何属性值。这样子查询具有独立性,可独自求解,形成一个子查询计划先于外层的查询求解。

      例如:

select t1.c1,t1.c2
        from t1
        where t1.c1 in (
         select c2
         from t2
         where t2.c2 IN (2,3,4)
        );
         QUERY PLAN
        ----------------------------------------------------------------
         Hash Join 
         Hash Cond: (t1.c1 = t2.c2)
         ->  Seq Scan on t1 
         Filter: (c1 = ANY ('{2,3,4}'::integer[]))
         ->  Hash 
         ->  HashAggregate 
         Group By Key: t2.c2
         ->  Seq Scan on t2
         Filter: (c2 = ANY ('{2,3,4}'::integer[]))
        (9 rows)
*   **相关子查询Correlated-SubQuery**

    子查询的执行依赖于外层父查询的一些属性值(如下列示例t2.c1 = t1.c1条件中的t1.c1)作为内层查询的一个AND-ed条件。这样的子查询不具备独立性,需要和外层查询按分组进行求解。

    例如:
select t1.c1,t1.c2
        from t1
        where t1.c1 in (
         select c2
         from t2
         where t2.c1 = t1.c1 AND t2.c2 in (2,3,4)
        );
         QUERY PLAN
        ------------------------------------------------------------------------
         Seq Scan on t1
         Filter: (SubPlan 1)
         SubPlan 1
         ->  Seq Scan on t2
         Filter: ((c1 = t1.c1) AND (c2 = ANY ('{2,3,4}'::integer[])))
        (5 rows)

243.1.2 openGauss对SubLink的优化

针对SubLink的优化策略主要是让内层的子查询提升(pullup),能够和外表直接做关联查询,从而避免生成SubPlan+Broadcast內表的执行计划。判断子查询是否存在性能风险,可以通过explain查询语句查看Sublink的部分是否被转换成SubPlan的执行计划。

例如:

[图片上传失败...(image-16e503-1710500789256)]

箭头右侧执行计划应替换成下面的执行计划:

QUERY PLAN
--------------------------------
Seq Scan on t1
Filter: (SubPlan 1)
SubPlan 1
->  Seq Scan on t2
Filter: (c1 = t1.c1)
(5 rows)
  • 目前openGauss支持的Sublink-Release场景

    • IN-Sublink无相关条件

      • 不能包含上一层查询的表中的列(可以包含更高层查询表中的列)。

      • 不能包含易变函数。

      [图片上传失败...(image-2831d7-1710500789252)]

      箭头右侧执行计划应替换成下面的执行计划:

QUERY PLAN
        --------------------------------------
        Hash Join
        Hash Cond: (t1.c1 = t2.c2)
        ->  Seq Scan on t1
        ->  Hash
        ->  HashAggregate
        Group By Key: t2.c2
        ->  Seq Scan on t2
        Filter: (c1 = 1)
        (8 rows)
*   Exist-Sublink包含相关条件

    Where子句中必须包含上一层查询的表中的列,子查询的其它部分不能含有上层查询的表中的列。其它限制如下。

    *   子查询必须有from子句。

    *   子查询不能含有with子句。

    *   子查询不能含有聚集函数。

    *   子查询里不能包含集合操作、排序、limit、windowagg、having操作。

    *   不能包含易变函数。

    [图片上传失败...(image-83394c-1710500789252)] 

    箭头右侧执行计划应替换成下面的执行计划:

    QUERY PLAN

    * * *

    Hash Join

    Hash Cond: (t1.c1 = t2.c1)

    -> Seq Scan on t1

    -> Hash

    -> HashAggregate

    Group By Key: t2.c1

    -> Seq Scan on t2

    (7 rows)

*   包含聚集函数的等值相关子查询的提升

    子查询的where条件中必须含有来自上一层的列,而且此列必须和子查询本层涉及表中的列做相等判断,且这些条件必须用and连接。其它地方不能包含上层的列。其它限制条件如下。

    *   子查询中where条件包含的表达式(列名)必须是表中的列。

    *   子查询的Select关键字后,必须有且仅有一个输出列,此输出列必须是聚集函数(如max),并且聚集函数的参数(t2.c2)不能是来自外层表(t1)中的列。聚集函数不能是count。

        例如,下列示例可以提升。
select * from t1 where c1 >(
             select max(t2.c1) from t2 where t2.c1=t1.c1
            );
        下列示例不能提升,因为子查询没有聚集函数。
select * from t1 where c1 >(
             select  t2.c1 from t2 where t2.c1=t1.c1
            );
        下列示例不能提升,因为子查询有两个输出列。
select * from t1 where (c1,c2) >(
             select  max(t2.c1),min(t2.c2) from t2 where t2.c1=t1.c1
            );
    *   子查询必须是from子句。

    *   子查询中不能有groupby、having、集合操作。

    *   子查询只能是inner join。

        例如:下列示例不能提升。
select * from t1 where c1 >(
             select max(t2.c1) from t2 full join t3 on (t2.c2=t3.c2) where t2.c1=t1.c1
            );
    *   子查询的targetlist中不能包含返回set的函数。

    *   子查询的where条件中必须含有来自上一层的列,而且此列必须和子查询层涉及表中的列做相等判断,且这些条件必须用and连接。其它地方不能包含上层的上层中的列。例如:下列示例中的最内层子链接可以提升。
select * from t3 where t3.c1=(
             select t1.c1
             from t1 where c1 >(
             select max(t2.c1) from t2 where t2.c1=t1.c1 
            ));
        基于上面的示例,再加一个条件,则不能提升,因为最内侧子查询引用了上层中的列。示例如下:
select * from t3 where t3.c1=(
             select t1.c1
             from t1 where c1 >(
             select max(t2.c1) from t2 where t2.c1=t1.c1 and t3.c1>t2.c2
            
            ));
*   提升OR子句中的SubLink

    当WHERE过滤条件中有OR连接的EXIST相关SubLink,

    例如:
select a, c from t1
        where t1.a = (select avg(a) from t3 where t1.b = t3.b) or
        exists (select * from t4 where t1.c = t4.c);
    将OR-ed连接的EXIST相关子查询OR字句的提升过程:

    1.  提取where条件中,or子句中的opExpr。为:t1.a = (select avg(a) from t3 where t1.b = t3.b)

    2.  这个op操作中包含subquery,判断是否可以提升,如果可以提升,重写subquery为:select avg(a)、 t3.b from t3 group by t3.b,生成not null条件t3.b is not null,并将这个opexpr用这个not null条件替换。此时SQL变为:
select a, c
            from t1 left join (select avg(a) avg, t3.b from t3 group by t3.b)  as t3 on (t1.a = avg and t1.b = t3.b)
            where t3.b is not null or exists (select * from t4 where t1.c = t4.c);
    3.  再次提取or子句中的exists sublink、exists (select * from t4 where t1.c = t4.c),判断是否可以提升,如果可以提升,转换subquery为:select t4.c from t4 group by t4.c生成NotNull条件t4.c is not null提升查询,SQL变为:
select a, c
            from t1 left join (select avg(a) avg, t3.b from t3 group by t3.b)  as t3 on (t1.a = avg and t1.b = t3.b)
            left join (select t4.c from t4 group by t4.c) where t3.b is not null or t4.c is not null;
  • 目前openGauss不支持的Sublink-Release场景

    除了以上场景之外都不支持Sublink提升,因此关联子查询会被计划成SubPlan+Broadcast的执行计划,当inner表的数据量较大时则会产生性能风险。

    如果相关子查询中跟外层的两张表做join,那么无法提升该子查询,需要通过将父SQL创建成with子句,然后再跟子查询中的表做相关子查询查询。

    例如:

select distinct t1.a, t2.a
    from t1 left join t2 on t1.a=t2.a and not exists (select a,b from test1 where test1.a=t1.a and test1.b=t2.a);
改写为
with temp as
    (
     select * from (select t1.a as a, t2.a as b from t1 left join t2 on t1.a=t2.a)
    
    )
    select distinct a,b
    from temp
    where not exists (select a,b from test1 where temp.a=test1.a and temp.b=test1.b);
*   出现在targetlist里的相关子查询无法提升(不含count)

    例如:
explain (costs off)
        select (select c2 from t2 where t1.c1 = t2.c1) ssq, t1.c2
        from t1
        where t1.c2 > 10;
    执行计划为:
explain (costs off)
        select (select c2 from t2 where t1.c1 = t2.c1) ssq, t1.c2
        from t1
        where t1.c2 > 10;
         QUERY PLAN
        --------------------------------
         Seq Scan on t1
         Filter: (c2 > 10)
         SubPlan 1
         ->  Seq Scan on t2
         Filter: (t1.c1 = c1)
        (5 rows)
    由于相关子查询出现在targetlist(查询返回列表)里,对于t1.c1=t2.c1不匹配的场景仍然需要输出值,因此使用left-outerjoin关联T1&T2确保t1.c1=t2.c1在不匹配时,子SSQ能够返回不匹配的补空值。

    > [图片上传失败...(image-1c950-1710500789251)]
    > 
    >   **说明:**
    > 
    > SSQ和CSSQ的解释如下:
    > 
    > *   SSQ:ScalarSubQuery一般指返回1行1列scalar值的sublink,简称SSQ。
    >     
    >     
    > *   CSSQ:Correlated-ScalarSubQuery和SSQ相同不过是指包含相关条件的SSQ。

    上述SQL语句可以改写为:
with ssq as
        (
         select t2.c2 from t2
        )
        select ssq.c2, t1.c2
        from t1 left join ssq on t1.c1 = ssq.c2
        where t1.c2 > 10;
    改写后的执行计划为:
QUERY PLAN
        ---------------------------------
         Hash Right Join
         Hash Cond: (ssq.c2 = t1.c1)
         CTE ssq
         ->  Seq Scan on t2
         ->  CTE Scan on ssq
         ->  Hash
         ->  Seq Scan on t1
         Filter: (c2 > 10)
        (8 rows)
    可以看到出现在SSQ返回列表里的相关子查询SSQ,已经被提升成Right Join,从而避免当內表T2较大时出现SubPlan计划导致性能变差。

*   出现在targetlist里的相关子查询无法提升(带count)

    例如:
select (select count(*) from t2 where t2.c1=t1.c1) cnt, t1.c1, t3.c1
        from t1,t3
        where t1.c1=t3.c1 order by cnt, t1.c1;
    执行计划为
QUERY PLAN
        --------------------------------------------
         Sort
         Sort Key: ((SubPlan 1)), t1.c1
         ->  Hash Join
         Hash Cond: (t1.c1 = t3.c1)
         ->  Seq Scan on t1
         ->  Hash
         ->  Seq Scan on t3
         SubPlan 1
         ->  Aggregate
         ->  Seq Scan on t2
         Filter: (c1 = t1.c1)
        (11 rows)
    由于相关子查询出现在targetlist(查询返回列表)里,对于t1.c1=t2.c1不匹配的场景仍然需要输出值,因此使用left-outerjoin关联T1&T2确保t1.c1=t2.c1在不匹配时子SSQ能够返回不匹配的补空值,但是这里带了count语句及时在t1.c1=t2.t1不匹配时需要输出0,因此可以使用一个case-when NULL then 0 else count(*)来代替。

    上述SQL语句可以改写为:
with ssq as
        (
         select count(*) cnt, c1 from t2 group by c1
        )
        select case when
         ssq.cnt is null then 0
         else ssq.cnt
         end cnt, t1.c1, t3.c1
        from t1 left join ssq on ssq.c1 = t1.c1,t3
        where t1.c1 = t3.c1
        order by ssq.cnt, t1.c1;
    改写后的执行计划为
QUERY PLAN
        -------------------------------------------
         Sort
         Sort Key: ssq.cnt, t1.c1
         CTE ssq
         ->  HashAggregate
         Group By Key: t2.c1
         ->  Seq Scan on t2
         ->  Hash Join
         Hash Cond: (t1.c1 = t3.c1)
         ->  Hash Left Join
         Hash Cond: (t1.c1 = ssq.c1)
         ->  Seq Scan on t1
         ->  Hash
         ->  CTE Scan on ssq
         ->  Hash
         ->  Seq Scan on t3
        (15 rows)
*   相关条件为不等值场景

    例如:
select t1.c1, t1.c2
        from t1
        where t1.c1 = (select agg() from t2.c2 > t1.c2);
    对于非等值相关条件的SubLink目前无法提升,从语义上可以通过做2次join(一次CorrelationKey,一次rownum自关联)达到提升改写的目的。

    改写方案有两种。

    *   子查询改写方式
select t1.c1, t1.c2
            from t1, (
             select t1.rowid, agg() aggref
             from t1,t2
             where t1.c2 > t2.c2 group by t1.rowid
            ) dt /* derived table */
            where t1.rowid = dt.rowid AND t1.c1 = dt.aggref;
    *   CTE改写方式
WITH dt as
            (
             select t1.rowid, agg() aggref
             from t1,t2
             where t1.c2 > t2.c2 group by t1.rowid
            )
            select t1.c1, t1.c2
            from t1, derived_table
            where t1.rowid = derived_table.rowid AND
            t1.c1 = derived_table.aggref;
> [图片上传失败...(image-96a2e8-1710500789253)]
> 
>   **须知:**
> 
> *   对于AGG类型为count(*)时需要进行CASE-WHEN对没有match的场景补0处理,非COUNT(*)场景NULL处理。
>     
>     
> *   CTE改写方式如果有sharescan支持性能上能够更优。

243.1.3 更多优化示例

示例:修改select语句,将子查询修改为和主表的join,或者修改为可以提升的subquery,但是在修改前后需要保证语义的正确性。

explain (costs off) select * from t1 where t1.c1 in (select t2.c1 from t2 where t1.c1 = t2.c2);
 QUERY PLAN
--------------------------------
 Seq Scan on t1
 Filter: (SubPlan 1)
 SubPlan 1
 ->  Seq Scan on t2
 Filter: (t1.c1 = c2)
(5 rows)

上面事例计划中存在一个subPlan,为了消除这个subPlan可以修改语句为:

explain (costs off) select * from t1 where exists (select t2.c1 from t2 where t1.c1 = t2.c2 and t1.c1 = t2.c1);
 QUERY PLAN
------------------------------------------
 Hash Join
 Hash Cond: (t1.c1 = t2.c2)
 ->  Seq Scan on t1
 ->  Hash
 ->  HashAggregate
 Group By Key: t2.c2, t2.c1
 ->  Seq Scan on t2
 Filter: (c2 = c1)
(8 rows)

从计划可以看出,subPlan消除了,计划变成了两个表的hash join,这样会大大提高执行效率。

👍 点赞,你的认可是我创作的动力!

⭐️ 收藏,你的青睐是我努力的方向!

✏️ 评论,你的意见是我进步的财富!

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容