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');

这是一个命令(尽管可以随意拆分)。

CTE DL_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】:

我认为满足要求的最佳方法是使用DATEDIFFFIRST_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 获取最新记录?

如何在linq to sql中使用orderby和partition by获取第一行

如何在 Snowflake sql 中使用 partition by 和 order by 计算不同的值?