分享到

简介

JOINs header image

JOIN 允许您组合来自关系型数据库中多个表的相关数据。请继续阅读以了解更多关于 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>:表示应如何连接这两个表。它通常由来自两个表的列之间的相等比较组成。

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

这是一个示例查询:

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

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

customer_id姓名年龄电子邮件地址order_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(修改后的)表:

customer_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,并且仅当两个表之间存在匹配时才返回行。以下是 customers 表和 orders 表的相关查询:

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

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

customer_id姓名年龄电子邮件地址order_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 找到的所有行以及第一个表中的所有记录。以下是 customers 表和 orders 表的相关查询:

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

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

customer_id姓名年龄电子邮件地址order_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.ioNULLNULLNULL

当用 Venn 图表示时,LEFT JOIN 表示整个左圆。换句话说,来自两个表的匹配值将与 customers 表中的所有记录一起包含在内。

left-join

RIGHT JOIN

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

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

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

customer_id姓名年龄电子邮件地址order_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 中 customers 表和 orders 表的相关查询:

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 后的结果表:

customer_id姓名年龄电子邮件地址order_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.ioNULLNULLNULL

在本例中,结果将与 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 迭代结果集中的每一行,并使用每一行来评估子查询。

以下是 customers 表和 orders 表的相关查询:

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

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

customer_id姓名年龄电子邮件地址order_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.ioNULLNULLNULL

在本例中,结果将与 LEFT JOINFULL JOIN 相同。

注意: Postgres 支持 LATERAL JOIN,但仅在 MySQL 版本 > 8 中支持

为不同的关系实现 JOIN

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

在本节中,我们将说明一对一和一对多关系,以及为每种关系选择适当的 JOIN 类型的注意事项。

一对一 (1-1)

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

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)。这是可能的,因为每个电子邮件都唯一标识一个客户。这表示一对一关系,因为每个客户只能有一个 profile,而一个 profile 只能属于一个客户。

在一对一关系中,JOIN 非常简单。您可以使用传统的 INNER JOIN 将客户与其 profile 组合在一起。这是一个示例查询:

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 和指定的连接条件连接 customers 表和 profiles 表时,结果集中的每一行都代表一个客户及其对应的 profile。换句话说,我们包括来自 customers 表的 idemail 列,以及来自 profiles 表的这些客户的 namegenderage

一对多 (1-n)

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

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电子邮件post_idpost_title
1jenny@prisma.io1Prisma ORM 简介
1jenny@prisma.io2JOIN 简介
1jenny@prisma.io3SQL 简介
2meera@prisma.io4使用 Prisma Client

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

为了解决这种重复问题,我们可以将 LATERAL JOIN 与 JSON 聚合配对。JSON 聚合通过将多个相关记录整合为单个字段(JSON 数组)来帮助避免重复,而 LATERAL JOIN 有助于提高可读性并可以优化性能。

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

ID电子邮件post_titles
1jenny@prisma.io[{"post title": "Prisma ORM 简介"}, {"post title": "JOIN 简介"}, {"post title": "SQL 简介"}]
2meera@prisma.io[{"post title": "使用 Prisma Client"}]

如您所见,LATERAL JOIN 与 JSON 聚合相结合,可产生更简洁的结果集,且没有不必要的重复。珍妮的每个帖子都被整合到一个 JSON 数组中,将之前结果集中的三行转换为一行。

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

结论

总而言之,我们介绍了 JOIN(连接)的概念,以及它们如何允许我们组合来自关系数据库中相关表的数据。我们探讨了数据在关系数据库中是如何组织的,以及主键和外键在形成表之间关系中的重要性。然后,我们讨论了 MySQL 和 PostgreSQL 中不同类型的连接,并确定了适用于一对一和一对多关系的适当连接类型,无论是传统的还是横向的。

关于作者
Grishma Patel

Grishma Patel

Grishma 是一位刚毕业的计算机科学专业学生,曾在 Prisma 担任开发者布道师实习生。