Имея таблицу контрактов с периодами активности, как рассчитать ежедневное количество активных контрактов? - PullRequest
0 голосов
/ 05 июня 2018

У меня есть две таблицы:

  • 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с подходящими индексами.

1 Ответ

0 голосов
/ 05 июня 2018

Вы подходите хорошо, и я не думаю, что есть фундаментальное лучшее решение.Чтобы получить все контрактные транзакции, вы должны рассмотреть, по крайней мере, дважды каждую запись в таблице contract, которую вы делаете.

Небольшая другая возможность состоит в том, чтобы вместо двойного сканирования таблицы выполнить одно сканирование и соединиться сдве таблицы записей, содержащие записи активации и завершения.

with two as (
select 1 n from dual union all
select -1 n from dual),
contract_transactions as (
select 
case when n = 1 then c.d1 else c.d2+1 end as d,
n
from contract c
cross join two d)
select d, sum(n) n 
from contract_transactions
group by d

Вместо вашей двойной FULL TABLE SCAN

----------------------------------------------------------------------------------------
| Id  | Operation                   | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |          |  1528K|    13M|   107K  (1)| 00:25:06 |
|   1 |  UNION-ALL                  |          |       |       |            |          |
|   2 |   HASH GROUP BY             |          |   764K|  6715K| 53785   (1)| 00:12:33 |
|   3 |    TABLE ACCESS STORAGE FULL| CONTRACT |   764K|  6715K| 53767   (1)| 00:12:33 |
|   4 |   HASH GROUP BY             |          |   764K|  6715K| 53785   (1)| 00:12:33 |
|   5 |    TABLE ACCESS STORAGE FULL| CONTRACT |   764K|  6715K| 53767   (1)| 00:12:33 |
----------------------------------------------------------------------------------------

есть только один FULL TABLE SCAN, но затем присоединяется ктаблица с двумя записями

-----------------------------------------------------------------------------------------
| Id  | Operation                    | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |          |  1528K|    30M|   107K  (1)| 00:25:07 |
|   1 |  HASH GROUP BY               |          |  1528K|    30M|   107K  (1)| 00:25:07 |
|   2 |   MERGE JOIN CARTESIAN       |          |  1528K|    30M|   107K  (1)| 00:25:06 |
|   3 |    VIEW                      |          |     2 |     6 |     4   (0)| 00:00:01 |
|   4 |     UNION-ALL                |          |       |       |            |          |
|   5 |      FAST DUAL               |          |     1 |       |     2   (0)| 00:00:01 |
|   6 |      FAST DUAL               |          |     1 |       |     2   (0)| 00:00:01 |
|   7 |    BUFFER SORT               |          |   764K|    13M|   107K  (1)| 00:25:07 |
|   8 |     TABLE ACCESS STORAGE FULL| CONTRACT |   764K|    13M| 53767   (1)| 00:12:33 |
-----------------------------------------------------------------------------------------

Я не думаю, что будет существенная разница в производительности (то же самое мнение - CBO;).

Для меня более естественно разделить contract запись в две транзакции с использованием объединения.

...