简介
在关系型数据库中,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;
以下是基于相关查询的结果集:
客户 ID | 姓名 | 年龄 | 电子邮件地址 | 订单 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
(已修改)表:
客户 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
后的结果表:
客户 ID | 姓名 | 年龄 | 电子邮件地址 | 订单 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
后的结果表:
客户 ID | 姓名 | 年龄 | 电子邮件地址 | 订单 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 | 空 | 空 | 空 |
当以 Venn 图表示时,LEFT JOIN
表示整个左侧圆。换句话说,将包含两个表中的匹配值以及customers
表中的所有记录。
RIGHT JOIN
RIGHT JOIN
返回使用 INNER JOIN
找到的所有行以及第二个表中的所有记录。以下是 customers
和 orders
表的相关查询:
SELECT*FROMcustomersRIGHT JOIN ordersON customers.customer_id = orders.customer_id;
以下是使用RIGHT JOIN
后的结果表:
客户 ID | 姓名 | 年龄 | 电子邮件地址 | 订单 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
后的结果表:
客户 ID | 姓名 | 年龄 | 电子邮件地址 | 订单 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 | 空 | 空 | 空 |
在这种情况下,结果将与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
后的结果表:
客户 ID | 姓名 | 年龄 | 电子邮件地址 | 订单 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 | 空 | 空 | 空 |
在这种情况下,结果将与LEFT JOIN
和FULL JOIN
相同。
注意:
LATERAL JOIN
受 Postgres 支持,但在 MySQL 8.0.14 及更高版本才受支持。
💡 Prisma ORM 让您无需考虑 SQL 操作的复杂性和底层细节,即可轻松查询表之间的关系。阅读文档以了解更多关于关系查询的信息。
针对不同关系实现 JOIN
在选择合适的 JOIN 类型时,了解数据库关系非常重要。在数据库的上下文中,关系描述了数据库中表之间的关联。
在本节中,我们将演示一对一和一对多关系,并考虑为每种关系选择合适的 JOIN 类型。
一对一 (1-1)
在一对一关系中,一个表中的每条记录都与另一个表中的一条记录精确关联,反之亦然。以下是演示customers
和profiles
表之间一对一关系的 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*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
表时,结果集中的每一行都代表一个客户及其相应的个人资料。换句话说,我们包含了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 | 电子邮件 | 帖子 ID | 帖子标题 |
---|---|---|---|
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 客户端 |
如您所见,在结果集中,Jenny 的 email
和 id
在她的每个帖子中都重复了多行。随着我们包含更多嵌套关系,结果集可能会因冗余数据而变得臃肿。这会降低查询效率(因为它需要更多带宽来传输结果),并且可能使数据难以解释。
为了解决这种重复,我们可以将LATERAL JOIN
与 JSON 聚合结合使用。JSON 聚合通过将多个相关记录合并到一个 JSON 数组字段中来帮助避免重复,而LATERAL JOIN
则有助于提高可读性并优化性能。
以下是使用LATERAL JOIN
和 JSON 聚合的结果集示例:
ID | 电子邮件 | 帖子标题 |
---|---|---|
1 | jenny@prisma.io | [{"post title": "Prisma ORM 简介"}, {"post title": "JOINs 简介"}, {"post title": "SQL 简介"}] |
2 | meera@prisma.io | [{"post title": "使用 Prisma 客户端"}] |
如您所见,LATERAL JOIN
与 JSON 聚合结合使用可产生更清晰的结果集,没有不必要的重复。Jenny 的所有帖子都被合并到一个 JSON 数组中,将之前结果集中的三行转换为一行。
注意:在此上下文中不讨论多对多(m-n)关系,因为它描述了两个一对多关系,因此将支持相同类型的 JOIN
结论
总结一下,我们介绍了 JOIN 的概念以及它们如何在关系型数据库中组合相关数据。我们探讨了关系型数据库中数据的组织方式,以及主键和外键在形成表之间关系方面的重要性。然后,我们讨论了 MySQL 和 PostgreSQL 中不同类型的 JOIN,并确定了对于一对一和一对多关系应使用哪种 JOIN 类型,无论是传统 JOIN 还是 lateral JOIN。
💡 在实际应用中,JOIN 可能会非常复杂,尤其是在查询中添加更多条件时,例如过滤和分页。Prisma ORM 让您轻松查询关系,并在底层为您找出有效的 JOIN 查询。阅读文档以了解更多关于关系查询的信息。