分享到

简介

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

在关系数据库中,连接(joins)提供了一种根据公共字段值组合两个或多个表中记录的方法。不同类型的连接可以根据如何处理不匹配的行来达到不同的结果。在本指南中,我们将讨论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表,每行都可能包含一个引用表中其他peoplemother列。自连接将允许您通过将表的第二个实例与第一个实例连接起来,其中这些值匹配,从而将这些不同的行拼接在一起。

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

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

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的行将被连接。由于使用了内连接,结果将只显示连接的行。由于查询使用了通配符*,因此将显示两个表的所有列。

这意味着table1中的id列和table2中的ident列都将显示,尽管它们因满足连接条件而具有完全相同的值。您可以通过在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子句的冗余。自然连接不指定任何要匹配的列。相反,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子句过滤数据行的比较具有许多共同特征。这两种构造都定义了必须求值为真的表达式才能考虑该行。因此,将额外的比较包含在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.idorder.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.idorder.customer_id匹配。
  2. 由于我们使用的是左连接,因此包含左表(customer)中任何不匹配的行,并用NULL值填充右表(order)中的列。
  3. 评估WHERE子句以删除任何order.product_id列的值不为12345的行。
  4. 仅显示SELECT列规范中列出的列。

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

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

结论

在本指南中,我们介绍了连接如何使关系数据库能够组合来自不同表的数据,以提供更有价值的答案。我们讨论了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;

PostgreSQL中的LATERAL关键字可以放在子-SELECT FROM项之前,并允许子-SELECT引用FROM列表中出现在它之前的FROM项的列。(如果没有LATERAL,每个子-SELECT将独立评估,因此不能交叉引用任何其他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

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