计划选择
前面的章节介绍了数据库中几类常用的路径生成方式以及优化器的代价估算模型原理,在此基础上CBO优化器可以选择生成最终的执行计划。在路径生成阶段,优化器利用动态规划、遗传算法等尝试不同的连接路径,并且为这些路径标记了代价信息,优化器只需要根据代价进行比较把更小代价的路径选择出来,这样就得到了一棵最优路径树。
路径选择过程中,主要关注如下几个因素:
- 启动代价
- 总代价
- 路径的过滤性
- 是否有HINT
HINT是数据库中的一种重要调优手段,可以在代价估算不准确时作为一种补偿措施,因此对于有特定HINT应用的路径其被选择的比重也会提升。本章节暂不讨论此种情况。
基于上述因素,路径选择的总体原则如下(以A路径为已选路径, B路径为新路径做比较说明):
- 如果A的总体代价优于B,那么进一步看B的启动代价是否优于A,如果B的启动代价优于A,则保留A路径同时接受新路径B作为候选路径。这里保留B路径是因为最终生成上层计划时如果有LIMIT等一些约束条件,B路径可能带来更好的性能,在整个路径选择过程中,优化器中并非只会保留一条可选路径。如果B路径的启动代价也没有优势,那么该路径就会被舍弃。
- 如果B路径的总体代价优于A,但A的启动代价优于B,那么保留A路径同时接受新路径B作为候选路径。如果A的启动代价也没有优势,那么接受B作为新的候选路径并舍弃A路径。
- 如果A路径和B路径的总体代价相差不大,那么进一步比较其启动代价,保留启动代价更优的路径。如果启动代价相差也不大,那么需要看哪条路径的过滤性更好,预估返回行数更少的路径被保留。
上述是路径选择的总体原则,在实际应用中,比较逻辑更为复杂。例如:可以生成并行计划的场景需要考虑数据分布等因素,有路径参数的场景要进一步比较路径参数个数等,但总体策略都是基于代价信息来比较的,CBO优化器通过多层迭代,不断淘汰差的路径,最终选择出一棵完整的最优路径树。但这棵路径树上包含了大量执行器不需要的信息,并且缺少对于AGG函数、LIMIT等约束的处理,因此优化器会进一步补充一些算子逻辑,并将其转化为执行器可以理解的PlanTree,即最终呈现的计划树。这个计划树在GaussDB中可以通过EXPLAIN命令打印出来,如下是一个简单的SeqScan计划示例:
gaussdb=# EXPLAIN SELECT * FROM t1 WHERE t1.a > 10;
QUERY PLAN
------------------------------------------------------------------
Streaming (type: GATHER) (cost=4.05..236.84 rows=4991 width=16)
Node/s: All datanodes
-> Seq Scan on t1 (cost=0.05..28.84 rows=4991 width=16)
Filter: (a > 10)
(4 rows) 该示例是一个完整的计划展示,包含了最终计划扫描路径(Seq Scan on t1 )、过滤条件约束(Filter: (a > 10))以及代价信息(cost=0.05..28.84 rows=4991 width=16),如启动代价、总体代价和返回行数估算。
下面结合示例,对GaussDB中常见的计划类型和算子进行介绍。建表语句如下:
gaussdb=# CREATE TABLE t1(a INT, b INT, c INT, d INT); NOTICE: The 'DISTRIBUTE BY' clause is not specified. Using 'a' as the distribution column by default. HINT: Please use 'DISTRIBUTE BY' clause to specify suitable data distribution column. CREATE TABLE gaussdb=# CREATE INDEX idx_t1_a ON t1(a); CREATE INDEX gaussdb=# CREATE INDEX idx_t1_b ON t1(b); CREATE INDEX gaussdb=# INSERT INTO t1 (a, b, c) VALUES (generate_series(1, 10000), generate_series(1, 5000), floor(random()*10)); INSERT 0 10000 gaussdb=# ANALYZE t1; ANALYZE gaussdb=# CREATE TABLE t2(a INT, b INT, c INT, d INT); NOTICE: The 'DISTRIBUTE BY' clause is not specified. Using 'a' as the distribution column by default. HINT: Please use 'DISTRIBUTE BY' clause to specify suitable data distribution column. CREATE TABLE gaussdb=# CREATE INDEX idx_t2_a ON t2(a); CREATE INDEX gaussdb=# CREATE INDEX idx_t2_b ON t2(b); CREATE INDEX gaussdb=# INSERT INTO t2 (a, b, c) VALUES (generate_series(1, 10000), generate_series(1, 5000), floor(random()*10)); INSERT 0 10000 gaussdb=# ANALYZE t2; ANALYZE gaussdb=# CREATE TABLE t3(a INT, b INT, c INT, d INT); NOTICE: The 'DISTRIBUTE BY' clause is not specified. Using 'a' as the distribution column by default. HINT: Please use 'DISTRIBUTE BY' clause to specify suitable data distribution column. CREATE TABLE gaussdb=# CREATE INDEX idx_t3_a ON t3(a); CREATE INDEX gaussdb=# CREATE INDEX idx_t3_b ON t3(b); CREATE INDEX gaussdb=# INSERT INTO t3 (a, b, c) VALUES (generate_series(1, 100), generate_series(1, 50), floor(random()*10)); INSERT 0 100 gaussdb=# ANALYZE t3; ANALYZE gaussdb=# SET explain_perf_mode=normal; SET gaussdb=# SET enable_nestloop=off; SET gaussdb=# SET enable_hashjoin=on; SET gaussdb=# SET enable_mergejoin=off; SET
- 分布式Stream计划:
gaussdb=# EXPLAIN SELECT count(t1.a),t1.c FROM t1 LEFT JOIN t2 ON t1.b = t2.b WHERE t1.a > ALL(SELECT a FROM t3) GROUP BY t1.c ORDER BY t1.c LIMIT 10; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------ Limit (cost=5260.77..5260.78 rows=5 width=16) -> Sort (cost=5260.77..5260.78 rows=5 width=16) Sort Key: t1.c -> HashAggregate (cost=5260.35..5260.71 rows=5 width=16) Group By Key: t1.c -> Streaming (type: GATHER) (cost=5260.35..5260.71 rows=15 width=16) Node/s: All datanodes -> HashAggregate (cost=5260.04..5260.09 rows=15 width=16) Group By Key: t1.c -> Hash Left Join (cost=364.96..5243.37 rows=10000 width=8) Hash Cond: (t1.b = t2.b) -> Streaming(type: REDISTRIBUTE) (cost=0.00..4836.74 rows=5000 width=12) Spawn on: All datanodes -> Index Scan using idx_t1_b on t1 (cost=0.00..4699.21 rows=5000 width=12) Filter: (SubPlan 1) SubPlan 1 -> Materialize (cost=0.00..2.02 rows=900 width=4) -> Streaming(type: BROADCAST) (cost=0.00..1.52 rows=300 width=4) Spawn on: All datanodes -> Seq Scan on t3 (cost=0.00..1.33 rows=100 width=4) -> Hash (cost=323.30..323.30 rows=9999 width=4) -> Streaming(type: REDISTRIBUTE) (cost=0.00..323.30 rows=10000 width=4) Spawn on: All datanodes -> Seq Scan on t2 (cost=0.00..48.33 rows=10000 width=4) (24 rows)这是一个典型的分布式SQL计划,也是分布式数据库中最常见的一类计划。该计划中包含了前文提到的主要扫描路径(索引扫描、顺序扫描路径)和连接路径(Hash Left Join)。此外,t1.a > ALL(SELECT a FROM t3)部分生成了一个子计划(SubPlan)在执行阶段可以由t1表的约束条件驱动独立执行,t1表的扫描算子上层是一个Stream(Redistribute) 算子,在分布式数据库中,每张表的数据会根据分布键分布存储在不同的DN节点上,因此t1表与t2表Join时,需要根据b列等值条件对数据进行重分布,从而保证在每个DN节点上数据能够正确连接。Left Join上层是一个Group By的算子(HashAggregate),然后是Stream(Gather算子),该算子主要在CN节点上用于汇总各DN节点返回的计算结果,数据汇总完成后执行排序算子(Sort),最后是LIMIT算子(Limit)。从该计划可以看到,除用于分布式计算的Stream算子外,SQL执行计划的处理逻辑与原始SQL语句一一映射。
- 分布式PGXC计划:
gaussdb=# SET enable_stream_operator = off; SET gaussdb=# EXPLAIN SELECT count(t1.a),t1.c FROM t1 LEFT JOIN t2 ON t1.b = t2.b WHERE t1.a > ALL(SELECT a FROM t3) GROUP BY t1.c ORDER BY t1.c LIMIT 10; QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------- Limit (cost=250.11..250.12 rows=5 width=8) -> Sort (cost=250.11..250.12 rows=5 width=8) Sort Key: t1.c -> HashAggregate (cost=250.00..250.05 rows=5 width=8) Group By Key: t1.c -> Hash Right Join (cost=62.50..200.00 rows=10000 width=8) Hash Cond: (t2.b = t1.b) -> Data Node Scan on t2 "_REMOTE_TABLE_QUERY_" (cost=0.00..0.00 rows=10000 width=4) Node/s: All datanodes -> Hash (cost=0.00..0.00 rows=5000 width=12) -> Data Node Scan on t1 "_REMOTE_TABLE_QUERY_" (cost=0.00..0.00 rows=5000 width=12) Node/s: All datanodes Coordinator quals: (SubPlan 1) SubPlan 1 -> Materialize (cost=0.00..0.50 rows=100 width=4) -> Data Node Scan on t3 "_REMOTE_TABLE_QUERY_" (cost=0.00..0.00 rows=100 width=4) Node/s: All datanodes (17 rows)PGXC计划的原理是将部分简单SQL查询直接下推到DN节点上计算并将返回的结果在CN节点上进行汇总,其并行度比Stream计划差,因此通常在Stream计划无法生成时才会选择PGXC计划。如上面示例,通过设置GUC参数enable_stream_operator关闭Stream计划生成逻辑强制选择PGXC计划,对比Stream计划该计划中没有Stream算子,并且直接将t1表、t2表和t3表的SeqScan扫描下推到了DN上执行,但其他的Join、分组、排序等操作均在CN节点上执行。
- 分布式下推语句计划:
分布式场景下还有一类查询计划,即对于简单的SQL语句直接将语句下推到每个DN节点上,由DN节点的优化器各自生成执行计划并执行,CN节点只负责汇总结果。典型示例如下所示:
gaussdb=# EXPLAIN SELECT * from t1 WHERE b > 10; QUERY PLAN ----------------------------------------------------------------------------------- Data Node Scan on t1 "_REMOTE_TABLE_QUERY_" (cost=0.00..0.00 rows=9982 width=16) Node/s: All datanodes (2 rows)从上面的SQL可以看出,这类计划的查询语句逻辑一般比较简单,不需要多表关联数据分布计算,也没有复杂的聚集函数、窗口函数、分组、排序等约束条件,其不需要依赖CN节点的额外计算能力,可以最大化利用节点的分布式并行计算能力。
上述示例删除建表语句如下:
gaussdb=# DROP TABLE t1; DROP TABLE gaussdb=# DROP TABLE t2; DROP TABLE gaussdb=# DROP TABLE t3; DROP TABLE gaussdb=# RESET explain_perf_mode; RESET