特定の列の連続する行の最長のシーケンスを取得するPostgreSQL 9.0でクエリを構築しようとしています。
次の表を検討してください。
lap_id (serial), lap_no (int), car_type (enum), race_id (int FK)
ここで、lap_no
は(race_id, car_type)
ごとに一意です。
クエリで指定されたrace_id
およびcar_type
の最長シーケンスを生成して、最高のint
(またはlong)を返すようにします。
次のデータで:
1, 1, red, 1
2, 2, red, 1
3, 3, red, 1
4, 4, red, 1
5, 1, blue, 1
6, 5, red, 1
7, 2, blue, 1
8, 1, green, 1
car_type = red and race_id = 1
の場合、クエリは5
フィールドの最長シーケンスとしてlap_no
を返します。
同様の質問 here が見つかりましたが、私の状況はもう少し簡単です。
(また、すべてのレースで特定のcar_type
の最長シーケンスを知りたいのですが、それを自分で解決する予定でした。)
説明の結果は、次のようにtable definitionになります。
CREATE TABLE tbl (
lap_id serial PRIMARY KEY
, lap_no int NOT NULL
, car_type enum NOT NULL
, race_id int NOT NULL -- REFERENCES ...
, UNIQUE(race_id, car_type, lap_no)
);
最長のシーケンス(1つの結果、すべての最長、同数の場合は任意のピック)を取得するには:
SELECT race_id, car_type, count(*) AS seq_len
FROM (
SELECT *, count(*) FILTER (WHERE step)
OVER (ORDER BY race_id, car_type, lap_no) AS grp
FROM (
SELECT *, (lag(lap_no) OVER (PARTITION BY race_id, car_type ORDER BY lap_no) + 1)
IS DISTINCT FROM lap_no AS step
FROM tbl
) x
) y
GROUP BY race_id, car_type, grp
ORDER BY seq_len DESC
LIMIT 1;
count(*) FILTER (WHERE step)
はTRUE
(=次のグループへのステップ)のみをカウントするため、新しいグループごとに新しい数が得られます。
SOに関する関連する質問、1つの回答はplpgsqlを使用した手続き型ソリューション:
最上位の要件がパフォーマンスである場合、単一スキャンで結果を計算できるため、この特定のケースではplpgsql関数の方が通常は高速です。
consecutivelap_no
がシーケンスを定義するという事実を利用できますはるかにシンプルで高速なバージョン:
SELECT race_id, car_type, count(*) AS seq_len
FROM (
SELECT race_id, car_type
, row_number() OVER (PARTITION BY race_id, car_type ORDER BY lap_no) - lap_no AS grp
FROM tbl
) x
GROUP BY race_id, car_type, grp
ORDER BY seq_len DESC
LIMIT 1;
連続したラップは同じgrp
になります。ラップがないと、パーティションごとのgrp
が低くなります。
これは、(race_id, car_type, lap_no)
がUNIQUE NOT NULL
であることを前提としています。 NULL値または重複はロジックを壊す可能性があります。
@ Jackのバージョン は、このlap_no
内の以前のrace_id
が同じcar_type
であったすべてのラップ(行)を効果的にカウントします。これは、より単純で高速で正確です。各car_type
がrace_id
ごとにoneシーケンスしか持てない場合に限ります。
しかしクエリを単純にするタスクの場合は、さらに単純になります。論理的には、lap_no
ごとのすべての(car_type, race_id)
はin sequenceでなければならず、ラップを数えるだけです。
SELECT race_id, car_type, count(*) AS seq_len
FROM tbl
GROUP BY race_id, car_type
ORDER BY seq_len DESC
LIMIT 1;
一方、1つのcar_type
がrace_idごとに複数の個別のシーケンスを持つことができる場合(および、質問で特に指定されていない場合)、Jackのバージョンは失敗します。
コメントへの返信/質問の明確化:クエリをone one(race_id, car_type)
に制限すると、もちろん、はるかに高速になります、
SELECT count(*) AS seq_len
FROM (
SELECT row_number() OVER (ORDER BY lap_no) - lap_no AS grp
FROM tbl
WHERE race_id = 1
AND car_type = 'red'
) x
GROUP BY grp
ORDER BY seq_len DESC
LIMIT 1;
最高のパフォーマンスの鍵は、適切なインデックスです(単一の順次スキャンで動作する前述の手続き型ソリューションを除く)。このような マルチカラムインデックス が最適です。
CREATE INDEX tbl_mult_idx ON tbl (race_id, car_type, lap_no);
テーブルに、私が最初に想定したUNIQUE
制約がある場合、これは内部的にこの(一意の)インデックスだけで実装され、別のインデックスを作成する必要がありますnot。
create table tbl (lap_no int, car_type text, race_id int);
insert into tbl values (1,'red',1),(2,'red',1),(3,'red',1),(4,'red',1), (1,'blue',1),(5,'red',1),(2,'blue',1),(1,'green',1);
select car_type, race_id, sum(case when lap_no=(prev+1) then 1 else 0 end)+1 seq_len from ( select *, lag(lap_no) over (partition by car_type, race_id order by lap_no) prev from tbl ) z group by car_type, race_id order by seq_len desc limit 1;
/* |car_type|race_id|seq_len| |:-------|------:|------:| |red | 1| 5| */