分组或窗口

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

1. 窗口函数加子查询

计算形成组的步骤,类似于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_typeTRUE),它不会在这种特殊情况下无所谓。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)

    细节:

2、两个窗口函数相减,一个子查询

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 。

相关答案有更多解释:

在表中已经有运行数字的相关情况下,我们可以使用单个窗口函数:

3. plpgsql 功能的顶级性能

由于这个问题出乎意料地流行,我将添加另一个解决方案来展示最佳性能。

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,性能差异更大。


Eva*_*oll 8

就几点而言,

  • 不要调用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 BYORDER选择最小最大(范围)

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)


Eri*_*ikE 7

您可以将其作为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_typeSeq本身不是唯一的,可能会有重叠——您还必须分组 by id_type

有关此主题的进一步阅读:

如果您希望开始或结束日期与上一期或下一期的结束/开始日期相同(因此没有间隙),则第一个链接为您提供了一些可以使用的代码。加上可以帮助您进行查询的其他版本。尽管它们必须从 SQL Server 语法转换而来......


McN*_*ets 6

在 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


And*_*y M 5

这是另一种方法,它类似于 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 的解决方案,我相信也需要对其进行类似的更改。


Jac*_*las 5

出于学术兴趣而不是作为实用的解决方案,您还可以使用用户定义的聚合来实现这一点。与其他解决方案一样,这甚至可以在 Postgres 8.4 上运行,但正如其他人所评论的那样,如果可以,请升级。

聚合处理null就好像它是不同的一样foo_type,因此将给出相同的空值运行grp- 这可能是也可能不是您想要的。

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;
Run Code Online (Sandbox Code Playgroud)
开始时间 | 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在这里