entitymanager执行多个sql_Presto逻辑执行计划生成

4f749896516b28d9053ef943fc921b85.png

引言

本文假设读者已经熟悉了 Presto QE 执行模型的一些基本概念,比如 Statement、Query、Stage、Task、Split、Driver、Operator、Exchange。该篇文章主要讲解 Presto的逻辑执行计划是如何生成的。

d21e625550bbc6577f279e7aed96bc2d.png
图1-1

95c4aa5cbbacea58b915536c6fbc69bd.png
图1-2

如图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。

4ab593c5082e4dc406e6d06f2764b529.png
图2

到此,一个被分割后的逻辑执行计划已经完成,但是此时还不能够被执行引擎进行执行,需要被执行引擎生成对应的物理执行计划后才能够被执行,俺的下一篇文章将会讲述如何实现物理执行计划并被执行引擎执行。

帮助工具

可以使用 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物理执行计划生成

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用EntityManager进行多个条件查询的示例代码: ```java // 创建查询条件 CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<MyEntity> cq = cb.createQuery(MyEntity.class); Root<MyEntity> root = cq.from(MyEntity.class); List<Predicate> predicates = new ArrayList<>(); predicates.add(cb.equal(root.get("name"), "John")); predicates.add(cb.greaterThan(root.get("age"), 18)); predicates.add(cb.lessThan(root.get("salary"), 50000)); // 构建查询 cq.where(predicates.toArray(new Predicate[0])); TypedQuery<MyEntity> query = entityManager.createQuery(cq); // 执行查询 List<MyEntity> results = query.getResultList(); ``` 在上面的代码中,我们首先使用EntityManager获取CriteriaBuilder实例,然后创建一个CriteriaQuery对象来表示我们要查询的实体类型。接着,我们创建一个Root对象来表示查询的根实体,然后创建一个Predicate列表来存储我们的查询条件。在本例中,我们添加了三个条件:name等于"John",age大于18,salary小于50000。 在添加完所有的查询条件后,我们使用cq.where()方法将它们作为参数传递给查询对象,然后使用entityManager.createQuery()方法创建一个TypedQuery对象。最后,我们执行查询并将结果存储在一个List对象中。 注意,这只是一个简单的示例,实际应用中可能需要更复杂的查询条件。另外,如果你使用的是JPA 2.0之前的版本,你可能需要手动拼接SQL语句来实现多个条件查询。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值