получить результаты в 7 дней скользящего окна, которое варьируется для каждого пользователя - PullRequest
2 голосов
/ 05 октября 2019

У меня есть данные транзакций клиентов около 20 миллионов записей в месяц. Для кампании мне необходимо получить квалификацию клиентов для получения вознаграждения по следующим схемам:

  1. Клиент будет претендовать на каждую транзакцию, которую он выполнит через 7 дней.
  2. Любая транзакция, выполненная в течение 7 днейокно будет игнорироваться

Например: 1) Если пользователь А совершил транзакцию 3, 4, 6, 7, 9, 11, 28 - он будет вознагражден 3, 9 и 28датированные транзакции и все транзакции между ними будут игнорироваться. 2) Если Пользователь-B совершил транзакцию 1-го, 4-го, 11-го, 17-го, 21-го, 30-го - он будет вознагражден за 1-ю, 11-ю, 17-ю и 30-ю датированные транзакции, и все промежуточные транзакции будут игнорироваться. 3) если пользователь C выполнил транзакцию 1-го и 30-го числа - он будет вознагражден за обе транзакции.

Я потратил 3 дня и пытался сделать это многими способами, но из-за своих ограниченных знаний я могне удалось.

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

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

This picture shows the sample transaction data of customers

This image is to help understand the problem statement

Ниже приведено простое выражение запроса, которое ни к чему хорошему, но я пробовал:

SELECT  t1.[FINANCIAL ID], 
        t1.MSISDN, 
        t1.[DATE],
        MIN(t2.[DATE]) AS [NEXT DATE],
        ISNULL(DATEDIFF(DAY, t1.[DATE], MIN(t2.[DATE])), 0) AS DAYSDIFF1
FROM    mydb.dbo.RequiredTrxnForCampaign t1
        LEFT JOIN mydb.dbo.RequiredTrxnForCampaign t2
            ON t1.MSISDN = t2.MSISDN
            AND t2.[DATE] > t1.[DATE]
GROUP BY t1.[FINANCIAL ID], t1.MSISDN, t1.[DATE]

Ниже приводится циклический запрос, который я пробовал, но он требует 40 минут для записей 100 КБ со всеми возможными оптимизациями, которые я мог бы сделать.

DECLARE @minid int = (SELECT MIN(rownumber) FROM mydb.dbo.Test_5k t)
DECLARE @maxid int = (SELECT MAX(rownumber) FROM mydb.dbo.Test_5k t)

DECLARE @fid varchar(11) = NULL
DECLARE @msisdn varchar(20) = NULL
DECLARE @date datetime = NULL
DECLARE @product varchar(50) = NULL
DECLARE @checkmsisdn smallint = NULL
DECLARE @checkdate datetime = NULL
DECLARE @datediff int = NULL

TRUNCATE TABLE mydb.dbo.MinDateTable
TRUNCATE TABLE mydb.dbo.Test_5k_Result


WHILE (@minid <= @maxid)
BEGIN

SET @fid =          (SELECT tk.[FINANCIAL ID] FROM dbo.Test_5k tk WHERE tk.rownumber = @minid)
SET @msisdn =       (SELECT tk.MSISDN FROM dbo.Test_5k tk WHERE tk.rownumber = @minid)
SET @date =         (SELECT tk.[DATE] FROM dbo.Test_5k tk WHERE tk.rownumber = @minid)
SET @product =      (SELECT tk.[PRODUCT NAME] FROM dbo.Test_5k tk WHERE tk.rownumber = @minid)
SET @checkmsisdn =  (SELECT count(*) FROM dbo.MinDateTable mdt WHERE mdt.MSISDN=@msisdn)
SET @checkdate =    (SELECT mdt.[MIN DATE] FROM dbo.MinDateTable mdt WHERE mdt.MSISDN=@msisdn)
SET @datediff =     (ISNULL(DATEDIFF(DAY, @checkdate, @date), 0))


IF (@checkmsisdn = 0)
BEGIN
    INSERT INTO dbo.MinDateTable (MSISDN, [MIN DATE])
    VALUES (@msisdn, @date);

    INSERT INTO dbo.Test_5k_Result (MSISDN, [DATE], [PRODUCT NAME], [FINANCIAL ID], DAYSDIFF)
    VALUES (@msisdn, @date, @product, @fid, @datediff);
END
ELSE
BEGIN
    IF (@checkmsisdn > 0 AND @datediff >= 6)
    BEGIN
        UPDATE dbo.MinDateTable
        SET [MIN DATE] = @date
        WHERE MSISDN=@msisdn

        INSERT INTO dbo.Test_5k_Result (MSISDN, [DATE], [PRODUCT NAME], [FINANCIAL ID], DAYSDIFF)
        VALUES (@msisdn, @date, @product, @fid, @datediff);
    END
END

SET @minid = @minid + 1
END;

Требуемый результат для получениявсе транзакции из 20 миллионов, в которых клиент будет вознагражден в соответствии с вышеуказанными деталями.

Ответы [ 3 ]

1 голос
/ 05 октября 2019

Вы можете легко реализовать произвольную логику агрегирования, используя 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      |
1 голос
/ 05 октября 2019

Вы можете сделать это с помощью рекурсивных CTE. ,,что может быть не так уж плохо для небольшого количества данных для каждого клиента:

with cte as (
      select msisdn, date
      from (select t.*,
                   row_number() over (partition by msisdn order by date) as seqnum
            from RequiredTrxnForCampaign t
           ) t
      where seqnum = 1
      union all
      select t.msisdn, t.date
      from cte cross apply
           (select top (1) t.*
            from RequiredTrxnForCampaign t
            where t.msisdn = cte.msisdn and
                  t.date >= dateadd(day, 7, cte.date)
            order by t.date asc
           ) t
    )
select msisdn, date
from cte
order by msisdn, date;

Не пытайтесь делать это без индекса на (msisdn, date).

. Затем вы можете применить фильтрацию. логика на определенный период времени. Я бы порекомендовал фильтрацию в первой части CTE.

0 голосов
/ 05 октября 2019

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

create table foo (
    id int,
    customer varchar(10),
    dayn int
);

insert foo values
     ( 1,'A', 3) 
    ,( 2,'A', 4)
    ,( 3,'A', 6)
    ,( 4,'A', 7)
    ,( 5,'A', 9)
    ,( 6,'A',11)
    ,( 7,'A',28) 
    ,( 8,'B', 1)
    ,( 9,'B', 4)
    ,(10,'B',11)
    ,(11,'B',17)
    ,(12,'B',21)
    ,(13,'B',30);

select top(1) with ties id, customer, dayn
from foo 
order by row_number() over(partition by customer, (dayn - 1) / 7 order by dayn);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...