У меня есть две таблицы:
contract(..., d1 date, d2 date, ...)
, которая описывает некоторые контракты и содержит несколько миллионов (n * 10 ^ 6 !!!) строк, d1
& d2
- этоДаты начала и окончания контракта соответственно calendar (d date)
, который содержит список непрерывных дат (такая таблица очень полезна для различных целей)
Мне нужно рассчитать количество активных контрактов длякаждая строка calendar
, при условии, что контракт активен, если contract.d1 <= calendar.d <= contract.d2
.
. Сейчас я ищу самый разумный способ решения этой проблемы.
Сначала я попробовалподход грубой силы (на всякий случай):
select d, count(*)
from data
join calendar on d between d1 and d2
group by d;
Этот запрос явно очень тяжелый, поскольку он заставляет Oracle работать с временной таблицей, где каждая запись из contract
появляется N раз, где N - этоколичество дней между d1
и d2
для этого контракта.
Другой метод состоял в том, чтобы избегать объединения таблиц и подсчета контрактов в подвыборке, но он также не настолько умен:
select d,
(select count(*) from contract where d between d1 and d2)
from calendar;
В этом случае Oracleдолжен выполнить огромный подзапрос для каждой записи calendar
.
Затем я решил преобразовать свою таблицу contract
в CTE, описывающую отдельные события:
- заключение нового контракта:это событие срабатывает на
d1
и увеличивает количество контрактов на 1, поэтому я описал его как select d1, +1 from contract
- расторжение контрактов: это событие вызывает объявление
d2
+ 1 и уменьшает количество контрактов на 1поэтому я описал его как select d2+1, -1 from contract
Этот новый CTE позволяет мне рассчитывать ежедневные и текущие изменения в общей сумме контрактов.Понятно, что текущее изменение для каждого дня равно общему количеству контрактов на этот день, поэтому я могу немедленно ВЛЕВО присоединить результат к календарю:
with
events (d,n) as (
select d1, +1 from contract -- new contracts
union all
select d2+1, -1 from contract -- terminated contracts
),
counts_daily(d,n) as (
select d, sum(n)
from events
group by d
),
counts_running(d,n) as (
select d, sum(n) over (order by d)
from counts_daily
order by d
)
select d,
nvl(n, lag(n ignore nulls) over(order by d)) n
from calendar
left join counts_running using(d)
order by d;
Я использую ВЛЕВОЕ объединение в качестве counts_running
существуют только для дат, когда некоторые контракты начинаются или прекращаются.Конструкция nvl(...,lag(...))
в результирующем операторе выбора необходима для получения значений для календарных дат без событий, она берет значение с предыдущей предыдущей даты с событиями.
Этот запрос намного лучше, чем предыдущие.поскольку это значительно не увеличивает размеры исходных данных во время вычислений.
Я также нашел способ еще больше улучшить этот запрос и сделать его примерно в 1,5 ... 2 раза быстрее.Принимая во внимание отдельные события вместо контрактов, я не помещал их в отдельный CTE, но немедленно выполнил агрегацию, позволяющую получать ежедневные подсчеты +1 и -1 событий.Все остальное осталось почти таким же:
with
events_agg (d,n) as (
select d1, count(*) from contract group by d1 -- new contracts
union all
select d2+1, -count(*) from contract group by d2 -- term. contracts
),
counts_daily(d,n) as (
select d, sum(n)
from events_agg
group by d
),
counts_running(d,n) as (
select d, sum(n) over (order by d)
from counts_daily
order by d
)
select d,
nvl(n, lag(n ignore nulls) over(order by d)) n
from calendar
left join counts_running using(d)
order by d;
Так что я просто агрегировал события и позволил oracle UNION гораздо меньшим наборам данных.
Не могли бы вы предложить другие методы?Спасибо.
Вы можете эмулировать тестовые данные со следующими CTE:
with
--- TEST DATA EMULATION ---
params as (
select trunc(sysdate) - 100 d0,
1e5 number_of_contracts,
100 max_contract_term
from dual
),
start_dates (d1) as(
select trunc(d0 + dbms_random.value(0, sysdate-d0)) d1
from params connect by level <= number_of_contracts
),
contract (d1, d2) as (
select d1,
trunc(d1 + dbms_random.value(0, max_contract_term)) d2
from start_dates, params
),
calendar (d) as (
select d0 + level - 1
from params
connect by level <= sysdate - d0 + 1
),
--- END OF TEST DATA ---
------------------------
...
... или даже поместить их в физические таблицы calendar
и contract
с подходящими индексами.