【问题标题】:SQL: Use LEAD() and PARTITION BY to access to the next row following the current rowSQL:使用 LEAD() 和 PARTITION BY 访问当前行之后的下一行
【发布时间】:2021-06-02 23:25:51
【问题描述】:

我有一个移动应用浏览历史数据集,如下所示。

  • DeviceDateTime:用户在移动应用中查看页面的日期和时间。
  • UserID:每个 UserID 代表登录移动应用的访问者。
  • PageName:移动应用中有不同的页面。所有访客 将首先登陆主页,然后导航到不同的 页面。
  • PageSequence:访问页面的顺序。例如,Seq_1 首页 > Seq_2 我的帐户 = 首先登陆主页,然后导航到“我的 帐户”页面。
DeviceDateTime UserID PageName PageSequence
2021-01-19 16:40:00.000 UserA Home Seq_1
2021-01-19 16:40:00.000 UserA My Account Seq_2
2021-01-19 16:40:07.000 UserA My Activity Seq_3
2021-01-19 16:40:07.000 UserA Restaurant Listing Seq_4
2021-01-19 16:40:18.000 UserA Restaurant Details Page Seq_5
2021-01-19 16:40:31.000 UserA Restaurant Details Page Seq_6
2021-01-19 16:40:31.000 UserA Restaurant Booking Confirmation Seq_7
2021-01-19 16:40:40.000 UserA Home Seq_8
2021-01-19 16:40:45.000 UserA Write To Us Seq_9
2021-01-19 16:40:46.000 UserA Home Seq_10
2021-01-28 21:11:53.000 UserB Home Seq_1
2021-01-28 21:12:01.000 UserB Restaurant Listing Seq_2
2021-01-28 21:13:37.000 UserB Restaurant Listing Seq_3
2021-02-16 09:43:27.000 UserA Home Seq_1
2021-02-16 09:43:43.000 UserA Write To Us Seq_2
2021-02-16 09:44:50.000 UserA My Account Seq_3
2021-02-16 09:45:03.000 UserA My Activity Seq_4

我需要在SQL中做如下预处理:

  1. 将数据集汇总成一个表格,如下所示。我想在表格中显示“FROM”(源页面)和“TO”(目标页面)。例如,有 1 位访问者经历了从“主页”页面到“我的帐户”页面的旅程; 2 位访问者经历了从“主页”页面到“写信给我们”的旅程。
  2. 当旅程FROM和TO同一页时,不计算旅程。例如,UserA Seq_5 和 Seq_6,“FROM Restaurant Details Page”到“Restaurant Details Page”不应包含在内。
  3. 不应包括两个不同用户的旅程。例如,不应包含 FROM "Restaurant Listing" (UserB Seq_3) TO "Home" (UserA Seq_1)。

结果表:

FROM TO No_of_Users
Home My Account 1
My Account My Activity 2
My Activity Restaurant Listing 1
Restaurant Listing Restaurant Details Page 1
Restaurant Details Page Restaurant Booking Confirmation 1
Restaurant Booking Confirmation Home 1
Home Write To Us 2
Write To Us Home 1
Home Restaurant Listing 1
Home Restaurant Listing 1
Write To Us My Account 1

我在数据集中有大约 600,000 个用户和总共 21 个唯一的 PageName。

我尝试了以下脚本,但没有成功。我未能在汇总表中捕获所有可能的旅程。例如,从“写信给我们”到“我的帐户”,从“餐厅预订确认”到“家”都在结果中丢失。

DROP TABLE IF EXISTS #App
CREATE TABLE #App (
    DeviceDateTime SMALLDATETIME,
    UserID VARCHAR(100),
    PageName VARCHAR(100),
    PageSequence VARCHAR(100))
INSERT INTO #App VALUES
    ('2021-01-19 16:40:00.000','UserA', 'Home', 'Seq_1'),
    ('2021-01-19 16:40:00.000','UserA', 'My Account', 'Seq_2'),
    ('2021-01-19 16:40:07.000','UserA', 'My Activity', 'Seq_3'),
    ('2021-01-19 16:40:07.000','UserA', 'Restaurant Listing', 'Seq_4'),
    ('2021-01-19 16:40:18.000','UserA', 'Restaurant Details Page', 'Seq_5'),
    ('2021-01-19 16:40:31.000','UserA', 'Restaurant Details Page', 'Seq_6'),
    ('2021-01-19 16:40:31.000','UserA', 'Restaurant Booking Confirmation', 'Seq_7'),
    ('2021-01-19 16:40:40.000','UserA', 'Home', 'Seq_8'),
    ('2021-01-19 16:40:45.000','UserA', 'Write To Use', 'Seq_9'),
    ('2021-01-19 16:40:46.000','UserA', 'Home', 'Seq_10'),
    ('2021-01-28 21:11:53.000','UserB', 'Home', 'Seq_1'),
    ('2021-01-28 21:12:01.000','UserB', 'Restaurant Listing', 'Seq_2'),
    ('2021-01-28 21:13:37.000','UserB', 'Restaurant Listing', 'Seq_3'),
    ('2021-02-16 09:43:27.000','UserA', 'Home', 'Seq_1'),
    ('2021-02-16 09:43:43.000','UserA', 'Write To Us', 'Seq_2'),
    ('2021-02-16 09:44:50.000','UserA', 'My Account', 'Seq_3'),
    ('2021-02-16 09:45:03.000','UserA', 'My Activity', 'Seq_4');

DROP TABLE IF EXISTS #SD
with seq_fixed as
(
  select
    UserID,
    DeviceDateTime,
    PageName,
    cast(right(PageSequence, charindex('_', reverse(PageSequence)) - 1) as int) as pagesequencefinal
  from #App
)
, with_next as
(
  select
    UserID,
    DeviceDateTime,
    PageName,
    lead(PageName) over (partition by UserID, DeviceDateTime order by UserID, DeviceDateTime ASC) as next_pagename
  from seq_fixed
  group by UserID, DeviceDateTime, PageName
)
select PageName, next_pagename, count(*) AS No_of_User
into #SD
from with_next
where next_pagename is not null
group by PageName, next_pagename
order by PageName, next_pagename;

SELECT * FROM #SD

【问题讨论】:

  • “没用” 为什么没用?它是否出错,给出了意想不到的结果,不受欢迎的行为,或者其他什么?你实际上在这里问什么?您在这里向我们倾倒了大量代码、数据和文本,但我看不到明确的问题陈述(除了“它不起作用”,它什么也没告诉我们)和问题。纯粹是因为您没有正确终止所有语句,因此您的 CTE 语句失败了吗?
  • @Larnu 我为混乱道歉。我设法得到了一个非常接近的结果,但我未能在汇总表中捕获所有可能的旅程。例如,从“写信给我们”到“我的帐户”,从“餐厅预订确认”到“家”都在结果中缺失。

标签: sql sql-server count left-join aggregate


【解决方案1】:

正如 Thorsten Kettner 所指出的:当用户昨天结束 PageX 并从 Home 开始时,这将被视为 PageX->Home。我接受了他的建议,并尝试通过时间戳列和 LAG 函数来防止这种情况发生。但是,它看起来超长。有什么办法可以缩短吗?

DROP TABLE IF EXISTS #App
CREATE TABLE #App (
    DeviceDateTime SMALLDATETIME,
    UserID VARCHAR(100),
    PageName VARCHAR(100),
    PageSequence VARCHAR(100))
INSERT INTO #App VALUES
    ('2021-01-19 16:40:00.000','UserA', 'Home', 'Seq_1'),
    ('2021-01-19 16:40:00.000','UserA', 'My Account', 'Seq_2'),
    ('2021-01-19 16:40:07.000','UserA', 'My Activity', 'Seq_3'),
    ('2021-01-19 16:40:07.000','UserA', 'Restaurant Listing', 'Seq_4'),
    ('2021-01-19 16:40:18.000','UserA', 'Restaurant Details Page', 'Seq_5'),
    ('2021-01-19 16:40:31.000','UserA', 'Restaurant Details Page', 'Seq_6'),
    ('2021-01-19 16:40:31.000','UserA', 'Restaurant Booking Confirmation', 'Seq_7'),
    ('2021-01-19 16:40:40.000','UserA', 'Home', 'Seq_8'),
    ('2021-01-19 16:40:45.000','UserA', 'Write To Us', 'Seq_9'),
    ('2021-01-19 16:40:46.000','UserA', 'Home', 'Seq_10'),
    ('2021-01-28 21:11:53.000','UserB', 'Home', 'Seq_1'),
    ('2021-01-28 21:12:01.000','UserB', 'Restaurant Listing', 'Seq_2'),
    ('2021-01-28 21:13:37.000','UserB', 'Restaurant Listing', 'Seq_3'),
    ('2021-02-16 09:43:27.000','UserA', 'Home', 'Seq_1'),
    ('2021-02-16 09:43:43.000','UserA', 'Write To Us', 'Seq_2'),
    ('2021-02-16 09:44:50.000','UserA', 'My Account', 'Seq_3'),
    ('2021-02-16 09:45:03.000','UserA', 'My Activity', 'Seq_4');

    With #App2 as (
select
    DeviceDateTime,
    UserID,
    PageName,
    cast(right(PageSequence, charindex('_', reverse(PageSequence)) - 1) as int) as pagesequencefinal,
    lead(DeviceDateTime) over (partition by UserID order by DeviceDateTime) as next_day,
    cast(lag(DeviceDateTime,1) over (partition by UserID order by DeviceDateTime) as date) as Previous_day,
    CASE WHEN DATEDIFF(day, cast(lag(DeviceDateTime,1) over (partition by UserID order by DeviceDateTime) as date), DeviceDateTime) <1 THEN 0 ELSE 1 END AS Dayend_flag,
    DATEDIFF(day, cast(lag(DeviceDateTime,1) over (partition by UserID order by DeviceDateTime) as date), DeviceDateTime) as test
  from #App
), #App3 as (
select
    DeviceDateTime,
    UserID,
    PageName,
    pagesequencefinal,
    next_day,
    Previous_day,
    Dayend_flag,
    test,
    SUM(Dayend_flag) OVER (ORDER BY UserID, DeviceDateTime, pagesequencefinal) AS Session_Num
  from #App2
), #App4 as (
select
    UserID,
    PageName,
    lead(PageName) over (partition by Session_Num order by pagesequencefinal) as next_pagename
  from #App3
)
select PageName, next_pagename, count(*) AS No_of_User
from #App4
where next_pagename is not null
group by PageName, next_pagename
order by PageName, next_pagename;

【讨论】:

    【解决方案2】:

    不要按devicedatetime 分区,按它排序

    select
        UserID,
        DeviceDateTime,
        PageName,
        lead(PageName) over (partition by UserID order by DeviceDateTime) as next_pagename
      from seq_fixed
    

    您不应该按时间分区,因为分区是“使所有行都被视为属于同一集合的原因”,即相同的用户 ID 是按时间排序的“一组行”(或序列)。如果将行划分为“每用户每秒”的集合,则不会获得“一个用户访问十页”的“每用户”旅程,而是获得“十个用户访问一页”的“每用户”旅程

    另外,不要使用 GROUP BY 挤压“导航到同一页面”,使用 ... from with_next WHERE next_pagename &lt;&gt; pagename

    您计算 pageseq 但从不使用它 - 在您的 LEAD 中按它排序(如果您认为同一用户的不同页面的两个日期时间将在每秒精度下相同)或转储它

    最后,如果“用户计数”应该是“从该页面导航到该页面的不同用户的数量”,则它需要是 count(distinct userid) AS No_of_User,而不是 count(*) - countstar 是“数字此页面导航发生的次数,包括同一用户的多次导航”。包含这两个统计数据可能会很方便,以了解有多少用户转来转去

    编辑:

    这是您的查询的修改版本,其中注释掉了我建议删除的所有位:

    DROP TABLE IF EXISTS #App;
    CREATE TABLE #App (
        DeviceDateTime SMALLDATETIME,
        UserID VARCHAR(100),
        PageName VARCHAR(100),
        PageSequence VARCHAR(100))
    INSERT INTO #App VALUES
        ('2021-01-19 16:40:00.000','UserA', 'Home', 'Seq_1'),
        ('2021-01-19 16:40:00.000','UserA', 'My Account', 'Seq_2'),
        ('2021-01-19 16:40:07.000','UserA', 'My Activity', 'Seq_3'),
        ('2021-01-19 16:40:07.000','UserA', 'Restaurant Listing', 'Seq_4'),
        ('2021-01-19 16:40:18.000','UserA', 'Restaurant Details Page', 'Seq_5'),
        ('2021-01-19 16:40:31.000','UserA', 'Restaurant Details Page', 'Seq_6'),
        ('2021-01-19 16:40:31.000','UserA', 'Restaurant Booking Confirmation', 'Seq_7'),
        ('2021-01-19 16:40:40.000','UserA', 'Home', 'Seq_8'),
        ('2021-01-19 16:40:45.000','UserA', 'Write To Use', 'Seq_9'), --TYPO ALERT!!!--TYPO ALERT!!!--TYPO ALERT!!!--TYPO ALERT!!!--TYPO ALERT!!!--TYPO ALERT!!!
        ('2021-01-19 16:40:46.000','UserA', 'Home', 'Seq_10'),
        ('2021-01-28 21:11:53.000','UserB', 'Home', 'Seq_1'),
        ('2021-01-28 21:12:01.000','UserB', 'Restaurant Listing', 'Seq_2'),
        ('2021-01-28 21:13:37.000','UserB', 'Restaurant Listing', 'Seq_3'),
        ('2021-02-16 09:43:27.000','UserA', 'Home', 'Seq_1'),
        ('2021-02-16 09:43:43.000','UserA', 'Write To Us', 'Seq_2'),
        ('2021-02-16 09:44:50.000','UserA', 'My Account', 'Seq_3'),
        ('2021-02-16 09:45:03.000','UserA', 'My Activity', 'Seq_4');
    
    DROP TABLE IF EXISTS #SD;
    with seq_fixed as
    (
      select
        UserID,
        DeviceDateTime,
        PageName/*,
        cast(right(PageSequence, charindex('_', reverse(PageSequence)) - 1) as int) as pagesequencefinal*/
      from #App
    )
    , with_next as
    (
      select
        UserID,
        DeviceDateTime,
        PageName,
        lead(PageName) over (partition by UserID/*, DeviceDateTime*/ order by /*UserID,*/ DeviceDateTime ASC) as next_pagename
      from seq_fixed
      /*group by UserID, DeviceDateTime, PageName*/
    )
    select PageName, next_pagename, count(*) AS No_of_User
    into #SD
    from with_next
    where next_pagename is not null
    group by PageName, next_pagename
    order by PageName, next_pagename;
    
    SELECT * FROM #SD
    

    【讨论】:

    • 感谢 Caius 的及时回复。我尝试了你的方法,但我仍然未能捕捉到第二个 FROM "Home" To "Write To Us"。这次旅行应该有两份记录,但我只拍到了一份。你知道为什么吗?
    • Er.. 其中一个说“Write to Use”有错字?另外,不要忘记转储group by UserID, DeviceDateTime, PageName - 你不需要在每个级别都分组......当你计数时分组
    • 是的,仔细检查,这不是一个错字。是因为这两条记录都来自 UserA 并且在不同的日期?下面的 Thorsten Kettner 的解决方案成功地捕获了这两个记录。
    • 如果不是拼写错误,那就是问题的原因。这是我在执行我给你的所有建议时看到的截图:i.stack.imgur.com/U7C5O.png - 看看底部,因为你按页面名称分组,并且你在 US -> USE 上打错了,它将这两个分开将突出显示的行分成两行计数 1。在该屏幕截图中,其他突出显示是我给出的建议:“转储 pageseq calc;你不使用它” - 顶部突出显示,以及“在中间步骤中转储分组” - 你我肯定忘了做我建议你做的事情
    • 我在答案中添加了您的查询版本,并注释掉了一些内容-您非常接近;最大的问题是分区太细了,中间的分组混淆了页面
    【解决方案3】:

    在您之前的请求中,您只想确认每个用户页面的第一次出现, 所以 Home->PageX->Home->PageY 将被解释为 Home->PageX->PageY。为此,您必须按用户和页面进行分组才能找到第一次出现的位置。

    这个新请求不是这样,所以不要聚合:

    with seq_fixed as
    (
      select
        userid,
        pagename,
        cast(right(pagesequence, charindex('_', reverse(pagesequence)) - 1) as int) as pagesequencefinal
      from app
    )
    , with_next as
    (
      select
        userid,
        pagename,
        lead(pagename) over (partition by userid order by pagesequencefinal) as next_pagename
      from seq_fixed
    )
    select pagename, next_pagename, count(*)
    from with_next
    where next_pagename is not null
    group by pagename, next_pagename
    order by pagename, next_pagename;
    

    唯一的问题是:当用户昨天在 PageX 结束并在今天从 Home 开始时,这将被视为 PageX->Home。如果您想防止这种情况发生,您需要对这种情况进行一些检测,例如当条目的前身至少有 1 小时或类似的时间时,不要将条目视为页面更改。为此,您可以使用您的时间戳列和LAG

    【讨论】:

    • 感谢 Thorsten,让我对 LAG 函数进行一些研究,并尽快回复您!
    • 您好,Thorsten,我听取了您的建议,并尝试使用延迟和时间戳功能解决“昨天-今天”问题。看来我设法解决了它,但我的脚本超长。我将我的解决方案作为这篇文章的答案之一发布。有没有机会我可以简单地编写脚本?
    • 如果您扩展我的with_next 以选择DeviceDateTimeLAG(DeviceDateTime) OVER (partition by userid order by pagesequencefinal),您可以扩展主查询的WHERE 子句以仅考虑间隔小于1 小时的行.
    猜你喜欢
    • 1970-01-01
    • 2019-11-14
    • 2022-01-01
    • 1970-01-01
    • 2020-04-20
    • 2019-02-18
    • 2021-03-09
    • 1970-01-01
    • 2017-09-27
    相关资源
    最近更新 更多