Задачи вида "пробелы и острова", или "gaps and islands", возникают, когда в некоторой последовательности нужно найти участки (диапазоны, интервалы) непрерывных данных – "острова" – и/или участки отсутствия данных – "пробелы".
С наступленим эры оконных функций (они же аналитические, они же over-функции) задачи вида "пробелы и острова" решаются с помощью этих функций.
Примеры ниже с таблицей t1
взяты мной из статьи Solving Gaps and Islands with Enhanced Window Functions by Itzik Ben-Gan и адаптированы для выполнения на PostgreSQL.
После примеров с таблицей t1
я разберу более прикладной оригинальный пример с таблицей ежедневных продаж daily_sales
.
Итак, в таблице t1
имеется последовательность целых чисел:
create table t1 (
col1 int not null primary key
);
insert into t1(col1)
values (2),(3),(7),(8),(9),(11),(15),(16),(17),(28)
;
select * from t1;
col1 |
---|
2 |
3 |
7 |
8 |
9 |
11 |
15 |
16 |
17 |
28 |
Выделить пробелы в последовательности проще, чем выделить острова. Сделаем это с помощью функции lead()
:
with c as (
select col1 cur,
-- next col1 value in ascending order
lead(col1) over (order by col1) nxt
from t1
)
select cur + 1 range_start,
nxt - 1 range_end
from c
-- detect gaps
where nxt - cur > 1
;
range_start | range_end |
---|---|
4 | 6 |
10 | 10 |
12 | 14 |
18 | 27 |
Здесь подзапрос c
формирует пары соседних значений столбца col1
в порядке возрастания, а внешний запрос отбирает из них те, у которых разность между соседними значениями больше 1, то есть, пары, представющие "пробелы".
"Симметричный" запрос с lag()
вместо lead()
дает такой же результат:
with c as (
select col1 cur,
lag(col1) over (order by col1) prv
from t1
)
select prv + 1 range_start,
cur - 1 range_end
from c
where cur - prv > 1
;
Чтобы выделить из последовательности "острова", потребуется, как минимум, двухходовка. Воспользуемся функциями lag()
и lead()
:
with c1 as (
select col1,
case
when col1 - lag(col1) over (order by col1) <= 1 then
0
else
1
end is_start,
case
when lead(col1) over (order by col1) - col1 <= 1 then
0
else
1
end is_end
from t1
), c2 as (
select col1 range_start,
case
when is_end = 1 then
col1
else
lead(col1, 1) over (order by col1)
end range_end,
is_start
from c1
where is_start = 1 or is_end = 1
)
select range_start, range_end
from c2
where is_start = 1
order by 1
;
range_start | range_end |
---|---|
2 | 3 |
7 | 9 |
11 | 11 |
15 | 17 |
28 | 28 |
Здесь подзапрос c1
помечает значения col1
как первые и последние значения неперывного диапазона. Подзапрос c2
работает только с первыми и последними значениями диапазонов, где is_start = 1 or is_end = 1
, и формирует последнее значение диапазона (range_end
) для всех строк, в результате чего строки с is_start = 1
содержат и первое и последнее значения диапазонов. Внешний запрос выводит эти строки.
Другой подход к выделению "островов" использует фукнции lead()
и count()
:
with c1 as (
select col1,
case
when col1 - lag(col1) over (order by col1) <= 1 then
null
else
1
end is_start
from t1
), c2 as (
select col1,
-- 1 for all members of the 1st island,
-- 2 for all members of the 2nd island,
-- and so on
count(is_start) over (order by col1) grp
from c1
)
select min(col1) range_start,
max(col1) range_end
from c2
group by grp
order by 1
;
Здесь подзапрос c1
помечает первые значения неперывных диапазонов. Но, в отличие от предыдущего примера, для остальных значений col1
столбец is_start
получает значение null
. Оконная функция count()
в подзапросе c2
формирует для членов каждого непрерывного диапазона в стоблце grp
номер диапазона, к которому они принадлежат:
col1 | grp |
---|---|
2 | 1 |
3 | 1 |
7 | 2 |
8 | 2 |
9 | 2 |
11 | 3 |
15 | 4 |
16 | 4 |
17 | 4 |
28 | 5 |
(Такой же реузльтат в данном случае дает оконная функция sum()
вместо count()
.)
А внешний запрос группирует строки по номеру диапазона и выводит минимальное и максимальное значение каждого диапазона:
range_start | range_end |
---|---|
2 | 3 |
7 | 9 |
11 | 11 |
15 | 17 |
28 | 28 |
Два рассмотренных подхода обладают неочевидным на первый взгляд свойством. Они позволяют выделять не только нормальные острова
, где разность между членами диапазона всегда равна 1, но и специальные острова
, где разность между членами диапазона не превышает некоторой пороговой величины.
Например, если принять, что значения, разность между которыми не больше 2, принадлежат к одному диапазону, то острова 7 - 9 и 11 - 11 сольются в один:
with c1 as (
select col1,
case
when col1 - lag(col1) over (order by col1) <= 2 then
null
else
1
end is_start
from t1
), c2 as (
select col1,
-- 1 for all members of the 1st island,
-- 2 for all members of the 2nd island,
-- and so on
count(is_start) over (order by col1) grp
from c1
)
select min(col1) range_start,
max(col1) range_end
from c2
group by grp
order by 1
;
range_start | range_end |
---|---|
2 | 3 |
7 | 11 |
15 | 17 |
28 | 28 |
Пороговое значение 2 установлено в подзапросе c1
.
Для выделения нормальных островов существует популярный подход, использующий оконные функциии dens_rank()
или row_number()
. Рассмотрим его:
with c as (
select col1,
-- the difference between col1 and the dense_rank value
-- is constant per island and unique per island.
-- This means that it identifies the island.
col1 - dense_rank() over (order by col1) grp
from t1
)
select
min(col1) as range_start,
max(col1) as range_end
from c
group by grp
order by 1
;
Здесь подзапрос c
формирует для членов каждого непрерывного диапазона в стоблце grp
номер диапазона, к которому они принадлежат:
col1 | grp |
---|---|
2 | 1 |
3 | 1 |
7 | 4 |
8 | 4 |
9 | 4 |
11 | 5 |
15 | 8 |
16 | 8 |
17 | 8 |
28 | 18 |
(Аналогичный результат в данном случае дает оконная функция row_number()
вместо dens_rank()
.)
А внешний запрос группирует строки по номеру диапазона и выводит минимальное и максимальное значение каждого диапазона:
range_start | range_end |
---|---|
2 | 3 |
7 | 9 |
11 | 11 |
15 | 17 |
28 | 28 |
На этом заканчиваю работу с таблицей t1
:
drop table t1;
Были рассмотрены базовые подходы к решению задач вида "пробелы и острова" на последовательности целых чисел. Теперь рассмотрим прикладной пример.
Читайте продолжение Пробелы и острова, или Gaps and islands. Часть II.
Комментариев нет:
Отправить комментарий