CockroachDB 物理执行计划简单解析

来源:互联网 发布:购房增值税的算法 编辑:程序博客网 时间:2024/06/08 19:32

如果还没有看过 CockroachDB 逻辑执行计划, 请先看那篇文章。

本文翻译自:
CockroachDB 物理执行计划的分析

概述


本篇文章主要是讲一个分布式SQL语句的执行过程。总的目标就是处理或者移动的计算要靠近数据源。

概念


  • KV - 逻辑存储层的操作,对应 rangebatch API
  • k/v - 一个键值对,通常是对应 KV 中的 entry
  • Node - 集群中的一个机器
  • Client / Client-side - SQL 客户端
  • Gateway node / Gateway-side - 接收到 SQL 语句的集群中的节点
  • Leader node / Leader-side - 执行 KV 操作并且在本地获取数据的集群节点

动机(需求分析)


物理执行计划的实现如下分析。

1.Remote-side filtering

能够将过滤语句尽可能的下推,在每个 node 中获取数据的时候都能尽可能用刀过滤语句。

2.Remote-side updates and deletes

对于 UPDATE .. WHEREDELETE .. WHERE,可以尽量避免太多的数据来回传递。可以在过滤的同时判断是不是直接可以更新,若是在同一个节点可以操作,就可以避免数据的来回传递。

3.Distributed SQL operations

利用上分布式的多个机器的优势,让 SQL 运算在多个机器上执行。分布式SQL 包含如下三种情况(这三种情况可能出现在同一个语句中)。
1. 分布式的join
2. 分布式的aggregation
3. 分布式的sorting

设计


概述


对于理解本文比较有帮助的文章是Sawzall,这是Google提供的一个高级语言,目标是能够简化利用MapReduce的过程。简而言之就是:
Sawzall = MapReduce + high-level syntax + new terminology

CockroachDB的物理执行计划与上面文章有类似之处,但是最终的模型与 MapReduce 还是有些区别,下面我们详细的解释一下:

  1. 物理执行计划中包含许多提前定义好的 aggregators,这些 aggregators 是为执行SQL的处理部分。多数的 aggregators 是可以配置的,但是不可编程可以配置的意思是,能够给定义好的参数赋值。不可编程 的意思是不可以给一些运算,比如 a + b
  2. 一个特别的 aggregator 是 evaluator,他可以用简单的编程语句。但是这个 aggregator 只能一行一行处理。
  3. 通过 routing,将信息在 aggregatoraggregator 之间进行传递。
  4. 逻辑执行计划是不考虑数据存放位置的,但是它存有足够的信息来说明数据存放位置,进而可以分布式计算执行。

除了累加和分组数据等操作,aggregator 的功能还可以是将数据传递给其它 node ,也可能是作为其他程序的输入。最终,有批量处理结果的特殊功能并且提供 KV 命令的aggregators ,是用来读取数据或者写入数据的。

核心思想就是我们转化一个 SQL逻辑模型,再从逻辑模型 转换到 分布式物理执行计划

逻辑模型


我们会将 SQL 语句解析成一个 逻辑模型(这个与之前的逻辑执行计划有些相似)。这个逻辑模型代表着抽象数据流的不同的计算阶段。这个逻辑模型并不知道数据最终的存放方式,但是它存有足够查询到的信息。在后续阶段,我们会将这个逻辑模型转换为物理执行计划。物理执行计划会将抽象计算对应成各个 processors,同时会定义好 processor 之间的混合的数据流和他们之间的交互通道。

逻辑模型由各种 aggregators 组成,每一个 aggregator 包含一个 input stream 和一个 output stream。一个 stream 就代表一个 rows 的流。一个 row 代表一个 列元组 的值。每一个 input streamoutput stream 都包含一个 schema。这个schema 就是 列元组 中列的属性。再次强调,这个 streams (input/output) 是一个逻辑上的概念,并不一定会真实出现在一个真正的物理计算中。(本段实际上就是一个输入的结果集,经过处理后,变成一个输出的结果集。整体叫做 aggregator , 输入输出流的元数据信息就叫 schema

在一个 aggregator 内引入一个概念叫 groupinggrouping的作用是概括一个特定的计算。实际上就是给 aggregator 内部能够并发的操作起了一个名字。一个 aggregator 内部的所有 grouping 就被叫做 groupsgroups 是基于一个 group key 存在的。这个 group key 就是上文中schema内的column信息的一个子集。每个 group 都独立执行,并且最终 aggregator 也不会将信息整合,而是直接传递给下一层的aggregator。有些aggregator会保证有序,有些不会。aggregator保证有序,就意味着 group 保证有序。

aggregator 中还可以包含filter,在输出之前会判断这个 filter,决定某列是否可以输出。

比较特别的 table reader aggregator 没有输入,是被用作数据源的。一个 table reader 可以只输出特定的需要的列。另一个比较特别的 final aggregator 是没有输出流,被用作一条语句的结果集。

有一些 aggregator 是有输入有序的需求的,同时一些 aggregator 是有一个特定的顺序的保证的。每一个 aggregator 都是有一个被叫做 ordering characterization 函数的,它可以根据输入的排序方式组建输出的排序方式。如果需要排序的 aggregator 遇到了没有排序的 input stream ,那么就需要添加一个 sorting aggregator

下面举例子,介绍 aggregator

Example 1

TABLE Orders (OId INT PRIMARY KEY, CId INT, Value DECIMAL, Date DATE)SELECT CID, SUM(VALUE) FROM Orders  WHERE DATE > 2015  GROUP BY CID  ORDER BY 1 - SUM(Value)

这一段所输出的逻辑上的 aggregator 和 stream 如下:

TABLE-READER src  Table: Orders  Table schema: Oid:INT, Cid:INT, Value:DECIMAL, Date:DATE  Output filter: (Date > 2015)  Output schema: Cid:INT, Value:DECIMAL  Ordering guarantee: OidAGGREGATOR summer  Input schema: Cid:INT, Value:DECIMAL  Output schema: Cid:INT, ValueSum:DECIMAL  Group Key: Cid  Ordering characterization: if input ordered by Cid, output ordered by CidEVALUATOR sortval  Input schema: Cid:INT, ValueSum:DECIMAL  Output schema: SortVal:DECIMAL, Cid:INT, ValueSum:DECIMAL  Ordering characterization:    ValueSum -> ValueSum and -SortVal    Cid,ValueSum -> Cid,ValueSum and Cid,-SortVal    ValueSum,Cid -> ValueSum,Cid and -SortVal,Cid  SQL Expressions: E(x:INT) INT = (1 - x)  Code {    EMIT E(ValueSum), CId, ValueSum  }AGGREGATOR final:  Input schema: SortVal:DECIMAL, Cid:INT, ValueSum:DECIMAL  Input ordering requirement: SortVal  Group Key: []Composition: src -> summer -> sortval -> final

上面的初步的逻辑模型没有包含排序,包含了排序就是最后的逻辑模型了。

src -> summer -> sortval -> sort(OrderSum) -> final

每一个箭头都是一个逻辑上的数据流(stream),这就是整体的逻辑模型。
在上面的例子中,添加排序的位置只能是在 sortval 之后,final 之前。下面的例子我们来看另一种情况。

Example 2

TABLE People (Age INT, NetWorth DECIMAL, ...)SELECT Age, Sum(NetWorth) FROM v GROUP BY AGE ORDER BY AGE

初步的逻辑模型如下所示:

TABLE-READER src  Table: People  Table schema: Age:INT, NetWorth:DECIMAL  Output schema: Age:INT, NetWorth:DECIMAL  Ordering guarantee: XXX  // will consider different cases laterAGGREGATOR summer  Input schema: Age:INT, NetWorth:DECIMAL  Output schema: Age:INT, NetWorthSum:DECIMAL  Group Key: Age  Ordering characterization: if input ordered by Age, output ordered by AgeAGGREGATOR final:  Input schema: Age:INT, NetWorthSum:DECIMAL  Input ordering requirement: Age  Group Key: []Composition: src -> summer -> final

这个 summer aggregator 可以有两种方式处理,如果输入的时候已经按照 Age 进行排序,那么输出的时候也就是排序完成的结果了。如果输入的时候没有按照 Age 进行排序,那么输出后就是无须的状态。

  1. src 就是有序的,按照 Age 进行排序,那么就不需要添加 sort aggregator
  2. src 是没有排序的,那么就需要添加一个 sort aggregator 对这个结果集进行排序。但是,这里可以有两种选择形式,如下:

在final之前, 进行添加排序能够完成需求。

src -> summer -> sort(Age) -> final

但是我们发现,在 summer 之前添加这个排序同样可以达到效果。

src -> sort(Age) -> summer -> final

这两种情况,就是需要我们去选择,在那种情况排序会性能更好。

上面我们可以看到,如果前一个 aggregator 有顺序保证,那么后面的aggregator 则尽可能的会保证这个顺序。这样能够使得尽量减少排序的发生。但是,保持排序状态很可能会产生额外的开销,所以我们需要在生成整个原始逻辑模型以后,查看是否真正的需要保持有序,如果不需要,则将顺序删除掉,避免出现过多的性能损失。总结上面所说的,共需要三个步骤。

  1. 原始逻辑模型尽量保持已经存在的顺序。
  2. 查看整体的逻辑模型,将需要添加sort aggregator的地方添加。
  3. 如果哪里不需要保持顺序,则删除这个顺序保证。

Example 3

TABLE v (Name STRING, Age INT, Account INT)SELECT COUNT(DISTINCT(account)) FROM v  WHERE age > 10 and age < 30  GROUP BY age HAVING MIN(Name) > 'k'
TABLE-READER src  Table: v  Table schema: Name:STRING, Age:INT, Account:INT  Filter: (Age > 10 AND Age < 30)  Output schema: Name:STRING, Age:INT, Account:INT  Ordering guarantee: NameAGGREGATOR countdistinctmin  Input schema: Name:String, Age:INT, Account:INT  Group Key: Age  Group results: distinct count as AcctCount:INT                 MIN(Name) as MinName:STRING  Output filter: (MinName > 'k')  Output schema: AcctCount:INT  Ordering characterization: if input ordered by Age, output ordered by AgeAGGREGATOR final:  Input schema: AcctCount:INT  Input ordering requirement: none  Group Key: []Composition: src -> countdistinctmin -> final

aggregator 的几种类型

  • TABLE READER 特殊的,没有输入流,只有输出流。它的参数是 spans (spans 请参考 逻辑执行计划)。它内部可以有filter
  • EVALUATOR 是一个可以编程的,不做分组的 aggregator。它会在每一行上执行这个程序,通过程序的输出结果选择删掉或者更改某些行内的值。
  • JOIN 将两个 stream 流合并,必须包含等值的条件。分组的情况就是两个stream中的配置的等值条件相等被分为一组。
  • JOIN READER point lookups
    …..
    具体请参考代码和原文。

从逻辑模型到物理模型

为了能够将计算分布式出去,我们选择了下面的几种方式:

  • 对于任何的 aggregator , 内部的groups 都是可以并行的执行的。
  • 对于排序来说,每一个 input 排好顺序,就代表有序,而不需要统一到一个 input 中。
  • 没有 group keys 的情况( limit, final),最终只能在一个单独的node上执行。

每一个逻辑上的 aggregator 都可以被分布倒不同的分布式实例上去,同时要保证每一个 aggregator 内的 stream 都是相同的顺序保证。

可以通过如下的规则进行分布:

  • table readers 会有许多的实例同时获取数据,我们可以通过获取数据需要用到的 ranges 来获知哪个实例(node)是被需要的,在这些实例上创建一个 table reader,同时创建一个输出流将获取到的数据传递给下一层次的 aggregator
  • 之后的 aggregator 都可以使用这种分布式的方式,需要提前指定可以用到几个实例,通过对 group keyhash 的计算, 将数据分布到这些实例中进行计算。空的 group key 只能在一个实例中进行计算。
  • sorting 的计算也是可以分布的,不必须在同一个实例中实现排序。

在次以 Example 1 作为例子,语句的执行在 Gateway 节点上,而数据的分布是在 nodes A 和 B 上。 逻辑上的模型依然如下:

TABLE-READER src  Table: Orders  Table schema: Oid:INT, Cid:INT, Value:DECIMAL, Date:DATE  Output filter: (Date > 2015)  Output schema: Cid:INT, Value:DECIMAL  Ordering guarantee: OidAGGREGATOR summer  Input schema: Cid:INT, Value:DECIMAL  Output schema: Cid:INT, ValueSum:DECIMAL  Group Key: Cid  Ordering characterization: if input ordered by Cid, output ordered by CidEVALUATOR sortval  Input schema: Cid:INT, ValueSum:DECIMAL  Output schema: SortVal:DECIMAL, Cid:INT, ValueSum:DECIMAL  Ordering characterization: if input ordered by [Cid,]ValueSum[,Cid], output ordered by [Cid,]-ValueSum[,Cid]  SQL Expressions: E(x:INT) INT = (1 - x)  Code {    EMIT E(ValueSum), CId, ValueSum  }

logical plan

根据上面说到的,逻辑模型可以被转化为物理模型如下:

physical plan

每一个方块可以叫做一个 processor,解释如下:

  • src 从表中读取数据,通过 KV Get 操作。同时,filter 会将 Date > 2015 的语句筛选出来,最终传递下一个 aggregator
  • summer-stage1 是将本地的数据先进行一次 aggregate 的操作,然后将数据传递给后面的下一步骤。通过对 group keyhash 之后,在进行后面的继续aggregate
  • summer-stage2 是将初次已经处理的数据在进行分组处理,最终处理完成的数据需要传递给后面的 appregator
  • sortval 计算操作,计算 1 - ValueSum,作为排序的依据。输出添加 SortVal
  • sort 根据 SortVal 对输入的流进行排序。
  • final 合并两个流,依据顺序输出。

summer-stage2 不一定需要与 summer-stage1 在同样的机器上执行,所以这个物理执行计划也有可能是如下形式的:

physical plan 2

上面的每一个方块都叫一个 processor。

Processors

Processor 中包含下面三个组成部分:
processor 组成部分

1.The input synchronizer 合并输入的流到一个单独的流中,分为三种情况

  • 单输入流,直接输出无需处理。
  • 无序多输入流,合并成一个就可以,无需考虑顺序。
  • 有序多输入流,要保证合成输出后一样是有序的流。

2.Data processor 实现数据的转换和合并流程

3.输出流分发到许多其它的 processor 作为输入流,由单个流变换成多个流。有如下几种情况:

  • 单输出流,直接转发即可。
  • 镜像模式,赋值所有值到所有输出流中。
  • hash 模式,每一列都只进入一个输出流,根据 group keyhash 值 确定到哪个输出值。
  • range 模式,根据后面要处理的数据的情况,根据 range 信息进行选择需要输出流。

Joins


Join-by-lookup

这种 Join-by-lookup 的方式是先获取到一个表的数据,然后通过这个表的数据再每行查询第二张表的数据。
举个例子:

TABLE t (k INT PRIMARY KEY, u INT, v INT, INDEX(u))SELECT k, u, v FROM t WHERE u >= 1 AND u <= 5

逻辑模型就是:

TABLE-READER indexsrcTable: t@u, span /1-/6Output schema: k:INT, u:INTOutput ordering: uJOIN-READER pksrcTable: tInput schema: k:INT, u:INTOutput schema: k:INT, u:INT, v:INTOrdering characterization: preserves any ordering on k/uAGGREGATOR finalInput schema: k:INT, u:INT, v:INTindexsrc -> pksrc -> final

用作 Join 的时候,例子:

TABLE t1 (k INT PRIMARY KEY, v INT, INDEX(v))TABLE t2 (k INT PRIMARY KEY, w INT)SELECT t1.k, t1.v, t2.w FROM t1 INNER JOIN t2 ON t1.k = t2.k WHERE t1.v >= 1 AND t1.v <= 5

逻辑模型就是:

TABLE-READER t1srcTable: t1@v, span /1-/6Output schema: k:INT, v:INTOutput ordering: vJOIN-READER t2srcTable: t2Input schema: k:INT, v:INTOutput schema: k:INT, v:INT, w:INTOrdering characterization: preserves any ordering on kAGGREGATOR finalInput schema: k:INT, u:INT, v:INTt1src -> t2src -> final

Stream joins

这种 aggregator,会基于两个逻辑上的输入流进行 Join,需要有特定列的等值的条件。这种 aggregator 会 group on 那些相等的列,例如:

TABLE People (First STRING, Last STRING, Age INT)TABLE Applications (College STRING PRIMARY KEY, First STRING, Last STRING)SELECT College, Last, First, Age FROM People INNER JOIN Applications ON First, LastTABLE-READER src1Table: PeopleOutput Schema: First:STRING, Last:STRING, Age:INTOutput Ordering: noneTABLE_READER src2Table: ApplicationsOutput Schema: College:STRING, First:STRING, Last:STRINGOutput Ordering: noneJOIN AGGREGATOR joinInput schemas:  1: First:STRING, Last:STRING, Age:INT  2: College:STRING, First:STRING, Last:STRINGOutput schema: First:STRING, Last:STRING, Age:INT, College:STRINGGroup key: (1.First, 1.Last) = (2.First, 2.Last)  // we need to get the group key from either streamOrder characterization: no order preserved  // could also preserve the order of one of the streamsAGGREGATOR final  Ordering requirement: none  Input schema: First:STRING, Last:STRING, Age:INT, College:STRING

join logical

stream join 的物理执行计划的核心部分,就是 Join processorJoin processor 会将一个 stream 中的所有行放置到一个hash map 中,然后再获取另一个 stream 中的数据。如果两个 stream 都是在 group key 上有序的话,那么就可以执行 merge join 从而减少内存开销。

应用同样的 join processor 的实现方式,我们可以有不同的分布式策略来具体执行,举个例子:

  • 第一种方式就是,路由方式可以是基于 hash 的方式,根据 group key 的 hash 值可以将 input streams 的 rows 进行分割,进入不同的实例中进行计算。如下所示:
    distribute join physical

  • 第二种方式就是,路由方式可以是基于镜像(mirror)的方式, 将一个较小的表中的所有的数据分别向大表所在的位置迁移。然后大表的数据在自己的 range 所在的 instance 中分别与这个小表进行计算。小表,大表是指在 stream 中的数据量,并不是指表的原始大小。 这种情况对于某些自查询比较有用处,比如: SELECT ... WHERE ... AND x IN (SELECT ...)
    依旧是上面的逻辑计划,如果 src2 的输出流包含的行数比较少,那么可以得出下面的物理执行计划:
    distribute join physical2

Inter-stream ordering


本功能是关于一个特定的优化的实现,但是不会修改逻辑模型和物理计划方面的东西。在最初的实现中,这个功能应该不会被实现,但是我们需要记得有这么个事情。

考虑一个情况如下:

TABLE t (k INT PRIMARY KEY, v INT)SELECT k, v FROM t WHERE k + v > 10 ORDER BY k

对应的简单的逻辑模型如下:

READER src  Table: t  Output filter: (k + v > 10)  Output schema: k:INT, v:INT  Ordering guarantee: kAGGREGATOR final:  Input schema: k:INT, v:INT  Input ordering requirement: k  Group Key: []Composition: src -> final

假设,这个表需要两个range,这两个rnage在不同的node上,其中一个rangekeyk <= 10,另一个rangek > 10。在物理计划中,我们就会有两个 streams 从两个 reader 上读取数据。这两份数据就会在 final 之前被merge 到一个 stream 中。这两个 stream 中的数据是有序的,我们可以叫这种情况为 inter-stream ordering这种情况下 merge 的时候是不需要保持数据的有序的,可以直接从第一个读取以后,再次读取第二个 stream 内的信息就可以。更进一步来说,对于第二个 stream 的读取操作不需要开始,直到第一个 stream 的所有 row 都被获取完成。这种情况对于 order bylimit 的情况是很有用处的。很有可能我们只需要读取一个 range 的信息就足够返回数据,从而简化了执行的流程。

更复杂的例子:Daily Promotion

来描绘一个更复杂的逻辑模型和物理计划的例子。语句的目的就是帮助做一个促销,目标的客户是过去一年花费在$1000以上的客户。然后将客户信息和最近的总订单数量存储在 DailyPromotion 中。

TABLE DailyPromotion (  Email TEXT,  Name TEXT,  OrderCount INT)TABLE Customers (  CustomerID INT PRIMARY KEY,  Email TEXT,  Name TEXT)TABLE Orders (  CustomerID INT,  Date DATETIME,  Value INT,  PRIMARY KEY (CustomerID, Date),  INDEX date (Date))INSERT INTO DailyPromotion(SELECT c.Email, c.Name, os.OrderCount FROM      Customers AS c    INNER JOIN      (SELECT CustomerID, COUNT(*) as OrderCount FROM Orders        WHERE Date >= '2015-01-01'        GROUP BY CustomerID HAVING SUM(Value) >= 1000) AS os    ON c.CustomerID = os.CustomerID)

逻辑上的模型如下:

TABLE-READER orders-by-date  Table: Orders@OrderByDate /2015-01-01 -  Input schema: Date: Datetime, OrderID: INT  Output schema: Cid:INT, Value:DECIMAL  Output filter: None (the filter has been turned into a scan range)  Intra-stream ordering characterization: Date  Inter-stream ordering characterization: DateJOIN-READER orders  Table: Orders  Input schema: Oid:INT, Date:DATETIME  Output filter: None  Output schema: Cid:INT, Date:DATETIME, Value:INT  // TODO: The ordering characterizations aren't necessary in this example  // and we might get better performance if we remove it and let the aggregator  // emit results out of order. Update after the  section on backpropagation of  // ordering requirements.  Intra-stream ordering characterization: same as input  Inter-stream ordering characterization: OidAGGREGATOR count-and-sum  Input schema: CustomerID:INT, Value:INT  Aggregation: SUM(Value) as sumval:INT               COUNT(*) as OrderCount:INT  Group key: CustomerID  Output schema: CustomerID:INT, OrderCount:INT  Output filter: sumval >= 1000  Intra-stream ordering characterization: None  Inter-stream ordering characterization: NoneJOIN-READER customers  Table: Customers  Input schema: CustomerID:INT, OrderCount: INT  Output schema: e-mail: TEXT, Name: TEXT, OrderCount: INT  Output filter: None  // TODO: The ordering characterizations aren't necessary in this example  // and we might get better performance if we remove it and let the aggregator  // emit results out of order. Update after the section on backpropagation of  // ordering requirements.  Intra-stream ordering characterization: same as input  Inter-stream ordering characterization: same as inputINSERT inserter  Table: DailyPromotion  Input schema: email: TEXT, name: TEXT, OrderCount: INT  Table schema: email: TEXT, name: TEXT, OrderCount: INTINTENT-COLLECTOR intent-collector  Group key: []  Input schema: k: TEXT, v: TEXTAGGREGATOR final:  Input schema: rows-inserted:INT  Aggregation: SUM(rows-inserted) as rows-inserted:INT  Group Key: []Composition:order-by-date -> orders -> count-and-sum -> customers -> inserter -> intent-collector                                                                  \-> final (sum)

一个可能的物理模型如下:
daily promotion

原创粉丝点击