简介
将相关数据拆分到不同的表中,从一致性、灵活性和某些类型的性能角度来看是有益的。但是,当相关信息跨越多个表时,您仍然需要一种合理的方式来重新整合记录。
在关系型数据库中,连接(joins)提供了一种基于公共字段值组合两个或多个表中记录的方式。不同类型的连接可以根据如何处理不匹配的行来达到不同的结果。在本指南中,我们将讨论 PostgreSQL 提供的各种连接类型,以及如何使用它们来组合来自多个源的表数据。
什么是连接?
简而言之,连接(joins)是一种显示来自多个表数据的方式。它们通过基于某些列中的匹配值,将不同来源的记录拼接在一起。每个结果行都包含来自第一个表的一条记录,与来自第二个表的一行组合而成,基于每个表中的一个或多个列具有相同的值。
连接的基本语法如下:
SELECT*FROM<first_table><join_type> <second_table><join_condition>;
在连接中,每个结果行都由第一个表的所有列以及第二个表的所有列构成。SELECT
查询部分可以用来指定您希望显示的精确列。
如果用于比较的列中的值不唯一,则可以从原始表中构造多行。例如,假设您从第一个表比较的列中有两条记录的值为“red”。与此匹配的是第二个表中的一列,该列有三行具有该值。连接将为该值生成六个不同的行,表示可以实现的不同组合。
连接的类型和连接条件决定了每个显示行的构造方式。这会影响每个表中那些有匹配或没有匹配连接条件的行的处理方式。
为了方便起见,许多连接将一个表的主键与第二个表中的相关外键进行匹配。尽管主键和外键仅由数据库系统用于维护一致性保证,但它们的关系通常使其成为连接条件的好选择。
不同类型的连接
有多种类型的连接可用,每种都可能产生不同的结果。了解每种类型的构造方式将有助于您确定哪种类型适合不同的场景。
内连接
默认的连接称为内连接(inner join)。在 PostgreSQL 中,这可以通过使用INNER JOIN
或简单地使用JOIN
来指定。
下面是一个演示内连接语法的典型示例:
SELECT*FROMtable_1[INNER] JOIN table_2ON table_1.id = table_2.table_1_id;
内连接是最严格的连接类型,因为它只显示由每个表中的行组合创建的行。构成表中任何在另一个表中没有匹配对应项的行都将从结果中删除。例如,如果第一个表在比较列中有一个值“blue”,而第二个表中没有具有该值的记录,则该行将从输出中删除。
如果您将结果表示为组件表的维恩图,内连接允许您表示两个圆的重叠区域。只存在于其中一个表中的值都不会显示。
左连接
左连接是一种连接,它显示内连接中找到的所有记录,以及来自第一个表的所有不匹配行。在 PostgreSQL 中,这可以指定为LEFT OUTER JOIN
或仅为LEFT JOIN
。
左连接的基本语法遵循以下模式:
SELECT*FROMtable_1LEFT JOIN table_2ON table_1.id = table_2.table_1_id;
左连接首先执行内连接以从两个表中所有匹配的记录构建行。然后,第一个表中不匹配的记录也会被包含进来。由于连接中的每一行都包含两个表的列,不匹配的列将使用NULL
作为第二个表中所有列的值。
如果您将结果表示为组件表的维恩图,左连接允许您表示整个左圆。左圆中由两个圆的交集表示的部分将有右表补充的额外数据。
右连接
右连接是一种连接,它显示内连接中找到的所有记录,以及来自第二个表的所有不匹配行。在 PostgreSQL 中,这可以指定为RIGHT OUTER JOIN
或仅为RIGHT JOIN
。
右连接的基本语法遵循以下模式:
SELECT*FROMtable_1RIGHT JOIN table_2ON table_1.id = table_2.table_1_id;
右连接首先执行内连接以从两个表中所有匹配的记录构建行。然后,第二个表中不匹配的记录也会被包含进来。由于连接中的每一行都包含两个表的列,不匹配的列将使用NULL
作为第一个表中所有列的值。
如果您将结果表示为组件表的维恩图,右连接允许您表示整个右圆。右圆中由两个圆的交集表示的部分将有左表补充的额外数据。
全连接
全连接是一种连接,它显示内连接中找到的所有记录,以及来自两个组件表的所有不匹配行。在 PostgreSQL 中,这可以指定为FULL OUTER JOIN
或仅为FULL JOIN
。
全连接的基本语法遵循以下模式:
SELECT*FROMtable_1FULL JOIN table_2ON table_1.id = table_2.table_1_id;
全连接首先执行内连接以从两个表中所有匹配的记录构建行。然后,两个表中不匹配的记录也会被包含进来。由于连接中的每一行都包含两个表的列,不匹配的列将使用NULL
作为不匹配的另一个表中所有列的值。
如果您将结果表示为组件表的维恩图,全连接允许您完全表示两个组件圆。两个圆的交集将包含由每个组件表提供的值。圆中超出重叠区域的部分将包含它们所属表的值,并使用NULL
填充另一个表中找到的列。
交叉连接
还提供了一种特殊的连接,称为CROSS JOIN
。交叉连接不使用任何比较来确定每个表中的行是否相互匹配。相反,结果是通过简单地将第一个表中的每一行添加到第二个表中的每一行来构造的。
这会生成两个或多个表中行的笛卡尔积。实际上,这种连接方式无条件地组合了每个表中的行。因此,如果每个表有三行,则结果表将有九行,包含两个表中的所有列。
例如,如果您有一个名为t1
的表与一个名为t2
的表组合,每个表都有行r1
、r2
和r3
,则结果将是九行,组合如下:
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
自连接
自连接是任何将表的行与其自身结合的连接。这可能不会立即显现其用途,但它实际上有许多常见的应用。
通常,表描述了可以相互扮演多种角色的实体。例如,如果您有一个people
表,每行可能包含一个mother
列,该列引用表中的其他people
。自连接将允许您通过将表的第二个实例连接到第一个实例来拼接这些不同的行,其中这些值匹配。
由于自连接两次引用同一个表,因此需要表别名来消除引用歧义。例如,在上面的示例中,您可以使用别名people AS children
和people AS mothers
来连接people
表的两个实例。这样,您在定义连接条件时就可以指定所引用的表实例。
这是另一个例子,这次代表员工和经理之间的关系:
SELECT*FROMpeople AS employeeJOIN people AS managerON employee.manager_id = manager.id;
连接条件
在组合表时,连接条件决定了行将如何匹配以形成复合结果。基本前提是定义每个表中必须匹配的列,以便在该行上发生连接。
ON
子句
定义表连接条件最标准的方法是使用ON
子句。ON
子句使用等号来指定将要比较的每个表中的精确列,以确定何时可以发生连接。PostgreSQL 使用提供的列来拼接每个表中的行。
ON
子句是最冗长的,但也是可用连接条件中最灵活的。它允许无论组合的每个表的列名标准化程度如何,都能保持特异性。
ON
子句的基本语法如下:
SELECT*FROMtable1JOINtable2ONtable1.id = table2.ident;
在这里,当table1
的id
列与table2
的ident
列匹配时,table1
和table2
的行将被连接。由于使用了内连接,结果只会显示那些已连接的行。由于查询使用了通配符*
字符,因此将显示两个表中的所有列。
这意味着table1
的id
列和table2
的ident
列都将显示,尽管它们通过满足连接条件而具有完全相同的值。您可以通过在SELECT
列列表中明确指定要显示的列来避免这种重复。
USING
子句
USING
子句是指定ON
子句条件的简写,当被比较的列在两个表中具有相同的名称时可以使用。USING
子句接受一个用括号括起来的列表,其中包含应被比较的共享列名。
USING
子句的一般语法采用这种格式:
SELECT*FROMtable1JOINtable2USING(id, state);
当两个表共享的两个列(id
和state
)的值都匹配时,此连接将table1
与table2
组合。
同样的连接可以用ON
更冗长地表达如下:
SELECT*FROMtable1JOINtable2ONtable1.id = table2.id AND table1.state = table2.state;
虽然上述两种连接都会产生相同的数据,但它们的显示方式略有不同。虽然ON
子句包含两个表中的所有列,但USING
子句会抑制重复的列。因此,结果中将只有共享列的单个实例(而不是每个表有两个单独的id
列和两个单独的state
列),然后是table1
和table2
提供的所有其他列。
NATURAL
子句
NATURAL
子句是另一种简写,可以进一步减少USING
子句的冗长。一个NATURAL
连接不指定任何要匹配的列。相反,PostgreSQL 会根据每个数据库中所有具有匹配列的列自动连接表。
NATURAL
连接子句的一般语法如下:
SELECT*FROMtable1NATURAL JOINtable2;
假设table1
和table2
都具有名为id
、state
和company
的列,则上述查询等效于使用ON
子句的此查询:
SELECT*FROMtable1JOINtable2ONtable1.id = table2.id AND table1.state = table2.state AND table1.company = table2.company;
以及使用USING
子句的此查询:
SELECT*FROMtable1JOINtable2USING(id, state, company);
与USING
子句一样,NATURAL
子句会抑制重复列,因此结果中每个连接的列只有一个实例。
虽然NATURAL
子句可以减少查询的冗长,但在使用它时必须小心。由于用于连接表的列是自动计算的,如果组件表中的列发生变化,结果可能会因新的连接条件而大相径庭。
连接条件和WHERE
子句
连接条件与使用WHERE
子句过滤数据行所使用的比较有许多共同之处。两种结构都定义了必须求值为真的表达式才能考虑该行。因此,在WHERE
结构中包含额外比较与在连接子句本身中定义它们之间的区别并不总是直观的。
为了理解将产生的差异,我们必须查看 PostgreSQL 处理查询不同部分的顺序。在这种情况下,连接条件中的谓词首先被处理,以在内存中构建虚拟连接表。在此阶段之后,WHERE
子句中的表达式将被评估以过滤结果行。
例如,假设我们有两个表,分别叫做customer
和order
,我们需要将它们连接起来。我们希望通过匹配customer.id
列和order.customer_id
列来连接这两个表。此外,我们对order
表中product_id
为 12345 的行感兴趣。
根据上述要求,我们有两个关心条件。然而,我们表达这些条件的方式将决定我们收到的结果。
首先,让我们将两者都用作LEFT JOIN
的连接条件:
SELECTcustomer.id AS customer_id,customer.name,order.id AS order_id,order.product_idFROMcustomerLEFT JOINorderONcustomer.id = order.customer_id AND order.product_id = 12345;
结果可能看起来像这样:
customer_id | name | order_id | product_id------------+----------+----------+------------4380 | Acme Co | 480 | 123454380 | Acme Co | 182 | 12345320 | Other Co | 680 | 123454380 | Acme Co | |320 | Other Co | |20 | Early Co | |8033 | Big Co | |(7 rows)
PostgreSQL 通过执行以下操作得出此结果:
- 将
customer
表中的行与order
表中的行组合,条件是:customer.id
匹配order.customer_id
。order.product_id
匹配 12345。
- 因为我们使用的是左连接,所以包含来自左表(
customer
)的任何不匹配行,并用NULL
值填充右表(order
)的列。 - 仅显示
SELECT
列规范中列出的列。
结果是所有连接的行都符合我们正在寻找的两个条件。然而,左连接导致 PostgreSQL 也包含了第一个表中不满足连接条件的行。这导致“剩余”的行似乎不符合查询的明显意图。
如果我们将第二个查询(order.product_id
= 12345)移到WHERE
子句,而不是将其作为连接条件,我们将得到不同的结果:
SELECTcustomer.id AS customer_id,customer.name,order.id AS order_id,order.product_idFROMcustomerLEFT JOINorderONcustomer.id = order.customer_idWHEREorder.product_id = 12345;
这次,只显示了三行:
customer_id | name | order_id | product_id------------+----------+----------+------------4380 | Acme Co | 480 | 123454380 | Acme Co | 182 | 12345320 | Other Co | 680 | 12345(3 rows)
比较执行的顺序是造成这些差异的原因。这次,PostgreSQL 像这样处理查询:
- 将
customer
表中的任何行与order
表中的行组合,条件是customer.id
匹配order.customer_id
。 - 因为我们使用的是左连接,所以包含来自左表(
customer
)的任何不匹配行,并用NULL
值填充右表(order
)的列。 - 评估
WHERE
子句,以删除order.product_id
列值不为 12345 的任何行。 - 仅显示
SELECT
列规范中列出的列。
这次,即使我们使用的是左连接,WHERE
子句也会通过过滤掉所有没有正确product_id
的行来截断结果。因为任何不匹配的行都将product_id
设置为NULL
,这会删除所有由左连接填充的不匹配行。它还会删除任何通过连接条件匹配但未通过第二轮检查的行。
了解 PostgreSQL 执行查询的基本过程可以帮助您避免在处理数据时一些容易犯但难以调试的错误。
结论
在本指南中,我们介绍了连接如何使关系型数据库能够组合来自不同表的数据以提供更有价值的答案。我们讨论了 PostgreSQL 支持的各种连接、每种类型如何组合其结果以及在使用特定类型连接时应期待什么。之后,我们回顾了定义连接条件的不同方式,并探讨了连接与WHERE
子句之间的相互作用如何导致意外结果。
连接是关系型数据库强大且足够灵活以处理如此多不同类型查询的重要组成部分。使用逻辑边界组织数据,同时仍能根据具体情况以新颖的方式重新组合数据,这赋予了 PostgreSQL 等关系型数据库令人难以置信的多功能性。学习如何在表之间进行这种拼接将使您能够创建更复杂的查询,并依靠数据库创建完整的数据图景。
常见问题
是的,PostgreSQL 支持外连接。例如,您可以使用LEFT OUTER JOIN
或只使用LEFT JOIN
,如下所示:
SELECT*FROMtable_1LEFT JOIN table_2ON table_1.id = table_2.table_1_id;
PostgreSQL中的LATERAL
关键字可以置于子SELECT FROM项之前,允许子SELECT引用FROM列表中在其之前出现的FROM项的列。(如果没有 LATERAL,每个子SELECT都是独立评估的,因此不能交叉引用任何其他 FROM 项。)
是的,PostgreSQL 支持全连接。它们可以指定为FULL OUTER JOIN
或FULL JOIN
。
语法会像这样:
SELECT*FROMtable_1FULL JOIN table_2ON table_1.id = table_2.table_1_id;
PostgreSQL 中的默认连接是内连接,可以通过使用INNER JOIN
或仅使用JOIN
来指定。
语法是:
SELECT*FROMtable_1[INNER] JOIN table_2ON table_1.id = table_2.table_1_id;