分享到

简介

从一致性、灵活性和某些类型的性能角度来看,将相关数据拆分到不同的表中可能是有益的。但是,当相关信息跨越多个表时,您仍然需要一种合理的方式来重新整合记录。

在关系型数据库中,连接 提供了一种基于公共字段值组合两个或多个表中的记录的方法。不同类型的连接可以实现不同的结果,具体取决于应如何处理不匹配的行。在本指南中,我们将讨论 PostgreSQL 提供的各种连接类型,以及如何使用它们来组合来自多个来源的表数据。

什么是连接?

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

连接的基本语法如下所示

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

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

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

连接的类型和连接条件决定了如何构建显示的每一行。这会影响来自每个表的行(有和没有在连接条件上匹配的行)会发生什么。

为了方便起见,许多连接将一个表的主键与第二个表上的关联外键匹配。尽管主键和外键仅由数据库系统用于维护一致性保证,但它们的关系通常使它们成为连接条件的良好候选者。

不同类型的连接

有多种类型的连接可用,每种连接都可能产生不同的结果。了解每种类型的构造方式将帮助您确定哪种类型适合不同的场景。

内连接

默认连接称为内连接。在 PostgreSQL 中,可以使用 INNER JOIN 或简写 JOIN 来指定。

这是一个典型的示例,演示了内连接的语法

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

内连接是最严格的连接类型,因为它仅显示通过组合每个表中的行创建的行。构成表中的任何在另一个表中没有匹配项的行都将从结果中删除。例如,如果第一个表的比较列中有一个值为“蓝色”,而第二个表中没有该值的记录,则该行将从输出中删除。

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

左连接

左连接 是一种连接,它显示内连接中找到的所有记录,以及来自第一个表的所有不匹配行。在 PostgreSQL 中,可以将其指定为 LEFT OUTER JOIN 或简写为 LEFT JOIN

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

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

左连接的构造方式是首先执行内连接,以从两个表中的所有匹配记录构造行。之后,还包括来自第一个表的不匹配记录。由于连接中的每一行都包含两个表的列,因此不匹配的列使用 NULL 作为第二个表中所有列的值。

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

右连接

右连接 是一种连接,它显示内连接中找到的所有记录,以及来自第二个表的所有不匹配行。在 PostgreSQL 中,可以将其指定为 RIGHT OUTER JOIN 或简写为 RIGHT JOIN

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

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

右连接的构造方式是首先执行内连接,以从两个表中的所有匹配记录构造行。之后,还包括来自第二个表的不匹配记录。由于连接中的每一行都包含两个表的列,因此不匹配的列使用 NULL 作为第一个表中所有列的值。

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

全连接

全连接 是一种连接,它显示内连接中找到的所有记录,以及来自两个组成表的所有不匹配行。在 PostgreSQL 中,可以将其指定为 FULL OUTER JOIN 或简写为 FULL JOIN

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

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

全连接的构造方式是首先执行内连接,以从两个表中的所有匹配记录构造行。之后,还包括来自两个表的不匹配记录。由于连接中的每一行都包含两个表的列,因此不匹配的列使用 NULL 作为不匹配的另一个表中所有列的值。

如果将结果表示为组成表的维恩图,则全连接允许您完全表示两个组成圆。两个圆的交集将具有由每个组成表提供的值。圆的重叠区域外部的部分将具有来自其所属表的值,并使用 NULL 填充在另一个表中找到的列。

交叉连接

还提供了一种称为 CROSS JOIN 的特殊连接。交叉连接不使用任何比较来确定每个表中的行是否相互匹配。相反,结果的构造方式是将第一个表中的每一行简单地添加到第二个表中的每一行。

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

例如,如果您有一个名为 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

自连接

自连接是任何将表的行与其自身组合的连接。可能不会立即显而易见它如何有用,但它实际上有许多常见的应用。

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

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

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

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

连接条件

在组合表时,连接条件确定如何将行匹配在一起以形成复合结果。基本前提是定义每个表中的列,这些列必须匹配才能在该行上发生连接。

ON 子句

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

ON 子句是最冗长的,但也是可用的连接条件中最灵活的。无论要组合的每个表的列名标准化程度如何,它都允许特异性。

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

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

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

这意味着将显示来自 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 列(每个表一个)),然后是 table1table2 提供的所有其他列。

NATURAL 子句

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

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 子句过滤数据行的比较具有许多共同特征。两种构造都定义了必须评估为 true 才能考虑该行的表达式。因此,在 WHERE 构造中包含其他比较与在连接子句本身中定义它们之间的区别并不总是直观的。

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

作为一个示例,假设我们有两个名为 customerorder 的表,我们需要将它们连接在一起。我们希望通过将 customer.id 列与 order.customer_id 列匹配来连接两个表。此外,我们对 order 表中 product_id 为 12345 的行感兴趣。

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

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

SELECT
customer.id AS customer_id,
customer.name,
order.id AS order_id,
order.product_id
FROM
customer
LEFT JOIN
order
ON
customer.id = order.customer_id AND order.product_id = 12345;

结果可能看起来像这样

customer_id | name | order_id | product_id
------------+----------+----------+------------
4380 | Acme Co | 480 | 12345
4380 | Acme Co | 182 | 12345
320 | Other Co | 680 | 12345
4380 | Acme Co | |
320 | Other Co | |
20 | Early Co | |
8033 | Big Co | |
(7 rows)

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

  1. customer 表中的任何行与 order 表组合,其中
    • customer.id 匹配 order.customer_id
    • order.product_id 匹配 12345
  2. 因为我们正在使用左连接,所以包括来自左表 (customer) 的任何不匹配行,并使用 NULL 值填充来自右表 (order) 的列。
  3. 仅显示 SELECT 列规范中列出的列。

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

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

SELECT
customer.id AS customer_id,
customer.name,
order.id AS order_id,
order.product_id
FROM
customer
LEFT JOIN
order
ON
customer.id = order.customer_id
WHERE
order.product_id = 12345;

这次,仅显示三行

customer_id | name | order_id | product_id
------------+----------+----------+------------
4380 | Acme Co | 480 | 12345
4380 | Acme Co | 182 | 12345
320 | Other Co | 680 | 12345
(3 rows)

比较的执行顺序是造成这些差异的原因。这次,PostgreSQL 像这样处理查询

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

这次,即使我们使用了左连接(left join),WHERE 子句仍然通过过滤掉所有不包含正确 product_id 的行来截断结果。因为任何不匹配的行都会将 product_id 设置为 NULL,这会移除所有由左连接填充的不匹配行。它还会移除通过连接条件匹配,但未通过第二轮检查的任何行。

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

结论

在本指南中,我们介绍了连接(joins)如何使关系数据库能够组合来自不同表的数据,以提供更有价值的答案。我们讨论了 PostgreSQL 支持的各种连接类型、每种类型如何组装其结果,以及在使用特定类型的连接时应预期的结果。之后,我们回顾了定义连接条件的不同方法,并研究了连接和 WHERE 子句之间的相互作用如何导致意外情况。

连接是关系数据库之所以强大和灵活,足以处理如此多不同类型查询的重要组成部分。使用逻辑边界组织数据,同时仍然能够在具体情况下以新颖的方式重新组合数据,这赋予了像 PostgreSQL 这样的关系数据库令人难以置信的通用性。学习如何在表之间执行这种拼接将使您能够创建更复杂的查询,并依靠数据库来创建数据的完整视图。

常见问题解答

是的,PostgreSQL 支持外连接。例如,您可以像下面这样使用 LEFT OUTER JOIN 或仅使用 LEFT JOIN

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

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

是的,可以在 PostgreSQL 中完成 CROSS JOIN。 语法看起来像这样

SELECT select_list
FROM t1
CROSS JOIN t2;

上面的示例将显示类似这样的 示例输出

是的,PostgreSQL 支持 全连接。它们可以指定为 FULL OUTER JOINFULL JOIN

语法看起来像这样

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

PostgreSQL 中的默认连接是 内连接,可以使用 INNER JOIN 或仅使用 JOIN 来指定。

语法是

SELECT
*
FROM
table_1
[INNER] JOIN table_2
ON table_1.id = table_2.table_1_id;
关于作者
Justin Ellingwood

Justin Ellingwood

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