计划选择
前面的章节介绍了数据库中几类常用的路径生成方式以及优化器的代价估算模型原理,在此基础上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
---------------------------------------------------------
Seq Scan on t1 (cost=0.44..927.50 rows=54974 width=16)
Filter: (a > 10), (Expression Flatten Optimized)
(2 rows) 该示例是一个完整的计划展示,包含了最终计划扫描路径(Seq Scan on t1 )、过滤条件约束(Filter: (a > 10))以及代价信息(cost=0.44..927.50 rows=54974 width=16),如启动代价、总体代价和返回行数估算。
下面结合示例,对GaussDB中常见的计划类型和算子进行介绍。建表语句如下:
gaussdb=# CREATE TABLE t1(a INT, b INT, c INT, d INT); 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); 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); 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=on; SET gaussdb=# SET enable_mergejoin=off; SET gaussdb=# SET enable_hashjoin=off; SET
- 普通SQL计划:
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 --------------------------------------------------------------------------------------------------- [Parameterized] Limit (cost=15654.68..15654.69 rows=5 width=8) -> Sort (cost=15654.68..15654.69 rows=5 width=8) Sort Key: t1.c -> HashAggregate (cost=15654.57..15654.62 rows=5 width=8) Group By Key: t1.c -> Nested Loop Left Join (cost=0.00..15604.57 rows=10000 width=8) -> Index Scan using idx_t1_b on t1 (cost=0.00..14013.57 rows=5000 width=12) Filter: (SubPlan 1), (Expression Flatten Optimized) SubPlan 1 -> Materialize (cost=0.00..2.50 rows=100 width=4) -> Seq Scan on t3 (cost=0.00..2.00 rows=100 width=4) -> Index Only Scan using idx_t2_b on t2 (cost=0.00..0.30 rows=2 width=4) Index Cond: (b = t1.b) (14 rows)该示例是一个典型的普通SQL计划,包含了前面提到的主要扫描路径(索引扫描、顺序扫描路径)和连接路径(Nestloop Left Join)。此外,t1.a > ALL(SELECT a FROM t3)部分生成了一个子计划(SubPlan)在执行阶段可以由t1表的约束条件驱动独立执行,Left Join上层是一个Group By的算子(HashAggregate),然后是排序算子(Sort),最后是LIMIT算子(Limit),从该计划可以看到典型的执行计划逻辑与原始SQL语句是一一映射的。
- 并行SQL计划:
gaussdb=# SET enable_force_smp = on; SET gaussdb=# SET query_dop = 4; 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 -------------------------------------------------------------------------------------------------------------------------------------- [Parameterized] Limit (cost=50783.41..50783.42 rows=5 width=16) -> Sort (cost=50783.41..50783.42 rows=5 width=16) Sort Key: t1.c -> Streaming(type: LOCAL GATHER dop: 1/4) (cost=50783.33..50783.35 rows=5 width=16) -> HashAggregate (cost=50783.33..50783.34 rows=5 width=16) Group By Key: t1.c -> Streaming(type: LOCAL REDISTRIBUTE dop: 4/4) (cost=50783.17..50783.32 rows=5 width=16) -> HashAggregate (cost=50783.17..50783.18 rows=5 width=16) Group By Key: t1.c -> Nested Loop Left Join (cost=0.00..50770.67 rows=10000 width=8) Join Filter: (t1.b = t2.b), (Expression Flatten Optimized) -> Streaming(type: LOCAL REDISTRIBUTE dop: 4/1) (cost=0.00..3701.86 rows=5000 width=12) -> Index Scan using idx_t1_b on t1 (cost=0.00..3626.07 rows=5000 width=12) Filter: (SubPlan 1), (Expression Flatten Optimized) SubPlan 1 -> Materialize (cost=0.00..2.50 rows=100 width=4) -> Seq Scan on t3 (cost=0.00..2.00 rows=100 width=4) -> Materialize (cost=0.00..200.06 rows=10000 width=4) -> Streaming(type: LOCAL REDISTRIBUTE dop: 4/4) (cost=0.00..187.56 rows=10000 width=4) -> Seq Scan on t2 (cost=0.00..36.00 rows=10000 width=4) (21 rows)在GaussDB中除了普通计划还有一类可以并行的Stream计划,其原理是利用多线程并发执行的优势,以空间换时间来加速执行。如上述示例,要生成Stream计划需要同时开启enable_force_smp和 query_dop两个GUC参数。和普通计划相比,该计划中增加了Stream算子,通过Redistribute算子对t1表和t2表的数据进行重分布,重分布后数据可以根据分布键在4个线程上独立Join,最后再通过Gather算子进行汇总。
上面示例删除建表语句如下:
gaussdb=# DROP TABLE t1; DROP TABLE gaussdb=# DROP TABLE t2; DROP TABLE gaussdb=# DROP TABLE t3; DROP TABLE gaussdb=# RESET explain_perf_mode; RESET