220

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

我知道LATERALjoin 可能会对我有所帮助,但即使在阅读了 Heap Analytics 中的这篇文章之后,我仍然不太了解。

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

4

5 回答 5

249

什么加入LATERAL

该功能是在 PostgreSQL 9.3 中引入的。手册

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

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

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

更像是一个相关的子查询

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

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

子查询不能做的事情

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

这仅在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列表中设置返回函数

您也可以直接使用列表unnest()中的集合返回函数SELECTSELECT在Postgres 9.6 之前的同一个列表中,这曾经表现出令人惊讶的行为,其中不止一个这样的函数。但它终于用 Postgres 10 进行了清理,现在是一个有效的替代方案(即使不是标准 SQL)。看:

基于上面的例子:

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

比较:

第 9.6 页的 dbfiddle此处
为 pg 10 的dbfiddle

澄清错误信息

手册:

对于INNERandOUTER连接类型,必须指定连接条件,即恰好是NATURALON join_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 的代码示例是正确的(CROSS JOIN不需要连接条件)而Attila 的 不是

于 2015-02-17T08:40:58.763 回答
92

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

于 2015-02-16T22:16:49.763 回答
20

数据库表

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

博客表

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

ID 创建于 标题 网址
1 2013-09-30 Vlad Mihalcea 的博客 https://vladmihalcea.com
2 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必须定义 3 次,因为在计算next_anniversarydays_to_next_anniversary值时需要它。

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

使用 SQL LATERAL JOIN 获取报告

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

  • 自 12c 以来的甲骨文
  • 从 9.3 开始的 PostgreSQL
  • MySQL 自 8.0.14 起

SQL Server 可以模拟LATERAL JOINusingCROSS APPLYOUTER APPLY.

LATERAL JOIN 允许我们重用该age_in_years值并在计算next_anniversaryanddays_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 年龄_in_years next_anniversary days_to_next_anniversary
1 7 2021-09-30 295
2 3 2021-01-22 44

好多了,对吧?

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

于 2021-01-22T15:01:21.487 回答
14

首先,横向和交叉应用是一回事。因此,您还可以阅读有关 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

在此查询中,由于限制子句,您不能使用普通连接。当没有简单的连接条件时,可以使用横向或交叉应用。

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

于 2015-02-16T21:51:40.990 回答
14

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

于 2018-10-05T18:12:32.640 回答