ウィンドウ関数で解決できると思いますが、よくわかりません。
次の表を想像してください
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)
いくつかの点について、
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);
_
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_type
(TRUE
)の実際の変更のみに関心があるため、この特定のケースでは問題になりません。 NULL
とFALSE
はどちらもstep
としてはカウントされません。
count(step OR NULL) OVER (ORDER BY date)
は、Postgres 9.3以前でも機能する最も短い構文です。 count()
はnull以外の値のみをカウントします...
最近のPostgresでは、より簡潔で同等の構文は次のようになります。
count(step) FILTER (WHERE step) OVER (ORDER BY date)
詳細:
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()
と同じであるため、row_number()
は無意味です。後者はかなり安価です。
date
が定義されていないUNIQUE
である場合(そして、重複が(date, id_type)
にのみ存在することはわかりません)、すべての結果は任意なので、これらのクエリは無意味です。
また、サブクエリは通常、PostgresのCTEよりも安価です。 CTEは、必要な場合にのみ使用してください。
関連する回答と詳細な説明:
テーブルにすでに通し番号がある関連するケースでは、単一のウィンドウ関数で間に合わせることができます:
この質問は予想外に人気が出てきたため、最高のパフォーマンスを示す別のソリューションを追加します。
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の場合 、パフォーマンスの違いはさらに大きくなります。
これは、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構文から変換する必要がありますが...
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
ここに別の方法があります。これは、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
_条件に一致する行のみを選択し、返された各行について、その行自体のdate
をbegin
として表示し、次の行の_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のソリューションを採用する場合は、同様の変更を追加する必要があると思います。
実用的なソリューションとしてよりも学術的な関心から外れて、あなたはこれを 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)のように見えます。再帰クエリは完全に非効率的であり、最後の手段と考える必要があります。