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.

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

laterallateral 联接之间的区别在于您是否可以查看左侧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

澄清错误信息

The manual:

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

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

使用SQL LATERAL JOIN

获取报告

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

  • 自 12c 以来的 Oracle
  • PostgreSQL 自 9.3
  • MySQL 自 8.0.14

SQL 服务器可以使用 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

好多了,对吧?

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