在PostgreSQL中,LATERAL JOIN和子查询的区别是什么?

自从PostgreSQL有能力做LATERAL连接以来,我一直在阅读它,因为我目前为我的团队做复杂的数据转储,有很多低效的子查询,使整个查询花费四分钟或更长时间。

我知道LATERAL连接可能能够帮助我,但即使在阅读了堆分析的这一个等文章后,我仍然不太明白。

LATERAL连接的用例是什么?LATERAL连接和子查询之间的区别是什么?

215462 次浏览

首先,横向应用和交叉应用是一回事。因此你也可以读到Cross Apply。由于它在SQL Server中实现了很长时间,你会发现更多关于它的信息,然后横向。

其次,根据我的理解,使用subquery代替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
在此查询中,由于限制子句,您不能使用普通连接。 横向或交叉应用可以使用当不存在简单连接条件时.

横向应用和交叉应用还有很多用法,但我发现这是最常见的用法。

非-lateral连接和lateral连接之间的区别在于是否可以查找左边表的行。例如:

select  *
from    table1 t1
cross join lateral
(
select  *
from    t2
where   t1.col1 = t2.col1 -- Only allowed because of lateral
) sub

这种“向外看”意味着必须对子查询求值不止一次。毕竟,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中引入的。手动:

出现在FROM中的子查询可以在关键字之前 LATERAL。这允许他们引用前面提供的列 FROM物品。(如果没有LATERAL,每个子查询都会被求值 因此不能交叉引用任何其他FROM项。 出现在FROM中的表函数也可以前面加键 LATERAL,但对于函数,关键字是可选的;的 函数的实参可以包含对列的引用

.在任何情况下,在FROM项之前

这里给出了基本的代码示例。

更像是相关子查询

LATERAL连接更像相关子查询,而不是普通子查询,因为LATERAL连接右边的表达式为它左边的每一行计算一次——就像相关子查询一样——而普通子查询(表表达式)只计算一次。(不过,查询计划器有方法优化这两者的性能。)
相关的答案与代码示例并排,解决相同的问题:

对于返回多于一列LATERAL连接通常更简单、更干净和更快。
另外,请记住,与相关子查询等价的是LEFT JOIN LATERAL ... ON true:

子查询不能做的事情

LATERAL连接可以做的事情,但(相关)子查询不能(容易)做。相关子查询只能返回单个值,而不能返回多列或多行——裸函数调用除外(如果返回多行,则会将结果行相乘)。但即使某些集返回函数也只允许在FROM子句中。就像Postgres 9.4或更高版本中带有多个参数的unnest()手册:

这只允许在FROM子句中;

所以这是可行的,但不能(轻易地)用子查询替换:

CREATE TABLE tbl (a1 int[], a2 int[]);
SELECT * FROM tbl, unnest(a1, a2) u(elem1, elem2);  -- implicit LATERAL
FROM子句中的逗号(,)是CROSS JOIN的缩写符号。
LATERAL被自动假定为表函数。
关于UNNEST( array_expression [, ... ] )的特殊情况:

SELECT列表中的集返回函数

你也可以直接在SELECT列表中使用集返回函数,比如unnest()。在Postgres 9.6之前,在同一个SELECT列表中出现多个这样的函数时,会出现令人惊讶的行为。但它最终被Postgres 10消毒了,现在是一个有效的替代(即使不是标准SQL)。看到的:

以上述例子为基础:

SELECT *, unnest(a1) AS elem1, unnest(a2) AS elem2
FROM   tbl;

比较:

< p > Dbfiddle for pg 9.6
here
dbfiddle for pg 10 在这里

澄清错误信息

手册: . b

对于INNEROUTER连接类型,连接条件必须为 ,即NATURALON< >强join_condition < / >强, 或USING (< >强join_column < / >强[,…])。请看下面的意思。
对于CROSS JOIN,这些子句都不能出现

所以这两个查询是有效的(即使不是特别有用):

SELECT *
FROM   tbl t
LEFT   JOIN LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t ON TRUE;


SELECT *
FROM   tbl t, LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t;

但这一条不是:

罢工< p > < > < >以前<代码> SELECT * 来自tbl t LEFT JOIN LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t;

这就是为什么Andomar的代码示例是正确的(CROSS JOIN不需要连接条件),而匈奴王的 则不需要。

没有人指出的一件事是,你可以使用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中如何做这类事情的唯一方法。

数据库表

有下面的blog数据库表存储由我们的平台托管的博客:

Blog table

我们现在有两个博客:

id created_on 标题 url
1 2013-09-30 Vlad Mihalcea的博客 https://vladmihalcea.com
2 2017-01-22 Hypersistence < a href = " https://hypersistence。io noreferrer“rel = > https://hypersistence.io < / >
< / div >

在不使用SQL LATERAL JOIN的情况下获取我们的报告

我们需要构建一个报告,从blog表中提取以下数据:

  • 博客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_anniversarydays_to_next_anniversary值时需要它。

而且,这正是LATERAL JOIN可以帮助我们的地方。

使用SQL LATERAL JOIN获取报告

以下关系数据库系统支持LATERAL JOIN语法:

  • Oracle从12c开始
  • PostgreSQL从9.3开始
  • MySQL从8.0.14开始

SQL Server可以使用CROSS APPLYOUTER APPLY来模拟LATERAL JOIN

LATERAL JOIN允许我们重用age_in_years值,并在计算next_anniversarydays_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值可以计算为1,并用于next_anniversarydays_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

好多了,对吧?

age_in_years是为blog表的每一条记录计算的。因此,它的工作方式类似于一个相关的子查询,但是子查询记录与主表连接,因此,我们可以引用子查询生成的列。