分享到

简介

JOINs header image

在关系型数据库中,JOIN 允许您组合来自多个表的相关数据。继续阅读以了解更多关于它们是什么、它们如何工作以及如何有效地利用它们处理不同关系。

关系型数据库中数据如何组织

在深入了解 JOIN 之前,了解关系型数据库的基础知识非常重要。关系型数据库提供了一种组织信息的方式。在关系型数据库中,数据被组织成,其中(属性)指定数据类型(例如字符串、整数等),包含记录(表中每列的值)。

我们用一个包含客户信息的表来演示这个概念。在下表中,列(即客户属性)是 nameageemail address,行表示每个客户的 nameageemail_address

姓名年龄电子邮件地址
珍妮30jenny@prisma.io
阿德里安28adrian@prisma.io
辛尼瓦25synniva@prisma.io

关系型数据库能够使用主键外键定义不同表之间的关系或连接。

关系型数据库中的表通常会有一个称为主键的列,它唯一标识每一行。为了看它在实践中如何工作,我们使用之前的例子:customers表的主键将是一个包含每个客户唯一 ID 的列,称为customer_id。换句话说,没有客户会共享相同的 ID。

table-properties

外键通过引用另一个表的主键来创建表之间的关系。为了演示外键的概念,假设我们有另一个名为orders的表,包含列:order_idcostorder_date。我们可以通过包含customer_id列(外键),将orders表(图中的左表)与customers表(图中的右表)关联起来,将每个订单与相应的客户关联。

relationship-between-tables

什么是 JOIN?

关系型数据库的一个有用特性是JOIN的概念,这是一种 SQL 操作,通常基于主键和外键组合来自不同表的相关数据。

JOIN操作的基本语法如下:

SELECT
*
FROM
<first_table>
<join_type> <second_table>
<join_condition>

以下是查询的每个部分的含义:

  • SELECT:指定要从结果数据集中包含哪些列。在本例中,它选择结果数据集中的所有 (*) 列。
  • FROM:指定从中检索数据的源表。在本例中,它是第一个表。
  • <join_type>:指定您要与第二个表执行的 JOIN 操作的特定类型。
  • <join_condition>:表示两个表应如何连接。它通常由两个表之间列的相等比较组成。

使用我们之前的例子,当您在customersorders表之间执行标准 JOIN 时,数据库会查找orders表(外键)的customer_id列和customers表(主键)的customer_id列中的匹配值。然后它将这些值匹配的行组合成一个结果集。

这是一个查询示例:

SELECT
*
FROM
customers
INNER JOIN orders
ON customers.customer_id = orders.customer_id;

以下是基于相关查询的结果集:

客户 ID姓名年龄电子邮件地址订单 ID成本订单日期
1珍妮30jenny@prisma.io1$150.004/1/2024
2阿德里安28adrian@prisma.io2$200.003/29/2024
1珍妮30jenny@prisma.io3$20.004/2/2024
3辛尼瓦25synniva@prisma.io4$320.004/4/2024

结果集根据customer_id列的匹配行,组合了orders表和customers表中的列。这允许您在客户信息旁边查看订单信息。

PostgreSQL 和 MySQL 中的 JOIN 类型

有时您可能想从每个表中获取不同的行。有不同类型的 JOIN 操作可以帮助您实现此目的。在本节中,我们将讨论 MySQL 和 PostgreSQL(两种关系型数据库)支持的传统 JOIN 以及 lateral JOIN(仅 PostgreSQL 支持)。

传统 JOIN

您将遇到的最常见的 JOIN 操作形式是 INNER JOINLEFT JOINRIGHT JOINFULL JOINCROSS JOIN。为了演示这些 JOIN 类型,我们将使用以下 customersorders(已修改)表:

客户 ID姓名年龄电子邮件地址
1珍妮30jenny@prisma.io
2阿德里安28adrian@prisma.io
3辛尼瓦25synniva@prisma.io
order_id(主键)成本订单日期customer_id(外键)
1$150.004/1/20241
2$200.003/29/20242
3$20.004/2/20241

INNER JOIN

INNER JOIN 是默认的 JOIN 类型,它只返回两个表中存在匹配的行。以下是customersorders表的关联查询:

SELECT
*
FROM
customers
INNER JOIN orders
ON customers.customer_id = orders.customer_id;

以下是使用INNER JOIN后的结果表:

客户 ID姓名年龄电子邮件地址订单 ID成本订单日期
1珍妮30jenny@prisma.io1$150.004/1/2024
1珍妮30jenny@prisma.io3$20.004/2/2024
2阿德里安28adrian@prisma.io2$200.003/29/2024

当以 Venn 图表示时,INNER JOIN 表示两个圆的重叠区域。换句话说,只包含两个表中都存在的值。

inner-join

LEFT JOIN

LEFT JOIN 返回使用 INNER JOIN 找到的所有行以及第一个表中的所有记录。以下是 customersorders 表的相关查询:

SELECT
*
FROM
customers
LEFT JOIN orders
ON customers.customer_id = orders.customer_id;

以下是使用LEFT JOIN后的结果表:

客户 ID姓名年龄电子邮件地址订单 ID成本订单日期
1珍妮30jenny@prisma.io1$150.004/1/2024
1珍妮30jenny@prisma.io3$20.004/2/2024
2阿德里安28adrian@prisma.io2$200.003/29/2024
3辛尼瓦25synniva@prisma.io

当以 Venn 图表示时,LEFT JOIN 表示整个左侧圆。换句话说,将包含两个表中的匹配值以及customers表中的所有记录。

left-join

RIGHT JOIN

RIGHT JOIN 返回使用 INNER JOIN 找到的所有行以及第二个表中的所有记录以下是 customersorders 表的相关查询:

SELECT
*
FROM
customers
RIGHT JOIN orders
ON customers.customer_id = orders.customer_id;

以下是使用RIGHT JOIN后的结果表:

客户 ID姓名年龄电子邮件地址订单 ID成本订单日期
1珍妮30jenny@prisma.io1$150.004/1/2024
1珍妮30jenny@prisma.io3$20.004/2/2024
2阿德里安28adrian@prisma.io2$200.003/29/2024

在这种情况下,结果与INNER JOIN相同,因为每个订单都有相应的客户。

当以 Venn 图表示时,RIGHT JOIN 表示整个右侧圆。换句话说,将包含两个表中的匹配值以及orders表中的所有记录。

right-join

FULL JOIN

FULL JOIN 在任一表中有匹配时返回所有行。以下是 PostgreSQL 中 customersorders 表的相关查询:

SELECT
*
FROM
customers
FULL JOIN orders
ON customers.customer_id = orders.customer_id;

MySQL 不原生支持FULL JOIN。作为一种变通方法,我们可以使用LEFT JOIN结合“反连接”(anti-JOIN),反连接是一种查找表中共同结果的 JOIN 操作(由NULL指定)。UNION ALL允许我们将它们组合在一起。

(SELECT *
FROM customers
LEFT JOIN orders ON customers.customer_id = orders.customer_id)
UNION ALL
(SELECT *
FROM customers
RIGHT JOIN orders ON customers.customer_id = orders.customer_id
WHERE customers.customer_id IS NULL );

以下是使用FULL JOIN后的结果表:

客户 ID姓名年龄电子邮件地址订单 ID成本订单日期
1珍妮30jenny@prisma.io1$150.004/1/2024
1珍妮30jenny@prisma.io3$20.004/2/2024
2阿德里安28adrian@prisma.io2$200.003/29/2024
3辛尼瓦25synniva@prisma.io

在这种情况下,结果将与LEFT JOIN相同,因为customers表中的每条记录都在orders表中有一个匹配项。

当以 Venn 图表示时,FULL JOIN 表示两个圆。换句话说,FULL JOIN 结合了两个表中的所有记录。

full-join

CROSS JOIN

CROSS JOIN 返回两个表的笛卡尔积,这意味着第一个表中的每一行都与第二个表中的每一行组合。在此语法中,结果是通过将第一个表中的每一行与第二个表中的每一行相加而形成的,如下所示:

t1.r1 + t2.r1
t1.r1 + t2.r2
t1.r1 + t2.r3
t1.r2 + t2.r1
t1.r2 + t2.r2
t1.r2 + t2.r3
t1.r3 + t2.r1
t1.r3 + t2.r2
t1.r3 + t2.r3

注意:在 MySQL 中,CROSS JOIN 的概念与 INNER JOIN 结合在一起。在数据指南中阅读更多信息。

使用 CROSS JOINcustomers 表中的每一行都与 orders 表中的每一行组合,总共产生 9 行。但是,我们不会显示结果表,因为 CROSS JOIN 无法准确地将客户与其各自的订单配对。

要了解有关 JOIN 操作的数据库特定详细信息,请查看数据指南中的这些附加页面:

LATERAL JOIN

与传统 JOIN 相比,Lateral JOIN 提供了不同的表组合语法。LATERAL JOIN 中,第二个表以子查询的形式呈现,JOIN 条件在子查询的WHERE子句中定义。

根据PostgreSQL 文档:

LATERAL 关键字可以放在子 SELECT FROM 项之前。这允许子 SELECT 引用其之前出现在 FROM 列表中的 FROM 项的列。(如果没有 LATERAL,每个子 SELECT 都是独立评估的,因此无法交叉引用任何其他 FROM 项。)”

换句话说,LATERAL JOIN 就像一个 foreach 循环,PostgreSQL 遍历结果集中的每一行,并使用每一行来评估子查询。

以下是customersorders表的关联查询:

SELECT *
FROM customers
LEFT JOIN LATERAL
(SELECT *
FROM orders
WHERE orders.customer_id = customers.customer_id ) AS ALIAS ON TRUE;

以下是使用LATERAL JOIN后的结果表:

客户 ID姓名年龄电子邮件地址订单 ID成本订单日期
1珍妮30jenny@prisma.io1$150.004/1/2024
1珍妮30jenny@prisma.io3$20.004/2/2024
2阿德里安28adrian@prisma.io2$200.003/29/2024
3辛尼瓦25synniva@prisma.io

在这种情况下,结果将与LEFT JOINFULL JOIN相同。

注意:LATERAL JOIN 受 Postgres 支持,但在 MySQL 8.0.14 及更高版本才受支持。

针对不同关系实现 JOIN

在选择合适的 JOIN 类型时,了解数据库关系非常重要。在数据库的上下文中,关系描述了数据库中表之间的关联。

在本节中,我们将演示一对一和一对多关系,并考虑为每种关系选择合适的 JOIN 类型。

一对一 (1-1)

在一对一关系中,一个表中的每条记录都与另一个表中的一条记录精确关联,反之亦然。以下是演示customersprofiles表之间一对一关系的 SQL 模式示例:

CREATE TABLE customers (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL
);
CREATE TABLE profiles (
name TEXT NOT NULL,
gender TEXT NOT NULL,
age TEXT NOT NULL,
customer_email TEXT NOT NULL,
CONSTRAINT fk_profiles_customer_email FOREIGN KEY (customer_email) REFERENCES customers (email) ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE UNIQUE INDEX customers_email_key ON customers(email);
CREATE UNIQUE INDEX profiles_customer_email_key ON profiles(customer_email);

在此示例中,我们使用 customers 表中的唯一字段 (email) 作为 profiles 表中的外键 (customer_email)。这是可能的,因为每个电子邮件都唯一标识一个客户。这表示一对一关系,因为每个客户只能有一个配置文件,并且一个配置文件只能属于一个客户。

在一对一关系中,JOIN 很简单。您可以使用传统的INNER JOIN来组合客户和他们的个人资料。这是一个查询示例:

SELECT
*
FROM
customers
INNER JOIN profile
ON customers.email = profiles.customer_email;

当对这两个表执行INNER JOIN时,我们将得到一个结果集,其中每一行都包含来自两个表的列,并根据关系标准进行匹配。这是一个结果集示例:

ID电子邮件姓名性别年龄
1meera@prisma.io米拉女性30
2xander@prisma.io桑德男性30
3zara@prisma.io扎拉女性32
4artemis@prisma.io阿耳特弥斯男性32

当您使用INNER JOIN和指定的连接条件连接customersprofiles表时,结果集中的每一行都代表一个客户及其相应的个人资料。换句话说,我们包含了customers表中的idemail列,以及profiles表中这些客户的namegenderage

一对多 (1-n)

在一对多关系中,一个表中的每条记录可以与另一个表中的多条记录关联。以下是一个 SQL 模式示例,演示了 usersposts 表之间的一对多关系:

CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL
);
CREATE TABLE posts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author_id INTEGER NOT NULL,
CONSTRAINT fk_posts_author_id FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE UNIQUE INDEX users_email_key ON users(email);

这意味着一个用户可以拥有多个帖子,但每个帖子只链接到一个用户。

在处理一对多关系时,传统的 JOIN 可能会导致数据重复。请考虑以下查询:

SELECT
user.id, user.email, post.id AS post_id, post.title AS post_title
FROM
user
INNER JOIN post
ON user.id = post.authorId;

以下是基于上述查询的结果集示例:

ID电子邮件帖子 ID帖子标题
1jenny@prisma.io1Prisma ORM 简介
1jenny@prisma.io2JOIN 简介
1jenny@prisma.io3SQL 简介
2meera@prisma.io4使用 Prisma 客户端

如您所见,在结果集中,Jenny 的 emailid 在她的每个帖子中都重复了多行。随着我们包含更多嵌套关系,结果集可能会因冗余数据而变得臃肿。这会降低查询效率(因为它需要更多带宽来传输结果),并且可能使数据难以解释。

为了解决这种重复,我们可以将LATERAL JOIN与 JSON 聚合结合使用。JSON 聚合通过将多个相关记录合并到一个 JSON 数组字段中来帮助避免重复,而LATERAL JOIN则有助于提高可读性并优化性能。

以下是使用LATERAL JOIN和 JSON 聚合的结果集示例:

ID电子邮件帖子标题
1jenny@prisma.io[{"post title": "Prisma ORM 简介"}, {"post title": "JOINs 简介"}, {"post title": "SQL 简介"}]
2meera@prisma.io[{"post title": "使用 Prisma 客户端"}]

如您所见,LATERAL JOIN 与 JSON 聚合结合使用可产生更清晰的结果集,没有不必要的重复。Jenny 的所有帖子都被合并到一个 JSON 数组中,将之前结果集中的三行转换为一行。

注意:在此上下文中不讨论多对多(m-n)关系,因为它描述了两个一对多关系,因此将支持相同类型的 JOIN

结论

总结一下,我们介绍了 JOIN 的概念以及它们如何在关系型数据库中组合相关数据。我们探讨了关系型数据库中数据的组织方式,以及主键和外键在形成表之间关系方面的重要性。然后,我们讨论了 MySQL 和 PostgreSQL 中不同类型的 JOIN,并确定了对于一对一和一对多关系应使用哪种 JOIN 类型,无论是传统 JOIN 还是 lateral JOIN。

关于作者
Grishma Patel

格里什玛·帕特尔 (Grishma Patel)

Grishma 是一位刚毕业的计算机科学专业学生,曾在 Prisma 担任开发者倡导实习生。
© . All rights reserved.