Виберіть найдовшу безперервну послідовність


12

Я намагаюся побудувати запит у 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найвищу (або довгу).

З такими даними:

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поля.

Я знайшов таке запитання тут , однак моя ситуація дещо простіша.

(Я також хотів би знати найдовшу послідовність даної car_typeдля всіх рас, але я планував це виправити.)

Відповіді:


20

Опис описує таке визначення таблиці :

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, одна відповідь, що містить процедурне рішення з plpgsql :

Якщо головна вимога - продуктивність, функція plpgsql, як правило, у цьому конкретному випадку швидша, оскільки вона може обчислити результат за один сканування.

Швидше для послідовних номерів

Ми можемо скористатися тим, що послідовно lap_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 можуть порушити логіку.

Обговорення простішої альтернативи Джека

@ Версія Джека ефективно підраховує всі круги (рядки), де попередній lap_noу цьому race_idбув такий самий car_type. Це простіше, швидше і правильніше - доки кожен car_typeможе мати лише одну послідовність на кожну race_id.

Але для завдання, яке простий запит, може бути і простішим. Це логічно випливає , що всі lap_noза (car_type, race_id)повинні бути в послідовності , і ми могли б просто порахувати кола:

SELECT race_id, car_type, count(*) AS seq_len
FROM   tbl
GROUP  BY race_id, car_type
ORDER  BY seq_len DESC
LIMIT  1;

Якщо, з іншого боку, у вас car_typeможе бути кілька окремих послідовностей на race_id (а питання не визначено інакше), версія Джека вийде з ладу.

Швидше для даного типу гонки / автомобіля

У відповідь на коментар / роз'яснення у запитанні: обмеження запиту одним заданим (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 Fiddle

Покажчик

Найважливішим показником є ​​відповідність індексу (за винятком згаданого процедурного рішення, що працює з одним послідовним скануванням). Такий індекс багатокольонових служб найкраще:

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

Якщо у вашій таблиці є UNIQUEобмеження, яке я припускав у верхній частині, воно реалізовано просто з цим (унікальним) індексом всередині, і вам не потрібно створювати інший індекс.


Привіт Ервіне, спасибі, що робить цю роботу, проте для моєї бази даних це займає ~ 17 сек! Не припускайте, що ви можете надати модифікацію, щоб вона брала race_id та car_type як параметри, а не порівнюючи всю таблицю? (Я спробував переписати його і продовжую працювати з помилками)
DaveB

7

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

а можливо, sum((lap_no=(prev+1))::integer)+1але я не впевнений, що це легше читати
Джек каже спробувати topanswers.xyz
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.