为了账号安全,请及时绑定邮箱和手机立即绑定

Apache Flink 漫谈系列 - JOIN 算子

标签:
MySQL

聊什么

在《Apache Flink 漫谈系列 - SQL概览》中我们介绍了JOIN算子的语义和基本的使用方式,介绍过程中大家发现Apache Flink在语法语义上是遵循ANSI-SQL标准的,那么再深思一下传统数据库为啥需要有JOIN算子呢?在实现原理上面Apache Flink内部实现和传统数据库有什么区别呢?本篇将详尽的为大家介绍传统数据库为什么需要JOIN算子,以及JOIN算子在Apache Flink中的底层实现原理和在实际使用中的优化!

什么是JOIN

在《Apache Flink 漫谈系列 - SQL概览》中我对JOIN算子有过简单的介绍,这里我们以具体实例的方式让大家对JOIN算子加深印象。JOIN的本质是分别从N(N>=1)张表中获取不同的字段,进而得到最完整的记录行。比如我们有一个查询需求:在学生表(学号,姓名,性别),课程表(课程号,课程名,学分)和成绩表(学号,课程号,分数)中查询所有学生的姓名,课程名和考试分数。如下:


webp

为啥需要JOIN

JOIN的本质是数据拼接,那么如果我们将所有数据列存储在一张大表中,是不是就不需要JOIN了呢?如果真的能将所需的数据都在一张表存储,我想就真的不需要JOIN的算子了,但现实业务中真的能做到将所需数据放到同一张大表里面吗?答案是否定的,核心原因有2个:

  • 产生数据的源头可能不是一个系统;

  • 产生数据的源头是同一个系统,但是数据冗余的沉重代价,迫使我们会遵循数据库范式,进行表的设计。简说NF如下:

    • 1NF - 列不可再分;

    • 2NF - 符合1NF,并且非主键属性全部依赖于主键属性;

    • 3NF - 符合2NF,并且消除传递依赖,即:任何字段不能由其他字段派生出来;

    • BCNF - 符合3NF,并且主键属性之间无依赖关系。

当然还有 4NF,5NF,不过在实际的数据库设计过程中做到BCNF已经足够了!(并非否定4NF,5NF存在的意义,只是个人还没有遇到一定要用4NF,5NF的场景,设计往往会按存储成本,查询性能等综合因素考量)

JOIN种类

JOIN 在传统数据库中有如下分类:

  • CROSS JOIN - 交叉连接,计算笛卡儿积;

  • INNER JOIN - 内连接,返回满足条件的记录;

  • OUTER JOIN

    • LEFT - 返回左表所有行,右表不存在补NULL;

    • RIGHT - 返回右表所有行,左边不存在补NULL;

    • FULL -  返回左表和右表的并集,不存在一边补NULL;

  • SELF JOIN - 自连接,将表查询时候命名不同的别名。

JOIN语法

JOIN 在SQL89和SQL92中有不同的语法,以INNER JOIN为例说明:

  • SQL89 - 表之间用“,”逗号分割,链接条件和过滤条件都在Where子句指定:

SELECT 
  a.colA, 
  b.colA
FROM  
  tab1 AS a , tab2 AS b
WHERE a.id = b.id and a.other > b.other
  • SQL92 - SQL92将链接条件在ON子句指定,过滤条件在WHERE子句指定,逻辑更为清晰:

SELECT 
  a.colA, 
  b.colA
FROM 
  tab1 AS a JOIN tab2 AS b ON a.id = b.id
WHERE 
  a.other > b.other

本篇中的后续示例将应用SQL92语法进行SQL的编写,语法如下:

tableExpression [ LEFT|RIGHT|FULL|INNER|SELF ] JOIN tableExpression [ ON joinCondition ] [WHERE filterCondition]

语义示例说明

在《Apache Flink 漫谈系列 - SQL概览》中对JOIN语义有过简单介绍,这里会进行展开介绍。 我们以开篇示例中的三张表:学生表(学号,姓名,性别),课程表(课程号,课程名,学分)和成绩表(学号,课程号,分数)来介绍各种JOIN的语义。

webp

CROSS JOIN

交叉连接会对两个表进行笛卡尔积,也就是LEFT表的每一行和RIGHT表的所有行进行联接,因此生成结果表的行数是两个表行数的乘积,如student和course表的CROSS JOIN结果如下:

mysql> SELECT * FROM student JOIN course;
+------+-------+------+-----+-------+--------+| no   | name  | sex  | no  | name  | credit |
+------+-------+------+-----+-------+--------+
| S001 | Sunny | M    | C01 | Java  |      2 || S002 | Tom   | F    | C01 | Java  |      2 |
| S003 | Kevin | M    | C01 | Java  |      2 || S001 | Sunny | M    | C02 | Blink |      3 |
| S002 | Tom   | F    | C02 | Blink |      3 || S003 | Kevin | M    | C02 | Blink |      3 |
| S001 | Sunny | M    | C03 | Spark |      3 || S002 | Tom   | F    | C03 | Spark |      3 |
| S003 | Kevin | M    | C03 | Spark |      3 |+------+-------+------+-----+-------+--------+9 rows in set (0.00 sec)

如上结果我们得到9行=student(3) x course(3)。交叉联接一般会消耗较大的资源,也被很多用户质疑交叉联接存在的意义?(任何时候我们都有质疑的权利,同时也建议我们养成自己质疑自己“质疑”的习惯,就像小时候不理解父母的“废话”一样)。
我们以开篇的示例说明交叉联接的巧妙之一,开篇中我们的查询需求是:在学生表(学号,姓名,性别),课程表(课程号,课程名,学分)和成绩表(学号,课程号,分数)中查询所有学生的姓名,课程名和考试分数。开篇中的SQL语句得到的结果如下:

mysql> SELECT 
    ->   student.name, course.name, score 
    -> FROM student JOIN  score ON student.no = score.s_no 
    ->              JOIN course ON score.c_no = course.no;
+-------+-------+-------+| name  | name  | score |+-------+-------+-------+| Sunny | Java  |    80 || Sunny | Blink |    98 || Sunny | Spark |    76 || Kevin | Java  |    78 || Kevin | Blink |    88 || Kevin | Spark |    68 |+-------+-------+-------+6 rows in set (0.00 sec)

如上INNER JOIN的结果我们发现少了Tom同学的成绩,原因是Tom同学没有参加考试,在score表中没有Tom的成绩,但是我们可能希望虽然Tom没有参加考试但仍然希望Tom的成绩能够在查询结果中显示(成绩 0 分),面对这样的需求,我们怎么处理呢?交叉联接可以帮助我们:

  • 第一步 student和course 进行交叉联接:

mysql> SELECT 
    ->   stu.no, c.no, stu.name, c.name
    -> FROM student stu JOIN course c  笛卡尔积
    -> ORDER BY stu.no; -- 排序只是方便大家查看:)
+------+-----+-------+-------+| no   | no  | name  | name  |
+------+-----+-------+-------+
| S001 | C03 | Sunny | Spark || S001 | C01 | Sunny | Java  |
| S001 | C02 | Sunny | Blink || S002 | C03 | Tom   | Spark |
| S002 | C01 | Tom   | Java  || S002 | C02 | Tom   | Blink |
| S003 | C02 | Kevin | Blink || S003 | C03 | Kevin | Spark |
| S003 | C01 | Kevin | Java  |+------+-----+-------+-------+9 rows in set (0.00 sec)
  • 第二步 将交叉联接的结果与score表进行左外联接,如下:

mysql> SELECT 
    ->   stu.no, c.no, stu.name, c.name,
    ->    CASE 
    ->     WHEN s.score IS NULL THEN 0
    ->     ELSE s.score
    ->   END AS score 
    -> FROM student stu JOIN course c  -- 迪卡尔积
    -> LEFT JOIN score s ON stu.no = s.s_no and c.no = s.c_no -- LEFT OUTER JOIN
    -> ORDER BY stu.no; -- 排序只是为了大家好看一点:)
+------+-----+-------+-------+-------+| no   | no  | name  | name  | score |+------+-----+-------+-------+-------+| S001 | C03 | Sunny | Spark |    76 || S001 | C01 | Sunny | Java  |    80 || S001 | C02 | Sunny | Blink |    98 || S002 | C02 | Tom   | Blink |     0 | -- TOM 虽然没有参加考试,但是仍然看到他的信息| S002 | C03 | Tom   | Spark |     0 || S002 | C01 | Tom   | Java  |     0 || S003 | C02 | Kevin | Blink |    88 || S003 | C03 | Kevin | Spark |    68 || S003 | C01 | Kevin | Java  |    78 |+------+-----+-------+-------+-------+9 rows in set (0.00 sec)

经过CROSS JOIN帮我们将Tom的信息也查询出来了!(TOM 虽然没有参加考试,但是仍然看到他的信息)

INNER JOIN

内联接在SQL92中 ON 表示联接添加,可选的WHERE子句表示过滤条件,如开篇的示例就是一个多表的内联接,我们在看一个简单的示例: 查询成绩大于80分的学生学号,学生姓名和成绩:

mysql> SELECT 
    ->   stu.no, stu.name , s.score
    -> FROM student stu JOIN score s ON  stu.no = s.s_no 
    -> WHERE s.score > 80;
+------+-------+-------+| no   | name  | score |+------+-------+-------+| S001 | Sunny |    98 || S003 | Kevin |    88 |+------+-------+-------+2 rows in set (0.00 sec)

上面按语义的逻辑是:

  • 第一步:先进行student和score的内连接,如下:

mysql> SELECT 
    ->   stu.no, stu.name , s.score
    -> FROM student stu JOIN score s ON  stu.no = s.s_no ;
+------+-------+-------+| no   | name  | score |+------+-------+-------+| S001 | Sunny |    80 || S001 | Sunny |    98 || S001 | Sunny |    76 || S003 | Kevin |    78 || S003 | Kevin |    88 || S003 | Kevin |    68 |+------+-------+-------+6 rows in set (0.00 sec)
  • 第二步:对内联结果进行过滤, score > 80 得到,如下最终结果:

-> WHERE s.score > 80;
+------+-------+-------+| no   | name  | score |+------+-------+-------+| S001 | Sunny |    98 || S003 | Kevin |    88 |+------+-------+-------+2 rows in set (0.00 sec)

上面的查询过程符合语义,但是如果在filter条件能过滤很多数据的时候,先进行数据的过滤,在进行内联接会获取更好的性能,比如我们手工写一下:

mysql> SELECT 
    ->   no, name , score
    -> FROM student stu JOIN ( SELECT s_no, score FROM score s WHERE s.score >80) as sc ON no = s_no;
+------+-------+-------+| no   | name  | score |+------+-------+-------+| S001 | Sunny |    98 || S003 | Kevin |    88 |+------+-------+-------+2 rows in set (0.00 sec)

上面写法语义和第一种写法语义一致,得到相同的查询结果,上面查询过程是:

  • 第一步:执行过滤子查询

mysql> SELECT s_no, score FROM score s WHERE s.score >80;
+------+-------+| s_no | score |
+------+-------+
| S001 |    98 || S003 |    88 |
+------+-------+
2 rows in set (0.00 sec)
  • 第二步:执行内连接

-> ON no = s_no;
+------+-------+-------+| no   | name  | score |+------+-------+-------+| S001 | Sunny |    98 || S003 | Kevin |    88 |+------+-------+-------+2 rows in set (0.00 sec)

如上两种写法在语义上一致,但查询性能在数量很大的情况下会有很大差距。上面为了和大家演示相同的查询语义,可以有不同的查询方式,不同的执行计划。实际上数据库本身的优化器会自动进行查询优化,在内联接中ON的联接条件和WHERE的过滤条件具有相同的优先级,具体的执行顺序可以由数据库的优化器根据性能消耗决定。也就是说物理执行计划可以先执行过滤条件进行查询优化,如果细心的读者可能发现,在第二个写法中,子查询我们不但有行的过滤,也进行了列的裁剪(去除了对查询结果没有用的c_no列),这两个变化实际上对应了数据库中两个优化规则:

  • filter push down

  • project push down

如上优化规则以filter push down 为例,示意优化器对执行plan的优化变动:

webp

LEFT OUTER JOIN

左外联接语义是返回左表所有行,右表不存在补NULL,为了演示作用,我们查询没有参加考试的所有学生的成绩单:

mysql> SELECT 
    ->   no, name , s.c_no, s.score
    -> FROM student stu LEFT JOIN score s ON stu.no = s.s_no
    -> WHERE s.score is NULL;
+------+------+------+-------+| no   | name | c_no | score |
+------+------+------+-------+
| S002 | Tom  | NULL |  NULL |+------+------+------+-------+1 row in set (0.00 sec)

上面查询的执行逻辑上也是分成两步:

  • 第一步:左外联接查询

mysql> SELECT 
    ->   no, name , s.c_no, s.score
    -> FROM student stu LEFT JOIN score s ON stu.no = s.s_no;
+------+-------+------+-------+| no   | name  | c_no | score |
+------+-------+------+-------+
| S001 | Sunny | C01  |    80 || S001 | Sunny | C02  |    98 |
| S001 | Sunny | C03  |    76 || S002 | Tom   | NULL |  NULL | -- 右表不存在的补NULL
| S003 | Kevin | C01  |    78 || S003 | Kevin | C02  |    88 |
| S003 | Kevin | C03  |    68 |+------+-------+------+-------+7 rows in set (0.00 sec)
  • 第二步:过滤查询

mysql> SELECT 
    ->   no, name , s.c_no, s.score
    -> FROM student stu LEFT JOIN score s ON stu.no = s.s_no
    -> WHERE s.score is NULL;
+------+------+------+-------+| no   | name | c_no | score |
+------+------+------+-------+
| S002 | Tom  | NULL |  NULL |+------+------+------+-------+1 row in set (0.00 sec)

这两个过程和上面分析的INNER JOIN一样,但是这时候能否利用上面说的 filter push down的优化呢?根据LEFT OUTER JOIN的语义来讲,答案是否定的。我们手工操作看一下:

  • 第一步:先进行过滤查询(获得一个空表)

mysql> SELECT * FROM score s WHERE s.score is NULL;Empty set (0.00 sec)
  • 第二步: 进行左外链接

mysql> SELECT 
    ->   no, name , s.c_no, s.score
    -> FROM student stu LEFT JOIN (SELECT * FROM score s WHERE s.score is NULL) AS s ON stu.no = s.s_no;
+------+-------+------+-------+| no   | name  | c_no | score |
+------+-------+------+-------+
| S001 | Sunny | NULL |  NULL || S002 | Tom   | NULL |  NULL |
| S003 | Kevin | NULL |  NULL |+------+-------+------+-------+3 rows in set (0.00 sec)

我们发现两种写法的结果不一致,第一种写法只返回Tom没有参加考试,是我们预期的。第二种写法返回了Sunny,Tom和Kevin三名同学都没有参加考试,这明显是非预期的查询结果。所有LEFT OUTER JOIN不能利用INNER JOIN的 filter push down优化。

RIGHT OUTER JOIN

右外链接语义是返回右表所有行,左边不存在补NULL,如下:

mysql> SELECT 
    ->   s.c_no, s.score, no, name
    -> FROM score s RIGHT JOIN student stu ON stu.no = s.s_no;
+------+-------+------+-------+| c_no | score | no   | name  |
+------+-------+------+-------+
| C01  |    80 | S001 | Sunny || C02  |    98 | S001 | Sunny |
| C03  |    76 | S001 | Sunny || NULL |  NULL | S002 | Tom   | -- 左边没有的进行补 NULL
| C01  |    78 | S003 | Kevin || C02  |    88 | S003 | Kevin |
| C03  |    68 | S003 | Kevin |+------+-------+------+-------+7 rows in set (0.00 sec)

上面右外链接我只是将上面左外链接查询的左右表交换了一下:)。

FULL OUTER JOIN

全外链接语义返回左表和右表的并集,不存在一边补NULL,用于演示的MySQL数据库不支持FULL OUTER JOIN。这里不做演示了。

SELF JOIN

上面介绍的INNER JOIN、OUTER JOIN都是不同表之间的联接查询,自联接是一张表以不同的别名做为左右两个表,可以进行如上的INNER JOIN和OUTER JOIN。如下看一个INNER 自联接:

mysql> SELECT * FROM student l JOIN student r where l.no = r.no;
+------+-------+------+------+-------+------+| no   | name  | sex  | no   | name  | sex  |
+------+-------+------+------+-------+------+
| S001 | Sunny | M    | S001 | Sunny | M    || S002 | Tom   | F    | S002 | Tom   | F    |
| S003 | Kevin | M    | S003 | Kevin | M    |+------+-------+------+------+-------+------+3 rows in set (0.00 sec)
不等值联接

这里说的不等值联接是SQL92语法里面的ON子句里面只有不等值联接,比如:

mysql> SELECT 
    ->   s.c_no, s.score, no, name
    -> FROM score s RIGHT JOIN student stu ON stu.no != s.c_no;
+------+-------+------+-------+| c_no | score | no   | name  |
+------+-------+------+-------+
| C01  |    80 | S001 | Sunny || C01  |    80 | S002 | Tom   |
| C01  |    80 | S003 | Kevin || C02  |    98 | S001 | Sunny |
| C02  |    98 | S002 | Tom   || C02  |    98 | S003 | Kevin |
| C03  |    76 | S001 | Sunny || C03  |    76 | S002 | Tom   |
| C03  |    76 | S003 | Kevin || C01  |    78 | S001 | Sunny |
| C01  |    78 | S002 | Tom   || C01  |    78 | S003 | Kevin |
| C02  |    88 | S001 | Sunny || C02  |    88 | S002 | Tom   |
| C02  |    88 | S003 | Kevin || C03  |    68 | S001 | Sunny |
| C03  |    68 | S002 | Tom   || C03  |    68 | S003 | Kevin |
+------+-------+------+-------+
18 rows in set (0.00 sec)

上面这示例,其实没有什么实际业务价值,在实际的使用场景中,不等值联接往往是结合等值联接,将不等值条件在WHERE子句指定,即, 带有WHERE子句的等值联接。

Apache Flink双流JOIN


CROSSINNEROUTERSELFONWHERE
Apache FlinkNYYY必选可选

Apache Flink目前支持INNER JOIN和LEFT OUTER JOIN(SELF 可以转换为普通的INNER和OUTER)。在语义上面Apache Flink严格遵守标准SQL的语义,与上面演示的语义一致。下面我重点介绍Apache Flink中JOIN的实现原理。

双流JOIN与传统数据库表JOIN的区别

传统数据库表的JOIN是两张静态表的数据联接,在流上面是 动态表(关于流与动态表的关系请查阅 《Apache Flink 漫谈系列 - 流表对偶(duality)性)》,双流JOIN的数据不断流入与传统数据库表的JOIN有如下3个核心区别:

  • 左右两边的数据集合无穷 - 传统数据库左右两个表的数据集合是有限的,双流JOIN的数据会源源不断的流入;

  • JOIN的结果不断产生/更新 - 传统数据库表JOIN是一次执行产生最终结果后退出,双流JOIN会持续不断的产生新的结果。在 《Apache Flink 漫谈系列 - 持续查询(Continuous Queries)》篇也有相关介绍。

  • 查询计算的双边驱动 - 双流JOIN由于左右两边的流的速度不一样,会导致左边数据到来的时候右边数据还没有到来,或者右边数据到来的时候左边数据没有到来,所以在实现中要将左右两边的流数据进行保存,以保证JOIN的语义。在Blink中会以State的方式进行数据的存储。State相关请查看《Apache Flink 漫谈系列 - State》篇。

数据Shuffle

分布式流计算所有数据会进行Shuffle,怎么才能保障左右两边流的要JOIN的数据会在相同的节点进行处理呢?在双流JOIN的场景,我们会利用JOIN中ON的联接key进行partition,确保两个流相同的联接key会在同一个节点处理。

数据的保存

不论是INNER JOIN还是OUTER JOIN 都需要对左右两边的流的数据进行保存,JOIN算子会开辟左右两个State进行数据存储,左右两边的数据到来时候,进行如下操作:

  • LeftEvent到来存储到LState,RightEvent到来的时候存储到RState;

  • LeftEvent会去RightState进行JOIN,并发出所有JOIN之后的Event到下游;

  • RightEvent会去LeftState进行JOIN,并发出所有JOIN之后的Event到下游。

webp



作者:阿里云云栖社区
链接:https://www.jianshu.com/p/21dbfb01fc63


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消