Как мне получить «следующее» событие, когда смещение является переменным для элементов, которые могут обрабатываться повторно?

Этот вопрос практически идентичен другому, который я недавно задавал, с тем очень важным отличием, что эти транзакции являются кредитными транзакциями и, следовательно, элементы могут повторно появляться в данных несколько раз. Вот почему я сейчас использую LEAD. С этим разъяснением я повторяю свой вопрос ниже.

У меня есть таблица транзакций в базе данных Oracle. Я пытаюсь собрать отчет для системы доставки, включающей несколько типов транзакций. Тип «запрос» может фактически быть одним из четырех подтипов («A», «B», «C» и «D» для этого примера), а тип «доставки» может быть одним из четырех различных подтипов. типы ('PULL', 'PICKUP', 'MAIL'). Может быть от 1 до 5 транзакций для получения элемента от «запроса» до «доставки», и ряд типов «доставки» также являются промежуточными транзакциями. Пример:

Item | Transaction | Timestamp
001  | REQ-A       | 2014-07-31T09:51:32Z
002  | REQ-B       | 2014-07-31T09:55:53Z
003  | REQ-C       | 2014-07-31T10:01:15Z
004  | REQ-D       | 2014-07-31T10:02:29Z
005  | REQ-A       | 2014-07-31T10:05:47Z
002  | PULL        | 2014-07-31T10:20:04Z
002  | MAIL        | 2014-07-31T10:20:06Z
001  | PULL        | 2014-07-31T10:22:21Z
001  | TRANSFER    | 2014-07-31T10:22:23Z
003  | PULL        | 2014-07-31T10:24:10Z
003  | TRANSFER    | 2014-07-31T10:24:12Z
004  | PULL        | 2014-07-31T10:26:28Z
005  | PULL        | 2014-07-31T10:28:42Z
005  | TRANSFER    | 2014-07-31T10:28:44Z
001  | ARRIVE      | 2014-07-31T11:45:01Z
001  | PICKUP      | 2014-07-31T11:45:02Z
003  | ARRIVE      | 2014-07-31T11:47:44Z
003  | PICKUP      | 2014-07-31T11:47:45Z
005  | ARRIVE      | 2014-07-31T11:49:45Z
005  | PICKUP      | 2014-07-31T11:49:46Z

Мне нужен отчет типа:

Item | Start Tx | End Tx | Time
001  | REQ-A    | PICKUP | 1:53:30
002  | REQ-B    | MAIL   | 0:24:13
003  | REQ-C    | PICKUP | 1:46:30
004  | REQ-D    | PULL   | 0:23:59
005  | REQ-A    | PICKUP | 1:43:59

Что я имею:

Item | Start Tx | End Tx   | Time
001  | REQ-A    | PULL     | 0:30:49
001  | REQ-A    | TRANSFER | 0:30:51
001  | REQ-A    | ARRIVE   | 1:53:29
001  | REQ-A    | PICKUP   | 1:53:30
002  | REQ-B    | PULL     | 0:24:11
002  | REQ-B    | MAIL     | 0:24:13
003  | REQ-C    | PULL     | 0:22:55
003  | REQ-C    | TRANSFER | 0:22:57
003  | REQ-C    | ARRIVE   | 1:46:29
003  | REQ-C    | PICKUP   | 1:46:30
004  | REQ-D    | PULL     | 0:23:59
005  | REQ-A    | PULL     | 0:22:55
005  | REQ-A    | TRANSFER | 0:22:57
005  | REQ-A    | ARRIVE   | 1:43:58
005  | REQ-A    | PICKUP   | 1:43:59

Что я делаю, чтобы получить эти данные:

SELECT Item, Transaction, nextTransaction, nextTimestamp - Timestamp
FROM (
    SELECT Item, Transaction, Timestamp,
      LEAD(Transaction, 5) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTransaction"
      LEAD(Timestamp, 5) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTimestamp"
    FROM Transactions
    UNION ALL 
    SELECT Item, Transaction, Timestamp,
      LEAD(Transaction, 4) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTransaction"
      LEAD(Timestamp, 4) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTimestamp"
    FROM Transactions
    UNION ALL 
    SELECT Item, Transaction, Timestamp,
      LEAD(Transaction, 3) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTransaction"
      LEAD(Timestamp, 3) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTimestamp"
    FROM Transactions
    UNION ALL 
    SELECT Item, Transaction, Timestamp,
      LEAD(Transaction, 2) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTransaction"
      LEAD(Timestamp, 2) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTimestamp"
    FROM Transactions
    UNION ALL 
    SELECT Item, Transaction, Timestamp,
      LEAD(Transaction, 1) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTransaction"
      LEAD(Timestamp, 1) OVER (PARTITION BY Item ORDER BY Timestamp) AS "nextTimestamp"
    FROM Transactions
)
WHERE nextTransaction IS NOT NULL
AND Transaction IN ('REQ-A', 'REQ-B', 'REQ-C', 'REQ-D')

Я мог бы вручную проанализировать это в сценарии (и, возможно, это на самом деле лучший способ действий), но ради обучения я хотел бы знать, возможно ли это сделать на самом деле только с помощью SQL.

Чтобы уточнить бит «ссуды», в этой таблице есть другие транзакции для возвратов и других форм обработки, которые не имеют отношения к этому отчету, помимо существующих как другие типы транзакций. Как только элемент возвращен, он может снова пройти цикл запроса. Например, для элемента 001 он может следовать циклу элемента 002 (REQ -> MAIL), затем он может получить транзакцию «Нет на полке», или ссуду без запроса, или несколько других вариантов использования. Затем он может вернуться через цикл REQ -> PICKUP или цикл REQ-> PULL.


person ND Geek    schedule 31.07.2014    source источник
comment
Ваши данные и результаты совпадают, поэтому неясно, в чем разница. Из комментария к ответу похоже, что у вас есть проблема с пробелами и островами с повторяющимися блоками записей для одного и того же элемента; в этом случае, как вы определяете начало группы, просто что-то, начинающееся с REQ? Дополнительные данные (в Fiddle?), чтобы продемонстрировать новую проблему и ожидаемые результаты, помогут. Я тоже не понимаю, как ваша lead версия справится с этим.   -  person Alex Poole    schedule 01.08.2014


Ответы (2)


Это проблема пробелов и островов, но островки, определяемые REQ транзакцией, делают ее немного сложнее, чем некоторые.

Вы можете использовать вложенные функции опережения и отставания и некоторые манипуляции, чтобы получить то, что вам нужно:

select distinct item,
  coalesce(start_tran,
    lag(start_tran) over (partition by item order by timestamp)) as start_tran,
  coalesce(end_tran,
    lead(end_tran) over (partition by item order by timestamp)) as end_tran,
  coalesce(end_time, 
    lead(end_time) over (partition by item order by timestamp))
    - coalesce(start_time,
        lag(start_time) over (partition by item order by timestamp)) as time
from (
  select item, timestamp, start_tran, start_time, end_tran, end_time
  from (
    select item,
      timestamp,
      case when lag_tran is null or transaction like 'REQ%'
        then transaction end as start_tran,
      case when lag_tran is null or transaction like 'REQ%'
        then timestamp end as start_time,
      case when lead_tran is null or lead_tran like 'REQ%'
        then transaction end as end_tran,
      case when lead_tran is null or lead_tran like 'REQ%'
        then timestamp end as end_time
    from (
      select item, transaction, timestamp,
        lag(transaction)
          over (partition by item order by timestamp) as lag_tran,
        lead(transaction)
          over (partition by item order by timestamp) as lead_tran
      from transactions
    )
  )
  where start_tran is not null or end_tran is not null
)
order by item, start_tran;

С дополнительными записями для второго цикла по пунктам 1 и 2, которые могут дать:

      ITEM START_TRAN END_TRAN   TIME      
---------- ---------- ---------- -----------
         1 REQ-A      PICKUP     0 1:53:30.0 
         1 REQ-E      PICKUP     0 1:23:30.0 
         2 REQ-B      MAIL       0 0:24:13.0 
         2 REQ-F      REQ-F      0 0:0:0.0   
         3 REQ-C      PICKUP     0 1:46:30.0 
         4 REQ-D      PULL       0 0:23:59.0 
         5 REQ-A      PICKUP     0 1:43:59.0 

SQL Fiddle, показывающий все промежуточные шаги.

Все не так страшно, как может показаться на первый взгляд. Самый внутренний запрос берет необработанные данные и добавляет дополнительный столбец для опережающих и отстающих транзакций. Взяв только первый набор записей item-1, который будет:

      ITEM TRANSACTION TIMESTAMP                LAG_TRAN   LEAD_TRAN
---------- ----------- ------------------------ ---------- ----------
         1 REQ-A       2014-07-31T09:51:32Z                PULL       
         1 PULL        2014-07-31T10:22:21Z     REQ-A      TRANSFER   
         1 TRANSFER    2014-07-31T10:22:23Z     PULL       ARRIVE     
         1 ARRIVE      2014-07-31T11:45:01Z     TRANSFER   PICKUP     
         1 PICKUP      2014-07-31T11:45:02Z     ARRIVE     REQ-E      

Заметили, что REQ-E появляется как последнее lead_tran? Это первая transaction для второго цикла записей для этого элемента, и она пригодится позже. Следующий уровень запроса использует эти значения опережения и запаздывания и обрабатывает значения REQ как начальные и конечные маркеры и использует эту информацию для обнуления всего, кроме первой и последней записи для каждого цикла.

      ITEM TIMESTAMP                START_TRAN START_TIME               END_TRAN   END_TIME               
---------- ------------------------ ---------- ------------------------ ---------- ------------------------
         1 2014-07-31T09:51:32Z     REQ-A      2014-07-31T09:51:32Z                                         
         1 2014-07-31T10:22:21Z                                                                             
         1 2014-07-31T10:22:23Z                                                                             
         1 2014-07-31T11:45:01Z                                                                             
         1 2014-07-31T11:45:02Z                                         PICKUP     2014-07-31T11:45:02Z     

Следующий уровень запроса удаляет все строки, которые не представляют начало или конец (или оба — см. REQ-F в Fiddle), поскольку они нам не интересны:

      ITEM TIMESTAMP                START_TRAN START_TIME               END_TRAN   END_TIME               
---------- ------------------------ ---------- ------------------------ ---------- ------------------------
         1 2014-07-31T09:51:32Z     REQ-A      2014-07-31T09:51:32Z                                         
         1 2014-07-31T11:45:02Z                                         PICKUP     2014-07-31T11:45:02Z     

Теперь у нас есть пары строк для каждого цикла (или одна строка для REQ-F). На последнем уровне снова используется опережение и отставание, чтобы заполнить пробелы; если start_tran равно нулю, то это конечная строка, и мы должны использовать начальные данные предыдущей строки; если end_tran равно нулю, то это начальная строка, и мы должны использовать конечные данные следующей строки.

  ITEM START_TRAN START_TIME               END_TRAN   END_TIME                 TIME      

     1 REQ-A      2014-07-31T09:51:32Z     PICKUP     2014-07-31T11:45:02Z     0 1:53:30.0 
     1 REQ-A      2014-07-31T09:51:32Z     PICKUP     2014-07-31T11:45:02Z     0 1:53:30.0 

Это делает обе строки одинаковыми, поэтому distinct удаляет дубликаты.

person Alex Poole    schedule 01.08.2014

Это должно дать вам тот же результат. Я воспроизвожу ответ Гордона, и он все еще остается в силе

select item,
       min(transaction) keep (dense_rank first order by timestamp) as StartTx, 
       min(transaction) keep (dense_rank last order by timestamp) as EndTx,
       max(timestamp) - min(timestamp)
from transactions t
group by item;

Даже если у вас есть дубликаты в txn, они будут обработаны аналитической функцией. См. документацию для плотного ранжирования и ключевого слова keep.

person SriniV    schedule 31.07.2014
comment
Используя ответ Гордона, я нахожу самую первую транзакцию, записанную для элемента, и самую последнюю транзакцию для элемента. Это означает, что если 001 проходит REQ -> PICKUP 3 раза, я получаю первую транзакцию REQ и последнюю транзакцию PICKUP. - person ND Geek; 31.07.2014
comment
Можете ли вы опубликовать то, что вы хотите увидеть и что вы получаете в этом запросе? - person SriniV; 01.08.2014