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 分钟时,会话被视为完成,下一次页面访问将被计为新会话。我要查找的是:
- 每个用户每次会话访问的页数(即行数)(HashID);
- 每个会话花费的平均分钟数
我已经使用 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 的乱序和重叠)以帮助确保它按要求工作。
我认为达到要求的最佳方法是结合使用 DATEDIFF
、FIRST_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
下面是 table 的图像:
https://i.stack.imgur.com/mPUGV.png
我有一个 table 可以跟踪用户对移动应用程序的访问。每行代表用户在应用程序中进入页面时的日期时间。 Min_btw_page 显示每次页面访问之间的分钟数。当Min_btw_page >= 30 分钟时,会话被视为完成,下一次页面访问将被计为新会话。我要查找的是:
- 每个用户每次会话访问的页数(即行数)(HashID);
- 每个会话花费的平均分钟数
我已经使用 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 的乱序和重叠)以帮助确保它按要求工作。
我认为达到要求的最佳方法是结合使用 DATEDIFF
、FIRST_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