Как объединить / разделить несколько диапазонов дат в одну временную шкалу (Oracle 11g)? - PullRequest
4 голосов
/ 19 октября 2011

Я боролся с этой проблемой в течение нескольких дней, и теперь я обращаюсь к массам за помощью.

Моя проблема похожа, но не совсем так, как предыдущее решение на этом сайте: PL / SQL Split, разделите дату на новые даты в соответствии с затемненными датами! Это решение довольно логично (включите / исключите), в то время как моя проблема связана с некоторыми из них, а также слиянием.

Хотя мне нравится думать, что я имею средний / продвинутый уровень понимания SQL + PL / SQL ... Функции Oracle Analytic, по-видимому, поражают меня. Я пытался читать / учиться, но у меня мало времени.

Поскольку я не уверен в правомерности совместного использования имен таблиц (COTS), сферы деятельности и т. Д., Я собираюсь подражать моей проблеме с более неопределенным сценарием / контекстом. Надеюсь, это отразит адвокатское настроение.

К проблеме: У меня есть таблица, в которой хранится история деятельности клиента. Клиент может приходить и уходить, поэтому в этой таблице может быть несколько строк (для каждого клиента).

CREATE TABLE activity AS
SELECT 1 AS cust_id,
       TO_DATE('01-JAN-2010') AS start_dt,
       TO_DATE('31-JUL-2010') AS end_dt,
       'EAST' AS region
FROM DUAL
UNION
SELECT 1 AS cust_id,
       TO_DATE('01-FEB-2011') AS start_dt,
       TO_DATE('31-DEC-2011') AS end_dt,
       'EAST' AS region
FROM DUAL;

У меня также есть таблица, в которой хранится информация об атрибутах по диапазону. Клиент может иметь несколько типов атрибутов одновременно и каждый тип несколько раз для разных интервалов времени.

CREATE TABLE attrib AS
SELECT 1 AS cust_id,
       'POWER' AS atb_cd,
       TO_DATE('01-JAN-2009') AS atb_start_dt,
       TO_DATE('31-JAN-2010') AS atb_end_dt,
       'LocalNuke' AS provider,
       1.80 AS per_kwh,
       0 AS per_gal
FROM DUAL
UNION
SELECT 1 AS cust_id,
       'POWER' AS atb_cd,
       TO_DATE('01-MAR-2010') AS atb_start_dt,
       TO_DATE('31-MAR-2010') AS atb_end_dt,
       'CoalGuys' AS provider,
       1.60 AS per_kwh,
       0 AS per_gal
FROM DUAL
UNION
SELECT 1 AS cust_id,
       'POWER' AS atb_cd,
       TO_DATE('01-JUN-2010') AS atb_start_dt,
       TO_DATE('30-SEP-2010') AS atb_end_dt,
       'LocalNuke' AS provider,
       1.70 AS per_kwh,
       0 AS per_gal
FROM DUAL
UNION
SELECT 1 AS cust_id,
       'POWER' AS atb_cd,
       TO_DATE('01-MAR-2011') AS atb_start_dt,
       TO_DATE('31-DEC-9999') AS atb_end_dt,
       'GeoHeat' AS provider,
       1.10 AS per_kwh,
       0 AS per_gal
FROM DUAL
UNION
SELECT 1 AS cust_id,
       'WATER' AS atb_cd,
       TO_DATE('01-MAR-2010') AS atb_start_dt,
       TO_DATE('31-DEC-9999') AS atb_end_dt,
       'GlacialGold' AS provider,
       0 AS per_kwh,
       0.60 AS per_gal
FROM DUAL;

Странности данных являются преднамеренными, я пытался сделать этот сценарий максимально реальным, не имея отношения к "реальному миру".

Результат должен ограничить периоды активности клиентов с этой фиктивной компанией и разделить все перекрывающиеся даты, чтобы сформировать график. Элементы данных должны быть объединены вместе для отчетности.

Визуально:

Cust:
         |----------------------|             |------------------------|
Power:
|-------------|    |--|    |-------|               |---------------------->
Water:
                   |------------------------------------------------------>    
Expected Result:
         |----|----|--|----|----|             |----|-------------------|

Решение должно быть масштабируемым, чтобы включать и другие атрибуты. В конце концов, у меня была бы эта денормализованная информация в таблице, чтобы я мог сообщать данные клиента в любой момент времени. Например, если у них была активность, сила и вода в определенный день; Я должен быть в состоянии экспортировать данные per_kwh, per_gal и активности за этот день.

Пример вывода (табличный):

CUST_ID  FROM_DT      THRU_DT      REGION  POWER_PROVIDER  WATER_PROVIDER  PER_KWH  PER_GAL
-------  -----------  -----------  ------  --------------  --------------  -------  -------
1        01-JAN-2010  31-JAN-2010  EAST    LocalNuke                       1.80     0
1        01-FEB-2010  28-FEB-2010  EAST                                    0        0
1        01-MAR-2010  31-MAR-2010  EAST    CoalGuys        GlacialGold     1.60     0.60
1        01-APR-2010  31-MAY-2010  EAST                    GlacialGold     0        0.60
1        01-JUN-2010  31-JUL-2010  EAST    LocalNuke       GlacialGold     1.70     0.60
1        01-FEB-2011  28-FEB-2011  EAST                    GlacialGold     0        0.60
1        01-MAR-2011  31-DEC-2011  EAST    GeoHeat         GlacialGold     1.10     0.60

Я написал что-то около 2 лет назад (когда требование было сродни просто Activity / Power), используя 2 асинхронных курсора, медленно обрабатывая (строка за строкой).

Несмотря на то, что производительность важна, главная причина, по которой я пытаюсь найти простое / массовое решение sql, - это обслуживание. Вложенность курсора if / else моего исходного решения уже сложна и будет экспоненциально хуже, по крайней мере, с еще двумя «атрибутивными» интервалами, на которые нужно разделить.

Буду признателен за любую помощь, которую может оказать любой из вас.

Ответы [ 2 ]

1 голос
/ 19 октября 2011

Это может сработать.Он не объединяет смежные области, но все же должен выполнить свою работу.

WITH

  milestone AS
  (
    SELECT cust_id, start_dt     AS point_in_time FROM ACTIVITY
  UNION
    SELECT cust_id, atb_start_dt AS point_in_time FROM ATTRIB
  UNION
    SELECT cust_id, LEAST(end_dt,     TO_DATE('30-DEC-9999')) + 1 AS point_in_time FROM ACTIVITY
  UNION
    SELECT cust_id, LEAST(atb_end_dt, TO_DATE('30-DEC-9999')) + 1 AS point_in_time FROM ATTRIB
  )

SELECT
  milestone.cust_id                 AS cust_id,
  milestone.point_in_time           AS from_dt,
  LEAD(point_in_time)
    OVER (PARTITION BY milestone.cust_id ORDER BY milestone.point_in_time) - 1
                                    AS thru_dt,
  activity.region                   AS region,
  power_attrib.provider             AS power_provider,
  water_attrib.provider             AS water_provider,
  COALESCE(power_attrib.per_kwh, 0) AS per_kwh,
  COALESCE(water_attrib.per_gal, 0) AS per_gal
FROM
  MILESTONE

  LEFT OUTER JOIN ACTIVITY
    ON milestone.cust_id = activity.cust_id
       AND milestone.point_in_time BETWEEN activity.start_dt AND activity.end_dt

  LEFT OUTER JOIN ATTRIB power_attrib
    ON milestone.cust_id = power_attrib.cust_id
       AND power_attrib.atb_cd = 'POWER'
       AND milestone.point_in_time BETWEEN power_attrib.atb_start_dt AND power_attrib.atb_end_dt

  LEFT OUTER JOIN ATTRIB water_attrib
    ON milestone.cust_id = water_attrib.cust_id
       AND water_attrib.atb_cd = 'WATER'
       AND milestone.point_in_time BETWEEN water_attrib.atb_start_dt AND water_attrib.atb_end_dt
1 голос
/ 19 октября 2011

Это действительно очень сложная проблема, и я ожидаю, что у вас получится большой грязный запрос.Основная проблема, с которой вы столкнулись, заключается в том, что вам нужно создать строки «psudeo» для пробелов в таблице атрибутов.Это проблематично.

Я взял урезанную версию вашей проблемы, просто пытаясь создать пробелы для атрибута POWER.Я взял attidute, что каждой строке атрибута может предшествовать пробел.Придумали это

SELECT  PS.cust_id
    ,   G.is_gap
    ,   DECODE( G.is_gap, 'Y', PS.prev_start, PS.atb_start_dt ) AS start_date
    ,   DECODE( G.is_gap, 'Y', PS.prev_end, PS.atb_end_dt ) AS end_date
    ,   DECODE( G.is_gap, 'Y', NULL, PS.provider ) AS provider
    ,   DECODE( G.is_gap, 'Y', NULL, PS.per_kwh ) AS per_kwh
    ,   DECODE( G.is_gap, 'Y', NULL, PS.per_gal ) AS per_gal
FROM
    (   SELECT  P.cust_id
            ,   P.atb_start_dt
            ,   P.atb_end_dt
            ,   P.provider
            ,   P.per_kwh
            ,   P.per_gal
            ,   P.atb_start_dt - 1      AS prev_end
            ,   NVL( MAX( P.atb_end_dt ) OVER ( ORDER BY P.atb_end_dt
                        ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING ) + 1
                   , '01-JAN-1900' )    AS prev_start
        FROM    attrib      P
        WHERE   P.atb_cd    = 'POWER'
    ) PS
,   (   SELECT  DECODE(LEVEL,1,'Y','N') AS is_gap
        FROM    DUAL
        CONNECT BY LEVEL <= 2
    ) G
WHERE   (   PS.prev_end > PS.prev_start
        OR  G.is_gap    = 'N' )
ORDER BY 3
/

Дали мне эти результаты

CUST_ID I START_DATE END_DATE   PROVIDER    PER_KWH PER_GAL
------- - ---------- ---------- ----------- ------- -------
      1 Y 01-JAN-00  31-DEC-08
      1 N 01-JAN-09  31-JAN-10  LocalNuke   1.8     0
      1 N 01-FEB-10  31-MAR-10  CoalGuys    1.6     0
      1 Y 01-APR-10  31-MAY-10
      1 N 01-JUN-10  30-SEP-10  LocalNuke   1.7     0
      1 Y 01-OCT-10  28-FEB-11
      1 N 01-MAR-11  31-DEC-99  GeoHeat     1.1     0

Некоторые примечания:

  • Я думаю, что строка 5 ваших примеров результатов имеет неверный конецДата.Должно ли это быть 31-JUL-2010, потому что тогда, когда заканчивается activity?
  • Я обновил дату начала CoalGuys до 01-FEB-2010, чтобы проверить, не было ли пропуска
  • Будет прикручен, еслинет активности, которая бежит в далекое будущее, потому что она не генерирует отставание, только предшествующий.Всегда можно UNION один, я думаю
  • Лучше не использовать 9999 как год, так как вы получаете ошибки, если вы пытаетесь что-то добавить к нему.Не имело значения, но было бы неприятно, если вы идете за конечными пробелами.

Теперь это долгий путь от полного решения, после того, как вы добавите клиента и даты воды, он станет более грязнымеще.Но вам, вероятно, понадобятся все вышеизложенное в качестве встроенного представления для включения в основной запрос.Тогда вам придется сделать то же самое для воды.Затем вам нужно объединить их вместе с проверками диапазона дат, а затем использовать LEAST и GREATEST для получения окончательных результатов.

Извините, после примерно 40 минут, которые я потратил наэто перешло от интересной проблемы к ощущению работы, поэтому мой ответ останется неполным.Надеюсь, это поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...