SQL: 当你不能使用 PARTITION 列时,你如何执行聚合?

SQL: how do you perform aggregations when you can't use PARTITION the column?

下面是 table 的图像:

https://i.stack.imgur.com/mPUGV.png

我有一个 table 可以跟踪用户对移动应用程序的访问。每行代表用户在应用程序中进入页面时的日期时间。 Min_btw_page 显示每次页面访问之间的分钟数。当Min_btw_page >= 30 分钟时,会话被视为完成,下一次页面访问将被计为新会话。我要查找的是:

  1. 每个用户每次会话访问的页数(即行数)(HashID);
  2. 每个会话花费的平均分钟数

我已经使用 lag() 函数创建了“Min_btw_next_page”。我还创建了列“row_no”,试图给出序列号。按会话按 HashID 到每一行,但我失败了。结果应该类似于列“Expected_row_no”。但是,即使我能够获得正确的行号,我仍然不知道如何按会话聚合行,因为我无法对行号进行分区。

我对你的问题的理解是你想给用户区分'sessions'。您将新的 'session' 定义为用户在超过 30 分钟内没有做任何事情。因此,如果某人做了很多动作,每个动作之间间隔 20 分钟左右,它仍然算作一个 'session'.

一种方法(绝对不是唯一的方法)将从对现有内容进行微小更改开始。另请注意,这里只是部分答案 - 这是为以后的分析做准备。

另请注意

  • 它写在 SQL 服务器中 - 如果您使用其他东西,您需要查看
  • 如果您 post 机器可读形式的数据,您将获得更快更好的解决方案,这样我们就不必重新输入!
  • 我已按照要求避免分区(第一个 LAG 除外)。我假设你在 LAG 中使用了一个分区来获取你的值,所以我在那里使用了一个。但是,它确实使用 SUM(column) OVER (ORDER BY ...) 获得 运行 总数。

在这里,我正在做的是创建一个列,其中 'session' 中的所有值都获得相同的值,例如,table 中的前六行获得值 1,下一个两行的值为 2,接下来的八行的值为 3。从那里,您可以分组以找到平均值等,还可以做其他事情,例如编号变得微不足道。

过程涉及

  • 找到下一个 VisitDateTime,而不是找到上一个访问日期时间。这非常重要,因为它使我们能够通过简单的 DATEDIFF
  • 确定(在一行中)它是否是一个新会话
  • 'new session' 的每一行都标有值 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 函数(我相信这类似于您创建原始 table 的函数)来确定最后 activity 时间
  • 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

从这里,随意保存到临时 table 左右以供进一步处理,例如,

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 添加了一些 'interweaved' 数据(例如,HashID ID2 的乱序和重叠)以帮助确保它按要求工作。

我认为达到要求的最佳方法是结合使用 DATEDIFFFIRST_VALUE 和整数数学将分钟差除以 30 分钟。这会在 HashID window 分区内创建不同的 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