web-dev-qa-db-ja.com

最長の連続シーケンスを選択

特定の列の連続する行の最長のシーケンスを取得する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の最長シーケンスを知りたいのですが、それを自分で解決する予定でした。)

12
DaveB

説明の結果は、次のように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_typerace_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_typerace_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;

db <> fiddle ここ
古い SQLフィドル

インデックス

最高のパフォーマンスの鍵は、適切なインデックスです(単一の順次スキャンで動作する前述の手続き型ソリューションを除く)。このような マルチカラムインデックス が最適です。

CREATE INDEX tbl_mult_idx ON tbl (race_id, car_type, lap_no);

テーブルに、私が最初に想定したUNIQUE制約がある場合、これは内部的にこの(一意の)インデックスだけで実装され、別のインデックスを作成する必要がありますnot

20
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|
*/