Выберите самую длинную непрерывную последовательность

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для всех рас, но планировал решить это сам.)

DaveB
источник

Ответы:

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 <> скрипеть здесь
Old 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|
*/
Джек говорит, попробуйте topanswers.xyz
источник
или, возможно, sum((lap_no=(prev+1))::integer)+1но я не уверен, что это легче читать,
говорит Джек, попробуйте topanswers.xyz