PostgreSQL中的LATERAL简介
PostgreSQL中的LATERAL简介
横向查询已经存在很长一段时间了——特别是从Pg 9.3开始——大约 10 年。
那么,横向查询是什么?
从广义上讲——横向子查询(有时也称为laterl join)是开发人员使PostgreSQL基于单行数据生成多行的一种方式。
最简单的例子:假设表包含一些事件作为两列(我知道我可以使用范围数据类型,但我想保持简单):event_start 和 event_end。像这样:
= $ CREATE TABLE events ( id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY , event_start date NOT NULL , event_end date NOT NULL , CHECK ( event_end >= event_start ) ) ;
现在,让我们添加一些事件:
= $ with event_starts as ( select now ( ) - '2 周' :: interval * random ( ) as start from generate_series ( 1 , 5 ) i ) 插入 事件( event_start , event_end ) select start , start + '3 days ' ::区间+随机( ) * '4 天' :: event_starts的间隔 ;
这给了我一些可以处理的好事件:
= $从事件中选择 * ; 编号| 事件开始| event_end ----+-------------+------------ 1 | 2022 - 09 - 15 | 2022 - 09 - 22 2 | 2022 - 09 - 06 | 2022 - 09 - 11 3 | 2022 - 09 - 06 | 2022 - 09 - 10 4 | 2022 - 09 - 12 | 2022- 09 - 18 5 | 2022 - 09 - 05 | 2022年9月10 日 (5 行)
现在,假设我想获取所有日期的列表,其中包含一些事件,并计算这些日子每天有多少事件。
我可以从简单的 select * from events 开始,然后使用横向获取所有日期的列表。让我们来看看:
= $ select e .*, l .* from events e , lateral ( select x:: date from generate_series ( e . event_start , e . event_end , '1 day' :: interval ) as x ) as l 编号| 事件开始| 事件结束 | x ----+-------------+------------+------------ 1 | 2022 - 09 - 15 | 2022 - 09 - 22 | 2022 - 09 - 15 1 | 2022 - 09 - 15 | 2022 - 09 - 22 | 2022 - 09 - 16 1 | 2022 - 09 - 15 | 2022 - 09 - 22 | 2022 - 09 - 17 1 | 2022 - 09 - 15 | 2022 - 09 - 22 | 2022 - 09 - 18 1 | 2022 - 09 - 15 | 2022 - 09 - 22 | 2022 - 09 - 19 1 | 2022 - 09 - 15 | 2022 - 09 - 22 | 2022 - 09 - 20 1 | 2022 - 09 - 15 | 2022 - 09 - 22 | 2022 - 09 - 21 1 | 2022 - 09 - 15 | 2022 - 09 - 22 | 2022 - 09 - 22 2 | 2022 - 09 - 06 | 2022 - 09 - 11 | 2022 - 09 - 06 2 | 2022 - 09 - 06 | 2022 - 09 - 11 | 2022 - 09 - 07 2 | 2022 - 09 - 06 | 2022 - 09 - 11 | 2022 - 09 - 08 2 | 2022 - 09 - 06 | 2022 - 09 - 11 | 2022 - 09 - 09 2 | 2022 - 09 - 06 | 2022 - 09 - 11 | 2022 - 09 - 10 2 | 2022 - 09 - 06 | 2022 - 09 - 11 | 2022 - 09 - 11 3 | 2022 - 09 - 06 | 2022 - 09 - 10 | 2022 - 09 - 06 3 | 2022 - 09 - 06 | 2022 - 09 - 10 | 2022 - 09 - 07 3 | 2022 - 09 - 06 | 2022 - 09 - 10 | 2022 - 09 - 08 3 | 2022 - 09 - 06 | 2022 - 09 - 10 | 2022 - 09 - 09 3 | 2022 - 09 - 06 | 2022 - 09 - 10 | 2022 - 09 - 10 4 | 2022 - 09 - 12 | 2022 - 09 - 18 | 2022 - 09 - 12 4 | 2022 - 09 - 12 | 2022 - 09 - 18 | 2022 - 09 - 13 4 | 2022 - 09 - 12 | 2022 - 09 - 18 | 2022 - 09 - 14 4 | 2022 - 09 - 12 | 2022 - 09 - 18 | 2022 - 09 - 15 4 | 2022 - 09 - 12 | 2022- 09 - 18 | 2022 - 09 - 16 4 | 2022 - 09 - 12 | 2022 - 09 - 18 | 2022 - 09 - 17 4 | 2022 - 09 - 12 | 2022 - 09 - 18 | 2022 - 09 - 18 5 | 2022 - 09 - 05 | 2022 -09 - 10 | 2022 - 09 - 05 5 | 2022 - 09 - 05 | 2022 - 09 - 10 | 2022 - 09 - 06 5 | 2022 - 09 - 05 | 2022 - 09 - 10 | 2022 - 09 - 07 5 | 2022 - 09 - 05 | 2022 -09 - 10 | 2022 - 09 - 08 5 | 2022 - 09 - 05 | 2022 - 09 - 10 | 2022 - 09 - 09 5 | 2022 - 09 - 05 | 2022 - 09 - 10 | 2022-09-10 (32行)_ _ _ _
请注意,前 3 列中的数据是重复的——因为它是 events 表中的同一行,由于横向魔术,只添加了第 4 列。
横向,在这个例子中,调用 generate_series() 函数,它生成一组时间戳,然后将其转换为日期数据类型,这样我们就只能得到日期。
现在,要获取所有天数及其计数,我只需要按 lx 分组,然后获取计数:
= $选择 l 。x , 从 事件 e中计数( * ) , 横向 (从generate_series ( e.event_start , e.event_end , ' 1 day' :: interval )中选择x::日期作为x )作为l按l 分组。x 按l排序。X x | 计数 ------------+-------- 2022 - 09 - 05 | 1 2022 - 09 - 06 | 3 2022 - 09 - 07 | 3 2022 - 09 - 08 | 3 2022 - 09 - 09 | 3 2022 - 09 - 10 | 3 2022 - 09 - 11 | 1 2022 -09 - 12 | 1 2022 - 09 - 13 | 1 2022 - 09 - 14 | 1 2022 - 09 - 15 | 2 2022 - 09 - 16 | 2 2022 - 09 - 17 | 2 2022 - 09 - 18 | 2 2022 - 09 - 19 | 1 2022 - 09 -20 | 1 2022 - 09 - 21 | 1 2022 - 09 - 22 | 1个 (18 行)
重要的部分是,对于源(事件)中的每一行,横向内部的查询被调用并具有对正常逻辑(包括 where 子句、函数、分组、聚合、排序、限制等任何内容)的完全访问权限,并且结果记录集可用于我们的查询。
例如,这可以用于获取诸如按薪水排序的每个部门的前五名员工:
select d . * , le . * from department d , lateral ( select * from employees e where e.dept_id = d.id order by e.salary desc limit 5 ) as le
通常,当您需要从复杂数据(json?)中提取信息或对连接数据集进行非显而易见的修改(如上例中的限制行数)时,横向是天赐之物。
我还想展示一件事。具体来说——虽然我不特别喜欢将横向查询称为“横向连接”,但事实上您可以使用连接语法。这在横向查询不返回任何内容的情况下很有用。
给定事件表,让我们尝试获取 9 月每天的事件数。
要获得 9 月的所有日期,我可以简单地:
= $ select d:: date as day from generate_series ( '2022-09-01' , '2022-09-30' , '1 day' :: interval ) d; 天 ------------ 2022 - 09 - 01 2022 - 09 - 02 2022 - 09 - 03 2022 - 09 - 04 ... 2022 - 09 - 30 (30 行)
现在,我可以添加横向子查询来获取当天发生的事件:
= $ select d:: date as day , l .* from generate_series ( '2022-09-01' , '2022-09-30' , '1 day' :: interval ) d , lateral ( select * from events e where d :: e.event_start和e.event_end之间的日期 ) 为l ; _ 天| 编号| 事件开始| event_end ------------+----+-------------+------------ 2022 - 09 - 05 | 5 | 2022 - 09 - 05 | 2022 - 09 - 10 2022 - 09 - 06 | 2 | 2022 - 09 - 06 | 2022 - 09 - 11 2022 - 09 - 06 | 3 | 2022 - 09- 06 | 2022 - 09 - 10 2022 - 09 - 06 | 5 | 2022 - 09 - 05 | 2022 - 09 - 10 2022 - 09 - 07 | 2 | 2022 - 09 - 06 | 2022 - 09 - 11 2022 - 09 - 07 | 3 | 2022 - 09- 06 | 2022 - 09 - 10 2022 - 09 - 07 | 5 | 2022 - 09 - 05 | 2022 - 09 - 10 2022 - 09 - 08 | 2 | 2022 - 09 - 06 | 2022 - 09 - 11 2022 - 09 - 08 | 3 | 2022 - 09- 06 | 2022 - 09 - 10 2022 - 09 - 08 | 5 | 2022 - 09 - 05 | 2022 - 09 - 10 2022 - 09 - 09 | 2 | 2022 - 09 - 06 | 2022 - 09 - 11 2022 - 09 - 09 | 3 | 2022 - 09- 06 | 2022 - 09 - 10 2022 - 09 - 09 | 5 | 2022 - 09 - 05 | 2022 - 09 - 10 2022 - 09 - 10 | 2 | 2022 - 09 - 06 | 2022 - 09 - 11 2022 - 09 - 10 | 3 | 2022 - 09- 06 | 2022 - 09 - 10 2022 - 09 - 10 | 5 | 2022 - 09 - 05 | 2022 - 09 - 10 2022 - 09 - 11 | 2 | 2022 - 09 - 06 | 2022 - 09 - 11 2022 - 09 - 12 | 4 | 2022 - 09- 12 | 2022 - 09 - 18 2022 - 09 - 13 | 4 | 2022 - 09 - 12 | 2022 - 09 - 18 2022 - 09 - 14 | 4 | 2022 - 09 - 12 | 2022 - 09 - 18 2022 - 09 - 15 | 1 | 2022 - 09 -15 | 2022 - 09 - 22 2022 - 09 - 15 | 4 | 2022 - 09 - 12 | 2022 - 09 - 18 2022 - 09 - 16 | 1 | 2022 - 09 - 15 | 2022 - 09 - 22 2022 - 09 - 16 | 4 | 2022 - 09 - 12 | 2022 - 09 - 18 2022 - 09 - 17 | 1 | 2022 - 09 - 15 | 2022 - 09 - 22 2022 - 09 - 17 | 4 | 2022 - 09 - 12 | 2022 - 09 - 18 2022 - 09 - 18 | 1 | 2022 - 09 - 15 | 2022 - 09 - 22 2022 - 09 - 18 | 4 | 2022 - 09 - 12 | 2022 - 09 - 18 2022 - 09 - 19 | 1 | 2022 - 09 - 15 | 2022 - 09 - 22 2022 - 09 - 20 | 1 | 2022 - 09 - 15 | 2022- 09 - 22 2022 - 09 - 21 | 1 | 2022 - 09 - 15 | 2022 - 09 - 22 2022 - 09 - 22 | 1 | 2022 - 09 - 15 | 2022-09-22 (32行)_ _ _ _
这显然有效,但如果我现在得到计数,我会错过一些日子:
= $ select d:: date as day , count ( l . id ) as events from generate_series ( '2022-09-01' , '2022-09-30' , '1 day' :: interval ) d , lateral ( select * 来自事件 e 其中d:: e . event_start和e . event_end 之间的日期 )为 l 按d分组 。d的日期 顺序。约会 日| 事件 ------------+-------- 2022 - 09 - 05 | 1 2022 - 09 - 06 | 3 2022 - 09 - 07 | 3 2022 - 09 - 08 | 3 2022 - 09 - 09 | 3 2022 - 09 - 10 | 3 2022 - 09 - 11 | 1 2022 - 09 - 12 | 1 2022 - 09 - 13 | 1 2022 - 09 - 14 | 1 2022 - 09 - 15 | 2 2022 - 09 - 16 | 2 2022 - 09 - 17 | 2 2022 - 09 - 18 | 2 2022 - 09 - 19 | 1 2022 - 09 - 20 | 1 2022 - 09 - 21 | 1 2022 - 09 - 22 | 1个 (18 行)
具体来说——我没有数据为 0 行的日子。这是因为这些天的横向返回了 0 行,而这个从 generate_series 中“取消”的行。
由于对横向进行了左连接,我们可以解决它:
= $ select d:: date as day , count ( l . id ) as events from generate_series ( '2022-09-01' , ' 2022-09-30' , '1 day' :: interval ) d left joinlateral ( select * from events e where d:: date between e . event_start和e . event_end ) 作为l on ( true ) group by d 。d的日期 顺序 。约会 日| 事件 ------------+-------- 2022 - 09 - 01 | 0 2022 - 09 - 02 | 0 2022 - 09 - 03 | 0 2022 - 09 - 04 | 0 2022 - 09 - 05 | 1 2022 - 09 - 06 | 3 2022 - 09 - 07 | 3 2022 - 09 - 08 | 3 2022 - 09 - 09 | 3 2022 - 09 - 10 | 3 2022 - 09 - 11 | 1 2022 - 09 - 12 | 1 2022 - 09 - 13 | 1 2022- 09 - 14 | 1 2022 - 09 - 15 | 2 2022 - 09 - 16 | 2 2022 - 09 - 17 | 2 2022 - 09 - 18 | 2 2022 - 09 - 19 | 1 2022 - 09 - 20 | 1 2022 - 09 - 21 | 1 2022 - 09- 22 | 1 2022 - 09 - 23 | 0 2022 - 09 - 24 | 0 2022 - 09 - 25 | 0 2022 - 09 - 26 | 0 2022 - 09 - 27 | 0 2022 - 09 - 28 | 0 2022 - 09 - 29 | 0 2022 - 09 - 30 | 0 (30 行)
这带来了 0 个计数,这是因为横向返回的所有列在没有事件的日子里都是 NULL。这意味着 count(l.id) 没有增加计数(count(...) 仅对非空值增加计数)。
对于左连接,我使用了奇怪的连接条件:on (true)。这是因为 JOIN 需要连接条件。在我们的例子中,真正的条件是在横向查询中构建的(其中 d::date ...),但语法仍然需要加入一些东西。由于横向返回的任何行都将是“可以加入”,所以我们加入的条件始终为真。
原文标题:What is LATERAL, what is it for, and how can one use it?
原文作者:Jérémie
原文链接:https://www.depesz.com/2022/09/18/what-is-lateral-what-is-it-for-and-how-can-one-use-it/