SQL Server求解連續操作(登錄)數量(次數)最大的記錄(用戶)問題


在園中大V深藍醫生中的一篇文中發現了這個問題,感覺挺有意思。

問題簡化為“求解連續日期登錄次數最大的用戶”。至少連續2天都登錄才能認為是連續日登錄。

數據島問題
這個問題讓我聯想到了數據島問題,數據島問題就是間隔相同且連續的一個數值區間。以下面的整型數據集合為例:
1,
4,
5,
7,
8,
11,
12,
13
以上示例中,間隔為1可以划分為[1,1],[4,5],[7, 8], [11,13]共4個數據島。針對如何獲取數據島的解決方案我以后在詳細來說明數據差距和數據島這個問題。我們假設一個數據島具有唯一一個標識符,只要找到每一個數據島中的這個標識符那就可以通過分組聚合解決該問題。

針對以上示例中的獲取數據島的解決方案的T-SQL腳本如下:
-- 創建表變量
DECLARE @tblTestData TABLE (
Val INT NOT NULL
);

-- 向其插入數據
INSERT INTO @tblTestData (Val) VALUES
(1),
(4),
(5),
(7),
(8),
(11),
(12),
(13);

-- 分組聚合
SELECT MIN(T.Val) AS StartVal, MAX(T.Val) AS EndVal
FROM (
-- 獲取每個數據島的標識符
SELECT Val, val - ROW_NUMBER() OVER (ORDER BY Val ASC) AS grp
FROM @tblTestData
) AS T
GROUP BY T.grp;
GO

執行后的結果如下:

該問題解決方案
我們繼續回到本文的問題啦,簡介了數據島問題,我們顯然針對該問題進行分拆如下:
第一步:先找到每個用戶的登錄日所在數據島的唯一標識符;
第二步:通過用戶和數據島唯一分隔符分組聚合獲得每個用戶的連續陸登錄日計數和登錄次數之和;
第三步:針對第二步的結果以連續陸登錄日計數倒序和登錄次數之和倒序來獲取結果。

准備測試數據
相關的T-SQL腳本如下:

IF OBJECT_ID(N'dbo.UserLoginInfo', N'U') IS NOT NULL
BEGIN
DROP TABLE dbo.UserLoginInfo;
END
GO

-- create testing table UserLoginInfo
CREATE TABLE dbo.UserLoginInfo (
ID INT IDENTITY(1, 1) PRIMARY KEY,
Name VARCHAR(50) NOT NULL,
LoginTime DATETIME NOT NULL
);
GO

-- insert testing data
INSERT dbo.UserLoginInfo (Name, LoginTime) VALUES
('zhang', '2015-11-10 12:01:50')
,('li', '2015-11-11 11:01:50')
,('wang', '2015-11-9 11:01:50')
,('zhang', '2015-11-11 12:01:50')
,('li', '2015-11-11 12:01:50')
,('wang', '2015-11-11 11:01:50')
,('zhang', '2015-11-12 12:01:50')
,('li', '2015-11-13 13:01:50')
,('wang', '2015-11-12 11:01:50')
,('zhang', '2015-11-13 12:01:50')
,('li', '2015-11-14 11:01:50')
,('wang', '2015-11-14 11:01:50')
,('zhang', '2015-11-10 12:01:50')
,('li', '2013-10-05 11:01:50')
,('li', '2013-10-06 11:01:50')
,('li', '2014-10-05 11:01:50')
,('li', '2014-10-06 11:01:50')
,('li', '2015-10-05 11:01:50')
,('li', '2015-10-06 11:01:50')
,('li', '2015-11-10 11:01:50')
,('li', '2015-11-11 11:01:50')
,('wang', '2015-11-09 11:01:50')
,('zhang', '2015-11-11 12:01:50')
,('li', '2015-11-11 12:01:50')
,('wang', '2015-11-11 11:01:50')
,('zhang', '2015-11-12 12:01:50')
,('li', '2015-11-13 13:01:50')
,('wang', '2015-11-12 11:01:50')
,('zhang', '2015-11-13 12:01:50')
,('li', '2015-11-14 11:01:50')
,('wang', '2015-11-14 11:01:50');
GO

通過執行以下T-SQL語句:
SELECT ID, Name, LoginTime
FROM dbo.UserLoginInfo;
GO
得到的結果如下:

注意:以上T-SQL來自園中深藍醫生的,我在此基礎上進行了調整。

在第一步開始之前,通過查看用戶登錄信息表中的記錄,可以發現:同一個用戶一天可以登錄多次。為了數據方便匯總,則進行分組匯總每個用戶每天的登錄次數。
合並每個用戶每天的登錄次數的T-SQL代碼如下:
-- 0、分組匯總每個用戶同一天登錄的次數
SELECT T.Name, T.LoginDate, COUNT(0) AS DayLoginTimes
FROM (
SELECT ID, Name, LoginTime, CAST(CONVERT(VARCHAR(10), LoginTime, 120) AS DATE) AS LoginDate
FROM dbo.UserLoginInfo
) AS T
GROUP BY T.Name, T.LoginDate;
GO

執行后的結果如下:

我們來實現第一步獲取每個用戶的登錄日所在的數據島的唯一標識符,實現的T-SQL代碼如下:
-- 1、獲取每個用戶的每個登錄日所在的數據島的唯一標識符
SELECT T.Name, T.LoginDate, T.DayLoginTimes, DATEADD(DAY, -1 * DENSE_RANK() OVER (PARTITION BY T.Name ORDER BY T.LoginDate ASC), T.LoginDate) AS GRP
FROM (
-- 0、分組匯總每個用戶同一天登錄的次數
SELECT T.Name, T.LoginDate, COUNT(0) AS DayLoginTimes
FROM (
SELECT ID, Name, LoginTime, CAST(CONVERT(VARCHAR(10), LoginTime, 120) AS DATE) AS LoginDate
FROM dbo.UserLoginInfo
) AS T
GROUP BY T.Name, T.LoginDate
) AS T
GO

執行后的結果如下:

第二步那就分組匯總每個用戶的連續登錄日的計數和登錄次數之和,實現的T-SQL代碼如下:
-- 2、分組匯總每個用戶的連續登錄日的計數和登錄次數之和
SELECT T.Name, T.GRP, COUNT(T.LoginDate) AS LoginDateCount, SUM(T.DayLoginTimes) AS LoginTimesTotal
FROM (

-- 1、獲取每個用戶的每個登錄日所在的數據島的唯一標識符
SELECT T.Name, T.LoginDate, T.DayLoginTimes, DATEADD(DAY, -1 * DENSE_RANK() OVER (PARTITION BY T.Name ORDER BY T.LoginDate ASC), T.LoginDate) AS GRP
FROM (
    -- 0、分組匯總每個用戶同一天登錄的次數
    SELECT T.Name, T.LoginDate, COUNT(0) AS DayLoginTimes
    FROM (
        SELECT ID, Name, LoginTime, CAST(CONVERT(VARCHAR(10), LoginTime, 120) AS DATE) AS LoginDate
        FROM dbo.UserLoginInfo
    ) AS T
    GROUP BY T.Name, T.LoginDate
) AS T

) AS T
GROUP BY T.Name, T.GRP
GO

執行后的結果如下:

第三步那就很簡單,直接通過用戶的連續登錄日計數和登錄次數均倒序排序,實現的T-SQL代碼如下:
-- 3、通過用戶的連續登錄日計數和登錄次數均倒序排序得到的查詢結果
SELECT T.Name, T.LoginDateCount, T.LoginTimesTotal
FROM (
-- 2、分組匯總每個用戶的連續登錄日的計數和登錄次數之和
SELECT T.Name, T.GRP, COUNT(T.LoginDate) AS LoginDateCount, SUM(T.DayLoginTimes) AS LoginTimesTotal
FROM (

    -- 1、獲取每個用戶的每個登錄日所在的數據島的唯一標識符
    SELECT T.Name, T.LoginDate, T.DayLoginTimes, DATEADD(DAY, -1 * DENSE_RANK() OVER (PARTITION BY T.Name ORDER BY T.LoginDate ASC), T.LoginDate) AS GRP
    FROM (
        -- 0、分組匯總每個用戶同一天登錄的次數
        SELECT T.Name, T.LoginDate, COUNT(0) AS DayLoginTimes
        FROM (
            SELECT ID, Name, LoginTime, CAST(CONVERT(VARCHAR(10), LoginTime, 120) AS DATE) AS LoginDate
            FROM dbo.UserLoginInfo
        ) AS T
        GROUP BY T.Name, T.LoginDate
    ) AS T
) AS T
GROUP BY T.Name, T.GRP

) AS T
ORDER BY T.LoginDateCount DESC, T.LoginTimesTotal DESC;
GO

執行后的結果如下:

上圖中紅色矩形框標識的就是我們尋找的答案。

注意:因為我們開始第一步前的處理保證了每個用戶一個登錄日只有一次登錄數據,第一步獲取GRP列時,使用了DENSE_RANK密度排名窗口函數,其OVER字句中的排序字句具有唯一確定性,當然也可以使用ROW_NUMBER行號窗口函數和RANK排名窗口函數。

園中博友如有其他更好的解決方案,也請不吝賜教,萬分感謝。


免責聲明!

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



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