在7天的滚动窗口中获得结果,每个用户的使用情况不同

时间:2019-10-05 17:44:38

标签: sql sql-server

我的客户一个月的交易数据约为2000万条记录。对于广告系列,我需要按照以下模式获得客户的奖励资格:

  1. 客户将有资格在7天后完成每笔交易。
  2. 在7天之内执行的任何交易都会被忽略

例如: 1)如果用户A在3日,4日,6日,7日,9日,11日,28日进行了交易-他将获得3日,9日和28日的交易奖励,而在此期间的所有交易都将被忽略。 2)如果用户B在1日,4日,11日,17日,21日,30日进行过交易-他将获得1日,11日,17日和30日的日期交易奖励,其间的所有交易都将被忽略。 3)如果User-C在1日和30日进行了交易-两次交易都将获得奖励。

我已经花了3天的时间尝试了很多方法,但是由于我的知识有限,我无法成功。

我尝试通过循环查询来实现,该查询给出了所需的结果,但是通过循环处理2000万条记录却浪费了很多时间。

请帮助我提供任何有效的解决方案来完成此任务。我真的会非常感激。

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]

以下是我尝试过的循环查询,但要完成10万条记录以及所有本可以做的优化,要花40分钟。

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;

要求的结果是使所有2000万笔交易中的所有交易都按上述详细信息奖励给客户。

3 个答案:

答案 0 :(得分:1)

您可以使用updatable cursor轻松实现任意聚合逻辑。 当我找不到合适的高级SQL函数来解决我的问题时, 这通常是我跌倒的最终杀手。

使用游标处理大型数据集的潜在优势是 它可以避免昂贵的联接操作,从而最大程度地减少数据I / O。

该解决方案仅通过2次数据传递即可完成。第一遍是创建一个单独的 答案数据集,在实际的业务用途中通常必须使用它来保护 原始数据集。第二遍是计算奖励还是不逐行计算。因此,对于 一个包含2千万条记录的数据集,它应该比任何涉及联接的解决方案都更有效率。

您可能还会看到my answer on another question,它基本上是问题的简化版本。

在sql server 2017最新版本上测试(Linux docker镜像)

测试数据集

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月1日获得奖励的客户可以在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 :(得分:1)

您可以使用递归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的第一部分进行过滤。

答案 2 :(得分:0)

使用整数算术对行进行分组,并找到一组中的最小天数。演示

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);