SQL:当你不能使用 PARTITION 列时如何执行聚合?
Posted
技术标签:
【中文标题】SQL:当你不能使用 PARTITION 列时如何执行聚合?【英文标题】:SQL: how do you perform aggregations when you can't use PARTITION the column? 【发布时间】:2020-10-22 10:46:37 【问题描述】:下面是表格的图片:
https://i.stack.imgur.com/mPUGV.png
我有一个表格来跟踪用户对移动应用的访问。每行代表用户在应用程序中进入页面的日期时间。 min_btw_page 显示每次页面访问之间的分钟数。当 Min_btw_page >= 30 mins 时,认为会话完成,下一页访问将计为新会话。我试图找到的是:
-
每个用户每次会话访问的页面数(即行数)(HashID);
每个会话花费的平均分钟数
我使用了 lag() 函数来创建“Min_btw_next_page”。我还创建了列“row_no”,试图给出一个序列号。按会话通过 HashID 到每一行,但我失败了。结果应该类似于“Expected_row_no”列。但是,即使我能够获得正确的行号,我仍然不知道如何按会话聚合行,因为我无法对行号进行分区。
【问题讨论】:
请提供样本数据、所需结果和适当的数据库标签。 【参考方案1】:我对您的问题的理解是您想为用户区分“会话”。您将一个新的“会话”定义为用户超过 30 分钟没有做任何事情的地方。因此,如果某人做了很多动作,每个动作之间间隔 20 分钟左右,它仍然算作一个“会话”。
一种方法(绝对不是唯一的方法)将从对您现有的内容进行微小更改开始。另请注意,这只是部分答案 - 为以后的分析做准备。
还要注意
它是用 SQL Server 编写的 - 如果您使用其他东西,则需要查看 如果您以机器可读的形式发布数据,您将获得更快更好的解决方案,因此我们无需重新输入! 我已经按照要求避免了分区(第一个 LAG 除外)。我假设你在 LAG 中使用了一个分区来获取你的值,所以我在那里使用了一个。但是,它确实使用SUM(column) OVER (ORDER BY ...)
来获得运行总数。
在这里,我要做的是创建一个列,其中“会话”中的所有值都获得相同的值,例如,表中的前六行获得值 1,接下来的两行获得值 2,下一个八行得到值 3。从那里,您可以分组以查找平均值等,并且还可以做其他事情,例如编号变得微不足道。
过程涉及
不是查找下一个 VisitDateTime,而是查找last 访问日期时间。这非常重要,因为它使我们能够(在一行上)通过简单的 DATEDIFF 确定它是否是新会话 作为“新会话”的每一行都标记为值 1,否则为 0。 然后通过简单地计算这些标志的总和来创建会话数数据设置
CREATE TABLE #DeviceLoads (LogID int IDENTITY(1,1), HashID nvarchar(10), DeviceDatetime datetime);
INSERT INTO #DeviceLoads (HashID, DeviceDatetime) VALUES
('ID1', '20201013 15:26'),
('ID1', '20201013 15:26'),
('ID1', '20201013 15:28'),
('ID1', '20201013 15:28'),
('ID1', '20201013 15:28'),
('ID1', '20201014 14:59'),
('ID1', '20201014 14:59'),
('ID1', '20201014 16:17'),
('ID1', '20201014 16:46'),
('ID1', '20201014 17:15'),
('ID1', '20201014 17:46');
这是一个命令(尽管可以随意拆分)。
CTEDL_Source
使用 LAG 函数(我相信类似于您创建原始表的函数)来确定上次活动时间
CTE DL_Session_Source
从上面获取数据,并用值 1 标记新会话
最终的 SELECT 从 DL_Session_Source
创建运行总计
WITH DL_source AS -- This is probably similar to what you have already
(SELECT LogID, HashID, DeviceDatetime, LAG(DeviceDatetime, 1) OVER (PARTITION BY HashId ORDER BY DeviceDatetime, LogID) AS Last_DeviceDateTime
FROM #DeviceLoads),
DL_Session_Source AS
(SELECT LogID, HashID, DeviceDatetime, Last_DeviceDateTime, CASE WHEN DATEDIFF(minute, Last_DeviceDateTime, DeviceDatetime) <= 30 THEN 0 ELSE 1 END AS New_Session_flag
FROM DL_source)
SELECT *, SUM(New_Session_flag) OVER (ORDER BY HashID, DeviceDatetime, LogID) AS Session_Num
FROM DL_Session_Source;
以下是结果(为简洁起见,截断了秒数)。请注意末尾的列 (Session_Num),它指示哪些行在哪个会话中。
LogID HashID DeviceDatetime Last_DeviceDateTime New_Session_flag Session_Num
1 ID1 2020-10-13 15:26 NULL 1 1
2 ID1 2020-10-13 15:26 2020-10-13 15:26 0 1
3 ID1 2020-10-13 15:28 2020-10-13 15:26 0 1
4 ID1 2020-10-13 15:28 2020-10-13 15:28 0 1
5 ID1 2020-10-13 15:28 2020-10-13 15:28 0 1
6 ID1 2020-10-14 14:59 2020-10-13 15:28 1 2
7 ID1 2020-10-14 14:59 2020-10-14 14:59 0 2
8 ID1 2020-10-14 16:17 2020-10-14 14:59 1 3
9 ID1 2020-10-14 16:46 2020-10-14 16:17 0 3
10 ID1 2020-10-14 17:15 2020-10-14 16:46 0 3
11 ID1 2020-10-14 17:46 2020-10-14 17:15 1 4
从这里,随意保存到一个临时表左右以进行进一步处理,例如,
SELECT Session_Num,
HashID,
COUNT(*) AS Num_Actions,
MIN(DeviceDateTime) AS First_Action,
MAX(DeviceDateTime) AS Last_Action
FROM #YourTempTable
GROUP BY Session_Num, HashID;
这是一个 db<>fiddle,其中添加了一些“交织”数据(例如,HashID ID2 的乱序和重叠)以帮助确保其按要求工作。
【讨论】:
嗨@seanb!谢谢你的提示!我很抱歉没有以机器可读的形式发布数据。您的解决方案非常清晰且很有帮助。我不敢相信我花了一整天的时间来解决这个问题,而你却如此轻松地解决了它。 你已经用 LAG 完成了艰苦的工作,我只是把它转了一点,以便在一行上进行计算。不过有一件事 - 在写完这篇文章之后,我看到(在另一个问题中)@GMB 写的关于gaps and islands 的答案并看到了类似的处理 - 我认为这也适用于这里。如果您研究“差距和孤岛”,您可能会发现相同的整体方法但更好/更有效的代码(当我编写上述内容时,我并没有试图理解那个问题/解决方案)【参考方案2】:我认为满足要求的最佳方法是使用DATEDIFF
、FIRST_VALUE
和整数数学的组合将微小差异除以 30 分钟。这会在 HashID 窗口分区内创建不同的 30 分钟会话分组。只需要一个 CTE。
数据(类似于seanb)
drop table if exists #DeviceLoads;
go
create table #DeviceLoads (
LogID int identity(1,1),
HashID nvarchar(10),
DeviceDatetime datetime);
insert into #DeviceLoads (HashID, DeviceDatetime) values
('ID1', '20201013 15:26'),
('ID1', '20201013 15:26'),
('ID1', '20201013 15:28'),
('ID1', '20201013 15:28'),
('ID1', '20201013 15:28'),
('ID1', '20201014 14:59'),
('ID1', '20201014 14:59'),
('ID1', '20201014 16:17'),
('ID1', '20201014 16:46'),
('ID1', '20201014 17:15'),
('ID1', '20201014 17:46'),
('ID2', '20201014 14:59'),
('ID2', '20201014 16:17'),
('ID2', '20201014 16:27'),
('ID2', '20201014 16:37'),
('ID2', '20201014 16:46'),
('ID3', '20201014 17:15'),
('ID3', '20201014 17:46');
查询
with session_cte as (
select *, datediff(minute, first_value(DeviceDatetime) over
(partition by HashID order by DeviceDatetime),
DeviceDatetime)/30 Session_Num
from #DeviceLoads)
select Session_Num,
HashID,
count(*) AS Num_Actions,
min(DeviceDateTime) AS First_Action,
max(DeviceDateTime) AS Last_Action
from session_cte
group by Session_Num, HashID;
查询以分钟为单位获取每个 HashID 的平均会话
with
session_cte as (
select *, datediff(minute, first_value(DeviceDatetime) over
(partition by HashID order by DeviceDatetime),
DeviceDatetime)/30 Session_Num
from #DeviceLoads),
hash_cte as (
select Session_Num,
HashID,
count(*) AS Num_Actions,
min(DeviceDateTime) AS First_Action,
max(DeviceDateTime) AS Last_Action
from session_cte
group by Session_Num, HashID)
select HashID, avg(datediff(minute, First_Action, Last_Action)*1.0) avg_session_min
from hash_cte
group by HashID;
输出
HashID avg_session_min
ID1 0.333333
ID2 6.333333
ID3 0.000000
【讨论】:
以上是关于SQL:当你不能使用 PARTITION 列时如何执行聚合?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我们在 SQL Server 中透视文本列时使用 Max 函数?
在 SQLite 中使用 DATETIME 列时如何避免 NumberFormatException?
如何使用“Partition By”或“Max”?对于 SQL 服务器
如何在 SQL 中以高性能的方式使用 PARTITION BY 获取最新记录?