Один из способов сделать это - считать это проблемой «разреженных данных». То есть у вас есть временные события, но не каждый продукт представлен в каждом событии.
Разделенное внешнее соединение может заполнить разреженные данные, что приведет к набору данных, в котором каждый продукт представлен в каждый момент времени. Тогда вам будет легче видеть, что добавлялось и удалялось каждый раз.
with event_table (product, event_time) as
( SELECT 'X1', trunc(sysdate)+1 FROM DUAL UNION ALL
SELECT 'X2', trunc(sysdate)+1 FROM DUAL UNION ALL
SELECT 'X1', trunc(sysdate)+2 FROM DUAL UNION ALL
SELECT 'X3', trunc(sysdate)+2 FROM DUAL UNION ALL
SELECT 'X4', trunc(sysdate)+10 FROM DUAL ),
-- solution begins here
-- start by getting a distinct list of event times
distinct_times as ( SELECT DISTINCT event_time FROM event_table ),
-- Next, do a partitioned right join to ensure that every product is represented at every event time. If the row is sparse data that was added by the right join, et.event_time will be null.
-- We use the lag() function to see what the product looked like at the last event and
-- compare with the current event.
-- NULL -> NULL ==> no change
-- NOT NULL -> NOT NULL ==> no change
-- NULL -> NOT NULL ==> added
-- NOT NULL -> NULL ==> removed
sparse_data_filled as (
select dt.event_time, et.product,
case when lag(et.event_time ) over ( partition by et.product order by dt.event_time ) is null then
-- product wasn't present during last event
case when et.event_time is null then
-- product still is not present
null -- no change
else
-- product is present now and was not before
'Added'
end
else
-- product was present during last event
case when et.event_time is null then
-- product is no longer present
'Removed'
else
-- product is still present
null -- no change
end
end message
from event_table et partition by (product)
right join distinct_times dt on et.event_time = dt.event_time )
SELECT * from sparse_data_filled
-- filter out the non-changes
where message is not null
order by event_time, product
;
+------------+---------+---------+
| EVENT_TIME | PRODUCT | MESSAGE |
+------------+---------+---------+
| 07-AUG-20 | X1 | Added |
| 07-AUG-20 | X2 | Added |
| 08-AUG-20 | X2 | Removed |
| 08-AUG-20 | X3 | Added |
| 16-AUG-20 | X1 | Removed |
| 16-AUG-20 | X3 | Removed |
| 16-AUG-20 | X4 | Added |
+------------+---------+---------+
Более компактная версия, предназначенная только для решения (без тестовых данных):
WITH
distinct_times as ( SELECT DISTINCT event_time FROM event_table ),
changes as (
select dt.event_time, et.product,
case nvl2(et.event_time,1,0) - nvl2(lag(et.event_time ) over ( partition by et.product order by dt.event_time ),1,0)
when +1 then 'Added'
when -1 then 'Removed'
end message
from event_table et partition by (product)
right join distinct_times dt on et.event_time = dt.event_time )
SELECT * from changes
where message is not null
order by event_time, product