共计 11883 个字符,预计需要花费 30 分钟才能阅读完成。
简介: 本文次要介绍什么是关联子查问以及如何将关联子查问改写为一般语义的 sql 查问。
本文次要介绍什么是关联子查问以及如何将关联子查问改写为一般语义的 sql 查问。
在背景介绍中咱们将讲讲常见的关联子查问的语义,关联子查问语法的益处以及其执行时对数据库系统的挑战。第二章中咱们将次要介绍如何将关联子查问改写为一般的查问的模式,也就是解关联。第三章中咱们会介绍解关联中的优化办法。
一 背景介绍
关联子查问是指和内部查问有关联的子查问,具体来说就是在这个子查问里应用了内部查问蕴含的列。
因为这种能够应用关联列的灵活性,将 sql 查问写成子查问的模式往往能够极大的简化 sql 以及使得 sql 查问的语义更加不便了解。上面咱们通过应用 tpch schema 来举几个例子以阐明这一点。tpch schema 是一个典型的订单零碎的 database,蕴含 customer 表,orders 表,lineitem 表等,如下图:
如果咱们心愿查问出“所有素来没有下过单的客户的信息”,那么咱们能够将关联子查问作为过滤条件。应用关联子查问写出的 sql 如下。能够看到这里的 not exists 子查问应用列内部的列 c_custkey。
-- 所有素来没有下过单的客户的信息
select c_custkey
from
customer where
not exists (
select
*
from
orders
where
o_custkey = c_custkey
)
如果不写成下面的模式,咱们则须要思考将 customer 和 orders 两个表先进行 left join,而后再过滤掉没有 join 上的行,同时咱们还须要 markorder 的每一行,使得原本就是 null 的那些。查问 sql 如下:
-- 所有素来没有下过单的客户的信息
select c_custkey
from
customer
left join (
select
distinct o_custkey
from
orders
) on o_custkey = c_custkey
where
o_custkey is null
从这个简略的例子中就能够看到应用关联子查问升高了 sql 编写的难度,同时进步了可读性。
除了在 exists/in 子查问中应用关联列,关联子查问还能够呈现在 where 中作为过滤条件须要的值。比方 tpch q17 中应用子查问求出一个聚合值作为过滤条件。
-- tpch q17
SELECT Sum(l1.extendedprice) / 7.0 AS avg_yearly
FROM lineitem l1,
part p
WHERE p.partkey = l1.partkey
AND p.brand = 'Brand#44'
AND p.container = 'WRAP PKG'
AND l1.quantity < (SELECT 0.2 * Avg(l2.quantity)
FROM lineitem l2
WHERE l2.partkey = p.partkey);
除了呈现在 where 外面,关联子查问能够呈现在任何容许呈现单行 (scalar) 的中央,比方 select 列表里。如果咱们须要做报表汇总一些 customer 的信息,心愿对每一个 customer 查问他们的订单总额,咱们能够应用上面蕴含关联子查问的 sql。
-- 客户以及对应的生产总额
select
c_custkey,
(select sum(o_totalprice)
from
orders
where o_custkey = c_custkey)from
customer
更简单一些的比方,咱们心愿查问每一个 customer 及其对应的在某个日期前曾经签收的订单总额。利用关联子查问只须要做一些小的扭转如下:
select
c_custkey,
(
select
sum(o_totalprice)
from
orders
where
o_custkey = c_custkey
and '2020-05-27' > (
select
max(l_receiptdate)
from
lineitem
where
l_orderkey = o_orderkey
))from
customer
看了这些例子,置信大家都曾经感触到应用关联子查问带来的便捷。然而同时关联子查问也带来了执行上的挑战。为了计算关联后果的值(子查问的输入),须要 iterative 的执行形式。
以之前探讨过的 tpch 17 为例子:
SELECT Sum(l1.extendedprice) / 7.0 AS avg_yearly
FROM lineitem l1,
part p
WHERE p.partkey = l1.partkey
AND p.brand = 'Brand#44'
AND p.container = 'WRAP PKG'
AND l1.quantity < (SELECT 0.2 * Avg(l2.quantity)
FROM lineitem l2
WHERE l2.partkey = p.partkey);
这里的子查问局部应用了内部查问的列 p.partkey。
SELECT 0.2 * Avg(l2.quantity)
FROM lineitem l2
WHERE l2.partkey = p.partkey -- p.partkey 是内部查问的列
优化器将这个查问示意为如下图的逻辑树:
如果数据库系统不反对查看逻辑树,能够通过 explain 命令查看物理打算,个别输入如下图:
+---------------+
| Plan Details |
+---------------+
1- Output[avg_yearly] avg_yearly := expr
2 -> Project[] expr := (`sum` / DOUBLE '7.0')
3 - Aggregate sum := `sum`(`extendedprice`)
4 -> Filter[p.`partkey` = l1.`partkey` AND `brand` = 'Brand#51' AND `container` = 'WRAP PACK' AND `quantity` < `result`]
5 - CorrelatedJoin[[p.`partkey`]]
6 - CrossJoin
7 - TableScan[tpch:lineitem l1]
8 - TableScan[tpch:part p]
9 - Scalar
10 -> Project[] result := (DOUBLE '0.2' * `avg`)
11 - Aggregate avg := `avg`(`quantity`)
12 -> Filter[(p.`partkey` = l2`partkey`)]
13 - TableScan[tpch:lineitem l2]
咱们将这个连贯内部查问和子查问的算子叫做 CorrelatedJoin(也被称之为 lateral join, dependent join 等等。它的左子树咱们称之为内部查问(input),右子树称之为子查问(subquery)。子查问中呈现的内部的列叫做关联列。在栗子中关联列为 p.partkey。
例子中对应的逻辑打算和相干定义如下图所示,explain 返回后果中第 6 - 8 行为内部查问,9-13 行为子查问,关联部位在子查问中第 12 行的 filter。
这个算子的输入等价于一种 iterative 的执行的后果。也就将左子树的每一行关联列的值带入到右子树中进行计算并返回一行后果。有些相似将子查问看成一个 user defined function(udf),内部查问的关联列的值作为这个 udf 的输出参数。须要留神的是,咱们须要子查问是确定的,也就是对同样值的关联列,每次运行子查问返回的后果应该是确定的。
在上图的栗子中,如果内部查问有一行的 p.partkey 的值为 25,那么这一行对应的 correlatedjoin 的输入就是上面这个查问的后果:
-- p.partkey = 25 时对应的子查问为
SELECT 0.2 * Avg(l2.quantity)
FROM lineitem l2
WHERE l2.partkey = 25
须要留神的是,如果计算结果为空集,则返回一行 null。而如果运行中子查问返回了超过一行的后果,应该报运行时谬误。在逻辑打算里,用 enforcesinglerow 这个 node 来束缚。
从下面的介绍中能够发现,CorrelatedJoin 这个算子突破了以往对逻辑树自上而下的执行模式。一般的逻辑树都是从叶子节点往根结点执行的,然而 CorreltedJoin 的右子树会被带入左子树的行的值重复的执行。
correlatedjoinnode 的输入就是在内部查问的后果上减少了一列,然而能够看到这种 iterative 的执行形式的复杂度和将内部查问和子查问关联产生之前的那局部树进行 cross join 的复杂度雷同。
同时,这样 iterative 的执行形式对分布式数据库系统来说是很大的挑战。因为须要批改执行时调度的逻辑。而且咱们能够看到,这样的执行形式如果不进行后果的缓存,会进行很多反复后果的计算。
传统的优化器的优化规定没有特地的针对 Correlatedjoin node 进行解决,为了反对关联子查问的这种 iterative 的模式,在优化器初始阶段就会把 Correlatedjoin 进行等价转换,转换过后的逻辑树用 join,aggregation 等一般算子来进行关联子查问后果的计算。这个过程被称为解关联(decorrelation/unnesting)。上面一章咱们次要介绍常见的解关联的形式。
二 常见的解关联形式
为了不便起见,咱们在这一章只探讨 scalar 关联子查问,就是会输入一列值的关联子查问。
在探讨如何解关联之前,咱们总结一下关联子查问的输入有以下特点:
- correlated join 算子的计算结果为在内部查问上减少一列。
- 减少的那一列的后果为将内部查问关联列的值带入子查问计算得出的。当计算结果超过一行则报错,计算结果为空则补充 null。
- 不同于 join 算子,correlated join 不扭转内部查问的其余列(不少行也不收缩)。
解开关联的关键在于使得子查问取得对应的内部查问的行的值。
体现在打算上,就是将 correleted join 算子向右下推到产生关联的部位的上面。当 correlated join 算子的左右子树没有关联列的时候,correlated join 算子就能够转换成 join 算子。这样子查问就通过和内部查问 join 的形式取得了关联列的值,从而能够自上而下计算,回到本来的计算形式。如下图,下图中 rest subquery 为在关联产生部位之前的子查问局部。当 correlated join 推到产生关联的部位之下,就能够转换为一般的 join 了。
correlated join 推过的那些算子都是须要进行改写,以放弃等价性(上图的栗子中 subquery 变为了 subquery’)。
1 下推规定
论文 Orthogonal Optimization of Subqueries and Aggregation[2]给出了将 correlatedjoin_算子下推到其余算子(filter,project,aggregation,union 等)上面的的等价转换规则。然而文中的 correlatedjoin_算子是会过滤内部查问的行数的,相似于 inner join(论文中称为)。咱们这里探讨更加 general 的相似于 left join 的 correlatedjoin (论文中称为),并探讨如果要保障内部查问行数不被过滤须要做哪些改写。
因为篇幅限度,上面咱们只介绍下推到 filter,project,aggregation 算子上面的等价规定。
为了简略起见,咱们在逻辑树中去掉了 enforcesinglerow。
转换 1 无关联时转换为 join
回顾前文所说,correlated join 算子的左子树为 input,右子树为 subquery。当 correlated join 的左右子树没有关联的时候,这个时候对外部查问的每一行,子查问的后果都是雷同的。
咱们就能够把 correlated join 转换成一般的没有 join criteria 的 leftjoin 算子。
注:须要在 subquery 上增加 enforcesinglerow 来保障 join 语义和 correlatedjoin 雷同(不会造成 input 的收缩)。
转换 2 简略关联条件时转换为 join
当 correlated join 右子树中最下面的节点为一个关联 filter 而他的上面无关联时,能够间接将这个 filter 放到 left join 的条件中,也能够了解为 filter 上提。如下图:
转换 3 下推穿过 filter
论文中 correlatedjoin* 能够间接推过 filter。如果须要下推的为 correlatedjoin,则须要对 filter 进行改写,改写成带有 case when 的 project。当 subquery 的行不满足 filter 的条件时应输入 null。
转换 4 下推穿过 project
correlated join 下推过 project,须要在 project 中增加 input 的输入列。
转换 5 下推穿过 aggregation
correlated join 下推到带有 group by 的 aggregation 时,须要对 aggregation 进行改写。
改写为在 aggregation 的 group by 的列中减少内部查问的全部列。这里要求内部查问肯定有 key,如果没有则须要生成长期的 key。生成能够的算子在图中为 assignuniqueid 算子。
如果 aggregation 为全局的,那么还须要进行额定的解决。如下图:
correlated join 下推到全局 aggregation 的时候,须要对 aggregation 减少 input 的列 (以及 key) 作为 group by 的列。这个下推规定还须要一个前提,那就是 aggregation 函数须要满足满足个性 agg(Ø)=agg(null)。这个的意思就是 aggragtion 函数须要对空集和对 null 的计算结果是雷同的。
注:在 mysql 和 AnalyticDB for MySQL(阿里云自研的云原生数据仓库[1],兼容 mysql 语法,下文简称 ADB)的语法里,sum, avg 等都不满足这个个性。空集的平均值为 0, 而对蕴含 null 值的任意汇合取平均值后果为 null 不为 0。所以须要 mark 子查问里的每一行,对空集进行特地的解决,在这里就不开展解释了。
论文 Orthogonal Optimization of Subqueries and Aggregation[2]重复使用下面这些规定进行 correlatedjoin 的下推,直到 correlatedjoin 能够转换为一般的 join。
带入之前的 tpch q17 的栗子中,咱们先应用将 correlated join 推到子查问中的 project 上面,查问变为:
而后下推穿过这个 agg,并改写这个 agg,如下图:
这里咱们疏忽 avg(Ø)!=avg(null)。如果思考这个状况,则须要 mark 子查问全副的行,在 correlated join 之后依据子查问的后果联合 mark 的值对空集进行特地解决(将 mark 了的行的值从 null 变为 0)。感兴趣的读者能够参考下一张中 q17 的最终打算。
接着间接调用之前的规定 2,上提这个 filter。这样这个查问就变为一般的没有关联的查问了。
2 后果复用
回顾上一节所说,子查问的查问后果是带入每一行关联列的值之后计算得出的,那么不言而喻雷同值的关联列带入子查问中计算出的后果是完全相同的。在下面的栗子中,对同样的 p.partkey,correlatedjoin 输入的子查问的后果是相等的。如下图中内部查问 partkey 为 25 的话产生的关联子查问时是完全相同的,那么后果也天然雷同。
15 年 Newmann 的论文 Unnesting Arbitrary Queries[3]介绍了一种办法就是先对外部查问里关联列取 distinct,再将 correlated join 返回的值和本来的内部查问依据关联列进行 left join,如下图所示:
这里的 not distinct join 的条件对应 mysql 外面的 <=>,null<=>null 的后果为 true,是能够 join 到一起的。
带入到之前的例子中如下图所示,对外部查问的关联列 partkey 先进行 distinct,而后带入子查问计算结果,最初再通过 join 将对应的后果接到本来的内部查问上。
如果进行了上述转换,那么咱们能够认为新的 input 的关联列永远是 distinct 的。而当初的 correlatedjoin* 算子能够容许 input 的列被过滤。这样做的益处除了对于雷同的列不进行反复的子查问的计算之外,次要还有上面两个:
- 新的内部查问是永远有 key 的,因为 distinct 过了。
- correlatedjoin* 算子因为过滤内部查问的列,所以它的下推更为简略(不须要 assignuniqueid,不须要保留全副行)。
进行上述的转换后,紧接着再套用之前的等价下推规定,咱们又能够将 correlatedjoin* 下推到一个左右子树没有关联的中央,从而改写为 inner join。
如果依照 Unnesting Arbitrary Queries[3]的办法进行解关联,须要将 input 的一部分后果进行复用,这个复用须要执行引擎的反对。须要留神的是,当零碎不反对复用的时候,咱们须要执行两次 input 的子树(如下图),这个时候就须要 input 这颗子树的后果是 deterministic 的,否则无奈用这个办法进行解关联。
三 关联子查问的优化
在 ADB 的优化器中,逻辑打算会依据每一条转换规则进行匹配和转换,也就意味着在关联解开之后不须要关怀解关联产生的打算的效率而将它间接交给后续的优化规定。然而事实并不是那么的美妙,因为后续规定不够齐备,以及解关联之后失落了内部查问和子查问之间的关系,咱们心愿在解关联的时候就将打算尽可能优化。
1 exists/in/filter 关联子查问
在之前的章节中为了简化,咱们只探讨了 scalar 子查问。因为 exists/in 这些子查问都能够改写成 scalar 子查问。比方将 exists 改写为 count(*) > 0
然而能够看到,如果子查问的返回后果被用来过滤内部查问的行,实际上会简化整个解关联的过程。所以咱们对 exists/in 这样的子查问进行非凡解决,在语法解析时就进行辨别。在解关联的过程中,如果能够应用 semijoin/antijoin 算子进行解关联则间接解开关联,否则后续会转化成 scalar 子查问也就是 correlatedjoin 的模式。
2 关联条件的上提
看到这里会发现,随着 correlatedjoin 的下推,这个逻辑树会变得更加简单,所以咱们在下推之前会在子查问外部进行关联算子的上提。当这个逻辑就是产生关联的算子越高,correlatedjoin 就能够早点推到关联部位的上面。比方上面这个查问:
SELECT t1.c2
FROM
t1
WHERE t1.c2 < (SELECT 0.2 * max(t2.x)
FROM
t2
WHERE t2.c1 = t2.c1
GROUP BY t2.c1
);
这里因为 t2.c1 = t2.c1 能够推到 agg 下面(因为对于子查问这是一个在 group by 列上的条件),咱们就能够进行上面的转换。先把关联的 filter 上提(有时须要改写),这样就只有把 correlatedjoin 推过 filter,调用转换 2 就能够了。
更具体的例子就是前文提到的 tpch q17。这里的 scalar 子查问作用在过滤条件中的状况也能够进行进一步改写。
下图为依照之前说的实践下推 correlated join 并改写为 left join 之后的逻辑打算。
而因为这个 scalar 子查问是作为 filter 条件的,这种状况下子查问没有后果返回为 null 对应的内部查问是肯定会被过滤掉的。所以 correlatedjoin 能够间接转为 correlatedjoin*,再加上将 filter 进行上提,咱们能够失去上面的打算。这样改写的益处是能够在 join 前先进行 agg(early agg)。害处就是如果不小心解决,很容易造成语义不等价造成 count bug。
3 代价相干的子查问优化
利用 window 算子解关联
回顾到目前为止咱们讲的这些,是不是印象最粗浅的在于 correlatedjoin 算子是在内部查问上减少一列。而他的这个行为和 window 算子相似。window 算子的语义就是不扭转输出的行数,只是在每一行上减少一个在 window 的 frame 里计算出的值。所以咱们能够利用 window 算子进行解关联,如果感兴趣能够参考这两篇论文 Enhanced Subquery Optimizations in Oracle[4]和 WinMagic : Subquery Elimination Using Window Aggregation[5]。
window 解关联的改写就是在内部查问蕴含子查问中全副的表和条件时,能够间接应用 window 将子查问的后果拼接到内部查问上。他益处是节约了很多 tablescan。比如说 tpch q2。能够进行上面的改写:
这里之所能改写成 window 是因为内部查问蕴含了外部查问全副的表和条件。而且 agg 函数 min 也满足个性 agg(Ø)=agg(null)(如果不满足,须要对行进行 mark 以及用 case when 改写输入)。
能够看到改写后 tablescan 的数量大大减少。更进一步,优化器前面的优化规定会进行依据 primarykey 的信息以及 agg 函数的个性进行 join 和 window 的程序替换从而进一步缩小 window 算子输出的数据量(filter-join pushdown)。
这些益处很多文章里都说了。咱们在这里讨论一下这样改写的不好的中央:
- 比方在 pk 未能显示提供 /agg 的函数对 duplicates 敏感的状况下,window 算子会阻挡 filter-join 的下推,从而打断了 joingraph 造成 join 的两头后果变大。
- 如果改写为两棵子树的 join,filter-join 能够下推到其中一颗子树上。而进行 window 改写后,filter-join 无奈下推。
- 在 pipeline 的执行模型下 /& 应用 cte 的状况下,扫表取得的收益无限。
- 传统优化器对 join&agg 的优化解决 / 优化规定比对 window 好 / 丰盛很多。
综上所述,什么时候应用 window 进行改写关联子查问须要进行收益和代价的预计。
CorrelatedJoin 在内部查问中的下推
在将 correlatedJoin 往子查问方向下推之前,咱们会将 correlatedjoin 先在内部查问中进行下推(比方推过 cross join 等)。
这样做是因为 correlatedJoin 永远不会造成数据的收缩,所以实践上应该早点做。但实际上 correlatejoin 下推后也可能切割 joingraph,从而造成和 window 改写差不多的问题。
4 等价列的利用
如果在子查问中存在和内部等价的列,那么能够先用这个列改写子查问中的关联列缩小关联的中央从而简化查问。上面举一个简略的例子。
Select t1.c2
From
t1
Where
t1.c3 < (Select min(t2.c3)
From t2
Where t1.c1 = t2.c1
group by t1.c1
)
-- 在子查问中应用 t2.c1 代替 t1.ct 进行简化
Select t1.c2
From
t1
Where
t1.c3 < (Select min(t2.c3)
From t2
Where t1.c1 = t2.c1
group by t2.c1
)
5 子查问相干的优化规定
一个方面 correaltedjoin 这个算子的个性给了咱们一些进行优化的信息。上面举一些例子:
- 通过 correaltedjoin 算子之后的行数与左子树的行数雷同。
- enforcesinglerow 的输入为 1 行。
- 内部查问的关联列决定(function dependency)correaltedjoin 的新增的输入列。
- assignuniqueid 产生的 key 具备 unique 的属性等,可用于之后化简 aggregation 和 group by 等。
- 子查问里的 sort 能够被裁剪。
另一个方面,在子查问的改写中,能够通过属性推导进行子查问的化简。比方:
- 如果本来内部查问就是 unique 的则没有别要减少 uniqueid 列。
- enforcesinglerow 的子节点的输入如果永远为 1 行则能够进行裁剪。
- 关联列在 project 上的子查问,如下图,在一些状况下改写为 exists 子查问。
select t1.orderkey,
(
select
min(t1.orderkey)
from
orders t2
where
t2.orderkey > t1.orderkey
)
from
orders t1
order by
1
6 须要留神的中央
子查问解关联中最须要留神的中央就是两个中央,一个是确保仅对外部查问进行加一列的操作,一个是对 null 值的解决。
计数谬误
文献中常提到的是一个经典的解关联容易出错的中央。比方上面的查问,咱们有一个前提条件就是 t1.c3 全都是小于 0 的。在这个状况下子查问参加的关联条件应该是没有任何过滤度的。而改写成 inner join 则会过滤掉一些行。语义上是不等价的。
Select t1.c2
From
t1
Where
t1.c3 < (Select COUNT (*)
From t2
Where t1.c1 = t2.c1
)
分布式下的 leftmarkjoin
另一个容易出错的中央是论文 Unnesting Arbitrary Queries[3]中的 LeftMarkJoin,其输入的后果与 in 的语义雷同。简略来说就是上面这个查问的后果。
select t1.c1
in ( select
t2.c1
from
t2)
from
t1
这个查问对应的逻辑打算如下:
其输入后果为在左子树后果上加一列 in 的后果,in 的后果有三种可能 true,false 和 null。
在分布式环境下,对这个算子进行 repartition 和落盘很容易造成和 null 值相干的计算出错。
举一个简略的例子,当 leftmarkjoin 为 repartition 的执行形式时,会对左表和右表的数据依据 c1 的 hash 值进行重散布 reshuffle。那么 t1.c1 中为 null 的行会被 shuffle 到同一台 executor 上。这个时候如果右表没有数据被 shuffle 到这台机器上,那么这一台 executor 并不知道对于 null 的这些行该输入 null 还是 false。因为 null in 空集的后果为 false,而 null in 任何非空集合的后果为 null。此时这台 executor 并不知道右表是否为空。
解开关联后的效率
在最开始的时候咱们提到了 iterative 的执行形式,这里咱们须要阐明对有些关联子查问来说即便关联被解开为 join/agg 等算子,计算查问后果也须要一个 cross join 的代价。
比方上面这个两个查问,第一个是咱们常见的关联子查问的样子,能够转换成 inner join + early agg 的模式。而第二个解开关联后则会变成一个 left join on 非等值条件(代价同 cross join)。
-- sql 1
SELECT t1.c1
FROM t1
WHERE t1.c2 > (SELECT min(t2.c2)
FROM t2
WHERE t2.c1 = t1.c1
);
-- sql 2
SELECT t1.c1
FROM t1
WHERE t1.c2 > (SELECT min(t2.c2)
FROM t2
WHERE t2.c1 > t1.c1
);
sq1 解开关联后的打算如下:
sql2 解开关联后的打算如下:
对于 sql1 来说,从语义上了解,内部查问的每一行带入子查问里扫过的行都是没有重叠的,所以代价和 innerjoin on 等值条件是一样的。再加上同样的内部行对应的子查问中 min 的后果雷同能够利用 early agg 从而能够进一步优化。
对于 sql2 来说,从语义上了解,内部查问的每一行都必须要带入子查问中扫过所有的行能力判断在满足 t2.c1 > t1.c1 这个条件下的子查问的输入应该是什么。为了计算出后果这个代价是无奈通过优化节约的。然而对同样的 t1.c1 输入始终是雷同的,Unnesting Arbitrary Queries[3]中的后果复用依然能够产生优化。
参考文献
[1] https://www.aliyun.com/product/ApsaraDB/ads
[2] Galindo-Legaria,César 和 Milind Joshi。“子查问和聚合的正交优化。”ACM SIGMOD 记录 30.2(2001):571-581。
[3] Neumann,Thomas 和 Alfons Kemper。“勾销嵌套任意查问。”商业,技术和网络数据库系统(BTW 2015)(2015 年)。
[4] 贝拉姆康达(Bellamkonda),斯里坎特(Srikanth)等。“加强了 Oracle 中的子查问优化。”VLDB 基金会论文集 2.2(2009):1366-1377
[5] Zuzarte,Calisto 等人。“Winmagic:应用窗口聚合打消子查问。”2003 ACM SIGMOD 国内数据管理国内会议论文集。2003。
[6] Neumann,Thomas,Viktor Leis 和 Alfons Kemper。“联接的残缺故事(inHyPer)。”商业,技术和网络数据库系统(BTW 2017)(2017)。
[7]加利福尼亚州加林多 - 莱加里亚(Galindo-Legaria),参数化查问和嵌套等效项。技术报告,Microsoft,2001 年。MSR-TR-2000-31,2000 年。原文链接
本文为阿里云原创内容,未经容许不得转载。