分享

介绍

尽管将数据分离到不同的表中对于性能和一致性目的通常很有用,但您经常需要查询多个表中的数据才能满足某些请求。联接表是一种通过基于公共字段值匹配每个记录来组合来自多个表的数据的方法。

有几种不同类型的联接,它们提供了各种组合表记录的方法。在本文中,我们将介绍 MySQL 如何实现联接,并讨论每种联接最适合的场景。

什么是联接?

简而言之,联接是显示来自多个表的数据的方法。它们通过根据某些列中的匹配值将来自不同源的记录拼接在一起来做到这一点。每行结果都包含来自第一个表的记录以及来自第二个表的记录,这些记录基于每个表中一个或多个列具有相同的值。

联接的基本语法如下

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

在联接中,每行结果都是通过包含第一个表的所有列,然后包含第二个表的所有列来构建的。查询的SELECT部分可用于指定要显示的精确列。

如果用于比较的列中的值不唯一,则可能会从原始表中构造多行。例如,假设您有一个来自第一个表的列,该列在比较列中具有两个值为“red”的记录。与之匹配的是来自第二个表的列,该列在该值下具有三行。联接将为该值生成六行,代表可以实现的各种组合。

联接类型和联接条件决定了如何构建显示的每一行。这会影响每个表中没有在联接条件上进行匹配的行以及进行匹配的行会发生什么。

为了方便起见,许多联接将一个表的主键与第二个表上的关联外键进行匹配。虽然主键和外键仅由数据库系统用来维护一致性保证,但它们之间的关系通常使它们成为联接条件的良好选择。

不同类型的联接

有各种类型的联接可用,每种联接可能会产生不同的结果。了解每种类型的构建方式将帮助您确定哪种联接适用于不同的场景。

内部联接和交叉联接

默认联接称为内部联接。在 MySQL 中,可以使用 INNER JOIN、仅使用 JOIN 或使用 CROSS JOIN 来指定此联接。对于其他数据库系统,INNER JOINCROSS JOIN 通常是两个不同的概念,但 MySQL 在同一个结构中实现它们。

以下是一个演示内部联接语法的典型示例

SELECT
*
FROM
table_1
[INNER] JOIN table_2
ON table_1.id = table_2.table_1_id;

内部联接是最严格的联接类型,因为它只显示通过组合来自每个表的行而创建的行。构成表的任何行,如果在另一个表中没有匹配的对应行,就会从结果中删除。例如,如果第一个表在比较列中有一个值为“blue”的行,而第二个表中没有包含该值的记录,则该行将从输出中删除。

如果将结果表示为组成表的维恩图,则内部联接允许您表示两个圆圈的重叠区域。只存在于其中一个表中的任何值都不会显示。

如上所述,MySQL 还使用此格式来生成交叉联接。在 MySQL 中,您可以使用没有匹配条件的内部联接来生成交叉联接。交叉联接不使用任何比较来确定每个表中的行是否匹配。相反,结果是通过简单地将第一个表中的每一行添加到第二个表中的每一行来构建的。

这会生成两个或多个表的行组成的笛卡尔积。实际上,这种联接类型无条件地组合了来自每个表中的行。因此,如果每个表都有三行,则结果表将有九行,包含来自两个表的所有列。

例如,如果您有一个名为 t1 的表与一个名为 t2 的表结合,每个表都有 r1r2r3 行,结果将是九行组合在一起,如下所示

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 中,这可以指定为 LEFT OUTER JOINLEFT JOIN

左联接的基本语法遵循以下模式

SELECT
*
FROM
table_1
LEFT JOIN table_2
ON table_1.id = table_2.table_1_id;

左联接是通过首先执行内联接来构建所有两个表中匹配记录的行来构建的。之后,第一个表中的未匹配记录也被包含在内。由于联接中的每一行都包含两个表的列,因此未匹配的列使用 NULL 作为第二个表中所有列的值。

如果您将结果表示为组件表的维恩图,则左联接允许您表示整个左圆。左圆中由两个圆之间的交集表示的部分将具有由右表补充的额外数据。

右联接

右联接是一种联接,它显示在内联接中找到的所有记录,加上第二个表中所有未匹配的行。在 MySQL 中,这可以指定为 RIGHT OUTER JOINRIGHT JOIN

右联接的基本语法遵循以下模式

SELECT
*
FROM
table_1
RIGHT JOIN table_2
ON table_1.id = table_2.table_1_id;

右联接是通过首先执行内联接来构建所有两个表中匹配记录的行来构建的。之后,第二个表中的未匹配记录也被包含在内。由于联接中的每一行都包含两个表的列,因此未匹配的列使用 NULL 作为第一个表中所有列的值。

如果您将结果表示为组件表的维恩图,则右联接允许您表示整个右圆。右圆中由两个圆之间的交集表示的部分将具有由左表补充的额外数据。

出于可移植性的原因,MySQL 建议您尽可能使用左联接而不是右联接。

全联接

全联接是一种联接,它显示在内联接中找到的所有记录,加上两个组件表中所有未匹配的行。MySQL 本身没有实现全联接,但我们可以使用一些技巧来模拟这种行为。

要复制全外联接的结果,我们将执行左联接以获取两个表共享的所有结果和左表中的所有未匹配行。然后,我们将使用 UNION ALL 集操作符 将这些结果与右表的“反联接”组合在一起。“反联接”是一种联接操作,它专门查找表之间共有的结果。

全联接的基本语法遵循以下模式

( SELECT
*
FROM
table_1
LEFT JOIN table_2
ON table_1.id = table_2.table_1_id
)
UNION ALL
( SELECT
*
FROM
table_1
RIGHT JOIN table_2
ON table_1.id = table_2.table_1_id
WHERE table_1.id IS NULL
);

由于联接中的每一行都包含两个表的列,因此未匹配的列使用 NULL 作为未匹配的其他表中所有列的值。

如果您将结果表示为组件表的维恩图,则全联接允许您完全表示两个组件圆。两个圆的交集将具有每个组件表提供的值。圆形外部区域部分将具有其所属表的相应值,使用 NULL 填充其他表中找到的列。

自联接

自联接是将表的行与自身组合的任何联接。可能没有立即清楚这如何有用,但它实际上有许多常见的应用。

通常,表描述实体,这些实体可以在彼此的关系中扮演多种角色。例如,如果您有一个 people 表,每一行都可能包含一个 mother 列,该列引用表中的其他 people。自联接将允许您通过将表的第二个实例与第一个实例连接起来来缝合这些不同的行,其中这些值匹配。

由于自联接两次引用同一个表,因此需要表别名来消除引用歧义。例如,在上面的示例中,您可以使用别名 people AS childrenpeople AS motherspeople 表的两个实例连接起来。这样,您就可以在定义联接条件时指定要引用的表实例。

以下是一个示例,这次表示员工和经理之间的关系

SELECT
*
FROM
people AS employee
JOIN people AS manager
ON employee.manager_id = manager.id;

联接条件

在组合表时,联接条件决定如何将行匹配在一起以形成复合结果。基本前提是定义每个表中必须匹配的列,以便在该行上执行联接。

ON 子句

定义表联接条件的最标准方法是使用 ON 子句。 ON 子句使用等号来指定每个表中将被比较的精确列,以确定何时可能发生联接。MySQL 使用提供的列将来自每个表的行缝合在一起。

ON 子句是最详细的,但也是可用的联接条件中最灵活的。它允许在组合的每个表的列名标准化程度如何的情况下进行特异性。

ON 子句的基本语法如下所示

SELECT
*
FROM
table1
JOIN
table2
ON
table1.id = table2.ident;

在这里, table1table2 的行将在 table1id 列与 table2ident 列匹配时联接。由于使用了内联接,因此结果将仅显示已联接的行。由于查询使用通配符 * 字符,因此将显示来自两个表的所有列。

这意味着 table1id 列和 table2ident 列都将被显示,即使它们由于满足联接条件而具有完全相同的值。您可以通过在 SELECT 列列表中列出您希望显示的精确列来避免这种重复。

USING 子句

USING 子句是指定 ON 子句条件的简写,可以在被比较的列在两个表中具有相同名称时使用。 USING 子句采用一个列表(括在括号中),该列表包含应比较的共享列名。

USING 子句的一般语法使用以下格式

SELECT
*
FROM
table1
JOIN
table2
USING
(id, state);

此联接在两个表都共享的两个列(idstate)都具有匹配值时将 table1table2 组合在一起。

可以使用 ON 以更详细的方式表达相同的联接,如下所示

SELECT
*
FROM
table1
JOIN
table2
ON
table1.id = table2.id AND table1.state = table2.state;

虽然上面的两个联接都将导致构建具有相同数据存在的相同行,但它们将以略微不同的方式显示。虽然 ON 子句包含来自两个表的所有列,但 USING 子句会抑制重复的列。因此,不是存在两个单独的 id 列和两个单独的 state 列(每个表一个),结果将只有一个 id 列和一个 state 列,后面跟着 table1table2 提供的所有其他列。

NATURAL 子句

NATURAL 子句是另一种简写,可以进一步减少 USING 子句的冗长性。 NATURAL 联接不指定任何要匹配的列。相反,MySQL 将根据每个数据库中具有匹配列的所有列自动联接表。

NATURAL 联接子句的一般语法如下所示

SELECT
*
FROM
table1
NATURAL JOIN
table2;

假设 table1table2 都包含名为 idstatecompany 的列,则上述查询等效于使用 ON 子句的以下查询。

SELECT
*
FROM
table1
JOIN
table2
ON
table1.id = table2.id AND table1.state = table2.state AND table1.company = table2.company;

以及使用 USING 子句的以下查询。

SELECT
*
FROM
table1
JOIN
table2
USING
(id, state, company);

USING 子句类似,NATURAL 子句会抑制重复列,因此结果中每个连接列只会出现一次。

虽然 NATURAL 子句可以减少查询的冗余,但在使用它时必须谨慎。由于用于连接表的列是自动计算的,如果组件表中的列发生更改,则由于新的连接条件,结果可能会有很大差异。

连接条件和 WHERE 子句

连接条件与使用 WHERE 子句过滤数据行的比较具有许多共同特征。这两种结构都定义了表达式,该表达式必须计算为真才能使该行被考虑在内。因此,将额外的比较包含在 WHERE 结构中与在连接子句本身内定义它们之间的区别并不总是直观的。

为了理解由此产生的差异,我们必须查看 MySQL 处理查询的不同部分的顺序。在这种情况下,连接条件中的谓词首先被处理以在内存中构建虚拟连接表。在此阶段之后,将评估 WHERE 子句中的表达式以过滤结果行。

例如,假设我们有两个名为 customersorders 的表需要连接在一起。我们希望通过匹配 customers.id 列与 orders.customer_id 列来连接这两个表。此外,我们还对 orders 表中具有 product_id 为 12345 的行感兴趣。

鉴于上述要求,我们有两个条件需要关心。但是,我们表达这些条件的方式将决定我们得到的结果。

首先,让我们将两者都用作 LEFT JOIN 的连接条件。

SELECT
customers.id AS customers_id,
customers.name,
orders.id AS orders_id,
orders.product_id
FROM
customers
LEFT JOIN
orders
ON
customers.id = orders.customers_id AND orders.product_id = 12345;

结果可能看起来像这样。

+--------------+----------+-----------+------------+
customers_id | name | orders_id | product_id |
+--------------+----------+-----------+------------+
20 | Early Co | NULL | NULL |
320 | Other Co | 680 | 12345 |
4380 | Acme Co | 182 | 12345 |
4380 | Acme Co | 480 | 12345 |
8033 | Big Co | NULL | NULL |
+--------------+----------+-----------+------------+
5 rows in set (0.00 sec)

MySQL 通过执行以下操作得出此结果。

  1. customers 表中的任何行与 orders 表中的行组合在一起,其中
    • customers.idorders.customers_id 相匹配。
    • orders.product_id 与 12345 相匹配。
  2. 因为我们使用的是左连接,所以包括左侧表 (customers) 中的任何不匹配行,并使用 NULL 值填充右侧表 (orders) 中的列。
  3. 仅显示 SELECT 列规范中列出的列。

结果是,我们所有连接的行都匹配我们正在寻找的两个条件。但是,左连接会导致 MySQL 还包括第一个表中未满足连接条件的任何行。这会导致一些似乎不符合查询明显意图的“剩余”行。

如果我们将第二个查询 (orders.product_id = 12345) 移动到 WHERE 子句中,而不是将其包含为连接条件,我们将得到不同的结果。

SELECT
customers.id AS customers_id,
customers.name,
orders.id AS orders_id,
orders.product_id
FROM
customers
LEFT JOIN
orders
ON
customers.id = orders.customers_id
WHERE
orders.product_id = 12345;

这次只显示了三行。

+--------------+----------+-----------+------------+
customers_id | name | orders_id | product_id |
+--------------+----------+-----------+------------+
4380 | Acme Co | 182 | 12345 |
4380 | Acme Co | 480 | 12345 |
320 | Other Co | 680 | 12345 |
+--------------+----------+-----------+------------+
3 rows in set (0.00 sec)

比较执行的顺序是造成这些差异的原因。这次,MySQL 处理查询的方式如下。

  1. customers 表中的任何行与 orders 表中的行组合在一起,其中 customers.idorders.customers_id 相匹配。
  2. 因为我们使用的是左连接,所以包括左侧表 (customers) 中的任何不匹配行,并使用 NULL 值填充右侧表 (orders) 中的列。
  3. 评估 WHERE 子句以删除 orders.product_id 列的值不为 12345 的任何行。
  4. 仅显示 SELECT 列规范中列出的列。

这次,即使我们使用的是左连接,WHERE 子句也会通过过滤掉所有没有正确 product_id 的行来截断结果。因为任何不匹配的行都会将 product_id 设置为 NULL,这会删除左连接填充的所有不匹配行。它还会删除所有通过连接条件匹配但在第二轮检查中未通过的任何行。

了解 MySQL 用于执行查询的基本过程可以帮助您在处理数据时避免一些容易犯但难以调试的错误。

结论

在本文中,我们讨论了什么是连接以及 MySQL 如何实现它们作为组合来自多个表的记录的一种方式。我们介绍了可用的不同类型的连接以及 ONWHERE 子句等不同条件如何影响数据库构建结果的方式。

随着您对连接越来越熟悉,您将能够将它们作为您工具箱的常规部分使用,以从各种来源提取数据并拼接信息片段,以创建一个更完整的画面。连接有助于将组织原则和性能考虑因素可能分离的数据整合在一起。学习如何有效地使用连接可以帮助您整合数据,无论它在系统中如何组织。

作者简介
Justin Ellingwood

Justin Ellingwood

Justin 从 2013 年开始撰写有关数据库、Linux、基础设施和开发人员工具的文章。他现在和妻子以及两只兔子住在柏林。他通常不必以第三人称写作,这对于所有相关方来说都是一种解脱。