PostgreSQL指南:内幕探索
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.6 创建多表查询计划树

本节将说明多表查询计划树的创建过程。

3.6.1 预处理

预处理由 planner.c 中定义的 subquery_planner()函数执行。第 3.3.1节已经描述了单表查询的预处理。本节将描述多表查询的预处理,这块内容很多,这里只挑其中一部分来讲。

1.对CTE进行计划与转换

如果存在WITH列表,计划器就会通过SS_process_ctes()函数对每个WITH查询进行处理。

2.上拉子查询

如果FROM子句带有一个子查询,且该子查询没有用到GROUP BY、HAVING、ORDER BY、LIMIT和DISTINCT、INTERSECT或EXCEPT,那么计划器就会使用pull_up_subqueries()函数将其转换为连接形式。例如下面一个 FROM 子句含子查询的查询就可以被转换为自然连接查询。自不必说,这种转换是在查询树上进行的。

    # SELECT * FROM tbl_a AS a, (SELECT * FROM tbl_b) as b WHERE a.id = b.id;
                            ↓
    # SELECT * FROM tbl_a AS a, tbl_b as b WHERE a.id = b.id;

3.将外连接转为内连接

如果可能的话,计划器会将OUTER JOIN查询转换为INNER JOIN查询。

3.6.2 获取代价最小的路径

为了获取最佳计划树,计划器必须考虑各个索引与各种连接方法之间的所有可能组合。如果表的数量超过某个水平,该过程的代价就会因为组合爆炸而变得非常昂贵,以至于根本不可行。

幸运的是,如果表的数量小于12张,计划器可以使用动态规划来获取最佳计划,否则计划器会使用遗传算法。详情如下:

基因查询优化器

当执行一个多表连接查询时,大量时间耗费在了优化查询计划上。为了应对这种情况, PostgreSQL实现了一个有趣的功能:基因查询优化器。这种近似算法能在合理时间内确定一个合理的计划。因此在查询优化阶段,如果参与连接的表数量超过参数geqo_threshold指定的阈值(默认值为12),PostgreSQL将使用遗传算法来生成查询计划。

以下是使用动态规划确定最佳计划树的过程,其步骤如下:

· 第一层

获得每张表上代价最小的路径,代价最小的路径存储在表相应的RelOptInfo结构中。

· 第二层

从所有表中选择两个表,为每种组合找出代价最低的路径。

举个例子,如果共有两张表,表A与表B,则表A和表B连接的各种路径中,代价最小的那条即为最终想要的答案。在下文中,两个表的RelOptInfo记作{A,B}。

如果有三个表,则需要获取{A,B}{、 A,C}{、 B,C}三种组合里各自代价最小的路径。

· 第三层及其后

继续进行同样的处理,直到层级等于表数量。

通过这种方式,在每个层级都能解决最小代价问题的一部分,且其结果能被更高层级的计算复用,从而使代价最小的计划树能够被高效地计算出来。

图3.31展示了使用动态规划获取代价最小的访问路径的方式。

图3.31 如何使用动态规划获取代价最小的访问路径

接下来会针对下面的查询,解释计划器是如何获取代价最小的计划的。

    testdb=# \d tbl_a
        Table "public.tbl_a"
     Column |  Type   | Modifiers
    --------+---------+-----------
     id      | integer | not null
     data   | integer |
    Indexes:
        "tbl_a_pkey" PRIMARY KEY, btree (id)
    testdb=# \d tbl_b
        Table "public.tbl_b"
     Column |  Type   | Modifiers
    --------+---------+-----------
     id      | integer |
     data   | integer |

    testdb=# SELECT * FROM tbl_a AS a, tbl_b AS b WHERE a.id = b.id AND b.data < 400;

3.6.2.1 第一层的处理

在第一层中,计划器会为查询中涉及的关系创建相应的RelOptInfo结构,并估计每个关系上的最小代价。在这一步中,RelOptInfo 结构会被添加至该查询对应 PlannerInfo 的simple_rel_arrey数组字段中,如图3.32所示。

图3.32 第一层处理后的PlannerInfo与RelOptInfo

表tbl_a的RelOptInfo有三条访问路径,它们被添加至RelOptInfo的路径列表中。这三条路径分别被三个指针所连接,即三个指向代价最小路径的指针,分别是启动代价最小的路径、总代价最小的路径和参数化代价最小的路径。启动代价最小的路径与总代价最小的路径含义显而易见,因此,这里只介绍参数化索引扫描代价最小的路径。

如第3.5.1.3节所述,计划器会考虑为索引嵌套循环连接使用参数化路径(极少数情况下也会用于带外表索引扫描的索引化归并连接)。参数化索引扫描代价最小的路径,就是所有参数化路径中代价最小的那个。

表tbl_b的RelOptInfo仅有顺序扫描访问路径,因为tbl_b上没有相关索引。

3.6.2.2 第二层的处理

在第二层中,计划器会在PlannerInfo的join_rel_list字段中创建一个RelOptInfo结构。然后估计所有可能连接路径的代价,并且选择代价最小的那条访问路径。RelOptInfo会将最佳访问路径作为总代价最小的路径,如图3.33所示。

表3.1~表3.3展示了本例中连接访问路径的所有组合。本例中查询的连接类型为等值连接,并对全部三种连接算法进行评估。为方便起见,这里引入了一些有关访问路径的符号:

· SeqScanPath(table)表示表table上的顺序扫描路径。

· Materialized -> SeqScanPath(table)表示表table上的物化顺序扫描路径。

图3.33 第二层处理后的PlannerInfo和RelOptInfo

· IndexScanPath(table,attribute)表示按表table中属性attribute上的索引扫描路径。

· ParameterizedIndexScanPath(table,attribute1,attribute2)表示表 table中属性attribute1上的参数化索引路径,并使用外表上的属性attribute2参数化。

表3.1 嵌套循环连接

表3.2 归并连接

表3.3 哈希连接

例如在嵌套循环连接的部分总共评估了 7条连接路径。第一条表示在外表 tbl_a 和内表tbl_b上都使用顺序扫描路径;第二条表示在外表tbl_a上使用路径顺序扫描路径,而在内表tbl_b上使用物化顺序扫描路径,诸如此类。

计划器最终从估计的连接访问路径中选择代价最小的那条,并且将其添加到RelOptInfo{tbl_a,tbl_b}的路径列表中,如图3.33所示。

在本例中,如下面EXPLAIN的结果所示,计划器选择了在内表tbl_b和外表tbl_c上进行散列连接。

    1.testdb=# EXPLAIN  SELECT * FROM tbl_b AS b, tbl_c AS c WHERE c.id = b.id AND b.data < 40
    0;
    2.                                  QUERY PLAN
    3.----------------------------------------------------------------------
    4. Hash Join  (cost=90.50..277.00 rows=400 width=16)
    5.   Hash Cond: (c.id = b.id)
    6.   ->  Seq Scan on tbl_c c  (cost=0.00..145.00 rows=10000 width=8)
    7.   ->  Hash  (cost=85.50..85.50 rows=400 width=8)
    8.          ->  Seq Scan on tbl_b b  (cost=0.00..85.50 rows=400 width=8)
    9.                 Filter: (data < 400)
    10.(6 rows)

3.6.3 获取三表查询代价最小的路径

涉及三表的查询,其代价最小的路径的获取过程如下所示:

    testdb=# \d tbl_a
        Table "public.tbl_a"
     Column |  Type   | Modifiers
    --------+---------+-----------
     id      | integer |
     data   | integer |

    testdb=# \d tbl_b
        Table "public.tbl_b"

    Column |  Type   | Modifiers
   --------+---------+-----------
    id      | integer |
    data   | integer |

   testdb=# \d tbl_c
        Table "public.tbl_c"
    Column |  Type   | Modifiers
   --------+---------+-----------
    id      | integer | not null
    data   | integer |
   Indexes:
      "tbl_c_pkey" PRIMARY KEY, btree (id)

   testdb=# SELECT * FROM tbl_a AS a, tbl_b AS b, tbl_c AS c
   testdb-#                   WHERE a.id = b.id AND b.id = c.id AND a.data < 40;

· 第一层

计划器估计所有表上各自开销最小的路径,并将该信息存储在表相应的 RelOptInfos 结构{tbl_a}、{tbl_b}和{tbl_c}中。

· 第二层

计划器从三个表中选出两个,列出所有组合,分别评估每种组合里代价最小的路径。然后,规划器将信息存储在组合相应的 RelOptInfos 结构{tbl_a,tbl_b}、{tbl_b,tbl_c}和{tbl_a,tbl_c}中。

· 第三层

计划器根据所有已获取的RelOptInfos,选择代价最小的路径。更确切地说,计划器会考虑三种 RelOptInfos 组合,即{tbl_a,{tbl_b,tbl_c}}、{tbl_b,{tbl_a,tbl_c}}和{tbl_c,{tbl_a,tbl_b}},而{tbl_a,tbl_b,tbl_c}如下所示:

{tbl_a, tbl_b, tbl_c} = min({tbl_a,{tbl_b, tbl_c}},{tbl_b,{ tbl_a, tbl_c}}, {tbl_c,{ tbl_a, tbl_b}}).

计划器会估算这里面所有可能连接路径的代价。

在处理{tbl_c,{tbl_a,tbl_b}}对应的 RelOptInfo 时,计划器会估计 tbl_c 和{tbl_a,tbl_b}连接代价最小的路径。本例中{tbl_a,tbl_b}已经选定内表为tbl_a且外表为tbl_b的散列连接。如第3.6.2.2节所述,在估计时三种连接算法及其变体都会被评估,即嵌套循环连接及其变体,归并连接及其变体,散列连接及其变体。

计划器以同样的方式处理{tbl_a,{tbl_b,tbl_c}}与{tbl_b,{tbl_a,tbl_c}}对应的RelOptInfo,并最终从所有估好的路径中选择代价最小的访问路径。

该查询的EXPLAIN命令结果如下所示:

最外层的连接是索引嵌套循环连接(第5行),第13行显示了内表上的参数化索引扫描,外表则是一个散列连接的结果,该散列连接的内表是 tbl_a,外表是 tbl_b(第 7~12行)。因此,执行程序首先执行tbl_a和tbl_b上的散列连接,再执行索引嵌套循环连接。