PostgreSQL中LATERAL JOIN和subquery有什么区别?

uujelgoq  于 2023-11-18  发布在  PostgreSQL
关注(0)|答案(5)|浏览(165)

自从PostgreSQL能够进行LATERAL连接以来,我一直在阅读它,因为我目前正在为我的团队做复杂的数据转储,其中有许多低效的子查询,使得整个查询需要四分钟或更长时间。
我知道LATERAL连接可能对我有帮助,但即使阅读了Heap Analytics的this one等文章,我仍然不太理解。
LATERAL连接的用例是什么?LATERAL连接和子查询有什么区别?

7jmck4yq

7jmck4yq1#

什么是LATERAL连接?

该功能是在PostgreSQL 9.3中引入的。手册:
出现在FROM中的子查询可以在关键字LATERAL之前。这允许它们引用前面FROM项提供的列。(没有LATERAL,每个子查询都是独立计算的,因此不能交叉引用任何其他FROM项。)
FROM中出现的表函数也可以在关键字LATERAL之前,但对于函数,关键字是可选的;函数的参数可以包含对前面FROM项提供的列的引用。
这里给出了基本的代码示例。

更像是一个 correlated 子查询

LATERAL连接更像是correlated subquery,而不是普通子查询,因为LATERAL连接右侧的表达式会为它左侧的每一行计算一次--就像 correlated 子查询一样--而普通子查询(表表达式)只计算 * 一次 *。(不过查询规划器有办法优化这两种情况的性能。)
相关的答案与代码的例子,为双方并排,解决同样的问题:

  • 优化GROUP BY查询以检索每个用户的最新行

对于返回 * 多个列 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子句中多个返回集合的函数的预期行为是什么?

基于上述示例:

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


比较:
fiddle for pg 9.6
fiddle for pg 10

注意:SELECT列表中的一个(组合)集返回函数产生no rows删除了该行。在内部它转换为CROSS JOIN LATERAL ROWS FROM ...,而不是LEFT JOIN LATERAL ... ON true

fiddle for pg 16显示差异。

澄清误传

手册:
对于INNEROUTER连接类型,必须指定连接条件,即NATURALONjoin_conditionUSING(*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;


而这一个不是:

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


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

v7pvogib

v7pvogib2#

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查询的一个例子,因为它与查询本身之外的行有关系。

q0qdq0h2

q0qdq0h23#

数据库表

使用以下blog数据库表存储我们平台托管的博客:
x1c 0d1x的数据
目前我们有两个博客:
| ID|创建于|标题|URL|
| --|--|--|--|
| 1 |2013-09-30 2013-09-30| Vlad Mihalcea的博客|https://vladmihalcea.com|
| 2 |2017-01-22 2017-01-22|超持久性|https://hypersistence.io的|

不使用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自12 c以来
  • 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|年龄|下一周年|距下一周年纪念日的天数|
| --|--|--|--|
| 1 | 7 |2021-09-30 -09- 09 - 09| 295 |
| 2 | 3 |2021-01-22 2021-01-22 2021-01-22| 44 |
好多了,对吧?
age_in_years是为blog表的每一条记录计算的。因此,它的工作方式类似于相关子查询,但子查询记录与主表连接,因此,我们可以引用子查询产生的列。

gywdnpxw

gywdnpxw4#

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

swvgeqrz

swvgeqrz5#

首先,Lateral and Cross Apply is same thing。因此,您还可以阅读有关交叉应用的信息。由于它在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


在此查询中,由于限制子句的原因,您不能使用普通连接。可以使用横向或交叉应用when there is not simple join condition
有更多的用法为横向或交叉适用,但这是最常见的一个我发现。

相关问题