Это продолжение поста Пробелы и острова, или Gaps and islands. Часть I
Напомню, что задачи вида "пробелы и острова", или "gaps and islands", возникают, когда в некоторой последовательности нужно найти участки (диапазоны, интервалы) непрерывных данных – "острова" – и/или участки отсутствия данных – "пробелы".
Часто последовательность данных – это хронологическая последовательность, то есть, развернутая во времени, например, по датам. Тогда "острова" – это непрерывные периоды, когда некое событие происходит ежедневно, а "пробелы" – это непрерывные периоды, в течение которых это событие не происходит.
Рассмотрим решение задачи "пробелы и острова" на хронологической последовательности данных.
Пусть в таблице daily_sales
хранятся данные ежедневных продаж товаров A, B и C за неделю с 2024-01-05
по 2024-01-11
:
create table daily_sales (
article varchar(10),
sales_date date,
sales int
);
insert into daily_sales
values
('A', date '2024-01-05', 15),
('B', date '2024-01-05', 10),
('A', date '2024-01-06', 12),
('B', date '2024-01-06', 12),
('A', date '2024-01-07', 12),
('B', date '2024-01-07', 15),
('С', date '2024-01-07', 3),
('B', date '2024-01-08', 11),
('С', date '2024-01-08', 5),
('A', date '2024-01-09', 14),
('B', date '2024-01-09', 15),
('С', date '2024-01-09', 2),
('B', date '2024-01-10', 10),
('A', date '2024-01-11', 15)
;
select * from daily_sales;
article | sales_date | sales |
---|---|---|
A | 2024-01-05 | 15 |
B | 2024-01-05 | 10 |
A | 2024-01-06 | 12 |
B | 2024-01-06 | 12 |
A | 2024-01-07 | 12 |
B | 2024-01-07 | 15 |
С | 2024-01-07 | 3 |
B | 2024-01-08 | 11 |
С | 2024-01-08 | 5 |
A | 2024-01-09 | 14 |
B | 2024-01-09 | 15 |
С | 2024-01-09 | 2 |
B | 2024-01-10 | 10 |
A | 2024-01-11 | 15 |
Нужно получить периоды ежедневных продаж для каждого товара – то есть, нужно решить задачу выделения нормальных островов. Воспользуемся ранее приведенным рецептом, модифицировав его для хронологической последовательности (для дат) и для нескольких последовательностей (для каждого товара):
with q as (
select article,
sales_date,
sales_date - date '1970-01-01' - row_number() over (partition by article order by sales_date) grp
from daily_sales
)
select
article,
min(sales_date) first_date,
max(sales_date) last_date
from q
group by article, grp
order by 1, 2
;
Здесь вычитание date '1970-01-01'
из sales_date
дает число, из которого мы вычитаем результат оконной функции row_numbr() over (partition by article order by sales_date)
и получаем значение, идентифицирующее нормальный "остров" в последовательности дат для каждого товара:
article | sales_date | grp |
---|---|---|
A | 2024-01-05 | 19726 |
A | 2024-01-06 | 19726 |
A | 2024-01-07 | 19726 |
A | 2024-01-09 | 19727 |
A | 2024-01-11 | 19728 |
B | 2024-01-05 | 19726 |
B | 2024-01-06 | 19726 |
B | 2024-01-07 | 19726 |
B | 2024-01-08 | 19726 |
B | 2024-01-09 | 19726 |
B | 2024-01-10 | 19726 |
С | 2024-01-07 | 19728 |
С | 2024-01-08 | 19728 |
С | 2024-01-09 | 19728 |
А внешний запрос группирует строки по товару и идентификатору "острова" и выводит для кадого товара минимальное и максимальное значение каждого "острова":
article | first_date | last_date |
---|---|---|
A | 2024-01-05 | 2023-01-07 |
A | 2024-01-09 | 2023-01-09 |
A | 2024-01-11 | 2023-01-11 |
B | 2024-01-05 | 2023-01-10 |
C | 2024-01-07 | 2023-01-09 |
Теперь найдем "пробелы" для каждого товара, адаптировав ранее приведенный рецепт для "пробелов" для таблицы daily_dales
:
with q as (
select article,
sales_date cur,
lead(sales_date) over (partition by article order by sales_date) nxt
from daily_sales
)
select
article,
cur + 1 first_date,
nxt - 1 last_date
from q
where nxt - cur > 1
;
article | first_date | last_date |
---|---|---|
A | 2024-01-08 | 2024-01-08 |
A | 2024-01-10 | 2024-01-10 |
Видим, что, хотя данные в таблице представляют неделю продаж, запрос находит только периоды без продаж ("пробелы"), ограниченные периодами продаж ("островами") и не находит периоды без продаж, примыкающие к началу или концу недели. Как же их получить?
Предлагаю рассмотреть универсальное решение для выделения и "пробелов" и "островов".
Для этого вначале дополним данные таблицы daily_sales
данными о нулевых продажах товаров в те дни, когда продаж не было. Создам соответствущее вью:
create view daily_sales_vw as
with prod as (
select article, date '2024-01-05' + n sales_date
from generate_series(0, 6) as d(n),
(select distinct article from daily_sales) a
)
select prod.article,
prod.sales_date,
coalesce(sales, 0) sales
from prod
left join daily_sales ds
on prod.article = ds.article
and prod.sales_date = ds.sales_date
;
select * from daily_sales_vw order by article, sales_date;
article | sales_date | sales |
---|---|---|
A | 2024-01-05 | 15 |
B | 2024-01-05 | 10 |
C | 2024-01-05 | 0 |
A | 2024-01-06 | 12 |
B | 2024-01-06 | 12 |
C | 2024-01-06 | 0 |
A | 2024-01-07 | 12 |
B | 2024-01-07 | 15 |
С | 2024-01-07 | 3 |
A | 2024-01-08 | 0 |
B | 2024-01-08 | 11 |
С | 2024-01-08 | 5 |
A | 2024-01-09 | 14 |
B | 2024-01-09 | 15 |
С | 2024-01-09 | 2 |
A | 2024-01-10 | 0 |
B | 2024-01-10 | 10 |
С | 2024-01-10 | 0 |
A | 2024-01-11 | 15 |
B | 2024-01-11 | 0 |
С | 2024-01-11 | 0 |
Запрос к daily_sales_vw
возвращает данные продаж каждого из трех товаров в каждый из дней периода 2024-01-05
- 2024-01-11
.
На этих данных решим задачу выделения "пробелов" и "островов", используя три группировки:
- группировка по товарам,
- группировка по товарам и событию продажи,
- группировка по товарам, событию продажи и критерию группировки, рассчитанному с помощью двух первых группировок.
Посмотрим вначале на две первые группировки:
with q as (
select
article,
sales_date,
sign(sales) sale_event,
row_number() over (partition by article order by sales_date) rn1,
row_number() over (partition by article, sign(sales) order by sales_date) rn2
from daily_sales_vw
)
select q.*, rn1 - rn2 grp
from q
;
article | sales_date | sale_event | rn1 | rn2 | grp |
---|---|---|---|---|---|
A | 2024-01-05 | 1 | 1 | 1 | 0 |
A | 2024-01-06 | 1 | 2 | 2 | 0 |
A | 2024-01-07 | 1 | 3 | 3 | 0 |
A | 2024-01-08 | 0 | 4 | 1 | 3 |
A | 2024-01-09 | 1 | 5 | 4 | 1 |
A | 2024-01-10 | 0 | 6 | 2 | 4 |
A | 2024-01-11 | 1 | 7 | 5 | 2 |
B | 2024-01-05 | 1 | 1 | 1 | 0 |
B | 2024-01-06 | 1 | 2 | 2 | 0 |
B | 2024-01-07 | 1 | 3 | 3 | 0 |
B | 2024-01-08 | 1 | 4 | 4 | 0 |
B | 2024-01-09 | 1 | 5 | 5 | 0 |
B | 2024-01-10 | 1 | 6 | 6 | 0 |
B | 2024-01-11 | 0 | 7 | 1 | 6 |
C | 2024-01-05 | 0 | 1 | 1 | 0 |
C | 2024-01-06 | 0 | 2 | 2 | 0 |
С | 2024-01-07 | 1 | 3 | 1 | 2 |
С | 2024-01-08 | 1 | 4 | 2 | 2 |
С | 2024-01-09 | 1 | 5 | 3 | 2 |
С | 2024-01-10 | 0 | 6 | 3 | 3 |
С | 2024-01-11 | 0 | 7 | 4 | 3 |
Здесь группировка по товару и группировка по товару и событию продажи используются для последовательной нумерации строк внутри групп, см. столбцы rn1
и rn2
, и получения разности этих номеров, см. столбец grp
.
Сочетание значений article
, sales_event
, grp
и позволяет выделить "острова" и "пробелы". Так, для товара A указанное сочетание одинаково для дат с 2024-01-05
по 2024-01-07
, что соответствует первому "острову" товара A. Далее идет однодневный "пробел" 2024-01-08
, однодневный "остров" 2024-01-09
и т.д.
Итак, получим непрерывные периоды продаж и периоды без продаж:
with q as (
select
article,
sales_date,
sign(sales) sales_event,
row_number() over (partition by article order by sales_date)
- row_number() over (partition by article, sign(sales) order by sales_date) grp
from daily_sales
)
select article,
sales_event,
min(sales_date) first_date,
max(sales_date) last_date
from q
group by article, sales_event, grp
order by article, first_date
;
article | sales_event | first_date | last_date |
---|---|---|---|
A | 1 | 2024-01-05 | 2024-01-07 |
A | 0 | 2024-01-08 | 2024-01-08 |
A | 1 | 2024-01-09 | 2024-01-09 |
A | 0 | 2024-01-10 | 2024-01-10 |
A | 1 | 2024-01-11 | 2024-01-11 |
B | 1 | 2024-01-05 | 2024-01-10 |
B | 0 | 2024-01-11 | 2024-01-11 |
C | 0 | 2024-01-05 | 2024-01-06 |
С | 1 | 2024-01-07 | 2024-01-09 |
С | 0 | 2024-01-10 | 2024-01-11 |
Если нас интересуют только периоды без продаж – "пробелы" – или только периоды продаж – "острова", – то достаточно добавить во внешний запрос условие where sales_event = 0
(нет продаж) или where sales_event = 1
(есть продажи).
Итак, мы рассмотрелии решение задачи "пробелы и острова" на хронологической последовательности данных.
На этом заканчиваю работу с демонстрационными данными:
drop viw daily_sales_vw;
drop table daily_sales;
В заключение замечу, что "пробелы" и "острова" можно с равным успехом выделять на последовательностях, члены которых представляют собой диапазоны. Например, интервалы времени или периоды дат, в течение (или в конце или начале) которых произошло некоторое событие. Тогда "пробелы" и "острова" образуются слиянием смежных диапазонов, отвечающих соответствующим условиям.
Комментариев нет:
Отправить комментарий