笛卡尔乘积是一个数学概念:
笛卡尔乘积是指在数学中,两个集合 X 和 Y 的笛卡尔积(Cartesian product),又称直积,表示为 X × Y,第一个对象是 X 的成员而第二个对象是 Y 的所有可能有序对的其中一个成员。公式表示就是如下:
1 | X×Y = {(x,y)|x∈X,y∈Y} |
---|---|
案例:
1 2 3 | X = {1,2} Y = {a,b,c} X×Y = {(1,a),(1,b),(1,c),(2,a),(2,b),(2,c)} |
---|---|
如果对同一个数据库的两张表进行 join 操作,例如表 A 记录 c~1,1~、c~1,2~、c~1,3~,表 B 有 c~2,1~ 以及 c~2,2~ 字段。
那么笛卡尔乘积的结果是:
c~1,1~+c~2,1~、c~1,1~+c~2,2~、c~1,2~+c~2,1~、c~1,2~+c~2,2~、c~1,3~+c~2,1~、c~1,3~+c~2,2~ 共 6 条记录。其中
+
的含义是两条记录并做一条记录。
join 是关系型数据库在关系二字上的集中体现,其作用在于将两张及以上表根据列中字段间的相关关系,将多表中的行融合在一起。
join 类型 | 语义 |
---|---|
cross join | Cross 即交叉,代表笛卡尔乘积中符号 ×,其也就是两表的笛卡尔乘积结果 |
inner join | 语义上等效为从笛卡尔乘积中选出符合条件的交集记录 |
left join | 语义上等效为从笛卡尔乘积中选出符合条件的交集记录+左表剩余的所有记录(把左表记录作为基础,依次添加右表字段,如果符合 ON 记录,那么赋值为右表字段值,否则赋值为 NULL) |
right join | 语义上等效为从笛卡尔乘积中选出符合条件的交集记录+右表剩余的所有记录 |
full join | MySQL 并不支持 full join,不过可以等效为相同条件的 left join 与 right 的 union |
full join 补充说明,在 MySQL 中如下语句是一个典型的 Full join:
1 2 3 | select * from t1 left join t2 on t2.name = t1.name union select * from t1 right join t2 on t2.name = t1.name; |
---|---|
也可以用集合的语言来表示,如下图所示:
在 SQL 实际上又把 inner join 称为内连接,其余所有 join 类型都称为外连接。因此 join 有等效别名关键字:
LEFT JOIN 和 RIGHT JOIN没什么差别,两者的结果差异取决于左右表的放置顺序。
典型带有 join 的 SQL 语句如下所示:
1 2 3 4 5 | SELECT <row_list> FROM <left_table> <inner|left|right> JOIN <right_table> ON <join condition> WHERE <where_condition> |
---|---|
我们按照 SQL 语句的执行顺序来对上述 SQL 语句进行说明:
注意事项:下面的说法仅仅从 MySQL 执行语义上进行说明,实际上 MySQL 在内存中不会建立 vt1、vt2、vt3 表。
ON 与 WHERE 的区别是什么?
ON 与 WHERE 在使用 inner join 时,无论是在结果上还是在性能上都没有区别。
MySQL 可以利用 FEDERATED 引擎等方式实现跨库 join,但查询效率实际上并不高。通常认为 MySQL join 操作指的同数据库的多表 join。
在单表查询中,我们通常会强调两点:
但在 join 多表问题中,索引不仅仅需要考虑上述两个问题。
MySQL 中的 join 操作并不会在内存中构造临时表,第四节中的说法只是方便从语义上进行理解。join 具体如何执行取决于查询优化器的选择。
MySQL 支持如下三种 join 操作(以两张表 join 为例):
可见,join 操作的性能非常取决于第二张表是否基于索引进行查询。不过,为什么不要求第一张表也使用索引?
实际上,第一张表被称为驱动表,亦可称之为基表,MySQL 总是要遍历该表的所有行,每一行都去第二张表中进行匹配查询。遍历可以不建立索引,走簇集索引即可,而查询操作则需要依赖于二级索引。
那么,MySQL 如何决定将哪一张表作为驱动表呢?
MySQL 选择驱动表的原则是:在对最终结果集没影响的前提下,优先选择结果集最少的那张表作为驱动表。原因在于驱动表的行数决定了在非驱动表中进行查询的次数,驱动表行数越少,进行查询的次数越少。
如果是 left join,那么基表通常是 left join 左侧表,right join 的基表通常为 right join 右侧表。
因此,我们要非常注意非驱动表的索引,在 ON 以及 WHERE 后的字段都应该被索引覆盖。
数据库范式有若干条[4],定义偏于学术性,但核心思路是简洁明了的:数据库范式目的是使结构更合理,消除存储异常,使数据冗余尽量小,便于插入、删除和更新。
join 操作的原因就在于多表之间有关系并且多个表之间数据几乎没有冗余。
举一个例子,我们有三个表:
如果要查询一个学生对应的班级描述,那么就需要对上述三标进行 join,join 的性能问题可能会使我们产生担心。
为此,我们可以故意破坏范式,制造出一张存在冗余的“大表”:
你会发现,class 的 description 可能存储在两个表中(student_class_full 与 class),这不符合范式,并且为写操作带来了一致性问题以及写性能下降。另一方面,我们不再需要使用 join 来完成查询,读性能得到提高。
可见,在一些场景下,我们可以选择破坏数据库范式,避免使用 join 来提高读性能。代价是不同表之间出现的字段冗余、写性能下降,写操作出现多表间的一致性问题。
join 比子查询在空间复杂度上要低,因此很多人建议利用 join 来代替子查询:
阿里巴巴在 Java 开发手册中建议[8]:超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致。
可见,阿里巴巴的意思是可以用 join,但是不要超过3张表。
(1)为什么 join 表的个数不能太多?
虽然我们可以利用索引来优化查询,但是如果是 k 张 n 行的数据库进行 join 查询,最坏的情况下时间复杂度为 O(n*(logn)^k-1^),因此 join 表的数量应当得到控制。
例如,我们假设每一张表的行数为 1000,000 行,那么时间复杂度有:
join 表的数量(k) | 时间复杂度 |
---|---|
2 | 20*1000,000 |
3 | 400*1000,000 |
4 | 8000*1000,000 |
k | O(n*(logn)^k-1^) |
(2)为什么可以使用 join?
很多场景下 join 是最优选择。例如两张表各有 10W 条数据,我们的确可以利用 service 层,分两步向两个数据库索要对应的行数据,然后在 service 层完成数据行的关联与过滤。但是 2*10 W 行数据有很大的网络传输压力,并且会对 service 层所在的服务器内存有一定压力。而 join 在 mysql server 处实际可能仅仅会得到 100 条符合要求的记录,那么对比起来,在 service 层的额外开销更难以接受。
当然,分库的 join 避免不了网络传输的额外开销(排除一机多库)。