如果还没有看过 CockroachDB 逻辑执行计划, 请先看那篇文章。
本文翻译自:
CockroachDB 物理执行计划的分析
概述
本篇文章主要是讲一个分布式SQL语句的执行过程。总的目标就是处理或者移动的计算要靠近数据源。
概念
- KV - 逻辑存储层的操作,对应
range
和batch
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 .. WHERE
和 DELETE .. 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
还是有些区别,下面我们详细的解释一下:
- 物理执行计划中包含许多提前定义好的
aggregator
s,这些aggregator
s 是为执行SQL的处理部分。多数的aggregator
s 是可以配置的
,但是不可编程
。可以配置
的意思是,能够给定义好的参数赋值。不可编程
的意思是不可以给一些运算,比如a + b
。 - 一个特别的
aggregator
是 evaluator,他可以用简单的编程语句。但是这个aggregator
只能一行一行处理。 - 通过
routing
,将信息在aggregator
和aggregator
之间进行传递。 - 逻辑执行计划是不考虑数据存放位置的,但是它存有足够的信息来说明数据存放位置,进而可以分布式计算执行。
除了累加和分组数据等操作,aggregator
的功能还可以是将数据传递给其它 node
,也可能是作为其他程序的输入。最终,有批量处理结果的特殊功能并且提供 KV
命令的aggregator
s ,是用来读取数据或者写入数据的。
核心思想就是我们转化一个 SQL
到 逻辑模型
,再从逻辑模型
转换到 分布式物理执行计划
。
逻辑模型
我们会将 SQL
语句解析成一个 逻辑模型
(这个与之前的逻辑执行计划
有些相似)。这个逻辑模型代表着抽象数据流的不同的计算阶段。这个逻辑模型并不知道数据最终的存放方式,但是它存有足够查询到的信息。在后续阶段,我们会将这个逻辑模型
转换为物理执行计划
。物理执行计划会将抽象计算对应成各个 processor
s,同时会定义好 processor
之间的混合的数据流和他们之间的交互通道。
逻辑模型由各种 aggregators
组成,每一个 aggregator
包含一个 input stream
和一个 output stream
。一个 stream
就代表一个 rows
的流。一个 row
代表一个 列元组
的值。每一个 input stream
和 output stream
都包含一个 schema
。这个schema
就是 列元组
中列的属性。再次强调,这个 streams (input/output)
是一个逻辑上的概念,并不一定会真实出现在一个真正的物理计算中。(本段实际上就是一个输入的结果集
,经过处理后,变成一个输出的结果集。整体叫做 aggregator
, 输入输出流的元数据信息就叫 schema
)
在一个 aggregator
内引入一个概念叫 grouping
,grouping
的作用是概括一个特定的计算。实际上就是给 aggregator
内部能够并发的操作起了一个名字。一个 aggregator
内部的所有 grouping
就被叫做 groups
。groups
是基于一个 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: Oid
AGGREGATOR 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 Cid
EVALUATOR 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 later
AGGREGATOR 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 Age
AGGREGATOR final:
Input schema: Age:INT, NetWorthSum:DECIMAL
Input ordering requirement: Age
Group Key: []
Composition: src -> summer -> final
这个 summer aggregator
可以有两种方式处理,如果输入的时候已经按照 Age
进行排序,那么输出的时候也就是排序完成的结果了。如果输入的时候没有按照 Age
进行排序,那么输出后就是无须的状态。
src
就是有序的,按照Age
进行排序,那么就不需要添加sort aggregator
。src
是没有排序的,那么就需要添加一个sort aggregator
对这个结果集进行排序。但是,这里可以有两种选择形式,如下:
在final之前, 进行添加排序能够完成需求。
src -> summer -> sort(Age) -> final
但是我们发现,在 summer 之前添加这个排序同样可以达到效果。
src -> sort(Age) -> summer -> final
这两种情况,就是需要我们去选择,在那种情况排序会性能更好。
上面我们可以看到,如果前一个 aggregator 有顺序保证,那么后面的aggregator 则尽可能的会保证这个顺序。这样能够使得尽量减少排序的发生。但是,保持排序状态很可能会产生额外的开销,所以我们需要在生成整个原始逻辑模型
以后,查看是否真正的需要保持有序,如果不需要,则将顺序删除掉,避免出现过多的性能损失。总结上面所说的,共需要三个步骤。
- 原始逻辑模型尽量保持已经存在的顺序。
- 查看整体的逻辑模型,将需要添加sort aggregator的地方添加。
- 如果哪里不需要保持顺序,则删除这个顺序保证。
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: Name
AGGREGATOR 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 Age
AGGREGATOR 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 key
的hash
的计算, 将数据分布到这些实例中进行计算。空的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: Oid
AGGREGATOR 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 Cid
EVALUATOR 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
}
根据上面说到的,逻辑模型可以被转化为物理模型如下:
每一个方块可以叫做一个 processor
,解释如下:
src
从表中读取数据,通过KV
Get
操作。同时,filter
会将Date > 2015
的语句筛选出来,最终传递下一个aggregator
。summer-stage1
是将本地的数据先进行一次aggregate
的操作,然后将数据传递给后面的下一步骤。通过对group key
的hash
之后,在进行后面的继续aggregate
。summer-stage2
是将初次已经处理的数据在进行分组处理,最终处理完成的数据需要传递给后面的appregator
。sortval
计算操作,计算1 - ValueSum
,作为排序的依据。输出添加SortVal
。sort
根据SortVal
对输入的流进行排序。final
合并两个流,依据顺序输出。
summer-stage2
不一定需要与 summer-stage1
在同样的机器上执行,所以这个物理执行计划也有可能是如下形式的:
上面的每一个方块都叫一个 processor。
Processors
Processor
中包含下面三个组成部分:
1.The input synchronizer 合并输入的流到一个单独的流中,分为三种情况
- 单输入流,直接输出无需处理。
- 无序多输入流,合并成一个就可以,无需考虑顺序。
- 有序多输入流,要保证合成输出后一样是有序的流。
2.Data processor 实现数据的转换和合并流程
3.输出流分发到许多其它的 processor
作为输入流,由单个流变换成多个流。有如下几种情况:
- 单输出流,直接转发即可。
- 镜像模式,赋值所有值到所有输出流中。
hash
模式,每一列都只进入一个输出流,根据group key
的hash
值 确定到哪个输出值。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 indexsrc
Table: t@u, span /1-/6
Output schema: k:INT, u:INT
Output ordering: u
JOIN-READER pksrc
Table: t
Input schema: k:INT, u:INT
Output schema: k:INT, u:INT, v:INT
Ordering characterization: preserves any ordering on k/u
AGGREGATOR final
Input schema: k:INT, u:INT, v:INT
indexsrc -> 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 t1src
Table: t1@v, span /1-/6
Output schema: k:INT, v:INT
Output ordering: v
JOIN-READER t2src
Table: t2
Input schema: k:INT, v:INT
Output schema: k:INT, v:INT, w:INT
Ordering characterization: preserves any ordering on k
AGGREGATOR final
Input schema: k:INT, u:INT, v:INT
t1src -> 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, Last
TABLE-READER src1
Table: People
Output Schema: First:STRING, Last:STRING, Age:INT
Output Ordering: none
TABLE_READER src2
Table: Applications
Output Schema: College:STRING, First:STRING, Last:STRING
Output Ordering: none
JOIN AGGREGATOR join
Input schemas:
1: First:STRING, Last:STRING, Age:INT
2: College:STRING, First:STRING, Last:STRING
Output schema: First:STRING, Last:STRING, Age:INT, College:STRING
Group key: (1.First, 1.Last) = (2.First, 2.Last) // we need to get the group key from either stream
Order characterization: no order preserved // could also preserve the order of one of the streams
AGGREGATOR final
Ordering requirement: none
Input schema: First:STRING, Last:STRING, Age:INT, College:STRING
在 stream join
的物理执行计划的核心部分,就是 Join processor
。Join processor
会将一个 stream
中的所有行放置到一个hash map
中,然后再获取另一个 stream
中的数据。如果两个 stream
都是在 group key
上有序的话,那么就可以执行 merge join
从而减少内存开销。
应用同样的 join processor 的实现方式,我们可以有不同的分布式策略来具体执行,举个例子:
第一种方式就是,路由方式可以是基于 hash 的方式,根据 group key 的 hash 值可以将 input streams 的 rows 进行分割,进入不同的实例中进行计算。如下所示:
第二种方式就是,路由方式可以是基于镜像(mirror)的方式, 将一个较小的表中的所有的数据分别向大表所在的位置迁移。然后大表的数据在自己的 range 所在的 instance 中分别与这个小表进行计算。小表,大表是指在
stream
中的数据量,并不是指表的原始大小。 这种情况对于某些自查询比较有用处,比如:SELECT ... WHERE ... AND x IN (SELECT ...)
。
依旧是上面的逻辑计划,如果 src2 的输出流包含的行数比较少,那么可以得出下面的物理执行计划:
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: k
AGGREGATOR final:
Input schema: k:INT, v:INT
Input ordering requirement: k
Group Key: []
Composition: src -> final
假设,这个表需要两个range
,这两个rnage
在不同的node
上,其中一个range
的key
是 k <= 10
,另一个range
是 k > 10
。在物理计划中,我们就会有两个 streams
从两个 reader
上读取数据。这两份数据就会在 final
之前被merge
到一个 stream
中。这两个 stream
中的数据是有序的,我们可以叫这种情况为 inter-stream ordering
。这种情况下 merge
的时候是不需要保持数据的有序的,可以直接从第一个读取以后,再次读取第二个 stream
内的信息就可以。更进一步来说,对于第二个 stream
的读取操作不需要开始,直到第一个 stream
的所有 row
都被获取完成。这种情况对于 order by
加 limit
的情况是很有用处的。很有可能我们只需要读取一个 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: Date
JOIN-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: Oid
AGGREGATOR 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: None
JOIN-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 input
INSERT inserter
Table: DailyPromotion
Input schema: email: TEXT, name: TEXT, OrderCount: INT
Table schema: email: TEXT, name: TEXT, OrderCount: INT
INTENT-COLLECTOR intent-collector
Group key: []
Input schema: k: TEXT, v: TEXT
AGGREGATOR 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)
一个可能的物理模型如下: