Вы можете легко реализовать произвольную логику агрегирования, используя updatable cursor
. Когда мне не удается найти подходящие расширенные функции SQL для решения моей проблемы, это, как правило, предельная причина, к которой я прибегаю.
Потенциальное преимущество использования курсора для обработки больших наборов данных состоит в том, что он может избежать дорогостоящих операций объединения. и, следовательно, сведение к минимуму ввода / вывода данных.
Решение выполняется только с 2 проходами данных. Первый этап - создание отдельного набора данных ответов, который обычно необходим в реальных деловых целях для защиты исходного набора данных. Второй проход состоит в том, чтобы вычислять вознаграждение или нет построчно. Следовательно, для набора данных из 20M записей он должен быть более эффективным, чем любые решения, связанные с объединениями.
Вы также можете увидеть мой ответ на другой вопрос , который в основном является упрощенным вариантом вашеговопрос.
Протестировано на сервере SQL Server 2017 самое позднее (образ докера Linux)
Набор тестовых данных
use [testdb];
if OBJECT_ID('testdb..test') is not null
drop table testdb..test;
create table test (
MSISDN varchar(50),
[date] datetime
);
GO
-- name list, need not be sorted
insert into test(MSISDN, [date])
values ('1', '2019-01-01'),
('1', '2019-01-06'),
('1', '2019-01-07'),
('1', '2019-01-08'),
('1', '2019-01-12'),
('1', '2019-01-17'),
('1', '2019-01-19'),
('1', '2019-01-22'),
('2', '2019-01-05'),
('2', '2019-01-09'),
('2', '2019-01-11'),
('2', '2019-01-12'),
('2', '2019-01-20'),
('2', '2019-01-31');
declare @reward_window int = 7; -- D = last reward day
-- Transaction on D, D+1, ... D+6 -> no reward
-- First transaction on and after D+7 -> rewarded
Решение
/* Setup */
-- Create answer dataset
if OBJECT_ID('tempdb..#ans') is not NULL
drop table #ans;
select
-- Create a unique key to enable cursor update
-- A pre-existing unique index can also be used
row_number() over(order by MSISDN, [date]) as rn,
MSISDN,
-- Date part only. Or just [date] to include the time part
CONVERT(date, [date]) as [date],
-- differnce between this and previous transactions from the same customer
datediff(day,
LAG([date], 1, '1970-01-01') over(partition by [MSISDN]
order by [date]),
[date]
) as diff_days,
-- no reward by default
0 as reward
into #ans
from test
order by MSISDN, [date];
create unique index idx_rn on #ans(rn);
-- check
-- select * from #ans;
-- cursor for iteration
declare cur cursor local
for select rn, MSISDN, [date], diff_days, reward
from #ans
order by MSISDN, [date]
for update of [reward];
open cur;
-- fetched variables
declare @rn int,
@MSISDN varchar(50),
@DT datetime,
@diff_days int,
@reward int;
-- State from previous row
declare @MSISDN_prev varchar(50) = '',
@DT_prev datetime = '1970-01-01',
@days_to_last_reward int = 0;
/* Main loop */
while 1=1 begin
-- read next line and check termination condition
fetch next from cur
into @rn, @MSISDN, @DT, @diff_days, @reward;
if @@FETCH_STATUS <> 0
break;
/* Main logic here **/
-- accumulate days_to_last_reward
set @days_to_last_reward += @diff_days;
-- Reward for new customer or days_to_last_reward >= @reward_window)
if @MSISDN <> @MSISDN_prev or @days_to_last_reward >= @reward_window begin
update #ans
set reward = 1
where current of cur;
-- reset days_to_last_reward
set @days_to_last_reward = 0;
end
-- setup next round
set @MSISDN_prev = @MSISDN;
set @DT_prev = @DT;
end
-- cleanup
close cur;
deallocate cur;
-- show
select * -- MSISDN, [date], reward
from #ans
order by MSISDN, [date];
Выход
Это имеет смысл, если клиент, вознагражденный 1 января, может быть снова вознагражден 8 января.
| rn | MSISDN | date | diff_days | reward |
|----|--------|------------|-----------|--------|
| 1 | 1 | 2019-01-01 | 17897 | 1 |
| 2 | 1 | 2019-01-06 | 5 | 0 |
| 3 | 1 | 2019-01-07 | 1 | 0 |
| 4 | 1 | 2019-01-08 | 1 | 1 |
| 5 | 1 | 2019-01-12 | 4 | 0 |
| 6 | 1 | 2019-01-17 | 5 | 1 |
| 7 | 1 | 2019-01-19 | 2 | 0 |
| 8 | 1 | 2019-01-22 | 3 | 0 |
| 9 | 2 | 2019-01-05 | 17901 | 1 |
| 10 | 2 | 2019-01-09 | 4 | 0 |
| 11 | 2 | 2019-01-11 | 2 | 0 |
| 12 | 2 | 2019-01-12 | 1 | 1 |
| 13 | 2 | 2019-01-20 | 8 | 1 |
| 14 | 2 | 2019-01-31 | 11 | 1 |