web-dev-qa-db-ja.com

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

ウィンドウ関数で解決できると思いますが、よくわかりません。

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

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(PostgresとPostgisがあるため、アップグレードは簡単ではありません。Postgis関数は名前が変更され、他の問題がありますが、うまくいけば、すべてを書き直しており、新しいバージョンでは新しいバージョン9.Xを使用しますpostgis 2.x)

13
Lelo

いくつかの点について、

  • 混乱するだけの非一時テーブル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);
_

アウトプット

_ 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をラップして、min max(範囲)を選択します

_SELECT id_type, grp, min(date), max(date)
FROM (
  .. stuff
) AS g
GROUP BY id_type, grp
ORDER BY min(date);
_
4
Evan Carroll

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が定義されていると仮定するとUNIQUE、そうでない場合は、タイブレーカーをORDER BY句に追加して確定的な結果を得る必要があります。例:ORDER BY date, id

詳細な説明(非常に類似した質問への回答):

特に注意してください:

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

    lag(id_type, 1, id_type) OVER ()
    

    id_typeTRUE)の実際の変更のみに関心があるため、この特定のケースでは問題になりません。 NULLFALSEはどちらもstepとしてはカウントされません。

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

dateUNIQUEと定義されている場合、上記で説明したように(明確にはしていません)、結果はdense_rank()と同じであるため、row_number()は無意味です。後者はかなり安価です。

date定義されていないUNIQUEである場合(そして、重複が(date, id_type)にのみ存在することはわかりません)、すべての結果は任意なので、これらのクエリは無意味です。

また、サブクエリは通常、PostgresのCTEよりも安価です。 CTEは、必要な場合にのみ使用してください。

関連する回答と詳細な説明:

テーブルにすでに通し番号がある関連するケースでは、単一のウィンドウ関数で間に合わせることができます:

3. plpgsql関数による最高のパフォーマンス

この質問は予想外に人気が出てきたため、最高のパフォーマンスを示す別のソリューションを追加します。

SQLには、簡潔で洗練された構文でソリューションを作成するための高度なツールが多数あります。ただし、宣言型言語には、手続き型の要素を含むより複雑な要件に対する制限があります。

server-side procedural function単一の順次スキャン以上を必要としないため、これまでに投稿されたものよりも高速ですテーブルと単一のソート操作。フィッティングインデックスが利用可能な場合、単一のインデックスのみのスキャンでも可能です。

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

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

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


dbfiddle Postgres 9.6の場合 、3つすべてのパフォーマンスを比較します。ビルド- Jackのテストケース 、変更されました。

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

16

これは、ROW_NUMBER()演算の単純な減算として行うことができます(または日付が一意ではないが、_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
_

論理的には、これを_PREORDER BY_を使用した単純なDENSE_RANK()と考えることができます。つまり、一緒にランク付けされているすべての項目の_DENSE_RANK_が必要で、それらを次の順序で並べます。日付は、日付が変更されるたびに_DENSE_RANK_が増加するという厄介な問題に対処する必要があります。これは、上で示した式を使用して行います。次の構文があるとします。DENSE_RANK() OVER (PREORDER BY date, ORDER BY id_type)ここで、PREORDERはランキング計算から除外され、_ORDER BY_のみがカウントされます。

生成されたSeq列と_GROUP BY_列の両方を_id_type_にすることが重要であることに注意してください。 Seqはそれ自体では一意ではなく、重複する可能性があります。_id_type_でグループ化する必要もあります。

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

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

7
ErikE

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/WCOYFP662

6
McNets

ここに別の方法があります。これは、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
;
_

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

それぞれの島の始まりである行の場合、前の日付は事実上、前の島の終了日です。それがメインSELECTが使用するものです。 _is_start = 1_条件に一致する行のみを選択し、返された各行について、その行自体のdatebeginとして表示し、次の行の_prev_date_をend。最後の行には後続の行がないため、LEAD(prev_date)はその行に対してnullを返し、COALESCE関数がデータセットの最後の日付を置き換えます。

このソリューションで遊ぶことができます dbfiddle

アイランドを識別する追加の列を導入する場合、おそらく、各ウィンドウ関数のOVER句にPARTITION BY副次句を導入する必要があります。たとえば、_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
Andriy M

実用的なソリューションとしてよりも学術的な関心から外れて、あなたはこれを ser-defined aggregate で達成することもできます。他のソリューションと同様に、これは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 ---(ここ

これは RECURSIVE CTE 「開始時間」を1つの行から次の行に渡すため、およびいくつかの追加の(便利な)準備。

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

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)のように表示されています。

エヴァンキャロルの方法は1秒未満(つまり、それでいい!)であり、O(n)のように見えます。再帰クエリは完全に非効率的であり、最後の手段と考える必要があります。

4
joanolo