简介
JOIN 允许您组合来自关系型数据库中多个表的相关数据。请继续阅读以了解更多关于 JOIN 是什么、它们如何工作以及如何有效地将它们用于不同关系的信息。
关系型数据库中数据的结构
在深入探讨 JOIN 之前,重要的是要理解关系型数据库的基础知识。关系型数据库提供了一种结构化和组织信息的方式。在关系型数据库中,数据被组织成表,列(属性)指定数据类型(想想字符串、整数等),而行包含记录(表中每列的值)。
让我们用一个包含客户信息的表来演示这个概念。在下表中,列或客户属性是 name
、age
和 email address
,行代表每个客户的 name
、age
和 email_address
。
姓名 | 年龄 | 电子邮件地址 |
---|---|---|
珍妮 | 30 | jenny@prisma.io |
艾德里安 | 28 | adrian@prisma.io |
西尼瓦 | 25 | synniva@prisma.io |
关系型数据库中的表通常会有一列被称为主键,它唯一标识每一行。为了了解这在实践中是如何工作的,让我们使用之前的例子:customers
表的主键将是一列用于每个客户的唯一 ID,称为 customer_id
。换句话说,没有客户会与另一个客户共享相同的 ID。
外键用于通过引用另一个表的主键来创建表之间的关系。为了演示外键的概念,假设我们有另一个名为 orders
的表,其中包含以下列:order_id
、cost
和 order_date
。我们可以通过包含 customer_id
列(外键)将 orders
表(图表中的左表)与 customers
表(图表中的右表)链接起来,将每个订单与相应的客户关联起来。
什么是 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*FROMcustomersINNER JOIN ordersON customers.customer_id = orders.customer_id;
以下是基于相关查询的结果集:
customer_id | 姓名 | 年龄 | 电子邮件地址 | order_id | 费用 | 订单日期 |
---|---|---|---|---|---|---|
1 | 珍妮 | 30 | jenny@prisma.io | 1 | $150.00 | 4/1/2024 |
2 | 艾德里安 | 28 | adrian@prisma.io | 2 | $200.00 | 3/29/2024 |
1 | 珍妮 | 30 | jenny@prisma.io | 3 | $20.00 | 4/2/2024 |
3 | 西尼瓦 | 25 | synniva@prisma.io | 4 | $320.00 | 4/4/2024 |
结果集基于 customer_id
列中匹配的行,组合了来自 orders
表和 customers
表的列。这使您可以查看订单信息以及客户信息。
PostgreSQL 和 MySQL 中的 JOIN 类型
有时您可能想要从每个表中获取不同的行。有不同类型的 JOIN 操作可以帮助您实现这一点。在本节中,我们将讨论 MySQL 和 PostgreSQL(两个关系型数据库)支持的传统 JOIN,以及 lateral JOIN(仅 PostgreSQL 支持)。
传统 JOIN
您将遇到的最常见的 JOIN 操作形式是 INNER JOIN
、LEFT JOIN
、RIGHT JOIN
、FULL JOIN
和 CROSS JOIN
。为了演示这些 JOIN 类型,我们将使用以下 customers
和 orders
(修改后的)表:
customer_id | 姓名 | 年龄 | 电子邮件地址 |
---|---|---|---|
1 | 珍妮 | 30 | jenny@prisma.io |
2 | 艾德里安 | 28 | adrian@prisma.io |
3 | 西尼瓦 | 25 | synniva@prisma.io |
order_id(主键) | 费用 | 订单日期 | customer_id(外键) |
---|---|---|---|
1 | $150.00 | 4/1/2024 | 1 |
2 | $200.00 | 3/29/2024 | 2 |
3 | $20.00 | 4/2/2024 | 1 |
INNER JOIN
INNER JOIN
是默认的 JOIN,并且仅当两个表之间存在匹配时才返回行。以下是 customers
表和 orders
表的相关查询:
SELECT*FROMcustomersINNER JOIN ordersON customers.customer_id = orders.customer_id;
以下是使用 INNER JOIN
后的结果表:
customer_id | 姓名 | 年龄 | 电子邮件地址 | order_id | 费用 | 订单日期 |
---|---|---|---|---|---|---|
1 | 珍妮 | 30 | jenny@prisma.io | 1 | $150.00 | 4/1/2024 |
1 | 珍妮 | 30 | jenny@prisma.io | 3 | $20.00 | 4/2/2024 |
2 | 艾德里安 | 28 | adrian@prisma.io | 2 | $200.00 | 3/29/2024 |
当用 Venn 图表示时,INNER JOIN
表示两个圆的重叠区域。换句话说,仅包含两个表中都存在的值。
LEFT JOIN
LEFT JOIN
返回使用 INNER JOIN
找到的所有行以及第一个表中的所有记录。以下是 customers
表和 orders
表的相关查询:
SELECT*FROMcustomersLEFT JOIN ordersON customers.customer_id = orders.customer_id;
以下是使用 LEFT JOIN
后的结果表:
customer_id | 姓名 | 年龄 | 电子邮件地址 | order_id | 费用 | 订单日期 |
---|---|---|---|---|---|---|
1 | 珍妮 | 30 | jenny@prisma.io | 1 | $150.00 | 4/1/2024 |
1 | 珍妮 | 30 | jenny@prisma.io | 3 | $20.00 | 4/2/2024 |
2 | 艾德里安 | 28 | adrian@prisma.io | 2 | $200.00 | 3/29/2024 |
3 | 西尼瓦 | 25 | synniva@prisma.io | NULL | NULL | NULL |
当用 Venn 图表示时,LEFT JOIN
表示整个左圆。换句话说,来自两个表的匹配值将与 customers
表中的所有记录一起包含在内。
RIGHT JOIN
RIGHT JOIN
返回使用 INNER JOIN
找到的所有行以及第二个表中的所有记录。以下是 customers
表和 orders
表的相关查询:
SELECT*FROMcustomersRIGHT JOIN ordersON customers.customer_id = orders.customer_id;
以下是使用 RIGHT JOIN
后的结果表:
customer_id | 姓名 | 年龄 | 电子邮件地址 | order_id | 费用 | 订单日期 |
---|---|---|---|---|---|---|
1 | 珍妮 | 30 | jenny@prisma.io | 1 | $150.00 | 4/1/2024 |
1 | 珍妮 | 30 | jenny@prisma.io | 3 | $20.00 | 4/2/2024 |
2 | 艾德里安 | 28 | adrian@prisma.io | 2 | $200.00 | 3/29/2024 |
在本例中,结果与 INNER JOIN
相同,因为每个订单都有一个对应的客户。
当用 Venn 图表示时,RIGHT JOIN
表示整个右圆。换句话说,来自两个表的匹配值将与 orders
表中的所有记录一起包含在内。
FULL JOIN
FULL JOIN
在任一表中存在匹配项时返回所有行。以下是 PostgreSQL 中 customers
表和 orders
表的相关查询:
SELECT*FROMcustomersFULL JOIN ordersON customers.customer_id = orders.customer_id;
MySQL 本身不支持 FULL JOIN
。作为一种解决方法,我们可以使用 LEFT JOIN
结合“anti-JOIN”,这是一种 JOIN 操作,用于查找表之间不常见的(由 NULL
指定)结果。UNION ALL
允许我们将它们组合在一起。
(SELECT *FROM customersLEFT JOIN orders ON customers.customer_id = orders.customer_id)UNION ALL(SELECT *FROM customersRIGHT JOIN orders ON customers.customer_id = orders.customer_idWHERE customers.customer_id IS NULL );
以下是使用 FULL JOIN
后的结果表:
customer_id | 姓名 | 年龄 | 电子邮件地址 | order_id | 费用 | 订单日期 |
---|---|---|---|---|---|---|
1 | 珍妮 | 30 | jenny@prisma.io | 1 | $150.00 | 4/1/2024 |
1 | 珍妮 | 30 | jenny@prisma.io | 3 | $20.00 | 4/2/2024 |
2 | 艾德里安 | 28 | adrian@prisma.io | 2 | $200.00 | 3/29/2024 |
3 | 西尼瓦 | 25 | synniva@prisma.io | NULL | NULL | NULL |
在本例中,结果将与 LEFT JOIN
相同,因为 customers
表中的每个记录在 orders
表中都有匹配项。
当用 Venn 图表示时,FULL JOIN
表示两个圆。换句话说,FULL JOIN
组合了来自两个表的所有记录。
CROSS JOIN
CROSS JOIN
返回两个表的笛卡尔积,这意味着第一个表中的每一行都与第二个表中的每一行组合在一起。在此语法中,结果是通过将第一个表中的每一行与第二个表中的每一行相加来形成的,如下所示:
t1.r1 + t2.r1t1.r1 + t2.r2t1.r1 + t2.r3t1.r2 + t2.r1t1.r2 + t2.r2t1.r2 + t2.r3t1.r3 + t2.r1t1.r3 + t2.r2t1.r3 + t2.r3
注意: 在 MySQL 中,
CROSS JOIN
的概念与INNER JOIN
相结合。在数据指南中阅读更多内容。
使用 CROSS JOIN
,customers
表中的每一行都与 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 customersLEFT JOIN LATERAL(SELECT *FROM ordersWHERE orders.customer_id = customers.customer_id ) AS ALIAS ON TRUE;
以下是使用 LATERAL JOIN
后的结果表:
customer_id | 姓名 | 年龄 | 电子邮件地址 | order_id | 费用 | 订单日期 |
---|---|---|---|---|---|---|
1 | 珍妮 | 30 | jenny@prisma.io | 1 | $150.00 | 4/1/2024 |
1 | 珍妮 | 30 | jenny@prisma.io | 3 | $20.00 | 4/2/2024 |
2 | 艾德里安 | 28 | adrian@prisma.io | 2 | $200.00 | 3/29/2024 |
3 | 西尼瓦 | 25 | synniva@prisma.io | NULL | NULL | NULL |
在本例中,结果将与 LEFT JOIN
和 FULL JOIN
相同。
注意: Postgres 支持
LATERAL JOIN
,但仅在 MySQL 版本 > 8 中支持
💡 Prisma ORM 使查询表之间的关系变得容易,而无需考虑 SQL 操作的复杂性和底层复杂性。阅读文档以了解更多关于关系查询的信息。
为不同的关系实现 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*FROMcustomersINNER JOIN profileON customers.email = profiles.customer_email;
当在两个表之间执行 INNER JOIN
时,我们将获得一个结果集,其中每行都包含来自两个表的列,并根据关系条件进行匹配。以下是一个示例结果集:
ID | 电子邮件 | 姓名 | 性别 | 年龄 |
---|---|---|---|---|
1 | meera@prisma.io | 米拉 | 女 | 30 |
2 | xander@prisma.io | 赞德 | 男 | 30 |
3 | zara@prisma.io | 扎拉 | 女 | 32 |
4 | artemis@prisma.io | 阿耳忒弥斯 | 男 | 32 |
当您使用 INNER JOIN
和指定的连接条件连接 customers
表和 profiles
表时,结果集中的每一行都代表一个客户及其对应的 profile。换句话说,我们包括来自 customers
表的 id
和 email
列,以及来自 profiles
表的这些客户的 name
、gender
和 age
。
一对多 (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 可能会导致数据重复。考虑以下查询:
SELECTuser.id, user.email, post.id AS post_id, post.title AS post_titleFROMuserINNER JOIN postON user.id = post.authorId;
以下是基于上述查询的示例结果集:
ID | 电子邮件 | post_id | post_title |
---|---|---|---|
1 | jenny@prisma.io | 1 | Prisma ORM 简介 |
1 | jenny@prisma.io | 2 | JOIN 简介 |
1 | jenny@prisma.io | 3 | SQL 简介 |
2 | meera@prisma.io | 4 | 使用 Prisma Client |
正如您在结果集中看到的那样,珍妮的 email
和 id
在多行中为她的每个帖子重复。随着我们加入更多嵌套关系,结果集可能会因冗余数据而变得臃肿。这使得查询效率降低(因为它需要更多带宽来传输结果),并且可能使数据更难以解释。
为了解决这种重复问题,我们可以将 LATERAL JOIN
与 JSON 聚合配对。JSON 聚合通过将多个相关记录整合为单个字段(JSON 数组)来帮助避免重复,而 LATERAL JOIN
有助于提高可读性并可以优化性能。
以下是使用 LATERAL JOIN
与 JSON 聚合的结果集示例:
ID | 电子邮件 | post_titles |
---|---|---|
1 | jenny@prisma.io | [{"post title": "Prisma ORM 简介"}, {"post title": "JOIN 简介"}, {"post title": "SQL 简介"}] |
2 | meera@prisma.io | [{"post title": "使用 Prisma Client"}] |
如您所见,LATERAL JOIN
与 JSON 聚合相结合,可产生更简洁的结果集,且没有不必要的重复。珍妮的每个帖子都被整合到一个 JSON 数组中,将之前结果集中的三行转换为一行。
注意: 多对多 (m-n) 关系在此上下文中未讨论,因为它描述了两个 1-n 关系,因此将通过相同类型的 JOIN 来支持
结论
总而言之,我们介绍了 JOIN(连接)的概念,以及它们如何允许我们组合来自关系数据库中相关表的数据。我们探讨了数据在关系数据库中是如何组织的,以及主键和外键在形成表之间关系中的重要性。然后,我们讨论了 MySQL 和 PostgreSQL 中不同类型的连接,并确定了适用于一对一和一对多关系的适当连接类型,无论是传统的还是横向的。
💡 在实际应用中,JOIN(连接)可能非常复杂,特别是当您向查询添加更多条件时,例如过滤和分页。Prisma ORM 让您可以轻松查询关系,并在底层为您计算出有效的 JOIN(连接)查询。阅读文档以了解更多关于关系查询的信息。