引言
本文假设读者已经熟悉了 Presto QE 执行模型的一些基本概念,比如 Statement、Query、Stage、Task、Split、Driver、Operator、Exchange。该篇文章主要讲解 Presto的逻辑执行计划是如何生成的。
如图1-1, 1-2 描述了一条查询语句(Statement)如何被转化一颗无法解析的AST,步骤1;然后经过转化成没有被优化的逻辑计划,步骤2;以及经过优化后,转化为一个优化的执行计划,步骤3;到最终被分割成可以被分布式分析的分段逻辑执行计划。
具体实现
CLI提交SQL之后,查询请求会被封装成一个 SqlQueryExecution对象提交给 Coordinator去执行。其中在 SqlQueryExecution#startExecution#analyzeQuery里,会生成逻辑执行计划,之后会被优化成一个优化后的逻辑执行计划,代码如下:
private void startExecution()
{
// analyze query
PlanRoot plan = analyzeQuery();
metadata.beginQuery(getSession(), plan.getConnectors());
// plan distribution of query
planDistribution(plan);
// transition to starting
if (!stateMachine.transitionToStarting()) {
// query already started or finished
return;
}
// if query is not finished, start the scheduler, otherwise cancel it
SqlQueryScheduler scheduler = queryScheduler.get();
if (!stateMachine.isDone()) {
scheduler.start();
}
}
下面的过程在 analyzeQuery 中。
// 构造逻辑执行计划
LogicalPlanner logicalPlanner = new LogicalPlanner(
stateMachine.getSession(),
planOptimizers,
idAllocator,
metadata,
sqlParser,
statsCalculator, // 用于获取或计算代价模型估算所需要的统计信息
costCalculator, // 调用代价模型估算计划代价
stateMachine.getWarningCollector());
// 优化后的执行计划
Plan plan = logicalPlanner.plan(analysis);
LogicalPlanner 负责整个SQL语句执行计划的生成,根据SQL语句类型的不同,生成不同的执行计划,然后针对生成的执行计划,分别使用注册进来的优化器进行优化,生成优化好的逻辑执行计划。
其中在 logicalPlanner.plan里面,会进行逻辑执行计划的优化。通过调试可以看见的采用的优化是 IterativeOptimizer。详细的关于 IterativeOptimizer的介绍,可以参考CMU的Andy Pavlo在数据库课程中有清晰详细的介绍:https://15721.courses.cs.cmu.edu/spring2018/slides/16-optimizer2.pdf
在AST绑定相应元数据后(Analysis 主要是用于绑定元数据),将把AST转换成逻辑计划树PlanNode。
// 生成逻辑计划树
public PlanNode planStatement(Analysis analysis, Statement statement)
{
if (statement instanceof CreateTableAsSelect && analysis.isCreateTableAsSelectNoOp()) {
Symbol symbol = symbolAllocator.newSymbol("rows", BIGINT);
PlanNode source = new ValuesNode(idAllocator.getNextId(), ImmutableList.of(symbol), ImmutableList.of(ImmutableList.of(new LongLiteral("0"))));
return new OutputNode(idAllocator.getNextId(), source, ImmutableList.of("rows"), ImmutableList.of(symbol));
}
// 这里会根据不同类型的SQL生成不同类型的 RelationPlan 传入第一个参数
// 比如 statement 是一个 Insert、Delete 或者还是一个 Query 等等,
// 比如 delete 语句,那么就会生成一个 DeleteNode,
return createOutputPlan(planStatementWithoutOutput(analysis, statement), analysis);
}
接下来该 PlanNode 所表示的逻辑执行计划会被 PlanOptimizer 进行优化,生成一颗优化后的PlanNode Tree。
public interface PlanOptimizer
{
PlanNode optimize(PlanNode plan,
Session session,
TypeProvider types,
SymbolAllocator symbolAllocator,
PlanNodeIdAllocator idAllocator,
WarningCollector warningCollector);
}
这里可以用一条SQL语句来验证优化前后的逻辑计划树长啥样,我采用 tpc-h catalog做为进入Presto的CLI,假设此时的SQL为:
select * from customer limit 12;
没有被优化前的逻辑计划树如下:
OutputNode
ProjectNode
LimitNode
TableScanNode
执行优化后的逻辑计划树如下,具体如何做PlanNode的优化,则不在本篇文章讨论的范围内,有兴趣的可以查看具体优化器的代码。
OutputNode
LimitNode
ExchangeNode
最后会把优化后的 PlanNode Tree 生成 Plan 执行计划。
分割Plan
分割 Plan 即为图1-2的第4步骤,Presto会用 PlanFragmenter 去创建SubPlan。PlanFragmenter 的作用是把逻辑执行计划分割成一个一个片段,使得这些片段最终可以在分布式的结点上被执行。
SubPlan fragmentedPlan = planFragmenter.createSubPlans(
stateMachine.getSession(),
plan,
false);
输入的是 Plan,输出的是 SubPlan,SubPlan的结构如图2,由 List<PlanFragment>构造而成,本质上就是一个一个的PlanFragment。而 Plan 是优化后的 PlanNode Tree 加上一些类型和统计以及代价的元信息。打开Presto ui 上的 Stage信息,可以看到一个SQL语句的执行会被分为多个stage,每个stage是一个 PlanFragment,并行的运行在多个机器上。PlanFragment 之间通过 ExchangeOperator 交换数据,而 SubPlan 由多个 PlanFragment构成,也就是在 Presto UI上看到的最大的那个plan。
到此,一个被分割后的逻辑执行计划已经完成,但是此时还不能够被执行引擎进行执行,需要被执行引擎生成对应的物理执行计划后才能够被执行,俺的下一篇文章将会讲述如何实现物理执行计划并被执行引擎执行。
帮助工具
可以使用 EXPLAIN TYPE DISTRIBUTED 来观察分布式的执行计划,每一个 Fragment 会被一个或者多个 Presto节点执行。执行如下的 SQL 将会被分割成2个 Fragment,也即是2个 Stage并发的执行在 Presto节点上。
explain (type distributed) select * from customer limit 12;
Query Plan
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Fragment 0 [SINGLE] // SINGLE 表明该 Fragment0 在一个单一的Presto节点上被执行
// 输出该表的字段信息
Output layout: [custkey, name, address, nationkey, phone, acctbal, mktsegment, comment]
Output partitioning: SINGLE []
Grouped Execution: false
- Output[custkey, name, address, nationkey, phone, acctbal, mktsegment, comment]
=> [
custkey:bigint,
name:varchar(25),
address:varchar(40),
nationkey:bigint,
phone:varchar(15),
acctbal:double,
mktsegment:varchar(10),
comment:varchar(117)
]
Cost:
{rows: 12 (2.14kB), cpu: 27386424.13, memory: 0.00, network: 2190.56}
- Limit[12] => [
custkey:bigint,
name:varchar(25),
address:varchar(40),
nationkey:bigint,
phone:varchar(15),
acctbal:double,
mktsegment:varchar(10),
comment:varchar(117)]
Cost: {
rows: 12 (2.14kB), cpu: 27386424.13, memory: 0.00, network: 2190.56}
- LocalExchange[SINGLE] ()
=> custkey:bigint, name:varchar(25),
address:varchar(40), nationkey:bigint,
phone:varchar(15), acctbal:double,
mktsegment:varchar(10), comment:varchar(117)
Cost: {rows: 12 (2.14kB), cpu: 27384233.56, memory: 0.00, network: 2190.56}
- RemoteSource[1]
=> [custkey:bigint, name:varchar(25),
address:varchar(40), nationkey:bigint,
phone:varchar(15), acctbal:double,
mktsegment:varchar(10), comment:varchar(117)]
Cost: {rows: 12 (2.14kB), cpu: 27384233.56, memory: 0.00, network: 2190.56}
Fragment 1 [SOURCE] //SOURCE表示这个SubPlan,即PlanFragment是数据源
Output layout: [custkey, name, address, nationkey, phone, acctbal, mktsegment, comment]
Output partitioning: SINGLE []
Grouped Execution: false
- LimitPartial[12] => [custkey:bigint, name:varchar(25), address:varchar(40), nationkey:bigint, phone:varchar(15), acctbal:double, mktsegment:varchar(10), comment:varchar(117)]
Cost: {
rows: 12 (2.14kB), cpu: 27384233.56, memory: 0.00, network: 0.00}
- TableScan[
tpch:customer:sf1.0, grouped = false]
=> [
custkey:bigint,
name:varchar(25),
address:varchar(40),
nationkey:bigint,
phone:varchar(15),
acctbal:double,
mktsegment:varchar(10),
comment:varchar(117)
]
Cost:
{
rows: 150000 (26.11MB),
cpu: 27382043.00,
memory: 0.00,
network: 0.00
}
mktsegment := tpch:mktsegment
nationkey := tpch:nationkey
address := tpch:address
phone := tpch:phone
custkey := tpch:custkey
name := tpch:name
comment := tpch:comment
acctbal := tpch:acctbal
(1 row)
Query 20190222_130827_00042_qeuhy, FINISHED, 1 node
Splits: 1 total, 1 done (100.00%)
此外,可以用 EXPLAIN ANALYZE 来分析在分布式执行的时候每一个操作的Cost。
EXPLAIN ANALYZE SELECT count(*), clerk FROM orders
WHERE orderdate > date '1995-01-01' GROUP BY clerk;
Query Plan
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Fragment 1 [HASH]
CPU: 24.73ms, Scheduled: 213.99ms, Input: 4000 rows (148.44kB); per task: avg.: 4000.00 std.dev.: 0.00, Output: 1000 rows (28.32kB)
Output layout: [clerk, count]
Output partitioning: SINGLE []
Grouped Execution: false
- Project[] => [clerk:varchar(15), count:bigint]
CPU: 6.00ms (0.03%), Scheduled: 40.00ms (0.19%), Input: 1000 rows (37.11kB), Output: 1000 rows (28.32kB), Filtered: 0.00%
Input avg.: 62.50 rows, Input std.dev.: 14.77%
// 按照 clerk 字段去做 hash
- Aggregate(FINAL)[clerk][$hashvalue] =>
[clerk:varchar(15), $hashvalue:bigint, count:bigint]
CPU: 9.00ms (0.05%), Scheduled: 28.00ms (0.13%),
Output: 1000 rows (37.11kB) //
Input avg.: 250.00 rows, Input std.dev.: 14.77%
count := "count"("count_8")
// 在 LocalExchange 这里,还会进一步做 hash分桶,这里是 Presto 计算引擎
// 特有的地方,增大 Driver数,让CPU可以充分的进行计算。
- LocalExchange[HASH][$hashvalue]
("clerk") =>
clerk:varchar(15), count_8:bigint, $hashvalue:bigint
CPU: 5.00ms (0.03%), Scheduled: 13.00ms (0.06%), Output: 4000 rows (148.44kB)
Input avg.: 250.00 rows, Input std.dev.: 387.30%
- RemoteSource[2] =>
[clerk:varchar(15), count_8:bigint, $hashvalue_9:bigint]
CPU: 0.00ns (0.00%), Scheduled: 0.00ns (0.00%), Output: 4000 rows (148.44kB)
Input avg.: 250.00 rows, Input std.dev.: 387.30%
Fragment 2 [tpch:orders:1500000]
CPU: 17.43s, Scheduled: 21.16s, Input: 1500000 rows (0B); per task: avg.: 1500000.00 std.dev.: 0.00, Output: 4000 rows (148.44kB)
Output layout: [clerk, count_8, $hashvalue_10]
Output partitioning: HASH [clerk][$hashvalue_10]
Grouped Execution: false
- Aggregate(PARTIAL)[clerk][$hashvalue_10] => [clerk:varchar(15), $hashvalue_10:bigint, count_8:bigint]
CPU: 72.00ms (0.41%), Scheduled: 93.00ms (0.44%), Output: 4000 rows (148.44kB)
Input avg.: 204514.50 rows, Input std.dev.: 0.05%
Collisions avg.: 5701.28 (17569.93% est.), Collisions std.dev.: 1.12%
count_8 := "count"(*)
- ScanFilterProject[table = tpch:orders:sf1.0, grouped = false, filterPredicate = ("orderdate" > DATE '1995-01-01')] => [clerk:varchar(15), $hashvalue_10:bigint]
Cost: {rows: 1500000 (14.32MB), cpu: 15015000.00, memory: 0.00, network: 0.00}/{rows: 816424 (7.79MB), cpu: 30030000.00, memory: 0.00, network: 0.00}/{rows: 816424 (10.91MB), cpu: 41468101.87, memory: 0.00, network: 0.00}
CPU: 17.35s (99.47%), Scheduled: 21.06s (99.18%), Input: 1500000 rows (0B), Output: 818058 rows (22.62MB), Filtered: 45.46%
Input avg.: 375000.00 rows, Input std.dev.: 0.00%
$hashvalue_10 := "combine_hash"(bigint '0', COALESCE("$operator$hash_code"("clerk"), 0))
clerk := tpch:clerk
orderdate := tpch:orderdate
tpch:orderstatus
:: [[F], [O], [P]]
(1 row)
Query 20190223_014000_00014_qeuhy, FINISHED, 1 node
Splits: 53 total, 53 done (100.00%)
0:06 [1.5M rows, 0B] [268K rows/s, 0B/s]
结语
本篇文章从代码层面描述了 Presto 的逻辑执行计划的过程以及使用相关的工具去查看逻辑执行计划以方便学习者形成一个客观的认识,要求读者对 sql 的执行过程有一定的基础知识,欢迎阅读留言并参与讨论,谢谢。
彩蛋
rice:Presto物理执行计划生成