首页 > 其他分享 >Subquery? No, it's join!

Subquery? No, it's join!

时间:2023-07-06 17:14:53浏览次数:48  
标签:join No t1 number mark Subquery 查询 select

作者:王旭东
Databend 研发工程师
https://github.com/xudong963

在 SQL 查询中,子查询是一种常用的技术,它允许我们在一个查询内部嵌套另一个查询,以实现更复杂的数据检索和分析。如何在数据库内核中高效的处理子查询是非常有挑战的,本文将介绍如何在 Databend 中构建 state-of-art 的子查询 optimizer。

从宽泛的角度,子查询分为关联和非关联子查询, 细分的种类包含:SCALAR/ANY/ALL/SOME/(NOT)IN/(NOT)EXISTS. 对于每一种子查询的含义,读者可以参考: https://www.postgresql.org/docs/current/functions-subquery.html 。尽管子查询有这么多种,但是在 Databend 中,我们只需要处理 SCALAR/EXISTS/ANY 三种子查询,因为在 binder 阶段,可以做如下 SQL 语义等价转换:

  • in => = any(...)
  • i > all() => not(i <= any(...))
  • some => any

子查询几乎可以出现在 SQL 的任何位置,如 from/where/select/group by/having/order by, 外加关联子查询的存在,所以处理子查询变得具有挑战性,在深入子查询之前,先介绍一下 Databend 为了高效处理子查询 而引入的非标准 join 类型: single join 和 mark join

  • single join: single join 的存在是为了处理 scalar 子查询,left single join 与 left outer join 类似,但是如果超过一个 join partner 被发现就会报错,这点对应 scalar 子查询只产生一列且最多一行结果。
  • mark join: mark join 引入了一个新的属性:mark, 用来标记 tuple 是否有 join partner. 其值可以是 TRUE, FALSE, NULL, 可以用来处理 ANY/EXISTS 子查询

有了这两种非标准 join, 我们可以保证所有的子查询在经过子查询 optimizer 后已经全部转化为 join, 这为 join reorder 提供了更多的可能,可以大幅降低执行代价。

非关联子查询

非关联子查询的处理相对简单,只需要做简单的变换即可。下面通过几个简单的例子来看一下如何展开非关联子查询:

非关联 scalar 子查询 select number, (select number from numbers(10) as t2 where number > 3 limit 1) from numbers(10) as t1, 直接用 single join 即可


非关联 exists 子查询 select number, exists(select * from numbers(10) as t2 where number > 3) from numbers(10) as t1;, 给子查询加上 limit 1count(*) 和 count(*) = 1 operator, 其中 limit 1 可以使查询更加高效,因为只需要判断是否存在即可

非关联 any 子查询 select number from numbers(10) as t1 where number > any(select number from numbers(20) as t2) or number > 1;, 在这条 SQL 中,因为包含 disjunction predicate,所以不能用 semi join 来对转化子查询,mark join 的 mark 列将替换子查询 => marker or number > 1 
非关联 any 子查询 select number from numbers(10) as t1 where number > any(select number from numbers(20) as t2) or number > 1;, 在这条 SQL 中,因为包含 disjunction predicate,所以不能用 semi join 来对转化子查询,mark join 的 mark 列将替换子查询 => marker or number > 1 

关联子查询

在介绍关联子查询前,需要引入 dependent join, dependent join 会为 LHS 中的每一行执行一次 RHS 

核心思想在于如何消除 dependent join 中的 correlated columns?

下面通过一条 ANY 关联子查询来看一下是如何去关联的

select a from t1 where a > any(select b from t2 where t1.a < 1) or a > 1;

mysql> desc t1;
+-------+-----------------+------+---------+-------+
| Field | Type            | Null | Default | Extra |
+-------+-----------------+------+---------+-------+
| a     | BIGINT UNSIGNED | NO   | 0       |       |
+-------+-----------------+------+---------+-------+
1 row in set (0.03 sec)
Read 0 rows, 0.00 B in 0.015 sec., 0 rows/sec., 0.00 B/sec.

mysql> desc t2;
+-------+-----------------+------+---------+-------+
| Field | Type            | Null | Default | Extra |
+-------+-----------------+------+---------+-------+
| b     | BIGINT UNSIGNED | NO   | 0       |       |
+-------+-----------------+------+---------+-------+
1 row in set (0.03 sec)
Read 0 rows, 0.00 B in 0.017 sec., 0 rows/sec., 0.00 B/sec.

子查询中包含 correlated column t1.a, 核心的一步就是对子查询中的表进行扩展(cross join),t2 x t1(a) , 扩展后的子查询自然就完成了去关联。

扩展后的表假设叫 t3 包含两列(b, a'), t3 会与 t1 进行 mark join, 返回 (t1.a, mark) 两列,mark 列用于 filter: mark or a > 1 中,对 mark join 后的结果进行进一步过滤

这里需要注意的是 mark join 的 equi condition 和 non-equi condition.

至此,子查询处理的核心思想已经介绍完了。还有很多工程上的优化和特殊情况就不展开讲述了,比如

  1. 如何正确的处理 NULL, 特别是在 mark join 的实现中,NULL 的正确处理对子查询结果的正确性非常重要
  2. 在对子查询中的表进行拓展时,直接 cross join 有一定的开销,能否避免 cross join?
  3. mark join 在什么情况下可以转化为 semi join?
  4. ......

纵观全文,所有的子查询最终的形态都是 join, 所以 join 的性能很大程度上决定了子查询的性能,下一篇我们讲一讲 Databend join 从 0 到 1 的一个迭代过程。

标签:join,No,t1,number,mark,Subquery,查询,select
From: https://www.cnblogs.com/databend/p/17532694.html

相关文章

  • Arduino 麦克风声音传感器指南
    麦克风声音传感器麦克风声音传感器,顾名思义,检测声音。它可以测量声音的响度。这些传感器的种类繁多。 在下图中,您可以看到Arduino最常用的。最左边是KY-038,右边是LM393麦克风声音传感器。两个传感器模块都有一个内置电位器,用于调节数字输出引脚的灵敏度。去哪买?您可以......
  • VSCode如何通过Ctrl+P快速打开node_modules中的文件
    背景咱们新建一个NodeJS项目,必然会安装许多依赖包,因此经常需要查阅某些依赖包的源码文件。但是,由于node_modules目录包含的文件太多,出于性能考虑,在VSCode中默认情况下是禁止搜索node_modules目录的。在这种情况下,我们将不得不依次展开node_modules的文件目录树,来查找我们所需要的......
  • 「NOIP 模拟赛 20230706」偷 WiFi
    summarization有一个长度为\(n\)的序列\(p\),将其中若干个数标记。对于序列中的每一个位置\(i\),其贡献为其左边与右边离它最近的被标记的数的数值的和。求出最大的贡献总和。(\(1\len\le2\times10^6\))solution首先显然,\(p_1,p_n\)一定要标记。然后考虑分别求相邻的标记数......
  • Git Merge Failed Merging is not possible because you have unmerged files. hint:
    ​ 这个错误提示意味着在进行gitmerge操作时,存在未解决的冲突(unmergedfiles)。Git无法自动合并这些冲突,因此您需要手动解决冲突并进行提交。要解决这个问题,您可以按照以下步骤进行操作:首先,运行gitstatus命令来查看未解决的冲突文件。您会看到类似下面的提示:Unmerged......
  • Git Merge Failed Merging is not possible because you have unmerged files. hint:
     这个错误提示意味着在进行gitmerge操作时,存在未解决的冲突(unmergedfiles)。Git无法自动合并这些冲突,因此您需要手动解决冲突并进行提交。要解决这个问题,您可以按照以下步骤进行操作:首先,运行gitstatus命令来查看未解决的冲突文件。您会看到类似下面的提示:Unmergedpaths:(use......
  • linq left join group by count组合统计,防止count()为null结果为1的错误。
    原生sqlselectcar.id,carnum,count(carplan.carid)astimeLenfromtab_carascarjointab_inComeTypeasincomeoncar.inComeTypeId=income.IdandinComeTypeId=1andstateCode=1andcarTypeId=1leftjointab_carPlanascarplanoncar.id=carplan.carIdandca......
  • 如何使用Arduino创建摩尔斯电码生成器
    摩尔斯电码工作原理摩尔斯电码发明于19世纪,使用非常简单的长短脉冲序列(通常为电和划)来远距离发送消息。通过将字母表中的字母编码为电和划的组合,信息可以只用一个单一的电子或声音信号来表达。为了说明这是如何工作的,我们将使用一个简单的蜂鸣器将文本转换为可听的摩尔斯电码信......
  • IBM总线代理接口SoupAction does not match
    IBM总线代理接口SoupActiondoesnotmatch问题描述:ThegivenSOAPActionuploadScheduledoesnotmatchanoperation.解决方案:增加一个ESQL:CREATECOMPUTEMODULETEST_Compute1CREATEFUNCTIONMain()RETURNSBOOLEANBEGINCALLCopyMessageHeaders();CALLCopy......
  • 【ChernoC++笔记】指针和引用
    指针【16】C++指针▶️指针的类型不影响指针的本质:任何type的指针都是保存着内存地址的整数(integer)。指针的type只用来使人更好理解。//一个最简单的void类型指针,储存内存地址0void*ptr=0;void*ptr=NULL;void*ptr=nullptr; //C++11//使ptr存储var的内存地......
  • 报错 Cannot construct instance of `java.time.LocalDate` LocalDateTime
    原因:报错的原因就是导入了JacksonObjectMapper对象映射器,导致不知道怎么将LocalDateTime转换成Json类型的数据了,在没有导入使用JacksonObjectMapper的时候是不会报错的。解决方式:指定LocalDateTime类型的数据如何进行序列化就好了,给实体类中LocalDateTime的属性加上注解就可以了:......