【问题标题】:What is the difference between LATERAL JOIN and a subquery in PostgreSQL?LATERAL JOIN 和 PostgreSQL 中的子查询有什么区别?
【发布时间】:2015-04-17 12:14:31
【问题描述】:

自从 Postgres 推出 LATERAL 连接功能以来,我一直在阅读它,因为我目前正在为我的团队进行复杂的数据转储,其中包含许多低效的子查询,这使得整个查询需要四分钟或更多。

我知道LATERAL joins 可能会对我有所帮助,但即使在阅读了诸如来自 Heap Analytics 的 this one 之类的文章后,我仍然不太了解。

LATERAL 加入的用例是什么? LATERAL 连接和子查询有什么区别?

【问题讨论】:

标签: sql postgresql subquery lateral-join


【解决方案1】:

LATERAL 加入什么?

该功能是在 PostgreSQL 9.3 中引入的。 The manual:

FROM 中出现的子查询前面可以加上关键字 LATERAL。这允许他们引用前面提供的列 FROM 项目。 (没有LATERAL,每个子查询都会被评估 独立,因此不能交叉引用任何其他 FROM 项目。)

FROM中出现的表函数也可以在key前面 word LATERAL,但对于函数,关键字是可选的;这 函数的参数可以包含对提供的列的引用 在任何情况下都在 FROM 项之前。

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

更像是一个相关子查询

LATERAL 连接更像是 correlated subquery,而不是普通的子查询,因为 LATERAL 连接右侧的表达式对其左侧的每一行都进行一次评估 - 就像 correlated 子查询 - 一个普通的子查询(表表达式)只被评估一次。 (不过,查询规划器有办法优化两者的性能。)
相关答案与并排的代码示例,解决相同的问题:

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

子查询不能做的事情

LATERAL 连接可以件事情,但(相关的)子查询不能(轻松地)。相关的子查询只能返回一个值,而不是多列和多行 - 除了裸函数调用(如果它们返回多行,则将结果行相乘)。但即使是某些 set-returning 函数也只允许在 FROM 子句中使用。就像 unnest() 在 Postgres 9.4 或更高版本中具有多个参数。 The manual:

这仅在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() 等设置返回函数。这曾经在相同的SELECT 列表中表现出令人惊讶的行为,直到 Postgres 9.6。 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
用于 pg 10 的 dbfiddle here

澄清错误信息

The manual:

对于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;

虽然这个不是:

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

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

【讨论】:

  • 子查询可以做一些 LATERAL JOIN 做不到的事情。像窗口函数。 As here
  • @EvanCarroll:我在链接中找不到任何相关的子查询。但我添加了另一个答案来演示 LATERAL 子查询中的窗口函数:gis.stackexchange.com/a/230070/7244
  • 更干净更快?在某些情况下,速度更快。切换到 LATERAL 后,我的查询从几天到几秒不等。
【解决方案2】:

laterallateral 连接之间的区别在于您是否可以查看左侧表格的行。例如:

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

【讨论】:

  • select * from table1 left join t2 using (col1) 比较如何?我不清楚使用 / on 条件的连接何时不足,使用横向连接更有意义。
【解决方案3】:

数据库表

拥有以下blog 数据库表来存储我们平台托管的博客:

而且,我们目前托管了两个博客:

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 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 必须定义 3 次,因为您在计算 next_anniversarydays_to_next_anniversary 值时需要它。

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

使用 SQL LATERAL JOIN 获取报告

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

  • 自 12c 以来的 Oracle
  • 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 的值可以计算一次,并重新用于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 表的每条记录计算的。因此,它的工作方式类似于关联子查询,但子查询记录与主表连接,因此,我们可以引用子查询生成的列。

【讨论】:

    【解决方案4】:

    首先,Lateral and Cross Apply is same thing。因此,您还可以阅读有关 Cross Apply 的信息。由于它在 SQL Server 中实现了很长时间,你会发现更多关于它的信息,然后是横向的。

    其次,据我了解,使用子查询代替横向查询没有什么不能做的。但是:

    考虑以下查询。

    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

    横向或交叉应用有更多用法,但这是我发现的最常见的一种。

    【讨论】:

    • 没错,我想知道为什么 PostgreSQL 使用lateral 而不是apply。也许微软已经申请了该语法的专利?
    • @Andomar AFAIK lateral 在 SQL 标准中,但 apply 不在。
    • LEFT JOIN 需要一个连接条件。除非您想以某种方式进行限制,否则请设为ON TRUE
    • Erwin 是对的,除非你使用 cross joinon 条件,否则你会得到一个错误
    • @Andomar:受到这种错误信息的刺激,我添加了另一个答案来澄清。
    【解决方案5】:

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

    【讨论】:

      猜你喜欢
      • 2010-12-11
      • 1970-01-01
      • 2020-05-07
      • 1970-01-01
      • 2016-12-15
      • 1970-01-01
      • 2010-09-07
      相关资源
      最近更新 更多