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排名窗口函数。
园中博友如有其他更好的解决方案,也请不吝赐教,万分感谢。