LATERAL JOIN 和 PostgreSQL 中的子查询有什么区别?
What is the difference between LATERAL JOIN and a subquery in PostgreSQL?
自从 Postgres 能够执行 LATERAL
连接以来,我一直在阅读它,因为我目前正在为我的团队做复杂的数据转储,其中包含许多低效的子查询,这些子查询构成了整个查询需要四分钟或更长时间。
我知道 LATERAL
联接可能会帮助我,但即使在阅读了堆分析中的 this one 等文章后,我仍然不太理解。
LATERAL
联接的用例是什么? LATERAL
连接和子查询有什么区别?
首先,Lateral and Cross Apply is same thing。因此,您还可以阅读有关 Cross Apply 的内容。由于它在 SQL Server 中实施了很长时间,因此您会在 Lateral 上找到更多关于它的信息。
其次,据我了解,没有什么是用子查询代替横向查询做不到的。但是:
考虑以下查询。
Select A.*
, (Select B.Column1 from B where B.Fk1 = A.PK and Limit 1)
, (Select B.Column2 from B where B.Fk1 = A.PK and Limit 1)
FROM A
这种情况下可以使用横向。
Select A.*
, x.Column1
, x.Column2
FROM A LEFT JOIN LATERAL (
Select B.Column1,B.Column2,B.Fk1 from B Limit 1
) x ON X.Fk1 = A.PK
由于 limit 子句,在此查询中您不能使用正常连接。
可以使用横向或交叉应用 when there is not simple join condition.
横向或交叉应用有更多用法,但这是我发现的最常见的一种。
非lateral
和lateral
联接之间的区别在于您是否可以查看左侧table 的行。例如:
select *
from table1 t1
cross join lateral
(
select *
from t2
where t1.col1 = t2.col1 -- Only allowed because of lateral
) sub
这"outward looking"意味着子查询必须被评估不止一次。毕竟,t1.col1
可以取很多值。
相比之下,非lateral
连接后的子查询可以评估一次:
select *
from table1 t1
cross join
(
select *
from t2
where t2.col1 = 42 -- No reference to outer query
) sub
正如在没有 lateral
的情况下所要求的那样,内部查询不以任何方式依赖于外部查询。 lateral
查询是 correlated
查询的一个示例,因为它与查询本身之外的行有关。
什么 是 LATERAL
加入?
该功能是在 PostgreSQL 9.3 中引入的。 The manual:
Subqueries appearing in FROM
can be preceded by the key word
LATERAL
. This allows them to reference columns provided by preceding
FROM
items. (Without LATERAL
, each subquery is evaluated
independently and so cannot cross-reference any other FROM
item.)
Table functions appearing in FROM
can also be preceded by the key
word LATERAL
, but for functions the key word is optional; the
function's arguments can contain references to columns provided by
preceding FROM
items in any case.
那里给出了基本代码示例。
更像是一个相关子查询
A LATERAL
连接更像是一个 correlated subquery,而不是一个普通的子查询,因为 LATERAL
连接右边的表达式对它左边的每一行计算一次- 就像 correlated 子查询一样 - 而普通子查询(table 表达式)仅计算 一次。 (不过,查询规划器有办法优化两者的性能。)
并排提供代码示例的相关答案,解决了同样的问题:
- Optimize GROUP BY query to retrieve latest row per user
对于 returning 多列 ,LATERAL
连接通常更简单、更干净、更快。
另外,请记住,相关子查询的等效项是 LEFT JOIN LATERAL ... ON true
:
- Call a set-returning function with an array argument multiple times
子查询不能做的事情
有 种 LATERAL
连接可以做,但(相关)子查询不能(轻易)做的事情。相关子查询只能 return 单个值,不能是多列也不能是多行 - 裸函数调用除外(如果它们 return 多行,则结果行相乘)。但即使某些 set-returning 函数也只允许在 FROM
子句中使用。就像 unnest()
在 Postgres 9.4 或更高版本中有多个参数。 The manual:
This is only allowed in the FROM
clause;
所以这可行,但不能(轻易)替换为子查询:
CREATE TABLE tbl (a1 int[], a2 int[]);
SELECT * FROM tbl, unnest(a1, a2) u(elem1, elem2); -- implicit LATERAL
FROM
子句中的逗号(,
)是CROSS JOIN
.
的缩写
LATERAL
自动假定为 table 函数。
关于UNNEST( array_expression [, ... ] )
的特例:
在SELECT
列表
中设置-returning函数
您也可以直接在 SELECT
列表中使用 set-returning 函数,例如 unnest()
。这曾经在 Postgres 9.6 之前的同一个 SELECT
列表中出现多个这样的函数时表现出令人惊讶的行为。 But it has finally been sanitized with Postgres 10 现在是一个有效的替代方案(即使不是标准的 SQL)。参见:
基于以上示例:
SELECT *, unnest(a1) AS elem1, unnest(a2) AS elem2
FROM tbl;
比较:
用于 pg 9.6 的 dbfiddle here
第 10 页的 dbfiddle here
澄清错误信息
For the INNER
and OUTER
join types, a join condition must be
specified, namely exactly one of NATURAL
, ON
join_condition,
or USING
(join_column [, ...]). See below for the meaning.
For CROSS JOIN
, none of these clauses can appear.
所以这两个查询是有效的(即使不是特别有用):
SELECT *
FROM tbl t
LEFT JOIN LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t <b>ON TRUE</b>;
SELECT *
FROM tbl t, LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t;
虽然这个不是:
SELECT *
FROM tbl t
LEFT JOIN LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t;
这就是为什么 code example is correct (the CROSS JOIN
does not require a join condition) and 是 不是的原因。
没有人指出的一件事是,您可以使用 LATERAL
查询在每个选定的行上应用用户定义的函数。
例如:
CREATE OR REPLACE FUNCTION delete_company(companyId varchar(255))
RETURNS void AS $$
BEGIN
DELETE FROM company_settings WHERE "company_id"=company_id;
DELETE FROM users WHERE "company_id"=companyId;
DELETE FROM companies WHERE id=companyId;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM (
SELECT id, name, created_at FROM companies WHERE created_at < '2018-01-01'
) c, LATERAL delete_company(c.id);
这是我知道如何在 PostgreSQL 中执行此类操作的唯一方法。
数据库table
有以下 blog
数据库 table 存储我们平台托管的博客:
而且,我们目前托管了两个博客:
id
created_on
title
url
1
2013-09-30
Vlad Mihalcea's Blog
https://vladmihalcea.com
2
2017-01-22
Hypersistence
https://hypersistence.io
在不使用 SQL 横向连接的情况下获取我们的报告
我们需要构建一个从 blog
table:
中提取以下数据的报告
- 博客 ID
- 博客年龄,以年为单位
- 下一个博客周年纪念日
- 距下一个周年纪念日剩余的天数。
如果您使用的是 PostgreSQL,则必须执行以下 SQL 查询:
SELECT
b.id as blog_id,
extract(
YEAR FROM age(now(), b.created_on)
) AS age_in_years,
date(
created_on + (
extract(YEAR FROM age(now(), b.created_on)) + 1
) * interval '1 year'
) AS next_anniversary,
date(
created_on + (
extract(YEAR FROM age(now(), b.created_on)) + 1
) * interval '1 year'
) - date(now()) AS days_to_next_anniversary
FROM blog b
ORDER BY blog_id
如您所见,age_in_years
必须定义三次,因为在计算 next_anniversary
和 days_to_next_anniversary
值时需要它。
而且,这正是 LATERAL JOIN 可以帮助我们的地方。
使用SQL LATERAL JOIN
获取报告
以下关系数据库系统支持 LATERAL JOIN
语法:
- 自 12c 以来的 Oracle
- PostgreSQL 自 9.3
- MySQL 自 8.0.14
SQL 服务器可以使用 CROSS APPLY
和 OUTER APPLY
.
模拟 LATERAL JOIN
LATERAL JOIN 允许我们重用 age_in_years
值,并在计算 next_anniversary
和 days_to_next_anniversary
值时进一步传递它。
可以使用 LATERAL JOIN 重写之前的查询,如下所示:
SELECT
b.id as blog_id,
age_in_years,
date(
created_on + (age_in_years + 1) * interval '1 year'
) AS next_anniversary,
date(
created_on + (age_in_years + 1) * interval '1 year'
) - date(now()) AS days_to_next_anniversary
FROM blog b
CROSS JOIN LATERAL (
SELECT
cast(
extract(YEAR FROM age(now(), b.created_on)) AS int
) AS age_in_years
) AS t
ORDER BY blog_id
并且,age_in_years
值可以计算一个并重复用于 next_anniversary
和 days_to_next_anniversary
计算:
blog_id
age_in_years
next_anniversary
days_to_next_anniversary
1
7
2021-09-30
295
2
3
2021-01-22
44
好多了,对吧?
对blog
table的每条记录计算age_in_years
。因此,它的工作方式类似于相关子查询,但子查询记录与主 table 相连,因此,我们可以引用子查询生成的列。
自从 Postgres 能够执行 LATERAL
连接以来,我一直在阅读它,因为我目前正在为我的团队做复杂的数据转储,其中包含许多低效的子查询,这些子查询构成了整个查询需要四分钟或更长时间。
我知道 LATERAL
联接可能会帮助我,但即使在阅读了堆分析中的 this one 等文章后,我仍然不太理解。
LATERAL
联接的用例是什么? LATERAL
连接和子查询有什么区别?
首先,Lateral and Cross Apply is same thing。因此,您还可以阅读有关 Cross Apply 的内容。由于它在 SQL Server 中实施了很长时间,因此您会在 Lateral 上找到更多关于它的信息。
其次,据我了解,没有什么是用子查询代替横向查询做不到的。但是:
考虑以下查询。
Select A.*
, (Select B.Column1 from B where B.Fk1 = A.PK and Limit 1)
, (Select B.Column2 from B where B.Fk1 = A.PK and Limit 1)
FROM A
这种情况下可以使用横向。
Select A.*
, x.Column1
, x.Column2
FROM A LEFT JOIN LATERAL (
Select B.Column1,B.Column2,B.Fk1 from B Limit 1
) x ON X.Fk1 = A.PK
由于 limit 子句,在此查询中您不能使用正常连接。 可以使用横向或交叉应用 when there is not simple join condition.
横向或交叉应用有更多用法,但这是我发现的最常见的一种。
非lateral
和lateral
联接之间的区别在于您是否可以查看左侧table 的行。例如:
select *
from table1 t1
cross join lateral
(
select *
from t2
where t1.col1 = t2.col1 -- Only allowed because of lateral
) sub
这"outward looking"意味着子查询必须被评估不止一次。毕竟,t1.col1
可以取很多值。
相比之下,非lateral
连接后的子查询可以评估一次:
select *
from table1 t1
cross join
(
select *
from t2
where t2.col1 = 42 -- No reference to outer query
) sub
正如在没有 lateral
的情况下所要求的那样,内部查询不以任何方式依赖于外部查询。 lateral
查询是 correlated
查询的一个示例,因为它与查询本身之外的行有关。
什么 是 LATERAL
加入?
该功能是在 PostgreSQL 9.3 中引入的。 The manual:
Subqueries appearing in
FROM
can be preceded by the key wordLATERAL
. This allows them to reference columns provided by precedingFROM
items. (WithoutLATERAL
, each subquery is evaluated independently and so cannot cross-reference any otherFROM
item.)Table functions appearing in
FROM
can also be preceded by the key wordLATERAL
, but for functions the key word is optional; the function's arguments can contain references to columns provided by precedingFROM
items in any case.
那里给出了基本代码示例。
更像是一个相关子查询
A LATERAL
连接更像是一个 correlated subquery,而不是一个普通的子查询,因为 LATERAL
连接右边的表达式对它左边的每一行计算一次- 就像 correlated 子查询一样 - 而普通子查询(table 表达式)仅计算 一次。 (不过,查询规划器有办法优化两者的性能。)
并排提供代码示例的相关答案,解决了同样的问题:
- Optimize GROUP BY query to retrieve latest row per user
对于 returning 多列 ,LATERAL
连接通常更简单、更干净、更快。
另外,请记住,相关子查询的等效项是 LEFT JOIN LATERAL ... ON true
:
- Call a set-returning function with an array argument multiple times
子查询不能做的事情
有 种 LATERAL
连接可以做,但(相关)子查询不能(轻易)做的事情。相关子查询只能 return 单个值,不能是多列也不能是多行 - 裸函数调用除外(如果它们 return 多行,则结果行相乘)。但即使某些 set-returning 函数也只允许在 FROM
子句中使用。就像 unnest()
在 Postgres 9.4 或更高版本中有多个参数。 The manual:
This is only allowed in the
FROM
clause;
所以这可行,但不能(轻易)替换为子查询:
CREATE TABLE tbl (a1 int[], a2 int[]);
SELECT * FROM tbl, unnest(a1, a2) u(elem1, elem2); -- implicit LATERAL
FROM
子句中的逗号(,
)是CROSS JOIN
.
的缩写
LATERAL
自动假定为 table 函数。
关于UNNEST( array_expression [, ... ] )
的特例:
在SELECT
列表
中设置-returning函数
您也可以直接在 SELECT
列表中使用 set-returning 函数,例如 unnest()
。这曾经在 Postgres 9.6 之前的同一个 SELECT
列表中出现多个这样的函数时表现出令人惊讶的行为。 But it has finally been sanitized with Postgres 10 现在是一个有效的替代方案(即使不是标准的 SQL)。参见:
基于以上示例:
SELECT *, unnest(a1) AS elem1, unnest(a2) AS elem2
FROM tbl;
比较:
用于 pg 9.6 的 dbfiddle here
第 10 页的 dbfiddle here
澄清错误信息
For the
INNER
andOUTER
join types, a join condition must be specified, namely exactly one ofNATURAL
,ON
join_condition, orUSING
(join_column [, ...]). See below for the meaning.
ForCROSS JOIN
, none of these clauses can appear.
所以这两个查询是有效的(即使不是特别有用):
SELECT *
FROM tbl t
LEFT JOIN LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t <b>ON TRUE</b>;
SELECT *
FROM tbl t, LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t;
虽然这个不是:
SELECT *
FROM tbl t
LEFT JOIN LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t;
这就是为什么 CROSS JOIN
does not require a join condition) and 是 不是的原因。
没有人指出的一件事是,您可以使用 LATERAL
查询在每个选定的行上应用用户定义的函数。
例如:
CREATE OR REPLACE FUNCTION delete_company(companyId varchar(255))
RETURNS void AS $$
BEGIN
DELETE FROM company_settings WHERE "company_id"=company_id;
DELETE FROM users WHERE "company_id"=companyId;
DELETE FROM companies WHERE id=companyId;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM (
SELECT id, name, created_at FROM companies WHERE created_at < '2018-01-01'
) c, LATERAL delete_company(c.id);
这是我知道如何在 PostgreSQL 中执行此类操作的唯一方法。
数据库table
有以下 blog
数据库 table 存储我们平台托管的博客:
而且,我们目前托管了两个博客:
id | created_on | title | url |
---|---|---|---|
1 | 2013-09-30 | Vlad Mihalcea's Blog | https://vladmihalcea.com |
2 | 2017-01-22 | Hypersistence | https://hypersistence.io |
在不使用 SQL 横向连接的情况下获取我们的报告
我们需要构建一个从 blog
table:
- 博客 ID
- 博客年龄,以年为单位
- 下一个博客周年纪念日
- 距下一个周年纪念日剩余的天数。
如果您使用的是 PostgreSQL,则必须执行以下 SQL 查询:
SELECT
b.id as blog_id,
extract(
YEAR FROM age(now(), b.created_on)
) AS age_in_years,
date(
created_on + (
extract(YEAR FROM age(now(), b.created_on)) + 1
) * interval '1 year'
) AS next_anniversary,
date(
created_on + (
extract(YEAR FROM age(now(), b.created_on)) + 1
) * interval '1 year'
) - date(now()) AS days_to_next_anniversary
FROM blog b
ORDER BY blog_id
如您所见,age_in_years
必须定义三次,因为在计算 next_anniversary
和 days_to_next_anniversary
值时需要它。
而且,这正是 LATERAL JOIN 可以帮助我们的地方。
使用SQL LATERAL JOIN
获取报告以下关系数据库系统支持 LATERAL JOIN
语法:
- 自 12c 以来的 Oracle
- PostgreSQL 自 9.3
- MySQL 自 8.0.14
SQL 服务器可以使用 CROSS APPLY
和 OUTER APPLY
.
LATERAL JOIN
LATERAL JOIN 允许我们重用 age_in_years
值,并在计算 next_anniversary
和 days_to_next_anniversary
值时进一步传递它。
可以使用 LATERAL JOIN 重写之前的查询,如下所示:
SELECT
b.id as blog_id,
age_in_years,
date(
created_on + (age_in_years + 1) * interval '1 year'
) AS next_anniversary,
date(
created_on + (age_in_years + 1) * interval '1 year'
) - date(now()) AS days_to_next_anniversary
FROM blog b
CROSS JOIN LATERAL (
SELECT
cast(
extract(YEAR FROM age(now(), b.created_on)) AS int
) AS age_in_years
) AS t
ORDER BY blog_id
并且,age_in_years
值可以计算一个并重复用于 next_anniversary
和 days_to_next_anniversary
计算:
blog_id | age_in_years | next_anniversary | days_to_next_anniversary |
---|---|---|---|
1 | 7 | 2021-09-30 | 295 |
2 | 3 | 2021-01-22 | 44 |
好多了,对吧?
对blog
table的每条记录计算age_in_years
。因此,它的工作方式类似于相关子查询,但子查询记录与主 table 相连,因此,我们可以引用子查询生成的列。