討論一個比較有意思的業務需求


    業務需求描述:

          有一個用戶登錄表,用戶每次登陸時,就會向這個表中插入一條數據,這個表記錄了用戶的用戶ID和登錄時間,表的數據量有幾千萬,

    現在需要求出從今天開始算,用戶持續登錄的時間(也就是用戶今天登陸了,昨天也登陸了,但是前天沒有登錄,那用戶的持續登錄時間就

    是一天)。

 

   分析:

     看似蠻簡單的一需求,在數據庫里面實際操作起來不是那么簡單的,並非一個簡單的Select能夠搞定的,從業務的描述我們起碼可以得到如下

     的分析結論:

       1. 業務需要統計這樣的數據,應該並不需要實時的數據,所以我們可以獲取某個時間的快照數據來做計算;

       2. 表數據量比較大,如果直接在這個表上操作,勢必會對產品的使用造成影響(因為每個用戶登錄時,都需要再往里面插數據的);

       3. 需要統計每個用戶的持續登錄時間,那意味着如果沒有持續登錄時間的用戶就是不需要的用戶,這里面應該可以篩選掉一大批用戶;

       4. 每個用戶都需要做統計計算,這個肯定是一個循環計算的過程,我們最好前期能過濾掉一部分用戶,那后面的統計計算無疑可以節省很多的時間;

       5. 用戶持續登錄,一般時間不可能很長(很少有人天天去登陸一個網站,持續100天的吧),意味着我們可以用持續天數來做為循環條件,

           而不必以用戶來作為循環條件(用戶做循環條件的話,循環次數應該會比較大);

       6. 大數據量做統計,而且是定位到每個用戶的,性能是必須要重點考慮的因素;

  

   造測試數據:

        測試數據其實有一個比較難的要求是能夠盡量的接近真實數據,這樣的測試效果才是最好的;我們預計造一個2千5百萬的表,造幾十萬的用戶,然后隨機

   的生成登陸時間,但是要求登錄時間盡量能貼合真實的情況;

   我們先創建測試表(其實最好是分區表):

--Create Test Table
create table dbo.UserLoginInfo
(
userid int
,logintime datetime
,CONSTRAINT [PK_UserInfo] PRIMARY KEY CLUSTERED
(
userid ASC
,logintime ASC
)WITH
(
PAD_INDEX = OFF
,STATISTICS_NORECOMPUTE = OFF
,IGNORE_DUP_KEY = OFF
,ALLOW_ROW_LOCKS = ON
,ALLOW_PAGE_LOCKS = ON
,FILLFACTOR = 90
) ON [PRIMARY]
) ON [PRIMARY]

--DST Table
create table dbo.DST_UserLoginInfo
(
userid int
,logindate varchar(10)
,ContinueDays smallint
,IsOver bit
,CONSTRAINT [PK_UserLoginInfo] PRIMARY KEY CLUSTERED
(
userid ASC
,logindate ASC
)WITH
(
PAD_INDEX = OFF
,STATISTICS_NORECOMPUTE = OFF
,IGNORE_DUP_KEY = OFF
,ALLOW_ROW_LOCKS = ON
,ALLOW_PAGE_LOCKS = ON
,FILLFACTOR = 90
) ON [PRIMARY]
) ON [PRIMARY]

    說明:我們創建了兩個表,一個是模擬真實的用戶登錄數據的表,另外一個是我們准備做數據統計的表;

    我們按時間由遠到近,分幾個批次來生成測試數據:

 

/*-----------造二千五百萬用戶登錄記錄的過程---------------------*/
--第一批次
set nocount on
go
--1000*5000=500W
declare @u_count int
set @u_count=0
--1000
while @u_count<1000
begin
declare @userid int,@count int
set @userid= rand()*100000
set @count=0
--5000
while @count<5000
begin
insert into dbo.UserLoginInfo(userid,logintime)
--4000
select @userid,CAST(convert(varchar(10),dateadd(d,abs(checksum(newid())%4000),getdate()-4000),120)+' '+ str(5+abs(checksum(newid())%6),2)+':'+replace(str(abs(checksum(newid())%60),2),'','0')+':'+replace(str(abs(checksum(newid())%60),2),'','0') as datetime)
set @count=@count+1
end
set @u_count=@u_count+1
end
go
set nocount off

--第二批次
set nocount on
go
--1000*6000=600W
declare @u_count int
set @u_count=0
while @u_count<1000
begin
declare @userid int,@count int
set @userid= rand()*1000000
set @count=0
while @count<6000
begin
--3000
insert into dbo.UserLoginInfo(userid,logintime)
select @userid,CAST(convert(varchar(10),dateadd(d,abs(checksum(newid())%3000),getdate()-3000),120)+' '+ str(5+abs(checksum(newid())%6),2)+':'+replace(str(abs(checksum(newid())%60),2),'','0')+':'+replace(str(abs(checksum(newid())%60),2),'','0') as datetime)
set @count=@count+1
end
set @u_count=@u_count+1
end
go
set nocount off

--第三批次
set nocount on
go
--1000*4000=400W
declare @u_count int
set @u_count=0
while @u_count<1000
begin
declare @userid int,@count int
set @userid= rand()*1000000
set @count=0
while @count<4000
begin
--2000
insert into dbo.UserLoginInfo(userid,logintime)
select @userid,CAST(convert(varchar(10),dateadd(d,abs(checksum(newid())%2000),getdate()-2000),120)+' '+ str(5+abs(checksum(newid())%6),2)+':'+replace(str(abs(checksum(newid())%60),2),'','0')+':'+replace(str(abs(checksum(newid())%60),2),'','0') as datetime)
set @count=@count+1
end
set @u_count=@u_count+1
end
go
set nocount off

--第四批次
set nocount on
go
--10000*700=700W
declare @u_count int
set @u_count=0
while @u_count<10000
begin
declare @userid int,@count int
set @userid= rand()*100000000
--select @userid
set @count=0
while @count<700
begin
--1000
insert into dbo.UserLoginInfo(userid,logintime)
select @userid,CAST(convert(varchar(10),dateadd(d,abs(checksum(newid())%1000),getdate()-1000),120)+' '+ str(5+abs(checksum(newid())%6),2)+':'+replace(str(abs(checksum(newid())%60),2),'','0')+':'+replace(str(abs(checksum(newid())%60),2),'','0') as datetime)
set @count=@count+1
end
set @u_count=@u_count+1
end
go
set nocount off

--第五批次
set nocount on
go
--100000*10=100W
declare @u_count int
set @u_count=0
while @u_count<100000
begin
declare @userid int,@count int
set @userid= rand()*100000000
set @count=0
while @count<10
begin
--500
insert into dbo.UserLoginInfo(userid,logintime)
select @userid,CAST(convert(varchar(10),dateadd(d,abs(checksum(newid())%500),getdate()-500),120)+' '+ str(5+abs(checksum(newid())%6),2)+':'+replace(str(abs(checksum(newid())%60),2),'','0')+':'+replace(str(abs(checksum(newid())%60),2),'','0') as datetime)
set @count=@count+1
end
set @u_count=@u_count+1
end
go
set nocount off

--第六批次
set nocount on
go
--100000*20=200W
declare @u_count int
set @u_count=0
while @u_count<100000
begin
declare @userid int,@count int
set @userid= rand()*100000000
set @count=0
while @count<20
begin
--365
insert into dbo.UserLoginInfo(userid,logintime)
select @userid,CAST(convert(varchar(10),dateadd(d,abs(checksum(newid())%365),getdate()-365),120)+' '+ str(5+abs(checksum(newid())%6),2)+':'+replace(str(abs(checksum(newid())%60),2),'','0')+':'+replace(str(abs(checksum(newid())%60),2),'','0') as datetime)
set @count=@count+1
end
set @u_count=@u_count+1
end
go
set nocount off

 

 造這些數據,在我本機上花了大半天時間才完成(痛苦呀),這些腳本運行完之后,我本機生成的數據量情況如下:

 用戶數量:

  隨機生成的時間情況:

    現在我們完成了一個兩千五百萬記錄,用戶數量二十二萬,時間從2001-03-05到2012-02-15的用戶登錄記錄;但是有個問題,就是今天的時間,

 沒有加上去,我們再補充一下今天登陸的記錄(假定今天每個用戶都登陸了)

--add today login recode for every user
insert into dbo.UserLoginInfo(userid,logintime)
select userid,DATEADD(MINUTE,-20,GETDATE()) from
(
select distinct userid from dbo.UserLoginInfo with(nolock)
) a

  加完之后就應該有今天的數據了:

  到這里造數據的過程就完了。

 

  持續天數計算:

     思路:1. 我們將UserLoginInfo的記錄先做篩選,篩選掉那些今天有登陸,但是昨天沒有登錄的用戶(也就是沒有持續登錄的用戶);

             2. 將篩選后的數據放入到中間表,我們通過中間表來計算用戶持續登錄的天數;

             3. 計算完成后,通過查詢中間表,輸出用戶和持續登錄天數;

     以下就按照前面的思路來開展步驟:

   1. 篩選掉不需要的記錄,並將其導入中間表:

--find data into other table
insert into dbo.DST_UserLoginInfo(userid,logindate,ContinueDays,IsOver)
select distinct userid,convert(varchar(10),logintime,23) as logindate,0,0
from dbo.UserLoginInfo a with(nolock)
where exists
--login today
( select 1 from dbo.UserLoginInfo b with(nolock) where b.userid=a.userid
and b.logintime<=GETDATE() and b.logintime>=convert(varchar(10),GETDATE(),23))
--login yesterday
and exists
(select 1 from dbo.UserLoginInfo c with(nolock) where a.userid=c.userid
and c.logintime<convert(varchar(10),GETDATE(),23) and c.logintime>=convert(varchar(10),GETDATE()-1,23)
)

   說明:在這里我們將時間字段變成了日期型的字符串,方便后面計算時做判斷,另外中間表增加了持續時間和標識位字段,也是為了方便后面的計算。    

   2. 通過天數來循環中間表,計算持續天數:
   我們先來看一下,篩選完成后的數據量為800多萬,節省了持續天數計算時大量的計算量;

     

  接着我們來計算持續天數:

   這里有兩種方式:

   1. 按用戶來循環計算:

           每次取一個用戶,然后根據用戶ID來循環計算這個用戶的持續登錄天數;但是設想一下,如果我們有10萬個用戶,那我們第一層次取用戶的循環將要

       循環10萬次,而且每個用戶又需要在第一層的循環里面做持續時間天數的循環計算,可以想象計算量是非常大的,不可取;

   2. 按持續天數來循環計算:

          前面分析階段已經提及過,正常情況下很少有用戶能持續100天,每天都登陸到一個網站上面的,那意味着我們循環的天數不會是一個非常大的量,而

      且這種循環一次性的將同一個持續天數的用戶一次性計算完成了,效率應該是比較高的,我們采用這種方式來進行計算;

--Update ContinueDays
declare @days smallint
set @days=1
while exists (select 1 from dbo.DST_UserLoginInfo where IsOver=0)
begin
update a set ContinueDays=@days,IsOver=1
from dbo.DST_UserLoginInfo a
where IsOver=0
and exists
(select 1 from dbo.DST_UserLoginInfo b with(nolock) where a.userid=b.userid and IsOver=0
and b.logindate=convert(varchar(10),GETDATE()-@days,23))
and not exists
(select 1 from dbo.DST_UserLoginInfo c with(nolock) where a.userid=c.userid and IsOver=0
and c.logindate=convert(varchar(10),GETDATE()-@days-1,23))

set @days=@days+1
end

  運行完成后,我們來查看下運行的結果:

   一共是14639個用戶有持續登錄,最長的登錄時間為259天(真實情況應該不會有這么大,計算的時候最耗時的就是這兩個持續時間大的用戶)。

 

  驗證結果:

  我現在來抽查幾條數據,看是否正確:

   先在中間表中隨便找一個持續時間為一天的記錄,再到原登錄表中找到他實際的登錄數據:

  打鈎的是持續時間,正好是一天(其實這里的理解好像有點問題,用戶明明是連續兩天登錄,而此處的持續時間只算做一天);

  接下來抽個三天的記錄:

  結果也是正確的。

 

  總結:

  分析、造數據、測試和計算我們都做完了,現在將上面的計算過程做成一個存儲過程,這樣就方便隨時調用了:

Create Proc usp_UserContinueDays
as
begin
set nocount on
--truncate dst table
if OBJECT_ID('dbo.DST_UserLoginInfo') is null
begin
--DST Table
create table dbo.DST_UserLoginInfo
(
userid int
,logindate varchar(10)
,ContinueDays smallint
,IsOver bit
,CONSTRAINT [PK_UserLoginInfo] PRIMARY KEY CLUSTERED
(
userid ASC
,logindate ASC
)WITH
(
PAD_INDEX = OFF
,STATISTICS_NORECOMPUTE = OFF
,IGNORE_DUP_KEY = OFF
,ALLOW_ROW_LOCKS = ON
,ALLOW_PAGE_LOCKS = ON
,FILLFACTOR = 90
) ON [PRIMARY]
) ON [PRIMARY]

end
else
truncate table dbo.DST_UserLoginInfo

--find data to other table
insert into dbo.DST_UserLoginInfo(userid,logindate,ContinueDays,IsOver)
select distinct userid,convert(varchar(10),logintime,23) as logindate,0,0
from dbo.UserLoginInfo a with(nolock)
where exists
--login today
( select 1 from dbo.UserLoginInfo b with(nolock) where b.userid=a.userid
and b.logintime<=GETDATE() and b.logintime>=convert(varchar(10),GETDATE(),23))
--login yesterday
and exists
(select 1 from dbo.UserLoginInfo c with(nolock) where a.userid=c.userid
and c.logintime<convert(varchar(10),GETDATE(),23) and c.logintime>=convert(varchar(10),GETDATE()-1,23)
)

--Update ContinueDays
declare @days smallint
set @days=1
while exists (select 1 from dbo.DST_UserLoginInfo where IsOver=0)
begin
update a set ContinueDays=@days,IsOver=1
from dbo.DST_UserLoginInfo a
where IsOver=0
and exists
(select 1 from dbo.DST_UserLoginInfo b with(nolock) where a.userid=b.userid and IsOver=0
and b.logindate=convert(varchar(10),GETDATE()-@days,23))
and not exists
(select 1 from dbo.DST_UserLoginInfo c with(nolock) where a.userid=c.userid and IsOver=0
and c.logindate=convert(varchar(10),GETDATE()-@days-1,23))

set @days=@days+1
end

--Result
select userid,MIN(logindate) as LoginDate ,ContinueDays from dbo.DST_UserLoginInfo
group by userid,ContinueDays
order by ContinueDays desc

set nocount off
end

    到此,測試計算的過程完成,不過有點遺憾的是,這個業務需求是另外一個公司的朋友提供的,我沒辦法拿到他們原始的計算方法,所以就沒用辦法比較

 算法的最終效果了;如果大家有更好的方法,歡迎討論。


 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM