Как написать запрос Oracle, чтобы найти общую длину возможных совпадений от дат

Я пытаюсь найти запрос для следующей задачи

У меня есть следующие данные и вы хотите найти общий сетевой день для каждого уникального идентификатора

ID  From        To          NetworkDay
1   03-Sep-12   07-Sep-12   5
1   03-Sep-12   04-Sep-12   2
1   05-Sep-12   06-Sep-12   2
1   06-Sep-12   12-Sep-12   5
1   31-Aug-12   04-Sep-12   3
2   04-Sep-12   06-Sep-12   3
2   11-Sep-12   13-Sep-12   3
2   05-Sep-12   08-Sep-12   3

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

ID  From        To          NetworkDay
1   31-Aug-12   12-Sep-12   9
2   04-Sep-12   08-Sep-12   4
2   11-Sep-12   13-Sep-12   3

а затем

ID  Total Network Day
1   9
2   7

Если расчет сетевого дня невозможен, достаточно получить только вторую таблицу.

Надеюсь, мой вопрос ясен.

Ответ 1

Мы можем использовать Oracle Analytics, а именно предложение OVER... PARTITION BY, в Oracle для этого. Предложение PARTITION BY похоже на GROUP BY, но без агрегационной части. Это означает, что мы можем группировать строки вместе (т.е. Разделять их), и они выполняют операцию над ними как отдельные группы. Когда мы работаем над каждой строкой, мы можем получить доступ к столбцам предыдущей строки выше. Это функция PARTITION BY дает нам. (PARTITION BY не относится к разбиению таблицы на производительность.)

Итак, как мы выводим неперекрывающиеся даты? Сначала мы заказываем запрос на основе полей (ID, DFROM), затем мы используем поле ID для создания наших разделов (группы строк). Затем мы проверяем значение предыдущей строки TO и текущее значение FROM строк для перекрытия с использованием выражения типа: (в псевдокоде)

 max(previous.DTO, current.DFROM) as DFROM

Это базовое выражение вернет исходное значение DFROM, если оно не перекрывается, но вернет предыдущее значение TO, если есть перекрытие. Поскольку наши строки упорядочены, нам нужно только иметь дело с последней строкой. В тех случаях, когда предыдущая строка полностью перекрывает текущую строку, мы хотим, чтобы строка имела нулевой диапазон дат. Итак, мы делаем то же самое для поля DTO:

max(previous.DTO, current.DFROM) as DFROM, max(previous.DTO, current.DTO) as DTO

Как только мы сгенерировали новые результаты с установленными значениями DFROM и DTO, мы можем их суммировать и подсчитать интервалы интервалов DFROM и DTO.

Помните, что большинство вычислений даты в базе данных не являются такими, как ваши данные. Так что что-то вроде DATEDIFF (dto, dfrom) не будет включать в себя день, на который фактически ссылается, поэтому мы хотим сначала отрегулировать dto на первый день.

У меня больше нет доступа к серверу Oracle, но я знаю, что это возможно с помощью Oracle Analytics. Запрос должен выглядеть примерно так: (Пожалуйста, обновите мой пост, если вы его заработаете.)

SELECT id, 
    max(dfrom, LAST_VALUE(dto) OVER (PARTITION BY id ORDER BY dfrom) ) as dfrom, 
    max(dto, LAST_VALUE(dto) OVER (PARTITION BY id ORDER BY dfrom) ) as dto
  from (
    select id, dfrom, dto+1 as dto from my_sample   -- adjust the table so that dto becomes non-inclusive
      order by id, dfrom
  ) sample;

Секрет здесь - выражение LAST_VALUE (dto) OVER (PARTITION BY id ORDER BY dfrom), которое возвращает значение, предшествующее текущей строке. Таким образом, этот запрос должен выводить новые значения dfrom/dto, которые не перекрываются. Тогда это просто вопрос подпроцесса этого выполнения (dto-dfrom) и суммирование итогов.

Использование MySQL

У меня был доступ к серверу mysql, поэтому я действительно работал там. MySQL не имеет разбиения на результат (Analytics), например Oracle, поэтому нам нужно использовать переменные набора результатов. Это означает, что мы используем выражения типа @var: = xxx для запоминания последнего значения даты и настройки dfrom/dto. Один и тот же алгоритм чуть более длинный и сложный синтаксис. Мы также должны забыть последнее значение даты в любое время, когда изменится поле ID!

Итак, вот пример таблицы (те же самые значения):

create table sample(id int, dfrom date, dto date, networkDay int);
insert into sample values
    (1,'2012-09-03','2012-09-07',5),
    (1,'2012-09-03','2012-09-04',2),
    (1,'2012-09-05','2012-09-06',2),
    (1,'2012-09-06','2012-09-12',5),
    (1,'2012-08-31','2012-09-04',3),
    (2,'2012-09-04','2012-09-06',3),
    (2,'2012-09-11','2012-09-13',3),
    (2,'2012-09-05','2012-09-08',3);

В ответ на запрос выводится негруппированный результирующий набор, как указано выше: Переменная @ld - "последняя дата", а переменная @lid - "последний id". Anytime @lid изменяется, мы reset @ld на null. FYI В mysql операторы: =, где выполняется присваивание, оператор an = просто равен.

Это 3-уровневый запрос, но он может быть уменьшен до 2. Я пошел с дополнительным внешним запросом, чтобы сделать вещи более читабельными. Внутренний самый запрос прост и настраивает столбец dto не включительно и делает правильный порядок строк. Средний запрос выполняет настройку значений dfrom/dto, чтобы сделать их неперекрывающимися. Внешний запрос просто отбрасывает неиспользуемые поля и вычисляет интервал диапазона.

set @ldt=null, @lid=null;
select id, no_dfrom as dfrom, no_dto as dto, datediff(no_dto, no_dfrom) as days from (
select if(@lid=id,@ldt,@ldt:=null) as last, dfrom, dto, if(@ldt>=dfrom,@ldt,dfrom) as no_dfrom, if(@ldt>=dto,@ldt,dto) as no_dto, @ldt:=if(@ldt>=dto,@ldt,dto), @lid:=id as id,
        datediff(dto, dfrom) as overlapped_days
  from (select id, dfrom, dto + INTERVAL 1 DAY as dto from sample order by id, dfrom) as sample
  ) as nonoverlapped
  order by id, dfrom;

Вышеприведенный запрос дает результаты (уведомление dfrom/dto здесь не перекрывается):

+------+------------+------------+------+
| id   | dfrom      | dto        | days |
+------+------------+------------+------+
|    1 | 2012-08-31 | 2012-09-05 |    5 |
|    1 | 2012-09-05 | 2012-09-08 |    3 |
|    1 | 2012-09-08 | 2012-09-08 |    0 |
|    1 | 2012-09-08 | 2012-09-08 |    0 |
|    1 | 2012-09-08 | 2012-09-13 |    5 |
|    2 | 2012-09-04 | 2012-09-07 |    3 |
|    2 | 2012-09-07 | 2012-09-09 |    2 |
|    2 | 2012-09-11 | 2012-09-14 |    3 |
+------+------------+------------+------+

Ответ 2

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

SELECT DISTINCT F.ID, F.From, L.To
 FROM Temp AS F, Temp AS L
 WHERE F.From < L.To AND F.ID = L.ID
   AND NOT EXISTS (SELECT *
                 FROM Temp AS T
                 WHERE T.ID = F.ID
                 AND F.From < T.From AND T.From < L.To
                         AND NOT EXISTS ( SELECT *
                         FROM Temp AS T1
                            WHERE T1.ID = F.ID
                            AND T1.From < T.From
                            AND T.From <= T1.To)
               )
   AND NOT EXISTS (SELECT *
              FROM Temp AS T2
              WHERE T2.ID = F.ID
              AND (
                    (T2.From < F.From AND F.From <= T2.To)
                 OR (T2.From < L.To AND L.To < T2.To)
                   )
             )

Ответ 3

with t_data as (
    select 1 as id,
           to_date('03-sep-12','dd-mon-yy') as start_date,
           to_date('07-sep-12','dd-mon-yy') as end_date from dual
    union all
    select 1,
           to_date('03-sep-12','dd-mon-yy'),
           to_date('04-sep-12','dd-mon-yy') from dual
    union all
    select 1,
           to_date('05-sep-12','dd-mon-yy'),
           to_date('06-sep-12','dd-mon-yy') from dual
    union all
    select 1,
           to_date('06-sep-12','dd-mon-yy'),
           to_date('12-sep-12','dd-mon-yy') from dual
    union all 
    select 1,
           to_date('31-aug-12','dd-mon-yy'),
           to_date('04-sep-12','dd-mon-yy') from dual
    union all 
    select 2,
           to_date('04-sep-12','dd-mon-yy'), 
           to_date('06-sep-12','dd-mon-yy') from dual
    union all 
    select 2, 
           to_date('11-sep-12','dd-mon-yy'), 
           to_date('13-sep-12','dd-mon-yy') from dual
    union all 
    select 2, 
           to_date('05-sep-12','dd-mon-yy'), 
           to_date('08-sep-12','dd-mon-yy') from dual
),
t_holidays as (
    select to_date('01-jan-12','dd-mon-yy') as holiday
      from dual
),
t_data_rn as (
    select rownum as rn, t_data.* from t_data
),
t_model as (
    select distinct id,
           start_date
    from t_data_rn
      model
         partition by (rn, id)
         dimension by (0 as i)
         measures(start_date, end_date)
         rules
         (  start_date[for i 
                      from 1 
                        to end_date[0]-start_date[0]
                 increment 1] = start_date[0] + cv(i),
            end_date[any] = start_date[cv()] + 1
         )
      order by 1,2
),
t_network_days as (
    select t_model.*,
           case when
               mod(to_char(start_date, 'j'), 7) + 1 in (6, 7)
               or t_holidays.holiday is not null               
               then 0 else 1
           end as working_day
      from t_model
      left outer join t_holidays
        on t_holidays.holiday = t_model.start_date
)
select id, 
       sum(working_day) as network_days
  from t_network_days
 group by id;
  • t_data - ваши исходные данные
  • t_holidays - содержит список праздников
  • t_data_rn - добавляет уникальный ключ (rownum) к каждой строке t_data
  • t_model - расширяет диапазоны дат t_data в плоский список дат
  • t_network_days - отмечает каждую дату от t_model как рабочий день или выходные дни, основанные на дне недели (Сб и Солнце) и списке праздников
  • окончательный запрос - вычисляет количество сетевых дней для каждой группы.