【问题标题】:Count status Id for each day计算每天的状态 ID
【发布时间】:2019-05-16 15:44:04
【问题描述】:

情况:

我有三张桌子。表 1 有 ID 和订阅日期。表 2 包含 ID、活动状态和活动状态更改的最近日期。 表 3 包含 Id 和所有状态更改的日志。 注意: 在订阅日期,所有 ID 都处于活动状态。当一天内有多个状态变化时,选择最近的一个。

目标:

我需要计算出每天每种状态的 ID 数。 IE。每天有多少人活跃、不活跃和冒险。 我的问题是确保每天都计算 ID 的状态,即使在特定日期没有数据也是如此。例如: ID 1(见下面的小提琴)自 5 月 2 日(加入日期)以来一直处于活动状态,并且状态没有变化,所以到现在为止,他应该每天都被视为活动。

在其他地方咨询过这个问题后,有人提出创建函数和交叉应用并将计数存储在表中。我没有这样做的技能,但这是解决此问题的一种选择吗?

所需的输出:

+------------+----------+-------+
|    date    |  status  | count |
+------------+----------+-------+
| 1-May-2019 | active   |     0 |
| 1-May-2019 | inactive |     0 |
| 1-May-2019 | risky    |     1 |
| 2-May-2019 | active   |     1 |
| 2-May-2019 | inactive |     0 |
| 2-May-2019 | risky    |     1 |
| 3-May-2019 | active   |     1 |
| 3-May-2019 | inactive |     0 |
| 3-May-2019 | risky    |     1 |
| 4-May-2019 | active   |     1 |
| 4-May-2019 | inactive |     0 |
| 4-May-2019 | risky    |     1 |
| 5-May-2019 | active   |     3 |
| 5-May-2019 | inactive |     0 |
| 5-May-2019 | risky    |     1 |
| ...        | ...      |   ... |
+------------+----------+-------+

小提琴:

--create date table (not sure if usable)
CREATE TABLE #dates ([date] date)
DECLARE @dIncr DATE = '2019-05-01'
DECLARE @dEnd DATE = dateadd(day,-1,getdate())
WHILE (@dIncr <= @dEnd)
BEGIN
  INSERT INTO #dates ([date]) VALUES (@dIncr)
  SELECT @dIncr = DATEADD(day,1,@dIncr)
END
GO

-- ID + Subscribed Date (starts active at joindate)
create table #t1 (id int, [subdate] date)
insert into #t1 values 
(9, '2019-01-01'),
(1, '2019-05-02'),
(2, '2019-05-05'),
(3, '2019-05-05'),
(4, '2019-05-10')
GO

-- ID + Latest activity date
create table #t2 (id int, [status] varchar(max), [datestatus] date)
insert into #t2 values 
(9,'risky', '2019-03-01'),
(1, 'active', '2019-05-02'),
(2, 'inactive', '2019-05-13'),
(3, 'active', '2019-05-14'),
(4, 'risky', '2019-05-15')
GO

-- ID + Activity Logs Date
create table #t3 (id int, [statuschange] varchar(max), [datechange] date)
insert into #t3 values 
(9,'inactive', '2019-01-01'),
(9,'active', '2019-02-01'),
(9,'risky', '2019-03-01'),
(2, 'risky', '2019-05-08'),
(2, 'inactive', '2019-05-13'),
(3, 'inactive', '2019-05-08'),
(3, 'active', '2019-05-14'),
(4, 'inactive', '2019-05-15'),
(4, 'risky', '2019-05-15')
GO

我现在拥有的:

;with cte as (
    select 
        #t1.id
        ,COALESCE(LAG(datechange) over(partition by #t1.id order by datechange),subdate) as StartDate
        ,#t3.datechange
        ,COALESCE(LAG(statuschange) over(partition by #t1.id order by datechange),'active') as PreviousStatusChange
        ,#t3.statuschange
    from #t1
    inner join #t2 on #t1.id=#t2.id
    left join #t3 on #t1.id=#t3.id
) 

        select 
            cte.id
            ,cte.StartDate
            ,coalesce(cte.datechange,'2099-01-01') as EndDate
            ,PreviousStatusChange
            ,coalesce(statuschange,previousstatuschange) AS NewStatus
        from cte 

【问题讨论】:

  • 使用日历和计数表。
  • @Larnu 我的小提琴里有它。但是如果没有我无法弄清楚的值,它的逻辑就是每天强制计数
  • 您需要使用包含每个日期的日历表或计数表。问题的根源是您在查询中的基表需要每天都在其中。然后将每天的表加入到数据中,以便在计数为零时获得一行。
  • @Larnu 我看不出这个问题是如何重复的?我知道需要使用日期表。这里的问题是考虑状态变化。
  • 发布示例数据、期望结果和当前尝试的工作非常棒!

标签: sql sql-server


【解决方案1】:

日期表是解决此问题的正确方法。您需要种子数据来获得所需的输出。我打开了你的日期表,以便年长的订阅者填写。

我还添加了一个状态表,因为您的输出要求每个状态的每个日期都需要一行。

DROP TABLE IF EXISTS #dates
CREATE TABLE #dates ([date] date)
DECLARE @dIncr DATE = '01/01/2019'
DECLARE @dEnd DATE = dateadd(day,-1,getdate())
WHILE (@dIncr <= @dEnd)
BEGIN
  INSERT INTO #dates ([date]) VALUES (@dIncr)
  SELECT @dIncr = DATEADD(day,1,@dIncr)
END
GO

DROP TABLE IF EXISTS #status
CREATE TABLE #status (status varchar(20))
INSERT INTO #status VALUES
('active'),
('inactive'),
('risky')
GO

DROP TABLE IF EXISTS #t1
create table #t1 (id int, [subdate] date)
insert into #t1 values 
(9, '2019-01-01'),
(1, '2019-05-02'),
(2, '2019-05-05'),
(3, '2019-05-05'),
(4, '2019-05-10')
GO

DROP TABLE IF EXISTS #t2
create table #t2 (id int, [status] varchar(max), [datestatus] date)
insert into #t2 values 
(9,'risky', '2019-03-01'),
(1, 'active', '2019-05-02'),
(2, 'inactive', '2019-05-13'),
(3, 'active', '2019-05-14'),
(4, 'risky', '2019-05-15')
GO

DROP TABLE IF EXISTS #t3
create table #t3 (id int, [statuschange] varchar(max), [datechange] date)
insert into #t3 values 
(9,'inactive', '2019-01-01'),
(9,'active', '2019-02-01'),
(9,'risky', '2019-03-01'),
(2, 'risky', '2019-05-08'),
(2, 'inactive', '2019-05-13'),
(3, 'inactive', '2019-05-08'),
(3, 'active', '2019-05-14'),
(4, 'inactive', '2019-05-15'),
(4, 'risky', '2019-05-15')
GO

DECLARE
    @From DATE
    , @Thru DATE;

SET @From = '05/01/2019';
SET @Thru = '05/19/2019';

WITH
output_foundation AS
(
    SELECT date, status
    FROM #dates CROSS JOIN #status
)
, id_foundation AS
(
    SELECT DISTINCT id, date
    FROM #t1 CROSS JOIN #Dates
)
, id_stat AS
(
    SELECT id, datechange, statuschange FROM #t3
    UNION
    SELECT id, subdate, 'active' FROM #t1
    UNION
    SELECT id, datestatus, status FROM #t2
)
, id_spread AS
(
    SELECT
        IFDN.id
        , IFDN.date
        , IDS.statuschange
    FROM
        id_foundation AS IFDN
        LEFT OUTER JOIN id_stat AS IDS
            ON IFDN.id = IDS.id
                AND IFDN.date = IDS.datechange
), id_fill AS
(
    SELECT
        IDS.id
        , IDS.date
        , COALESCE(IDS.statuschange, LS.statuschange) AS statuschange
    FROM
        id_spread AS IDS
        OUTER APPLY
        (
            SELECT TOP 1 statuschange
            FROM id_spread
            WHERE id = IDS.id AND date < IDS.date AND statuschange IS NOT NULL
            ORDER BY date DESC
        ) AS LS
    WHERE
        (IDS.statuschange IS NOT NULL OR LS.statuschange IS NOT NULL)
)

SELECT
    OFDN.date
    , OFDN.status
    , COUNT(statuschange) AS count
FROM
    output_foundation AS OFDN
    LEFT OUTER JOIN id_fill AS IDF
        ON OFDN.date = IDF.date
            AND OFDN.status = IDF.statuschange
WHERE
    OFDN.date >= @From
    AND OFDN.date <= @Thru
GROUP BY
    OFDN.date
    , OFDN.status
ORDER BY
    OFDN.date
    , OFDN.status;

【讨论】:

  • 这看起来很棒!您省略了 #t2 但它是一个关键组件。在#t3 的实际数据库中,仅导入了过去一年的数据。因此,如果在此之前特定 id 有任何状态更改,它将被视为活动的(根据您的代码)。这就是 #t2 存在的原因,它允许获取在 #t3 中没有状态日志的 id 的最新状态
  • 我应该联合到 #t2 并将 UNION ALL 替换为 UNION 吗?
  • @RogerSteinberg 更新了答案。我省略了表 2,因为表 3 已经有相同的数据。根据您刚刚概述的情况,您是正确的。 id_stat CTE 中的 UNION 表 2。
【解决方案2】:

下面的查询可能会对您有所帮助。我不确定它是否与您想要的结果相同,因为您提到了与您在临时表查询中提供的示例数据不匹配的所需输出。 目前我正在考虑像你需要每天的每个状态的交换和到现在。

SELECT 
R.date
,R.status
,SUM (StausValue) OVER (PARTITION BY [status] ORDER BY date) AS Count
FROM 
(
SELECT Q.* , CASE WHEN T3.datechange IS NOT NULL THEN 1 ELSE 0 END as StausValue FROM (
select D.Date, t2.[status] from #dates  D
CROSS JOIN (SELECT DISTINCT [status] FROM #t2 )t2
)Q
LEFT JOIN #T3 T3 ON T3.[statuschange]=Q.status AND T3.[datechange]=Q.Date
)R order by Date asc, Status ASC

【讨论】:

    【解决方案3】:

    在我看来,您需要在解决方案中再添加两件事:

    1. 将包含一整天的表,称为Dimdate。我会轻松获得最终结果。)
    2. 具有每个 id (slowly changing dimension type 2) 的状态历史记录的表,即:

    您可以在下面找到示例解决方案。这不是一个理想的,但我想让你大致了解它是如何工作的。例如,当一个 Id 在一天内两次更改状态时,我没有解决一个案例。基本上,我为每个 ID 创建了一个 SC2 表,DimDate 并将它们连接在一起

    --initial insert for new subscribers (they begin as active)
    drop table if exists #t4
    create table #t4 (id int, [Status] varchar(20), OpenDate date, CloseDate date, IsCurrent int)
    insert into #t4(id, [Status], OpenDate, CloseDate, IsCurrent)
    select
         id
        ,'active'
        ,[subdate]
        ,'9999-12-31' --we don't know CloseDate for this version
        ,1
    from #t1
    
    declare @i date = '2019-01-01'
    --filing versions till 2019-05-15
    while @i < '2019-05-15'
    begin
    
    update t4
    set t4.CloseDate = case when  t4.OpenDate = @i then @i else  dateadd(day,-1, @i) end--avoiding overlapping versions
        ,t4.IsCurrent = 0 -- there can only one version that is current
    from #t4 as t4
    join #t3 as t3
        on t3.id = t4.id
        and t4.IsCurrent = 1
    where t3.[datechange] = @i
    --inserting a new version
    insert into #t4(id, [Status], OpenDate, CloseDate, IsCurrent)
    select
         t3.id
        ,t3.statuschange
        ,t3.datechange
        ,'9999-12-31'
        ,1 --the newiest version
    from #t3 as t3
    where t3.[datechange] = @i
    
    set @i = DATEADD(day, 1, @i)
    
    end
    
    --populating an examplary DimDate
    drop table if exists #DimDate
    create table #DimDate (
    dat date,
    dateFormatted as FORMAT(dat, 'dd-MMM-yyyy')
    )
    
    set @i = '2019-01-01'
    
    while @i < '2019-06-01'
    begin
        insert into #DimDate(dat)
        select @i
    
        set @i = DATEADD(day, 1, @i)
    end
    
    --final result
    select
         d.dateFormatted
        ,v.statuses
        ,count(t4.Status) as [count]
    from #DimDate as d
    cross join (values ('inactive'), ('active'), ('risky')) as v(statuses)
    left join #t4 as t4
        on v.statuses = t4.Status
        and d.dat between t4.OpenDate and t4.CloseDate
    group by 
         d.dateFormatted
        ,v.statuses
        ,d.dat
    order by d.dat
    

    【讨论】:

      【解决方案4】:

      我省略了这部分“当一天有多个状态变化时,最近的一个是可以选择的。”您将需要找到一种方法来选择一天的最后状态,以您当前的设计这是不可能的,也许如果您在 #t3 上添加时间列或增量 id .... 它对我有用...,请复制整个代码并重试。

      --create date table (not sure if usable)
      CREATE TABLE #dates ([date] date)
      DECLARE @dIncr DATE = '2019-05-01'
      DECLARE @dEnd DATE = dateadd(day,-1,getdate())
      WHILE (@dIncr <= @dEnd)
      BEGIN
        INSERT INTO #dates ([date]) VALUES (@dIncr)
        SELECT @dIncr = DATEADD(day,1,@dIncr)
      END
      GO
      
      -- ID + Subscribed Date (starts active at joindate)
      create  table #t1 (id int, [subdate] date)
      insert into #t1 values 
      (9, '2019-01-01'),
      (1, '2019-05-02'),
      (2, '2019-05-05'),
      (3, '2019-05-05'),
      (4, '2019-05-10')
      GO
      
      -- ID + Latest activity date
      /*create  table #t2 (id int, [status] int, [datestatus] date)
      insert into #t2 values 
      (9,'risky', '2019-03-01'),
      (1, 1, '2019-05-02'),
      (2, 'inactive', '2019-05-13'),
      (3, 'active', '2019-05-14'),
      (4, 'risky', '2019-05-15')
      GO*/
      
      -- ID + Activity Logs Date
      create  table #t3 (id int, [statuschange] int, [datechange] date)
      insert into #t3 values 
      (9,2, '2019-01-01'),
      (9,1, '2019-02-01'),
      (9,3, '2019-03-01'),
      (2, 3, '2019-05-08'),
      (2, 2, '2019-05-13'),
      (3, 2, '2019-05-08'),
      (3, 1, '2019-05-14'),
      (4, 2, '2019-05-15'),
      (4, 3, '2019-05-15')
      GO
      ---Status Table
      create table #t4 (id int, [status] varchar(max))
      
      insert into #t4 values 
      (1, 'active'),
      (2,'inactive'),
      (3,'risky')
      
      
      
      
      ;WITH unionall AS--- join data from t1 and t3
      (SELECT id 
      ,1 as [statuschange]--starts active at joindate
      , [subdate] as datechange
      
      FROM #t1
      union ALL
      SELECT id , [statuschange] , [datechange] 
      FROM #t3
      ),
       userstatuslog as(
      SELECT id, [statuschange],datechange as beginingdate
      ,COALESCE( DATEADD(DAY,-1, lead(datechange) OVER(PARTITION BY id ORDER BY [datechange])), getdate()) as enddate
      from unionall 
      )
      ,datestatus as(
      SELECT  
      id, statuschange,   beginingdate,   enddate,    [date]
      ,case WHEN [date]< beginingdate then 0 
      WHEN [date]>=beginingdate AND [date]<=enddate then statuschange
      END as newstatus
      FROM userstatuslog CROSS JOIN #dates)
      ,crossjoin as (
      SELECT [date],id
      from #dates CROSS join #t4
      
      )
      
      ,removenulls as (
      SELECT *
      FROM  datestatus
      where newstatus is NOT NULL AND newstatus<>0
      
      )
      
      SELECT 
      crossjoin.date,crossjoin.id, sum(case when newstatus is null then 0 else 1 end) 
       FROM crossjoin  left join datestatus on crossjoin.date=datestatus.date AND crossjoin.id=newstatus
      GROUP BY crossjoin.date,crossjoin.id
      ORDER BY crossjoin.date,crossjoin.id
      

      【讨论】:

      • 运行代码时出现问题:转换 varchar 值时转换失败,我认为它在您的 unionall 中
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多