Greenplum查询计划分析

这里对查询计划的学习主要是对TPC-H中Query2的分析。

1.Query的查询语句

select
    s_acctbal,
    s_name,
    n_name,
    p_partkey,
    p_mfgr,
    s_address,
    s_phone,
    s_comment
from
    part,
    supplier,
    partsupp,
    nation,
    region
where
    p_partkey = ps_partkey
    and s_suppkey = ps_suppkey
    and p_size = 6
    and p_type like '%COPPER'
    and s_nationkey = n_nationkey
    and n_regionkey = r_regionkey
    and r_name = 'AFRICA'
    and ps_supplycost = (
        select
            min(ps_supplycost)
        from
            partsupp,
            supplier,
            nation,
            region
        where
            p_partkey = ps_partkey
            and s_suppkey = ps_suppkey
            and s_nationkey = n_nationkey
            and n_regionkey = r_regionkey
            and r_name = 'AFRICA'
    )
order by
    s_acctbal desc,
    n_name,
    s_name,
    p_partkey
LIMIT 100;

2.查看查询计划

Greenplum中有语句可以查看查询计划,使用explain命令即可:

例:testDB=#explain select * from test1;

所以Query2的查询计划查看命令即Query2的语句之前加explain。

3.查询中涉及到的表

testdb=# \d part
                             Append-Only Columnar Table "public.part"
    Column     |         Type          |                        Modifiers                         
---------------+-----------------------+----------------------------------------------------------
 p_partkey     | bigint                | not null default nextval('part_p_partkey_seq'::regclass)
 p_name        | character varying(55) | 
 p_mfgr        | character(25)         | 
 p_brand       | character(10)         | 
 p_type        | character varying(25) | 
 p_size        | integer               | 
 p_container   | character(10)         | 
 p_retailprice | numeric               | 
 p_comment     | character varying(23) | 
Checksum: t
Distributed by: (p_partkey)

testdb=# \d partsupp
    Append-Only Columnar Table "public.partsupp"
    Column     |          Type          | Modifiers 
---------------+------------------------+-----------
 ps_partkey    | bigint                 | not null
 ps_suppkey    | bigint                 | not null
 ps_availqty   | integer                | 
 ps_supplycost | numeric                | 
 ps_comment    | character varying(199) | 
Checksum: t
Indexes:
    "idx_partsupp_partkey" btree (ps_partkey)
    "idx_partsupp_suppkey" btree (ps_suppkey)
Distributed by: (ps_partkey, ps_suppkey)

testdb=# \d supplier
                            Append-Only Columnar Table "public.supplier"
   Column    |          Type          |                          Modifiers                           
-------------+------------------------+--------------------------------------------------------------
 s_suppkey   | bigint                 | not null default nextval('supplier_s_suppkey_seq'::regclass)
 s_name      | character(25)          | 
 s_address   | character varying(40)  | 
 s_nationkey | bigint                 | not null
 s_phone     | character(15)          | 
 s_acctbal   | numeric                | 
 s_comment   | character varying(101) | 
Checksum: t
Indexes:
    "idx_supplier_nation_key" btree (s_nationkey)
Distributed by: (s_suppkey)

testdb=# \d nation
                             Append-Only Columnar Table "public.nation"
   Column    |          Type          |                          Modifiers                           
-------------+------------------------+--------------------------------------------------------------
 n_nationkey | bigint                 | not null default nextval('nation_n_nationkey_seq'::regclass)
 n_name      | character(25)          | 
 n_regionkey | bigint                 | not null
 n_comment   | character varying(152) | 
Checksum: t
Indexes:
    "idx_nation_regionkey" btree (n_regionkey)
Distributed by: (n_nationkey)

testdb=# \d region
                             Append-Only Columnar Table "public.region"
   Column    |          Type          |                          Modifiers                           
-------------+------------------------+--------------------------------------------------------------
 r_regionkey | bigint                 | not null default nextval('region_r_regionkey_seq'::regclass)
 r_name      | character(25)          | 
 r_comment   | character varying(152) | 
Checksum: t
Distributed by: (r_regionkey)

上面是查询中涉及到的5个表。

可以看到Greenplum使用的是列存储。

Append-Only意思是不断追加的表,不能进行更新和删除,压缩表必须是Append-Only表。我理解Greenplum主要是处理OLAP,为了能够有更大的吞吐量,使用列存储的表结构,而列存储就可以压缩,而压缩表又必须是Append-Only表,所以表的标题都使用Appen-Only Columnar Table XXX。

每个表最后都有Distributed by:(XXX),是表的分布键,即表是按照这个键值分布在不同的segment上的。

4.数据库连接图

5.查询分析结果

                                                                                  QUERY PLAN                                                                                   
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=46881.43..46881.53 rows=5 width=198)
   ->  Gather Motion 4:1  (slice10; segments: 4)  (cost=46881.43..46881.53 rows=5 width=198)
         Merge Key: public.supplier.s_acctbal, public.nation.n_name, public.supplier.s_name, part.p_partkey
         ->  Limit  (cost=46881.43..46881.44 rows=2 width=198)
               ->  Sort  (cost=46881.43..46881.44 rows=2 width=198)
                     Sort Key (Limit): public.supplier.s_acctbal, public.nation.n_name, public.supplier.s_name, part.p_partkey
                     ->  Hash Join  (cost=42481.34..46881.39 rows=2 width=198)
                           Hash Cond: "Expr_SUBQUERY".csq_c0 = part.p_partkey AND "Expr_SUBQUERY".csq_c1 = public.partsupp.ps_supplycost
                           ->  HashAggregate  (cost=23828.08..25828.08 rows=40000 width=40)
                                 Group By: public.partsupp.ps_partkey
                                 ->  Redistribute Motion 4:4  (slice4; segments: 4)  (cost=18228.08..21428.08 rows=40000 width=40)
                                       Hash Key: public.partsupp.ps_partkey
                                       ->  HashAggregate  (cost=18228.08..18228.08 rows=40000 width=40)
                                             Group By: public.partsupp.ps_partkey
                                             ->  Hash Join  (cost=423.08..17428.08 rows=40001 width=16)
                                                   Hash Cond: public.partsupp.ps_suppkey = public.supplier.s_suppkey
                                                   ->  Append-only Columnar Scan on partsupp  (cost=0.00..11805.00 rows=200000 width=24)
                                                   ->  Hash  (cost=323.08..323.08 rows=2001 width=8)
                                                         ->  Broadcast Motion 4:4  (slice3; segments: 4)  (cost=9.08..323.08 rows=2001 width=8)
                                                               ->  Hash Join  (cost=9.08..223.08 rows=501 width=8)
                                                                     Hash Cond: public.supplier.s_nationkey = public.nation.n_nationkey
                                                                     ->  Append-only Columnar Scan on supplier  (cost=0.00..149.00 rows=2500 width=16)
                                                                     ->  Hash  (cost=8.83..8.83 rows=6 width=8)
                                                                           ->  Broadcast Motion 4:4  (slice2; segments: 4)  (cost=4.16..8.83 rows=6 width=8)
                                                                                 ->  Hash Join  (cost=4.16..8.58 rows=2 width=8)
                                                                                       Hash Cond: public.nation.n_regionkey = public.region.r_regionkey
                                                                                       ->  Append-only Columnar Scan on nation  (cost=0.00..4.25 rows=7 width=16)
                                                                                       ->  Hash  (cost=4.11..4.11 rows=2 width=8)
                                                                                             ->  Broadcast Motion 4:4  (slice1; segments: 4)  (cost=0.00..4.11 rows=2 width=8)
                                                                                                   ->  Append-only Columnar Scan on region  (cost=0.00..4.06 rows=1 width=8)
                                                                                                         Filter: r_name = 'AFRICA'::bpchar
                           ->  Hash  (cost=18623.96..18623.96 rows=489 width=214)
                                 ->  Redistribute Motion 4:4  (slice9; segments: 4)  (cost=4340.29..18623.96 rows=489 width=214)
                                       Hash Key: part.p_partkey
                                       ->  Hash Join  (cost=4340.29..18584.88 rows=489 width=214)
                                             Hash Cond: public.partsupp.ps_suppkey = public.supplier.s_suppkey
                                             ->  Redistribute Motion 4:4  (slice6; segments: 4)  (cost=4092.22..18287.96 rows=2443 width=58)
                                                   Hash Key: public.partsupp.ps_suppkey
                                                   ->  Hash Join  (cost=4092.22..18092.59 rows=2443 width=58)
                                                         Hash Cond: public.partsupp.ps_partkey = part.p_partkey
                                                         ->  Append-only Columnar Scan on partsupp  (cost=0.00..11805.00 rows=200000 width=24)
                                                         ->  Hash  (cost=3970.11..3970.11 rows=2443 width=34)
                                                               ->  Broadcast Motion 4:4  (slice5; segments: 4)  (cost=0.00..3970.11 rows=2443 width=34)
                                                                     ->  Append-only Columnar Scan on part  (cost=0.00..3848.00 rows=611 width=34)
                                                                           Filter: p_size = 6 AND p_type::text ~~ '%COPPER'::text
                                             ->  Hash  (cost=223.08..223.08 rows=501 width=172)
                                                   ->  Hash Join  (cost=9.08..223.08 rows=501 width=172)
                                                         Hash Cond: public.supplier.s_nationkey = public.nation.n_nationkey
                                                         ->  Append-only Columnar Scan on supplier  (cost=0.00..149.00 rows=2500 width=154)
                                                         ->  Hash  (cost=8.83..8.83 rows=6 width=34)
                                                               ->  Broadcast Motion 4:4  (slice8; segments: 4)  (cost=4.16..8.83 rows=6 width=34)
                                                                     ->  Hash Join  (cost=4.16..8.58 rows=2 width=34)
                                                                           Hash Cond: public.nation.n_regionkey = public.region.r_regionkey
                                                                           ->  Append-only Columnar Scan on nation  (cost=0.00..4.25 rows=7 width=42)
                                                                           ->  Hash  (cost=4.11..4.11 rows=2 width=8)
                                                                                 ->  Broadcast Motion 4:4  (slice7; segments: 4)  (cost=0.00..4.11 rows=2 width=8)
                                                                                       ->  Append-only Columnar Scan on region  (cost=0.00..4.06 rows=1 width=8)
                                                                                             Filter: r_name = 'AFRICA'::bpchar
 Settings:  enable_nestloop=off
 Optimizer status: legacy query optimizer
(60 rows)

下面对于查询分析中的一些参数做一些说明:

slice:Greenplum在实现分布式执行计划的时候,需要将SQL拆分成多个切片,每个slice是单裤执行的一部分SQL,每一个广播或者重分布会产生一个切片,每一个切片在每一个数据结点上都会对应的发起一个进程来处理该slice负责的数据,上一层负责该slice的进程会读取下级slice广播或重分布的数据,之后进行相应的计算。

segment:这里使用的是1个mdw、2个sdw,每个sdw中设置两个primary(greenplum安装时gpinitsystem使用的文件中设置),所以看到的segment是4。

cost:数据库自定义的消耗单位,通过统计信息来估计SQL消耗。(查询分析是根据analyze的固执生成的,生成之后按照这个查询计划执行,执行过程中analyze是不会变的。所以如果估值和真是情况差别较大,就会影响查询计划的生成。)

rows:根据统计信息估计SQL返回结果集的行数。

width:返回结果集每一行的长度,这个长度值是根据pg_statistic表中的统计信息来计算的。

6.对查询计划的结果进行分析

1.逻辑架构图

根据这个查询分析,画出查询的逻辑架构图:

2.对上图做一些补充

上图中我只画出了一个节点的逻辑架构,并没有表现出广播和重分布,下面用例子说明广播和重分布

上面这个图是两个节点数据重分布的例子,这个图要完成的任务是两个表的Hash Join,由于某种原因(之后会讲述到)要将其中一个表的数据重分布到其他结点上去,以完成连接的任务。橘色部分是slice1,绿色部分是slice2,slice1中的表重分布之后到了slice2中,在slice2中做Hash Join,对连接之后的结果收集到master上。下图说明重分布时数据在slice之间传输的过程

 所以这也是为什么在逻辑图中,redistribution和broadcast的椭圆处于两个slice的交界处,并且同时属于两个slice。 

3.能够产生slice的操作是redistribution、broadcast和gather

  1.gather、broadcast和redistribution的介绍

gather:聚合,在master上讲子节点所有的数据聚合起来。一般的聚合规则是哪一个子节点的数据先返回到master上就将该节点的数据先放在master上。

broadcast:广播,发生在两表关联的时候。将每个节点上的某个表的数据全部发送到所有节点,这样每个节点都相当于有全量数据。一般,小表的时候采用广播的方法。(注:的是不论大表还是小表,最初都是分散在所有子节点上的)

redistribution:重分布,发生在两表关联的时候和group by的时候。当不满足广播的条件或者代价太大的时候,选择重分布,即按照新的分布键将各个节点上的数据重新打散到各个节点。

下面着重介绍broadcast和redistribution

  2. join的时候的广播和重分布

两表连接的时候可能广播可能重分布那么什么时候使用广播,什么时候使用重分布呢?在Query中使用到的都是Hash Join,所以我们暂时只讨论这种情况下的广播和重分布。

通俗的讲,两表连接,如果是其中一个是小表,则将其广播,因为小表广播代价不会很大;如果两个表都是大表可能要重分布。分三种情况讨论:

默认情况,我们认为主键id即为分布键

①select * from A,B where A.id = B.id     分布键就是关联键,两表可以在本结点直接连接

②select * from A,B where A.id = B.id2    A的分布键就是关联键,B的分布键不是关联键。所以不能将A重分布,有两种解决方案:

a.将A广播(如果A是小表)

b.将B重分布(如果A是大表)

最终权衡取代价最小的方案

③select * from A,B where A.id2 = B.id2     A和B的分布键都不是关联键。

a.将A或B都按照id2重分布

b.将min(A,B)广播(如果较小的表是小表)

最终权衡取代价最小的方案

这里只讲述了Hash Join的情况,除此之外还有left join和full outer join,需要了解在书中135页有详解。

  3.group by时候的重分布

在group by的时候也可能会产生重分布,下面介绍一下group by时重分布的原理:

group by时时先在本机上进行一个group by,然后重分布,用group by时使用的字段作为分布键重分布,重分布之后再做一次group by,所以group by操作在分布式的环境下其实是做两次group by的。书上119页图有例子说明group by的数据重分布情况。

4.分析Query2的查询计划

下面按照每个切片的方式分析Query2的查询计划。

slice1~4,包括slice10左分支的部分,是Query中的子查询部分;

slice5~9是父查询中where子句中除了ps_supplycost = (子查询)的部分;

slice10是ps_supplycost = (子查询)和父查询中的order by和limit。

①slice1和slice2:region表和nation表的hash join,数据库连接图可以看出应该属于上述第二种情况region.id=nation.id2,这里采用将region广播,说明相比重分布nation,广播region的代价更小;hash join的时候将region表hash,说明region表是个小表,这与广播region代价更小相吻合。

②slice3:hash join的原理同①

③slice4:hash join的原理同①,三次hash join实现了四个表的连接。之后做了聚合操作,对应子查询中min的操作,min的时候需要使用到group by,根据上面的介绍,知道group by在分布式环境下其实是做两次的,中间是一次重分布,这里可以在slice4和slice10的左子树看到有两次hashAggregate。

④slice5和slice6:hash join的原理同①

⑤slice7和slice8:hash join的同①

⑥slice9:左子树的hash join与slice3中的一样,不同的是连接之后,slice3进行了广播,slice9是进行了hash映射,造成这种差别的原因是这个连接表将要连接的表不同:

slice3中hash join的中间表与partsupp连接,是hash join中的第二种情况,中间表.id=partsupp.id2,由于中间表是小表,所以将中间表广播;

slice9中hash join的中间表1与slice6的中间表2连接,是hash join中的第二种情况,中间表1.id=中间表.id2,虽然中间表1是小表,但是没有广播中间表1,而是将中间表2重分布,因为中间表1广播到4个节点的代价总和大于将中间表2重分布。 

⑦slice10,hash join之后,对每个子节点的数据按照四个键值排序,每个节点舍弃掉一部分数据(排序比较靠后,不可能包含在最后的limit结果中),将排序靠前的数据输出给master,这个过程叫做gather,使用排序时的四个键值作为merge key

取排序之后的一部分(这部分一定包含最终结果)输出给master,另一部分舍弃掉,不输出给master。master得到最终聚合的数据,再进行一次limit操作,这次limit得到的是query需要的100条数据。

 

以上就是对Query2的查询计划结果的分析,还有一些问题尚未弄清楚:

1.关联键是否默认是主键?是(书上201页)

2.列存储是否有主键,是否有完整性约束?应该是有的,因为列存储取到相应属性之后还要将他们组合成行表。

3.在查询计划的结果中,为什么query中的min(ps_supplycost)体现在查询计划中是group by(ps_partkey)而非group by(ps_supplycost)

4.书上讲的hashAggregate和groupAggregate的原理不是很清楚

5.slice10中的hash join的hash key不明白

6.order by多个键值是怎么排序的

 

心得:

1.觉得画图写东西有点浪费时间,但是发现不画出来写出来,其实有些东西理解的不够,或者有偏差。记录下来有助于深入理解知识

2.写东西相当于对知识体系的一次整合,没整理的时候,所有东西在大脑不是混沌的,真理之后知识变得逻辑、有序。

 

posted @ 2016-07-25 19:53  Isabel_r  阅读(8159)  评论(1编辑  收藏  举报