Lel*_*elo 13 postgresql window-functions group-by gaps-and-islands postgresql-8.4
我有一个我认为可以使用窗口函数解决的情况,但我不确定。
想象一下下表
CREATE TABLE tmp
( date timestamp,
id_type integer
) ;
INSERT INTO tmp
( date, id_type )
VALUES
( '2017-01-10 07:19:21.0', 3 ),
( '2017-01-10 07:19:22.0', 3 ),
( '2017-01-10 07:19:23.1', 3 ),
( '2017-01-10 07:19:24.1', 3 ),
( '2017-01-10 07:19:25.0', 3 ),
( '2017-01-10 07:19:26.0', 5 ),
( '2017-01-10 07:19:27.1', 3 ),
( '2017-01-10 07:19:28.0', 5 ),
( '2017-01-10 07:19:29.0', 5 ),
( '2017-01-10 07:19:30.1', 3 ),
( '2017-01-10 07:19:31.0', 5 ),
( '2017-01-10 07:19:32.0', 3 ),
( '2017-01-10 07:19:33.1', 5 ),
( '2017-01-10 07:19:35.0', 5 ),
( '2017-01-10 07:19:36.1', 5 ),
( '2017-01-10 07:19:37.1', 5 )
;
Run Code Online (Sandbox Code Playgroud)
我希望在 id_type 列的每次更改时都有一个新组。EG 第一组 7:19:21 到 7:19:25,第二组 7:19:26 开始和结束,以此类推。
运行后,我想包含更多标准来定义组。
此时,使用下面的查询......
SELECT distinct
min(min(date)) over w as begin,
max(max(date)) over w as end,
id_type
from tmp
GROUP BY id_type
WINDOW w as (PARTITION BY id_type)
order by begin;
Run Code Online (Sandbox Code Playgroud)
我得到以下结果:
begin end id_type
2017-01-10 07:19:21.0 2017-01-10 07:19:32.0 3
2017-01-10 07:19:26.0 2017-01-10 07:19:37.1 5
Run Code Online (Sandbox Code Playgroud)
虽然我想:
begin end id_type
2017-01-10 07:19:21.0 2017-01-10 07:19:25.0 3
2017-01-10 07:19:26.0 2017-01-10 07:19:26.0 5
2017-01-10 07:19:27.1 2017-01-10 07:19:27.1 3
2017-01-10 07:19:28.0 2017-01-10 07:19:29.0 5
2017-01-10 07:19:30.1 2017-01-10 07:19:30.1 3
2017-01-10 07:19:31.0 2017-01-10 07:19:31.0 5
2017-01-10 07:19:32.0 2017-01-10 07:19:32.0 3
2017-01-10 07:19:33.1 2017-01-10 07:19:37.1 5
Run Code Online (Sandbox Code Playgroud)
在我解决了这第一步之后,我将添加更多的列作为规则来打破组,而其他的这些列都是可以为空的。
Postgres 版本:8.4(我们有 Postgres 和 Postgis,所以不容易升级。Postgis 函数更改名称还有其他问题,但希望我们已经重新编写了所有内容,新版本将使用更新的 9.X 版本postgis 2.x)
Erw*_*ter 17
计算形成组的步骤,类似于Evan 的想法,有修改和修复:
SELECT id_type
, min(date) AS begin
, max(date) AS end
, count(*) AS row_ct -- optional addition
FROM (
SELECT date, id_type, count(step OR NULL) OVER (ORDER BY date) AS grp
FROM (
SELECT date, id_type
, lag(id_type, 1, id_type) OVER (ORDER BY date) <> id_type AS step
FROM tmp
) sub1
) sub2
GROUP BY id_type, grp
ORDER BY min(date);
Run Code Online (Sandbox Code Playgroud)
这假设涉及的列是NOT NULL. 否则你需要做更多。
还假设date被定义UNIQUE,否则您需要在ORDER BY条款中添加一个决胜局以获得确定性结果。喜欢:ORDER BY date, id。
详细解释(回答非常相似的问题):
特别注意:
在相关情况下,lag()3 个参数对于优雅地覆盖第一行(或最后一行)的极端情况至关重要。(如果没有前(下)行,则默认使用第三个参数。
lag(id_type, 1, id_type) OVER ()
Run Code Online (Sandbox Code Playgroud)
由于我们在实际只关心变化的id_type(TRUE),它不会在这种特殊情况下无所谓。NULL并且FALSE两者都不算作step。
count(step OR NULL) OVER (ORDER BY date)是最短的语法,也适用于 Postgres 9.3 或更早版本。count()只计算非空值......
在现代 Postgres 中,更简洁、等效的语法是:
count(step) FILTER (WHERE step) OVER (ORDER BY date)
Run Code Online (Sandbox Code Playgroud)
细节:
与Erik 的想法类似,但进行了修改:
SELECT min(date) AS begin
, max(date) AS end
, id_type
FROM (
SELECT date, id_type
, row_number() OVER (ORDER BY date)
- row_number() OVER (PARTITION BY id_type ORDER BY date) AS grp
FROM tmp
) sub
GROUP BY id_type, grp
ORDER BY min(date);
Run Code Online (Sandbox Code Playgroud)
If dateis defined UNIQUE,就像我上面提到的(你从未澄清过),dense_rank()将毫无意义,因为结果与 for 相同,row_number()而后者要便宜得多。
如果date是没有定义UNIQUE(我们不知道,只重复上(date, id_type)),所有这些查询都是没有意义的,因为结果是任意的。
此外,子查询通常比 Postgres 中的 CTE 便宜。仅在需要时使用 CTE 。
相关答案有更多解释:
在表中已经有运行数字的相关情况下,我们可以使用单个窗口函数:
由于这个问题出乎意料地流行,我将添加另一个解决方案来展示最佳性能。
SQL 有许多复杂的工具来创建具有简短而优雅的语法的解决方案。但是,对于涉及过程元素的更复杂的需求,声明性语言有其局限性。
一个服务器端程序的功能是这个速度比任何张贴到目前为止,因为它仅需要一个单一的顺序扫描在桌子上和一个单一的排序操作。如果有合适的索引可用,即使只是单个索引扫描。
CREATE OR REPLACE FUNCTION f_tmp_groups()
RETURNS TABLE (id_type int, grp_begin timestamp, grp_end timestamp) AS
$func$
DECLARE
_row tmp; -- use table type for row variable
BEGIN
FOR _row IN
TABLE tmp ORDER BY date -- add more columns to make order deterministic
LOOP
CASE _row.id_type = id_type
WHEN TRUE THEN -- same group continues
grp_end := _row.date; -- remember last date so far
WHEN FALSE THEN -- next group starts
RETURN NEXT; -- return result for last group
id_type := _row.id_type;
grp_begin := _row.date;
grp_end := _row.date;
ELSE -- NULL for 1st row
id_type := _row.id_type; -- remember row data for starters
grp_begin := _row.date;
grp_end := _row.date;
END CASE;
END LOOP;
RETURN NEXT; -- return last result row
END
$func$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)
称呼:
SELECT * FROM f_tmp_groups();
Run Code Online (Sandbox Code Playgroud)
测试:
EXPLAIN (ANALYZE, TIMING OFF) -- to focus on total performance
SELECT * FROM f_tmp_groups();
Run Code Online (Sandbox Code Playgroud)
您可以使用多态类型使函数泛型并传递表类型和列名。细节:
如果您不想或不能为此保留一个函数,甚至可以即时创建一个临时函数。花费几毫秒。
Postgres 9.6 的dbfiddle,比较所有三个的性能。基于Jack 的测试用例,已修改。
dbfiddle for Postgres 8.4,性能差异更大。
就几点而言,
tmp会造成混乱的非临时表。.0)date。如果它有日期和时间,它是一个时间戳(并将其存储为一个)最好使用窗口函数..
SELECT id_type, grp, min(date), max(date)
FROM (
SELECT date, id_type, count(is_reset) OVER (ORDER BY date) AS grp
FROM (
SELECT date, id_type, CASE WHEN lag(id_type) OVER (ORDER BY date) <> id_type THEN 1 END AS is_reset
FROM tmp
) AS t
) AS g
GROUP BY id_type, grp
ORDER BY min(date);
Run Code Online (Sandbox Code Playgroud)
输出
id_type | grp | min | max
---------+-----+-----------------------+-----------------------
3 | 0 | 2017-01-10 07:19:21.0 | 2017-01-10 07:19:25.0
5 | 1 | 2017-01-10 07:19:26.0 | 2017-01-10 07:19:26.0
3 | 2 | 2017-01-10 07:19:27.1 | 2017-01-10 07:19:27.1
5 | 3 | 2017-01-10 07:19:28.0 | 2017-01-10 07:19:29.0
3 | 4 | 2017-01-10 07:19:30.1 | 2017-01-10 07:19:30.1
5 | 5 | 2017-01-10 07:19:31.0 | 2017-01-10 07:19:31.0
3 | 6 | 2017-01-10 07:19:32.0 | 2017-01-10 07:19:32.0
5 | 7 | 2017-01-10 07:19:33.1 | 2017-01-10 07:19:37.1
(8 rows)
Run Code Online (Sandbox Code Playgroud)
首先我们需要重置..我们用以下命令生成它们lag()
SELECT date, id_type, CASE WHEN lag(id_type) OVER (ORDER BY date) <> id_type THEN 1 END AS is_reset
FROM tmp
ORDER BY date;
date | id_type | is_reset
-----------------------+---------+----------
2017-01-10 07:19:21.0 | 3 |
2017-01-10 07:19:22.0 | 3 |
2017-01-10 07:19:23.1 | 3 |
2017-01-10 07:19:24.1 | 3 |
2017-01-10 07:19:25.0 | 3 |
2017-01-10 07:19:26.0 | 5 | 1
2017-01-10 07:19:27.1 | 3 | 1
2017-01-10 07:19:28.0 | 5 | 1
2017-01-10 07:19:29.0 | 5 |
2017-01-10 07:19:30.1 | 3 | 1
2017-01-10 07:19:31.0 | 5 | 1
2017-01-10 07:19:32.0 | 3 | 1
2017-01-10 07:19:33.1 | 5 | 1
2017-01-10 07:19:35.0 | 5 |
2017-01-10 07:19:36.1 | 5 |
2017-01-10 07:19:37.1 | 5 |
(16 rows)
Run Code Online (Sandbox Code Playgroud)
然后我们数数得到组。
SELECT date, id_type, count(is_reset) OVER (ORDER BY date) AS grp
FROM (
SELECT date, id_type, CASE WHEN lag(id_type) OVER (ORDER BY date) <> id_type THEN 1 END AS is_reset
FROM tmp
ORDER BY date
) AS t
ORDER BY date
date | id_type | grp
-----------------------+---------+-----
2017-01-10 07:19:21.0 | 3 | 0
2017-01-10 07:19:22.0 | 3 | 0
2017-01-10 07:19:23.1 | 3 | 0
2017-01-10 07:19:24.1 | 3 | 0
2017-01-10 07:19:25.0 | 3 | 0
2017-01-10 07:19:26.0 | 5 | 1
2017-01-10 07:19:27.1 | 3 | 2
2017-01-10 07:19:28.0 | 5 | 3
2017-01-10 07:19:29.0 | 5 | 3
2017-01-10 07:19:30.1 | 3 | 4
2017-01-10 07:19:31.0 | 5 | 5
2017-01-10 07:19:32.0 | 3 | 6
2017-01-10 07:19:33.1 | 5 | 7
2017-01-10 07:19:35.0 | 5 | 7
2017-01-10 07:19:36.1 | 5 | 7
2017-01-10 07:19:37.1 | 5 | 7
(16 rows)
Run Code Online (Sandbox Code Playgroud)
然后我们包裹一个子选择GROUP BY并ORDER选择最小最大(范围)
SELECT id_type, grp, min(date), max(date)
FROM (
.. stuff
) AS g
GROUP BY id_type, grp
ORDER BY min(date);
Run Code Online (Sandbox Code Playgroud)
您可以将其作为ROW_NUMBER()操作的简单减法来执行(或者如果您的日期不是唯一的,但仍然是唯一的 per id_type,那么您可以DENSE_RANK()改为使用,尽管这将是一个更昂贵的查询):
WITH IdTypes AS (
SELECT
date,
id_type,
Row_Number() OVER (ORDER BY date)
- Row_Number() OVER (PARTITION BY id_type ORDER BY date)
AS Seq
FROM
tmp
)
SELECT
Min(date) AS begin,
Max(date) AS end,
id_type
FROM IdTypes
GROUP BY id_type, Seq
ORDER BY begin
;
Run Code Online (Sandbox Code Playgroud)
在 DB Fiddle 上查看这项工作 (或查看DENSE_RANK 版本)
结果:
begin end id_type
--------------------- --------------------- -------
2017-01-10 07:19:21 2017-01-10 07:19:25 3
2017-01-10 07:19:26 2017-01-10 07:19:26 5
2017-01-10 07:19:27.1 2017-01-10 07:19:27.1 3
2017-01-10 07:19:28 2017-01-10 07:19:29 5
2017-01-10 07:19:30.1 2017-01-10 07:19:30.1 3
2017-01-10 07:19:31 2017-01-10 07:19:31 5
2017-01-10 07:19:32 2017-01-10 07:19:32 3
2017-01-10 07:19:33.1 2017-01-10 07:19:37.1 5
Run Code Online (Sandbox Code Playgroud)
从逻辑上讲,您可以将其视为一个简单DENSE_RANK()的PREORDER BY,也就是说,您希望DENSE_RANK所有排列在一起的项目中的 ,并且您希望它们按日期排序,您只需要处理以下事实的讨厌问题在日期的每次更改时,DENSE_RANK都会增加。您可以通过使用我上面向您展示的表达式来做到这一点。试想一下,如果你有这样的语法:DENSE_RANK() OVER (PREORDER BY date, ORDER BY id_type)其中PREORDER从排名计算中排除,只有ORDER BY进行计数。
请注意,它对GROUP BY生成的Seq列和列都很重要id_type。Seq本身不是唯一的,可能会有重叠——您还必须分组 by id_type。
有关此主题的进一步阅读:
如果您希望开始或结束日期与上一期或下一期的结束/开始日期相同(因此没有间隙),则第一个链接为您提供了一些可以使用的代码。加上可以帮助您进行查询的其他版本。尽管它们必须从 SQL Server 语法转换而来......
在 Postgres 8.4 上,您可以使用RECURSIVE函数。
他们是怎么做到的呢
递归函数通过按降序一一选择日期为每个不同的 id_type 添加一个级别。
date | id_type | lv
--------------------------------------
2017-01-10 07:19:21.0 3 8
2017-01-10 07:19:22.0 3 8
2017-01-10 07:19:23.1 3 8
2017-01-10 07:19:24.1 3 8
2017-01-10 07:19:25.0 3 8
2017-01-10 07:19:26.0 5 7
2017-01-10 07:19:27.1 3 6
2017-01-10 07:19:28.0 5 5
2017-01-10 07:19:29.0 5 5
2017-01-10 07:19:30.1 3 4
2017-01-10 07:19:31.0 5 3
2017-01-10 07:19:32.0 3 2
2017-01-10 07:19:33.1 5 1
2017-01-10 07:19:35.0 5 1
2017-01-10 07:19:36.1 5 1
2017-01-10 07:19:37.1 5 1
Run Code Online (Sandbox Code Playgroud)
然后使用 MAX(date), MIN(date) 按级别分组,id_type 以获得所需的结果。
with RECURSIVE rdates as
(
(select date, id_type, 1 lv
from yourTable
order by date desc
limit 1
)
union
(select d.date, d.id_type,
case when r.id_type = d.id_type
then r.lv
else r.lv + 1
end lv
from yourTable d
inner join rdates r
on d.date < r.date
order by date desc
limit 1)
)
select min(date) StartDate,
max(date) EndDate,
id_type
from rdates
group by lv, id_type
;
+---------------------+---------------------+---------+
| startdate | enddate | id_type |
+---------------------+---------------------+---------+
| 10.01.2017 07:19:21 | 10.01.2017 07:19:25 | 3 |
| 10.01.2017 07:19:26 | 10.01.2017 07:19:26 | 5 |
| 10.01.2017 07:19:27 | 10.01.2017 07:19:27 | 3 |
| 10.01.2017 07:19:28 | 10.01.2017 07:19:29 | 5 |
| 10.01.2017 07:19:30 | 10.01.2017 07:19:30 | 3 |
| 10.01.2017 07:19:31 | 10.01.2017 07:19:31 | 5 |
| 10.01.2017 07:19:32 | 10.01.2017 07:19:32 | 3 |
| 10.01.2017 07:19:33 | 10.01.2017 07:19:37 | 5 |
+---------------------+---------------------+---------+
Run Code Online (Sandbox Code Playgroud)
检查它:http : //rextester.com/WCOYFP6623
这是另一种方法,它类似于 Evan 和 Erwin 的方法,因为它使用 LAG 来确定岛屿。它与那些解决方案的不同之处在于它只使用一层嵌套,没有分组,并且使用了更多的窗口函数:
SELECT
id_type,
date AS begin,
COALESCE(
LEAD(prev_date) OVER (ORDER BY date ASC),
last_date
) AS end
FROM
(
SELECT
id_type,
date,
LAG(date) OVER (ORDER BY date ASC) AS prev_date,
MAX(date) OVER () AS last_date,
CASE id_type
WHEN LAG(id_type) OVER (ORDER BY date ASC)
THEN 0
ELSE 1
END AS is_start
FROM
tmp
) AS derived
WHERE
is_start = 1
ORDER BY
date ASC
;
Run Code Online (Sandbox Code Playgroud)
is_start嵌套 SELECT 中的计算列标记了每个岛的开始。此外,嵌套的 SELECT 公开每一行的上一个日期和数据集的最后一个日期。
对于作为其各自岛开始的行,前一个日期实际上是前一个岛的结束日期。这就是主要的 SELECT 使用它的原因。它只选择与is_start = 1条件匹配的行,对于每个返回的行,它显示该行自己的dateasbegin和下一行的prev_dateas end。由于最后一行没有后续行,LEAD(prev_date)因此为其返回空值,COALESCE 函数将替换数据集的最后日期。
您可以在 dbfiddle使用此解决方案。
在引入标识孤岛的附加列时,您可能希望在每个窗口函数的 OVER 子句中引入一个 PARTITION BY 子句。例如,如果您想检测由 a 定义的组内的岛屿parent_id,则上述查询可能需要如下所示:
SELECT
parent_id,
id_type,
date AS begin,
COALESCE(
LEAD(prev_date) OVER (PARTITION BY parent_id ORDER BY date ASC),
last_date
) AS end
FROM
(
SELECT
parent_id,
id_type,
date,
LAG(date) OVER (PARTITION BY parent_id ORDER BY date ASC) AS prev_date,
MAX(date) OVER (PARTITION BY parent_id) AS last_date,
CASE id_type
WHEN LAG(id_type) OVER (PARTITION BY parent_id ORDER BY date ASC)
THEN 0
ELSE 1
END AS is_start
FROM
tmp
) AS derived
WHERE
is_start = 1
ORDER BY
date ASC
;Run Code Online (Sandbox Code Playgroud)
如果您决定采用 Erwin 或 Evan 的解决方案,我相信也需要对其进行类似的更改。
出于学术兴趣而不是作为实用的解决方案,您还可以使用用户定义的聚合来实现这一点。与其他解决方案一样,这甚至可以在 Postgres 8.4 上运行,但正如其他人所评论的那样,如果可以,请升级。
聚合处理null就好像它是不同的一样foo_type,因此将给出相同的空值运行grp- 这可能是也可能不是您想要的。
Run Code Online (Sandbox Code Playgroud)create function grp_sfunc(integer[],integer) returns integer[] language sql as $$ select array[$1[1]+($1[2] is distinct from $2 or $1[3]=0)::integer,$2,1]; $$;Run Code Online (Sandbox Code Playgroud)create function grp_finalfunc(integer[]) returns integer language sql as $$ select $1[1]; $$;Run Code Online (Sandbox Code Playgroud)create aggregate grp(integer)( sfunc = grp_sfunc , stype = integer[] , finalfunc = grp_finalfunc , initcond = '{0,0,0}' );
Run Code Online (Sandbox Code Playgroud)select min(foo_at) begin_at, max(foo_at) end_at, foo_type from (select *, grp(foo_type) over (order by foo_at) from foo) z group by grp, foo_type order by 1;开始时间 | end_at | foo_type :-------------------- | :-------------------- | -------: 2017-01-10 07:19:21 | 2017-01-10 07:19:25 | 3 2017-01-10 07:19:26 | 2017-01-10 07:19:26 | 5 2017-01-10 07:19:27.1 | 2017-01-10 07:19:27.1 | 3 2017-01-10 07:19:28 | 2017-01-10 07:19:29 | 5 2017-01-10 07:19:30.1 | 2017-01-10 07:19:30.1 | 3 2017-01-10 07:19:31 | 2017-01-10 07:19:31 | 5 2017-01-10 07:19:32 | 2017-01-10 07:19:32 | 3 2017-01-10 07:19:33.1 | 2017-01-10 07:19:37.1 | 5
dbfiddle在这里
| 归档时间: |
|
| 查看次数: |
1867 次 |
| 最近记录: |