我的客户一个月的交易数据约为2000万条记录。对于广告系列,我需要按照以下模式获得客户的奖励资格:
例如: 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万条记录却浪费了很多时间。
请帮助我提供任何有效的解决方案来完成此任务。我真的会非常感激。
以下是简单的查询语句,它什么也没有做,但我尝试过:
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万笔交易中的所有交易都按上述详细信息奖励给客户。
答案 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);