T/SQL - 分组/多条记录

Posted

技术标签:

【中文标题】T/SQL - 分组/多条记录【英文标题】:T/SQL - Group/Multiply records 【发布时间】:2016-04-12 14:34:41 【问题描述】:

来源日期:

CREATE TABLE #Temp (ID INT Identity(1,1) Primary Key, BeginDate datetime, EndDate datetime, GroupBy INT)
INSERT INTO #Temp
SELECT '2015-06-05 00:00:00.000','2015-06-12 00:00:00.000',7
UNION
SELECT '2015-06-05 00:00:00.000', '2015-06-08 00:00:00.000',7
UNION
SELECT '2015-10-22 00:00:00.000', '2015-10-31 00:00:00.000',7

SELECT *, DATEDIFF(DAY,BeginDate, EndDate) TotalDays FROM #Temp
DROP TABLE #Temp

ID  BeginDate       EndDate         GroupBy     TotalDays
1   6/5/15 0:00     6/8/15 0:00     7           3
2   6/5/15 0:00     6/12/15 0:00    7           7
3   10/22/15 0:00   10/31/15 0:00   7           9

期望的输出:

ID  BeginDate       EndDate         GroupBy TotalDays   GroupCnt    GroupNum
1   6/5/15 0:00     6/8/15 0:00     7       3           1           1
2   6/5/15 0:00     6/12/15 0:00    7       7           1           1
3   10/22/15 0:00   10/29/15 0:00   7       9           2           1
3   10/29/15 0:00   10/31/15 0:00   7       9           2           2

目标:

根据ID/BeginDate/EndDate 对记录进行分组。 基于 GroupBy number (# of days)TotalDays (days diff), 如果 GroupBy => TotalDays,则保留单个组记录 否则将组记录(每个 GroupBy 计数 1 条记录)相乘,同时保持在 TotalDays 限制内。

抱歉,如果它令人困惑,但基本上,在上面的示例中,每个组应该有一个记录 (ID/BeginDate/EndDate) 用于天差异 b/w Begin/End date = 7 or less (GroupBy) 的记录。

如果天数差异超过 7 天,则创建另一条记录(每增加 7 天差异)。

因此,由于第一条两条记录的天数相差 7 天或更短,因此只有一条记录。

第三条记录的天数差异为 9 (7 + 2)。因此,应该有 2 条记录(前 7 天第 1 条,后 2 天第 2 条)。

GroupCNT = how many records there're of the grouped records after applying the above records.

GroupNum 基本上是群组的row number

GroupBy # 每条记录可以不同。数据集很大,所以性能很重要。

我能够弄清楚的一个模式与模数 b/w GroupBy 和天数差异有关。

GroupBy value is < days diff时,模数总是小于GroupBy。当GroupBy value = days diff 时,模数始终为0。当GroupBy value > days diff 时,模数始终等于GroupBy。我不确定是否/如何使用它来分组/乘以记录以满足要求。

SELECT DISTINCT
  ID
, BeginDate
, EndDate
, GroupBy
, DATEDIFF(DAY,BeginDate, EndDate) TotalDays
, CAST(GroupBy as decimal(18,6))%CAST(DATEDIFF(DAY,BeginDate, EndDate) AS decimal(18,6)) Modulus
, CASE WHEN DATEDIFF(DAY,BeginDate, EndDate) <= GroupBy THEN BeginDate END NewBeginDate
, CASE WHEN DATEDIFF(DAY,BeginDate, EndDate) <= GroupBy THEN EndDate END NewEndDate
FROM #Temp

更新: 忘记提及/包括开始/结束日期,当记录成倍增加时,将相应地改变。换句话说,开始/结束日期将反映 GroupBy - 所需的输出在第三和第四条记录中更清楚地显示了我的意思。 此外,GroupCnt/GroupNum 的计算不如对记录的分组/相乘重要。

【问题讨论】:

【参考方案1】:

您可以使用递归 CTE 执行类似的操作..

;WITH cte AS (
    SELECT  ID,
            BeginDate,
            EndDate,
            GroupBy, 
            DATEDIFF(DAY, BeginDate, EndDate) AS TotalDays, 
            1 AS GroupNum 
    FROM    #Temp
    UNION ALL 
    SELECT  ID,
            BeginDate,
            EndDate,
            GroupBy,
            TotalDays,
            GroupNum + 1
    FROM    cte
    WHERE   GroupNum * GroupBy < TotalDays
)
SELECT  ID,
        BeginDate = CASE WHEN GroupNum = 1 THEN BeginDate 
                         ELSE DATEADD(DAY, GroupBy * (GroupNum - 1), BeginDate)
                         END ,
        EndDate   = CASE WHEN TotalDays <= GroupBy THEN EndDate
                         WHEN DATEADD(DAY, GroupBy * GroupNum, BeginDate) > EndDate THEN EndDate
                         ELSE DATEADD(DAY, GroupBy * GroupNum, BeginDate)
                         END ,
        GroupBy,
        TotalDays,
        COUNT(*) OVER (PARTITION BY ID) GroupCnt,
        GroupNum
FROM    cte
OPTION (MAXRECURSION 0)

cte 构建了一个这样的记录集。

ID          BeginDate               EndDate                 GroupBy     TotalDays   GroupNum
----------- ----------------------- ----------------------- ----------- ----------- -----------
1           2015-06-05 00:00:00.000 2015-06-08 00:00:00.000 7           3           1
2           2015-06-05 00:00:00.000 2015-06-12 00:00:00.000 7           7           1
3           2015-10-22 00:00:00.000 2015-10-31 00:00:00.000 7           9           1
3           2015-10-22 00:00:00.000 2015-10-31 00:00:00.000 7           9           2

那么你只需要接受这个并使用一些案例语句来确定开始日期和结束日期应该是什么。

你最终应该得到

ID          BeginDate               EndDate                 GroupBy     TotalDays   GroupCnt    GroupNum
----------- ----------------------- ----------------------- ----------- ----------- ----------- -----------
1           2015-06-05 00:00:00.000 2015-06-08 00:00:00.000 7           3           1           1
2           2015-06-05 00:00:00.000 2015-06-12 00:00:00.000 7           7           1           1
3           2015-10-22 00:00:00.000 2015-10-29 00:00:00.000 7           9           2           1
3           2015-10-29 00:00:00.000 2015-10-31 00:00:00.000 7           9           2           2

由于您使用的是 SQL 2012,因此您还可以在最终查询中使用 LAG 和 LEAD 函数。

;WITH cte AS (
    SELECT  ID,
            BeginDate,
            EndDate,
            GroupBy, 
            DATEDIFF(DAY, BeginDate, EndDate) AS TotalDays, 
            1 AS GroupNum 
    FROM    #Temp
    UNION ALL 
    SELECT  ID,
            BeginDate,
            EndDate,
            GroupBy,
            TotalDays,
            GroupNum + 1
    FROM    cte
    WHERE   GroupNum * GroupBy < TotalDays
)

SELECT  ID,
        BeginDate = COALESCE(LAG(BeginDate) OVER (PARTITION BY ID ORDER BY GroupNum) + GroupBy * (GroupNum - 1), BeginDate),
        EndDate   = COALESCE(LEAD(BeginDate) OVER (PARTITION BY ID ORDER BY GroupNum) + GroupBy * GroupNum, EndDate),
        GroupBy,
        TotalDays,
        COUNT(*) OVER (PARTITION BY ID) GroupCnt,
        GroupNum
FROM    cte
OPTION (MAXRECURSION 0)

【讨论】:

第一个解决方案非常简洁。谢谢! - 现在我需要理解它......哈哈。至于第二个解决方案,我得到 EndDate = BeginDate。你也一样吗?【参考方案2】:
CREATE TABLE dim_number (id INT);
INSERT INTO dim_number VALUES ((0), (1), (2), (3)); -- Populate this to a large number

SELECT
    #Temp.Id,
    CASE WHEN dim_number.id = 0
         THEN #Temp.BeginDate
         ELSE DATEADD(DAY,  dim_number.id      * #Temp.GroupBy, #Temp.BeginDate)
    END                                                             AS BeginDate,
    CASE WHEN dim_number.id = parts.count
         THEN #Temp.EndDate
         ELSE DATEADD(DAY, (dim_number.id + 1) * #Temp.GroupBy, #Temp.BeginDate)
    END                                                             AS EndDate,
    #Temp.GroupBy                                                   AS GroupBy,
    DATEDIFF(DAY, #Temp.BeginDate, #Temp.EndDate)                   AS TotalDays,
    parts.count + 1                                                 AS GroupCnt,
    dim_number.id + 1                                               AS GroupNum
FROM
    #Temp
CROSS APPLY
    (SELECT DATEDIFF(DAY, #Temp.BeginDate, #Temp.EndDate) / #Temp.GroupBy AS count)   AS parts
INNER JOIN
    dim_number
        ON  dim_number.id >= 0
        AND dim_number.id <= parts.count

【讨论】:

感谢@Mattballie 提供的解决方案……尽管我必须说,我需要一段时间才能理解这里发生了什么。 :)

以上是关于T/SQL - 分组/多条记录的主要内容,如果未能解决你的问题,请参考以下文章

选择日期范围,根据关闭条件计算范围内的多条记录,按班次分组

mysql关联表分组查询多条数据

sql如何取group by 分组的多条记录只取最上面的一条!

sql语句,合并多条记录中的相同字段。

利用SQL语句产生分组序号

SQL实现分组查询取前几条记录