【问题标题】:Build timeline from start and end dates从开始日期和结束日期构建时间表
【发布时间】:2021-06-18 19:08:06
【问题描述】:

我有一个包含用户 ID订阅开始日期订阅结束日期的订阅表。我还有一个带有 dateamp 字段的日历表,即从我的订阅表中的第一个订阅日期开始的每个日期。

我正在尝试写一些东西,它会给我一个包含日期列和三个数字的表格:总活跃人数(当天)、新订阅者数量、退订者数量。

(注意,我尝试使用建议的 GitHub Flavored Markdown 插入示例表,但它只是全部放在一行中。)

目前我正在使用在两个表之间创建多个连接的查询,每个数字一个:

select a.datestamp
,count(distinct case when b_sub.UserID is not null then b_sub.UserID end) as total_w_subscription
,count(distinct case when b_in.UserID is not null then b_in.UserID end) as total_subscribed
,count(distinct case when b_out.UserID is not null then b_out.UserID end) as total_unsubscribed

from Calendar as a

left join Subscription as b_sub -- all those with subscription on given date
on b_sub.sub_dt <= a.datestamp
and (b_sub.unsub_dt > a.datestamp or b_sub.unsub_dt is null)

left join Subscription as b_in -- all those that subscribed on given date
on b_in.sub_dt = a.datestamp

left join Subscription as b_out -- all those that unsubscribed on given date
on b_out.unsub_dt = a.datestamp

where a.datestamp > '2021-06-10'

group by a.datestamp
order by datestamp asc
;

我已为两个表中的日期字段编制索引。如果我只看一天,它会在 3 秒内运行。两天时间已经很漫长了。 Sub 表有超过 260 万条记录,理想情况下,我需要在 2012 年的某个时间开始我的时间表。

什么是最省时的方法?

【问题讨论】:

    标签: sql-server date subscription timeline


    【解决方案1】:

    你在正确的轨道上。我创建了一些表变量,并假设每个订阅都包含一个开始和结束日期的数据结构。

    --Create @dates table variable for calendar
    DECLARE @startDate DATETIME = '2018-01-01'
    DECLARE @endDate DATETIME = '2021-06-18'
    DECLARE @dates TABLE
    (
    reportingdate DATETIME
    )
    WHILE @startDate <= @endDate
    BEGIN
        INSERT INTO @dates SELECT @startDate
        SET @startDate += 1
    END
    
    --Create @subscriptions table variable for subcriptions to join onto calendar
    DECLARE @subscriptions TABLE
    (
    id INT
    ,startDate DATETIME
    ,endDate DATETIME
    )
    INSERT INTO @subscriptions
    VALUES
    (1,'2018-01-01 00:00:00.000','2019-10-07 00:00:00.000')
    ,(2,'2018-01-11 00:00:00.000','2019-12-21 00:00:00.000')
    ,(3,'2019-04-21 00:00:00.000','2020-03-19 00:00:00.000')
    ,(4,'2019-12-09 00:00:00.000','2020-05-14 00:00:00.000')
    ,(5,'2020-04-26 00:00:00.000','2020-07-06 00:00:00.000')
    ,(6,'2020-05-02 00:00:00.000',NULL)
    ,(7,'2020-08-31 00:00:00.000','2020-10-29 00:00:00.000')
    ,(8,'2020-12-13 00:00:00.000','2021-01-13 00:00:00.000')
    ,(9,'2021-02-12 00:00:00.000','2021-04-19 00:00:00.000')
    ,(10,'2021-06-10 00:00:00.000',NULL)
    ;
    

    然后我将订阅加入到日历表中。

    --CTE to join subscription onto calendar and use ROW_NUMBER functions
    WITH cte AS (
    SELECT
        s.id AS SubID
        ,d.ReportingDate
        ,ROW_NUMBER() OVER (PARTITION BY s.id ORDER BY d.ReportingDate) AS asc_rn   --used to identify 1st
        ,ROW_NUMBER() OVER (PARTITION BY s.id ORDER BY d.ReportingDate DESC) AS desc_rn --used to identify last
        ,CASE WHEN s.endDate IS NULL THEN 1 ELSE 0 END AS ActiveSub
    FROM @subscriptions s
    LEFT JOIN @dates d ON
        d.reportingdate BETWEEN s.startDate AND ISNULL(s.endDate,'9999-12-31')
        )
    

    我使用 ROW_NUMBER 来识别订阅的第一个和最后一个日期行,并检查订阅 endDate 是否为 NULL(仍然有效)。然后我查询 CTE 以计算按天分组的订阅,以及按天分组的新订阅和终止订阅。

    --Query CTE using asc_rn, desc_rn, and ActiveSub to identify new subscribers and unsubscribers.
    SELECT
        ReportingDate
        ,COUNT(*) AS TotalSubscribers
        ,SUM(CASE WHEN asc_rn = 1 THEN 1 ELSE 0 END) AS NewSubscribers
        ,SUM(CASE WHEN desc_rn = 1 AND ActiveSub = 0 THEN 1 ELSE 0 END) AS UnSubscribers
    FROM cte
    GROUP BY ReportingDate
    ORDER BY ReportingDate
    

    【讨论】:

    • 谢谢你,这真是太棒了!我已经用 10 万条记录尝试过它,它在不到 10 分钟的时间内运行,只是用完整的 2.6M 运行它。我也尝试过没有将表作为变量(即只是在表上运行它)并且从未完成,所以我猜是写入内存的临时表使它滴答得如此之快,对吧?
    • 从内存中读取总是有帮助的,如果您只需要日期,日历表变量是一个廉价且可重复的过程,但如果您最终可能使用日历表中的其他字段(会计年度/季度等...)您可能希望使用表来分析执行计划,以通过缺失的索引来寻找改进。如果我不得不猜测,查询可能会受益于订阅表上的索引,该索引使用包括 ID 在内的开始和停止日期。
    • 我会的!我还将它更改为在聚合上运行,这将运行时间从 4 小时以上减少到 1.5 小时,这太棒了!再次感谢您。