Apache Drill的plan详解
Apache Drill的plan详解
计划是什么?
本节是关于 Drill 的端到端计划流程。 Drill 的传入查询可以是 SQL 2003 查询/DrQL 或 MongoQL。 查询被转换为逻辑计划,它是查询的 Drill 内部表示(与语言无关)。 然后 Drill 使用其对逻辑计划的优化规则来优化它以获得最佳性能并制定物理计划。 物理计划是 Drill 随后为最终数据处理执行的实际计划。 下面是一个图表来说明流程:
逻辑计划描述了与语言无关的查询的抽象数据流,即它是输入查询的表示,不依赖于实际的输入查询语言。 它通常尝试使用原始操作而不关注优化。 这使得它比传统的查询语言更加冗长。 这是为了在定义更高级别的查询语言功能时提供相当大的灵活性。 它将被转发给优化器以获得物理计划。
物理计划通常称为执行计划,因为它是执行引擎的输入。 它描述了执行引擎为获得所需结果而将进行的物理操作。 它是查询计划器的输出,是应用优化规则后逻辑计划的转换。
通常,物理计划和执行计划将使用与逻辑计划相同的 JSON 格式表示。
详细解释
Drill 的目标之一是在不同的功能级别上定义清晰的接口,以允许未来的扩展。 为此,需要对 Drill 组件和通用词汇进行总体概述,以了解关键的集成点。 drill 的基本执行流程是创建用户或机器生成的查询并将其交给查询解析器。 从那里,查询被转换成一个逻辑计划。 逻辑计划描述了查询操作的基本数据流。 然后优化器负责读取这个逻辑计划并将其转换为物理(或执行)计划。 从那里,执行引擎负责执行计划。 执行引擎依赖于许多组件,包括各种类型的运算符、扫描器(读取器)和写入器。 元数据存储库负责向 Drill 查询执行框架的各个组件提供信息。 第一版 Drill 的关键可插拔组件是:
- 查询解析器:可以从元数据 API 请求模式信息,并为优化器消费生成标准的逻辑计划。
- 存储引擎:负责与特定数据源进行交互。 提供扫描仪和/或写入器功能。 将数据的任何已知特征(架构、文件大小、数据排序、二级索引、块数等)告知元数据存储库。 通知执行引擎任何本机功能(谓词下推、连接、SQL 等)。
- 运算符:负责转换数据流。
- 功能:
- 聚合函数:将值流转换为单个值。
- 标量函数:将一个或多个标量值转换为单个值。
数据结构
Drill 的核心是对树序列进行操作。 这与传统的基于行的系统形成对比。 在 Drill 中,数据的主要单位是数据树。 数据树由标记值组成。 每个节点的值可以是子树、标量或值列表。 在本文档中,“文档”和“行”通常与“树”一词互换使用。 无论是否无模式,抽象地您都可以将每个数据树视为一个 JSON 对象。 每棵树由一个键和一个根节点组成。
{
"key": "123",
"value": {
"foo": {
"x": "y"
},
"blue": [
"red",
"blue"
]
}
}
在 Drill 使用更传统的关系数据源的情况下,数据单元可以抽象地被认为是平面树。
{
"key": "123",
"value": {
"foo": "bar",
"blue": "orange"
}
}
一般注意事项
逻辑计划和物理计划的抽象结构都是有向无环图 (DAG) 的 JSON 表示。
数据类型
计划描述的关键是一组运算符及其定义,特别是包括它们可以作为输入和输出接受的事物类型。 查询优化器和执行引擎需要了解的一些类型可以从计划本身获知,但大多数类型信息将是动态的,除非在处理的实际数据的上下文中,否则很难知道。 目前尚不清楚我们将如何确定类型以及我们需要考虑哪些类型。
尽管类型不明确,我们或许可以锁定 Drill 所需的大部分运算符集。
逻辑计划
目的
逻辑计划描述了与语言无关的查询的抽象数据流。 它通常尝试使用原始操作而不关注优化。 这使得它比传统的查询语言更加冗长。 这是为了在定义更高级别的查询语言功能时提供相当大的灵活性。 通常,逻辑计划将由查询解析器生成。 当模式可用时,查询解析器也将能够利用此信息进一步验证查询并构建逻辑计划。
逻辑计划是数据流操作符的 DAG。 DAG 的边代表数据流。 (请注意,与传统的编程 SSA 不同,我们的 DAG 的 SSA 表示允许将高阶标量函数表达式作为参数提供,而不是直接将表达式作为 SSA 的一部分包含在内。这是为了简化人类理解并减少冗长。)因为逻辑计划是数据流的抽象表示,每个操作员只能有一个输出。 (这与许多操作员将有多个输出的物理计划形成对比。)另一方面,特定的操作员可以订阅多个其他操作员的输出。
因为支持分层数据,Drill 提供了一些explosion和implosion操作。 这些允许使用子 DAG 管理嵌套值的操作。 因为所有操作员都是独立的,所以任何数据的重新内爆都必须以流的形式提供。
逻辑计划词汇
参数
逻辑计划描述了逻辑运算符的集合和连接。 每个运算符都有许多参数。 这些参数可能具有的值在本节中使用以下值类的缩写进行描述:
- <name> 用作记录流中特定输出字段的定义或引用的名称。
- <string> 带引号的字符串。
- <expr> 可能包含一个或多个值函数、值和字段引用的值表达式。 请注意,值表达式可以返回标量值或复数值(映射或数组)。
- <aggexpr> 包含一个或多个标量表达式以及一个或多个聚合表达式(SUM、COUNT 等)的表达式。 请注意,聚合表达式绝不能将一个聚合嵌套在另一个聚合中,但可以在两个单独的聚合之上应用一个标量表达式。
- <runaggexpr> 包含支持运行操作的聚合的表达式。
- <opref> 定义或引用特定运算符的整数。
- <json> 不透明的 JSON 选项字符串。
- <operator> 运算符或 <opref> 的定义
在特定值可选的情况下,该值标有星号。 在序列运算符的 do 参数的上下文中,一些值是可选的。 在该上下文中可选的值将标有 †。
主要概念
- 值:Apache Drill 中的值可以是标量值(int32、int64、uint32、float32 等)、值数组或值映射。
- 记录:记录是零个或多个值的集合,在关系数据库中通常称为行。
- 字段:字段在记录中的特定位置。
- 流:流是从其生产操作员到达一个操作员的一组记录。
- 段:逻辑计划中讨论的一个重要概念是段的概念。 段定义了出于某种目的而收集在一起的输入记录的子集合。 在许多情况下,可以为操作员提供段密钥,然后根据上下文分别管理其在每个段上的操作,并避免在段边界之间携带状态。
逻辑计划Operators
运算符属于许多重叠的类别。 每个运算符标识其类。 当前的运算符类是:
- 0:不依赖任何输入运算符(来源)生成数据的运算符。
- 1:消耗单个输入源的运算符
- M:消耗多个输入数据源的运算符
- K:不产生输出流(接收器)的运算符。
属性
- †:此属性在序列外是必需的,在序列内是不允许的。
- *:此属性是可选的。
Scan (0)
Scan 运算符输出记录流。 “storageengine”参数必须按名称引用逻辑计划的 engines 子句中定义的存储引擎。 “选择”参数接受一个 JSON 对象,数据源本身使用该对象来限制实际检索的数据量。 该对象的格式和内容特定于所使用的实际输入源。 示例可能包括 HBase 表名、MongoDB 集合、HDFS 路径或 Hive 表的分区。 数据源将以特定于实现的方式使用选择参数。 提供的“ref”参数确保扫描源中的所有记录都保存在提供的命名空间中。
{ @id†: <opref>, op: “scan”,
storageengine: <string>,
selection*: <json>,
ref: <name>
}
Constant (0)
常量运算符返回常量结果。 这对于引入值是必要的,1) 在您不知道存在哪些表的情况下,或 2) 评估不属于表的表达式时。 该运算符类似于 SQL 中的 VALUES 运算符。
{ @id†: <opref>, op: “constant”,
content: <json>
}
例如,使用常量运算符可以实现 SQL VALUES 子句,如下例所示:
SQL: VALUES (1, 'iamastr') AS t(c1, c2)
DLP: { @id: 1, op: “constant”, content: { [ { c1: 1, c2: “iamastr” } ] } }**
此处描述的实现已使用隐式类型完成。 更多信息可以在第 57 期的 JIRA 页面上找到。 在未来的某个时候,我们将以某种形式添加对显式类型的支持。 我们尚未决定指定类型的具体格式,但已讨论了此处详细介绍的许多选项:https://docs.google.com/document/d/1nn8sxcuBvpAHm-BoreCWELPIQ8QhhsAUSENX0s5sSys/edit
Join (M)
Join 运算符根据一个或多个连接条件连接两个输入。 该运算符的输出是两个输入的组合。 这是通过为匹配所有提供的连接条件的每组输入记录提供组合记录来完成的。 在没有提供条件的情况下,将生成笛卡尔连接。 组合记录是单个记录,其中包含来自两个提供的输入记录的合并值映射。 例如,如果左侧记录为 {donuts: [data]},右侧记录为 {purchases: [data]},则组合记录将为 {donuts: [data], purchases: [data]}。 Join 还需要一个条件变量“type”,它描述了当记录与连接条件不匹配时会发生什么。 “Inner”意味着只应包括符合连接条件的记录。 “Outer”意味着包含来自任一输入的不匹配记录。 “Left”表示仅包含来自左侧输入的不匹配记录。 当包含不匹配的记录时,生成的记录仅包含来自记录端的已知值,不包含来自不匹配端的值。
可用的关系类型“reltype”包括:>、>=、<=、<、!=、==
{ @id†: <opref>, op: “join”,
left: <input>,
right: <input>,
type: <inner|outer|left>,
conditions*: [
{relationship: <reltype>, left: <expr>, right: <expr>}, ...
]
}
Union (M)
Union 运算符将两个或多个数据输入组合成一个流。 没有任何一种排序是隐含的或必须维护的。 如果“distinct”为真,则从流中删除重复的行。 重复行被定义为包含完全相同字段且每个字段具有完全相同值的两组记录。
{ @id†: <opref>, op: “union”,
distinct: <true|false>,
inputs: [
<input>, ... <input>
]
}
Store (1 K)
Store 运算符将流输出存储到存储引擎。 storageengine 参数是指在逻辑计划中定义的存储引擎。 “target”参数描述存储引擎特定的参数(例如,存储类型、文件名等)。 每个单独的存储引擎的功能都可以包括可能的并行化级别,因此这不会在逻辑计划级别表示。
{ @id†: <opref>, op: “store”,
input†: <input>,
storageengine: <string>,
target: <json>
}
Project (1)
Project 运算符返回与所提供的表达式相对应的传入数据流的特定字段子集。 “projections”参数指定要保留在输出记录中的值。 对于每个投影,输出记录中值的名称由“ref”参数指定,要在输入记录上计算以确定输出值的表达式由“expr”参数指定。 为了清晰并支持提升一组给定的输出,投影参考名称必须以“output”开头。 一个例子可能是 {ref: “output” expr: “sum(donuts.sales)}”。 这允许输出具有简单类型的值。 如果多个投影重叠,它们将与较晚的投影合并,覆盖较早投影的值。
{ @id†: <opref>, op: “project”,
input†: <input>,
projections: [
{ref: <name>, expr: <expr>}, ...
]
}
Order (1)
Order 运算符通过一个或多个排序表达式对输入流进行排序。 通过提供可选的“within”参数,可以将排序限制在特定的段内。 段由其他运算符定义,例如组运算符。 在提供段的情况下,只能在每个段的边界内进行排序。
“nullCollation”选项定义了空值在排序中的位置。 未提供时,默认情况下空值排在第一个。
{ @id†: <opref>, op: “order”,
input†: <input>,
within*: <name>,
orderings: [
{order: <desc|asc>, expr: <expr>, nullCollation: <first|last> }, ...
]
}
Filter (1)
Filter 运算符根据提供的表达式从流中删除某些值。 对于提供的表达式计算结果为真的每个输入记录,该记录将传递到输出。 如果表达式的计算结果为 false 或任何其他值,则记录不会传递到输出。
{ @id†: <opref>, op: “filter”,
input†: <input>,
expr: <expr>
}
Transform (1)
Transform 运算符转换数据以允许数据引用和明确数据流。 虽然匿名表达式可以在各种运算符中使用,但除非运算符专门输出表达式的值,否则它不会在该运算符的输出流中可用。 例如,当使用 Segment 运算符时,如果提供的分组表达式不是简单的字段引用,它们将被评估以用于分段目的,然后被丢弃。 如果查询需要维护这些值,则需要在分段操作之前或之后通过转换子句生成它们。 重新标记(AS 子句)可以写成身份转换,其中“expr”是现有字段引用,“ref”是新字段引用。
{ @id†: <opref>, op: “transform”,
input†: <input>,
transforms: [
{ref: <name>, expr: <expr>}, ...
]
}
Limit (1)
Limit 运算符将运算符的输出限制为仅包含输入的前缀。 输入记录从 0 开始隐式编号。“first”和“last”参数指定要包含在输出中的记录的半开范围。 Last = 0 不返回任何对象。 First = 0, Last = 1 返回第一个对象。 limit对整个数据集进行操作。
{ @id†: <opref>, op: “limit”,
input†: <input>,
first: <number>,
last: <number>
}
Segment (1)
Segment 运算符负责收集共享所提供表达式的相同值的所有记录,并将它们作为单个数据段(或组)一起输出。 对于所有提供的表达式,输出段的每条记录都将具有相同的值。 段运算符还将在提供的参考中输出段键。
段算子稳定。 这意味着段内的所有记录将按照它们在输入中出现的顺序出现。 但是,无法保证内部段的输出顺序。
Window Frame (1)
对于传入流的每个记录(让我们称之为目标记录),窗口操作符将在目标记录周围的滑动窗口中创建一个包含记录的段。 滑动窗口将包括目标记录之前和/或之后指定数量的记录。 Window Frame 运算符可以对整个输入流进行操作,或者可以被告知只允许在每个传入段内进行操作。
一个简单的例子:一个 -2 start 和 0 end 的窗口范围应用于一个 5 记录的单段输入 [0,1,2,3,4]。 这将导致 12 行的输出(括号表示每个输出段):[0] [0,1] [0,1,2] [1,2,3] [2,3,4]。 每个输出记录将包含两个附加字段:ref.segment 和 ref.position,前者保存当前窗口段,后者表示窗口内的负、正或零位置。 对于示例数据,其中每个的值将是:ref.segment: [0,1,1,2,2,2,3,3,3,4,4,4], ref.position: [0,-1,0,-2,-1,0,-2,-1,0,-2,-1,0]
window operator也可以选择将分段键作为输入。 该键用于限制窗口,使它们不跨越段边界。 窗口运算符将“开始”和“结束”值作为输入。 这些值定义窗口的范围。 窗口范围的定义是基于将目标记录视为零,并且正递增后的每条记录和负递增前的每条记录。 在未定义开始或结束的情况下,该范围的那部分将是无界的。 在所有情况下,start 必须大于或等于 end。 必须至少定义开始或结束之一。
{ @id†: <opref>, op: “windowframe”,
input†: <input>,
within*: <name>,
start*: <number>,
end*: <number>
ref: {
segment: <name>,
position: <name>
},
}
CollapsingAggregate (1)
Collapse Aggregate 运算符将一段记录折叠成一条记录。 在段(内)未定义的情况下,折叠聚合会将所有输入记录折叠为单个输出记录。
折叠聚合运算符的唯一输出是提供的聚合和定义为“结转”的值。 还可以为折叠聚合操作员提供一个目标字段参考,通过它来选择用于结转值的记录。
在未定义目标引用的情况下,折叠聚合操作将自由选择从哪个记录中提取结转值(通常,这是因为所有记录共享相同的值)。 在提供目标字段引用的情况下,将从目标字段引用具有真值的记录中提取结转变量。 如果多个记录的目标字段值为真,则将从其中一个记录中提取结转值。 如果目标段中没有记录的目标值为真,则不会从该目标段发出任何记录。 在任何情况下,每个段都不会发出一个以上的记录。
{ @id†: <opref>, op: “collapsingaggregate”,
input†: <input>,
within*: <name>,
target*: <name>,
carryovers: [<name>, … , <name>],
aggregations: [
{ref: <name>, expr: <aggexpr> },...
]
}
有关适用于 aggexpr 的聚合表达式的讨论,请参阅本文档第一部分的参数部分。
RunningAggregate (1)
Running Aaggregate 运算符获取输入记录并添加附加一组运行聚合并输出结果记录。 将按照提供的顺序对传入段内的每个记录重新评估聚合。 可以使用“within”值定义段焦点,以便在每个段边界处重置聚合。
{ @id†: <opref>, op: “runningaggregate”,
input†: <input>,
within*: <name>,
aggregations: [
{ref: <name>, expr: <aggexpr> },...
]
}
Flatten (1)
Flatten 运算符根据每个输入记录生成一个或多个输出记录。 输出记录将是输入记录以及用于展平的附加附加字段。 具体行为取决于提供的展平表达式的类型。 在所有情况下,“drop”参数定义目标表达式中引用的所有字段是保留在输出记录中还是删除。
- 标量/映射:在提供的表达式返回标量或映射的情况下,Flatten 将为每个输入记录返回一条记录,并在“ref”位置有一个附加字段,该字段定义为与输入表达式相同的值。
- 数组:如果提供的表达式返回一个数组,flatten 将返回原始输入记录的多个副本,其中每个副本都在数组中“ref”位置的值进行了扩充。
{ @id†: <opref>, op: “flatten”,
input†: <input>,
ref: <name>,
expr: <expr>,
drop: <boolean>
}
Sequence (1)
Sequence 运算符是一种语法运算符,用于简化简单流的定义。 由于绝大多数运算符都是单输入运算符,序列允许定义一个非分支流,它可以简单地转换元素(可能删除或添加它们),而不必专门定义输入和定义引用。 序列中第一个元素之后的所有元素都必须是单输入运算符。 第一个元素必须是源运算符或单输入运算符。 序列的最后一个元素可以是接收器。 当且仅当序列的最后一个元素是汇点时,序列才是汇点。
{ @id: <opref>, op: “sequence”,
input: <input>, do: [
<operator>, <operator>, ... <operator>
]
}
物理计划
目的
物理计划(通常称为执行计划)是对执行引擎为获得预期结果将进行的物理操作的描述。 它是查询计划器的输出,是逻辑计划的转换。 通常,物理计划和执行计划将使用与逻辑计划相同的 JSON 格式表示。
物理计划Operator
Storage Operators
存储操作符有:scan-json、scan-hbase、scan-trevni、scan-cassandra、scan-rcfile、scan-pb、scan-pbcol、scan-mongo、scan-text、scan-sequencefile、scan-odbc和 scan-jdbc(以及每个对应的 store-*)。
普通Operators
常规运算符是:limit, sort, hash-join, merge-join, streaming-aggregate, partial-aggregate, hash-aggregate, partition, and union.
附录 A:过滤嵌套值
因为 Drill 操作的是数据树而不是传统的行,所以过滤的使用不同于传统的数据库系统。 在过滤期间,如果对象节点的一个或多个子节点满足所应用的过滤器,则对象节点将被保留。 这样保留的对象节点将完整保留,包括所有子节点。 保留对象节点中的那些孩子以相同的方式递归过滤。
另一方面,如果至少有一个子节点满足过滤器,则保留数组节点,并且仅保留满足过滤器的那些子节点。 那些幸存的子节点以同样的方式递归过滤。
例如,假设这个 JSON 结构序列被表达式 a.x > 3 过滤
[{a:{b:1, x:5}, c:1},
{a:[{b:1}, {z:{x:4}}, {q:3, x:5}], c:2},
{a:{b:2, x:[1,2,3,4,5]}, c:3},
{a:{b:3, x:[1,2]}, c:4}]
结果如下
[{a:{b:1, x:5}, c:1},
{a:[~~{b:1}~~, ~~{z:{x:4}}~~, {q:3, x:5}], c:2},
{a:{b:2, x:[~~1,2,3~~,4,5]}, c:3},
{~~a:{b:3, x:[1,2]}, c:4}~~]
第一个包含 c:1 的结构被保留,因为第一个标记为 a 的子树有一个标记为 x 的成员,其标量值为 5。a.x=5 子树被保留,这导致 a 子树被保留 ,这导致 c 子树也被保留。
第二个结构具有 a 的矢量值。 向量中的第一个值没有 x 字段,因此被丢弃。 向量中的第二个值有一个 x 值,但 x 值的级别太深而无法匹配。 第三个值在正确的级别具有 x 值,并且该 x 字段具有大于三的值,因此它被保留。 这导致值 a.q=3 值也被保留。 由于a至少有一个保留值,所以它和c=2字段都被保留。 {q:3, x:5}。
为了能够重建正确的结构,我们必须保留一些关于数组在数据结构中的位置的概念。 这在处理嵌套数组时最为明显。 例如,对于这种结构,我们希望使用 a.x > 3 进行过滤
{a:[[{x:1},{x:4}], [{x:5}]]}
识别并保留数组嵌套并生成这个
{a:[[{x:4}], [{x:5}]]}
这里的问题是符号 x.a 无法记录额外的数组嵌套级别。 本质上,我们需要修改 a.x 路径,因为它在系统中作为更像 a[0][0].x 或 a[1][0].x 的东西传送。 当数据被编组用于输出时,这些数组指示器可用于重建正确的嵌套。 在绑定运算符中,给变量的名称采用 a.x 形式,但这仅用于匹配实际数据元素。 每个数据元素都记住它在数据树中的详细位置,以允许重建树。 当然,数据元素“记住”它们在树中位置的一种方法是基于对模式的引用。 在这种情况下,没有必要明确保留名称。
嵌套的这些困难只出现在没有强架构的数据中。 这意味着 Drill 的另一种选择是要求嵌套符号由解析器通过引用数据模式插入或由用户显式插入。 执行引擎仍然必须插入数组索引。 允许用户在原始查询中插入数组嵌套指示符是相对于 Dremel 语法的不兼容更改,应该需要讨论。 如果数据是使用无模式格式(如 JSON)编码的,那么使用 protobufs 编码的数据运行的程序产生相同的结果是非常可取的。 要求用户提供嵌套提示将违反该预期。
原文链接:原文链接:https://docs.google.com/document/d/1QTL8warUYS2KjldQrGUse7zp8eA72VKtLOHwfXy6c7I/edit