グループ化またはウィンドウ


13

ウィンドウ関数を使用して解決できると思う状況がありますが、よくわかりません。

次の表を想像してください

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 )
  ;

列id_typeの変更ごとに新しいグループが必要です。たとえば、7:19:21から7:19:25までの1番目のグループ、7:19:26での2番目の開始および終了など。
動作した後、グループを定義するための基準をさらに追加したいと思います。

現時点では、以下のクエリを使用して...

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;

次の結果が得られます。

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

私が欲しい間:

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

この最初のステップを解決した後、グループを分割するルールとして使用する列をさらに追加します。これらの列はNULL可能になります。

Postgresバージョン:8.4 postgis 2.x)


回答:


4

いくつかの点で、

  • tmp混乱を招くような一時的なテーブルを呼び出さないでください。
  • (あなたはタイムスタンプが切り捨てられていませんでしたので、あなたの例では、我々が言うことができることをやっているタイムスタンプのためのテキストを使用しないでください.0
  • 時間のあるフィールドを呼び出さないでくださいdate。日付と時刻がある場合、それはタイムスタンプです(そして1つとして保存します)

ウィンドウ関数を使用することをお勧めします。

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);

出力

 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)

説明

最初にリセットが必要です。 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)

次に、グループを取得します。

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)

その後、我々は副選択でラップGROUP BYし、ORDERかつ最小最大(範囲)を選択

SELECT id_type, grp, min(date), max(date)
FROM (
  .. stuff
) AS g
GROUP BY id_type, grp
ORDER BY min(date);

16

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);

これは、関連する列がであると想定していますNOT NULL。それ以外の場合は、さらに行う必要があります。

また、date定義されていると仮定するとUNIQUEORDER BY決定論的な結果を得るために節にタイブレーカーを追加する必要があります。のような:ORDER BY date, id

詳細な説明(よく似た質問への回答):

特に注意してください:

  • 関連する場合、最初の(または最後の)行のコーナーケースをエレガントにカバーするには、lag()3つのパラメーターが不可欠です。(前の(次の)行がない場合、3番目のパラメーターがデフォルトとして使用されます。

    lag(id_type, 1, id_type) OVER ()

    ()の実際の変更にのみ関心があるため、この特定のケースでは問題になりません。そして両方ともとしてカウントされません。id_typeTRUENULLFALSEstep

  • count(step OR NULL) OVER (ORDER BY date)Postgres 9.3以前でも動作する最も短い構文です。count()null以外の値のみをカウントします...

    最新のPostgresでは、よりクリーンで同等の構文は次のようになります。

    count(step) FILTER (WHERE step) OVER (ORDER BY date)

    詳細:

2. 2つのウィンドウ関数、1つのサブクエリを減算します

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);

dateが定義されている場合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;

コール:

SELECT * FROM f_tmp_groups();

でテストする:

EXPLAIN (ANALYZE, TIMING OFF)  -- to focus on total performance
SELECT * FROM  f_tmp_groups();

関数を多相型でジェネリックにし、テーブル型と列名を渡すことができます。詳細:

このために関数を保持したくない場合、または永続化できない場合は、その場で一時的な関数を作成することさえできます。数ミリ秒かかります。


Postgres 9.6の dbfiddle、3つすべてのパフォーマンスを比較します。ジャックのテストケースに基づいて構築します。、変更

Postgres 8.4の dbfiddleで、パフォーマンスの違いはさらに大きくなります。


これを数回読んでください-3引数ラグで何を話しているのか、いつ使用するcount(x or null)必要があるのか​​、それがそこで何をしているのかさえ分かりません。ここで必要ないので、必要な場所にいくつかのサンプルを表示できます。そして、これらのコーナーケースをカバーするための要件の鍵となるものは何でしょうか。ところで、私はpl / pgsqlの例だけのために、下票を上票に変更しました。かっこいい。(しかし、一般的に私は他の回答を要約したり、コーナーケースをカバーする回答に反対しています-これはコーナーケースだとは言いませんが、私はそれを理解していないので)。
エヴァンキャロル

私は自分が何をcount(x or null)しているのか疑問に思っているだけではないと確信しているので、2つの別々の自己回答の質問に入れます。ご希望の場合は、両方の質問をさせていただきます。
エヴァンキャロル


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
;

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

論理的には、これはの単純なもの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はそれ自体で一意ではありませんid_type。重複する可能性があります。グループ化する必要もあります。

このトピックの詳細については:

最初のリンクは、開始日または終了日を前または次の期間の終了/開始日と同じにしたい場合に使用できるコードを提供します(したがって、ギャップはありません)。クエリに役立つその他のバージョン。SQL Server構文から変換する必要がありますが...


6

Postgres 8.4では、RECURSIVE関数を使用できます。

どうやってやっているの

再帰関数は、日付を降順で1つずつ選択することにより、各異なる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

次に、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    |
+---------------------+---------------------+---------+

チェックしてください:http : //rextester.com/WCOYFP6623


5

別の方法があります。これは、島を決定するためにLAGを使用するという点で、EvanとErwinに似ています。これらのソリューションとは、1レベルのネストのみを使用し、グループ化を行わず、かなり多くのウィンドウ関数を使用するという点で異なります。

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
;

is_startネストされたSELECT の計算列は、各島の始まりを示します。さらに、ネストされたSELECTは、各行の前の日付とデータセットの最後の日付を公開します。

それぞれの島の始まりである行の場合、前の日付は事実上前の島の終了日です。それがメインのSELECTが使用するものです。is_start = 1条件に一致する行のみを選択し、返された各行について、行のdateas beginおよび次の行のprev_dateasを表示しendます。最後の行には次の行がないLEAD(prev_date)ため、NULLを返します。この場合、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
;

また、ErwinのソリューションまたはEvanのソリューションを使用する場合は、同様の変更を追加する必要があると考えています。


5

実用的なソリューションとしてよりも学問的関心から、ユーザー定義の集計でこれを達成することもできます。他のソリューションと同様に、これはPostgres 8.4でも機能しますが、他の人がコメントしているように、可能であればアップグレードしてください。

集計nullは異なるfoo_typeように処理するため、nullの実行には同じ値が与えられ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];
$$;
create function grp_finalfunc(integer[]) returns integer language sql as $$
  select $1[1];
$$;
create aggregate grp(integer)(
  sfunc = grp_sfunc
, stype = integer[]
, finalfunc = grp_finalfunc
, initcond = '{0,0,0}'
);
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;
begin_at | 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


4

これはRECURSIVE CTE、「開始時間」をある行から次の行に渡すために使用でき、いくつかの追加の(便利な)準備があります。

このクエリは、希望する結果を返します。

WITH RECURSIVE q AS
(
    SELECT
        id_type,
        "date",
        /* We compute next id_type for convenience, plus row_number */
        row_number()  OVER (w) AS rn,
        lead(id_type) OVER (w) AS next_id_type
    FROM
        t
    WINDOW
        w AS (ORDER BY "date") 
)

準備後...再帰部

, rec AS 
(
    /* Anchor */
    SELECT
        q.rn,
        q."date" AS "begin",
        /* When next_id_type is different from Look also at **next** row to find out whether we need to mark an end */
        case when q.id_type is distinct from q.next_id_type then q."date" END AS "end",
        q.id_type
    FROM
        q
    WHERE
        rn = 1

    UNION ALL

    /* Loop */
    SELECT
        q.rn,
        /* We keep copying 'begin' from one row to the next while type doesn't change */
        case when q.id_type = rec.id_type then rec.begin else q."date" end AS "begin",
        case when q.id_type is distinct from q.next_id_type then q."date" end AS "end",
        q.id_type
    FROM
        rec
        JOIN q ON q.rn = rec.rn+1
)
-- We filter the rows where "end" is not null, and project only needed columns
SELECT
    "begin", "end", id_type
FROM
    rec
WHERE
    "end" is not null ;

これはhttp://rextester.com/POYM83542で確認できます

この方法はうまくスケーリングしません。8_641行テーブルの場合は7秒かかり、そのサイズの2倍のテーブルの場合は28秒かかります。いくつかのサンプルは、O(n ^ 2)のような実行時間を示しています。

Evan Carrolの方法は1秒未満で(つまり、それでいいのです!)、O(n)のように見えます。再帰クエリは完全に非効率的であり、最後の手段と考えるべきです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.