【问题标题】:Splitting up group by with relevant aggregates beyond the basic ones?用基本聚合之外的相关聚合拆分分组?
【发布时间】:2017-07-19 21:06:55
【问题描述】:

我不确定以前是否有人问过这个问题,因为我自己也很难问。我认为解释我的困境的最好方法是举一个例子。

假设我 10 年来每天以 1 到 10 的等级对我的幸福进行评分,我将结果放在一个大表中,其中有一个日期对应于我的幸福评分的单个整数值。不过,我说,我只关心平均超过 60 天的幸福感(这可能看起来很奇怪,但这是一个简化的例子)。因此,我将这些信息汇总到一个表中,其中我现在有一个开始日期字段、一个结束日期字段和一个平均评分字段,其中开始日期是所有 10 年中从第一天到最后一天的每一天,但结束日期正好在 60 天后。需要明确的是,这 60 天是重叠的(一个与下一个共享 59 天,与下一个共享 58 天,依此类推)。

接下来我选择一个阈值评级,比如 5,我想将低于它的所有内容归为“差”类别,将高于该类别的所有内容归为“好”类别。我可以轻松地添加另一个字段并使用案例结构为每 60 天的范围指定一个“好”或“坏”标志。

然后总结一下,我想显示从最大开始日期到最大结束日期的“好”和“坏”的总时间段。这就是我卡住的地方。我可以按好/坏类别分组,然后只取最小值(开始日期)和最大值(结束日期),但是如果范围从好到坏再到好再到坏,输出将显示重叠范围好的和坏的。在上述情况下,我想展示四个不同的范围。

我意识到这对我来说可能比其他人更清楚,所以如果您需要澄清,请询问。

谢谢

---编辑---

以下是之前的示例:

开始日期|结束日期|情绪评级
------------+------------+------------
1991 年 1 月 1 日 |1991 年 3 月 1 日 | 7
1991 年 1 月 2 日 |1991 年 3 月 2 日 | 7
1991 年 1 月 3 日 |1991 年 3 月 3 日 | 4
1991 年 1 月 4 日 |1991 年 3 月 4 日 | 4
1991 年 1 月 5 日 |1991 年 3 月 5 日 | 7
1991 年 1 月 6 日 |1991 年 3 月 6 日 | 7
1991 年 1 月 7 日 |1991 年 3 月 7 日 | 4
1991 年 1 月 8 日 |1991 年 3 月 8 日 | 4
1991 年 1 月 9 日 |1991 年 3 月 9 日 | 4

之后:

最小开始|最大结束 |好/坏
------------+------------+----------
1991 年 1 月 1 日|1991 年 3 月 2 日 |好
1991 年 1 月 3 日|1991 年 3 月 4 日 |不好
1991 年 1 月 5 日|1991 年 3 月 6 日 |好
1991 年 1 月 7 日|1991 年 3 月 9 日 |不好

目前我对按评级分组的查询将显示:

最小开始|最大结束 |好/坏
------------+------------+----------
1991 年 1 月 1 日|1991 年 3 月 6 日 |好
1991 年 1 月 3 日|1991 年 3 月 9 日 |不好

这有点像

选择 min(StartDate), max(EndDate), Good_Bad
来自源表
由 Good_Bad 分组

【问题讨论】:

  • 这是一个很好的问题开始。请添加一些示例数据 - 以及您期望的结果。这将帮助每个人准确理解您的要求。
  • 添加了一个编辑@stan
  • 另外,请分享您正在使用的查询。
  • 嗯 - 我可以使用 CURSOR 来做到这一点,但必须有更好的方法。好问题!
  • 为什么要使用光标?除非我想错了问题,否则使用基于集合的解决方案很容易。

标签: sql sql-server group-by


【解决方案1】:

虽然 Jason A Long 的答案可能是正确的 - 我无法阅读或弄清楚,所以我想我会发布自己的答案。假设这不是您要持续运行的进程,CURSOR 的性能影响应该无关紧要。但是(至少对我来说)这个解决方案可读性很强,可以很容易地修改。

简而言之 - 我们将源表中的第一条记录插入到结果表中。接下来,我们抓取下一条记录,看看情绪得分是否与上一条记录相同。如果是,我们只需用当前记录的结束日期更新先前记录的结束日期(扩展范围)。如果没有,我们插入一条新记录。冲洗,重复。很简单。

这是您的设置和一些示例数据:

DECLARE @MoodRanges TABLE (StartDate DATE, EndDate DATE, MoodRating int)

INSERT INTO @MoodRanges
VALUES
('1/1/1991','3/1/1991', 7),
('1/2/1991','3/2/1991', 7),
('1/3/1991','3/3/1991', 4),
('1/4/1991','3/4/1991', 4),
('1/5/1991','3/5/1991', 7),
('1/6/1991','3/6/1991', 7),
('1/7/1991','3/7/1991', 4),
('1/8/1991','3/8/1991', 4),
('1/9/1991','3/9/1991', 4)

接下来,我们可以创建一个表格来存储我们的结果,以及一些用于光标的变量占位符:

DECLARE @MoodResults TABLE(ID INT IDENTITY(1, 1), StartDate DATE, EndDate DATE, MoodScore varchar(50))
DECLARE @CurrentStartDate DATE, @CurrentEndDate DATE, @CurrentMoodScore INT, 
        @PreviousStartDate DATE, @PreviousEndDate DATE, @PreviousMoodScore INT

现在我们将所有样本数据放入我们的 CURSOR 中:

DECLARE MoodCursor CURSOR FOR
SELECT StartDate, EndDate, MoodRating
FROM @MoodRanges

OPEN MoodCursor
FETCH NEXT FROM MoodCursor INTO @CurrentStartDate, @CurrentEndDate, @CurrentMoodScore

WHILE @@FETCH_STATUS = 0
    BEGIN

    IF @PreviousStartDate IS NOT NULL 
        BEGIN

        IF (@PreviousMoodScore >= 5 AND @CurrentMoodScore >= 5)
        OR  (@PreviousMoodScore < 5 AND @CurrentMoodScore < 5)
            BEGIN
                UPDATE @MoodResults
                SET EndDate = @CurrentEndDate
                WHERE ID = (SELECT MAX(ID) FROM @MoodResults)
            END
        ELSE
            BEGIN
                INSERT INTO 
                @MoodResults
                VALUES
                (@CurrentStartDate, @CurrentEndDate, CASE WHEN @CurrentMoodScore >= 5 THEN 'GOOD' ELSE 'BAD' END)
            END
        END
    ELSE
        BEGIN
            INSERT INTO 
            @MoodResults
            VALUES
            (@CurrentStartDate, @CurrentEndDate, CASE WHEN @CurrentMoodScore >= 5 THEN 'GOOD' ELSE 'BAD' END)
        END


    SET @PreviousStartDate = @CurrentStartDate
    SET @PreviousEndDate = @CurrentEndDate
    SET @PreviousMoodScore = @CurrentMoodScore

    FETCH NEXT FROM MoodCursor INTO @CurrentStartDate, @CurrentEndDate, @CurrentMoodScore
    END

CLOSE MoodCursor
DEALLOCATE MoodCursor

结果如下:

SELECT * FROM @MoodResults

ID          StartDate  EndDate    MoodScore
----------- ---------- ---------- --------------------------------------------------
1           1991-01-01 1991-03-02 GOOD
2           1991-01-03 1991-03-04 BAD
3           1991-01-05 1991-03-06 GOOD
4           1991-01-07 1991-03-09 BAD

【讨论】:

  • 虽然 Jason Longs 也有效,但我发现这对我来说更直观一些。感谢你们俩的帮助
  • @DougMacArthur 另外,您可以将条件逻辑合并到一个 if/else 语句中,但我不知道您对此有多熟悉,所以我使用了这种格式。
【解决方案2】:

这就是你要找的吗?

IF OBJECT_ID('tempdb..#MyDailyMood', 'U') IS NOT NULL 
DROP TABLE #MyDailyMood;

CREATE TABLE #MyDailyMood (
    TheDate DATE NOT NULL,
    MoodLevel INT NOT NULL 
    );

WITH 
    cte_n1 (n) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (n)), 
    cte_n2 (n) AS (SELECT 1 FROM cte_n1 a CROSS JOIN cte_n1 b),
    cte_n3 (n) AS (SELECT 1 FROM cte_n2 a CROSS JOIN cte_n2 b),
    cte_Calendar (dt) AS (
        SELECT TOP (DATEDIFF(dd, '2007-01-01', '2017-01-01'))
            DATEADD(dd, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1, '2007-01-01')
        FROM
            cte_n3 a CROSS JOIN cte_n3 b
        )
INSERT #MyDailyMood (TheDate, MoodLevel)  
SELECT 
    c.dt,
    ABS(CHECKSUM(NEWID()) % 10) + 1
FROM
    cte_Calendar c;

--==========================================================

WITH 
    cte_AddRN AS (
        SELECT 
            *,
            RN = ISNULL(NULLIF(ROW_NUMBER() OVER (ORDER BY mdm.TheDate) % 60, 0), 60)
        FROM
            #MyDailyMood mdm
        ),
    cte_AssignGroups AS (
        SELECT 
            *,
            DateGroup = DENSE_RANK() OVER (PARTITION BY arn.RN ORDER BY arn.TheDate)
        FROM
            cte_AddRN arn
        )
SELECT 
    BegOfRange = MIN(ag.TheDate),
    EndOfRange = MAX(ag.TheDate),
    AverageMoodLevel = AVG(ag.MoodLevel),
    CASE WHEN AVG(ag.MoodLevel) >= 5 THEN 'Good' ELSE 'Bad' END 
FROM
    cte_AssignGroups ag
GROUP BY 
    ag.DateGroup;

发布 OP 更新解决方案...

WITH 
    cte_AddRN AS (  -- Add a row number to each row that resets to 1 ever 60 rows.
        SELECT 
            *,
            RN = ISNULL(NULLIF(ROW_NUMBER() OVER (ORDER BY mdm.TheDate) % 60, 0), 60)
        FROM
            #MyDailyMood mdm
        ),
    cte_AssignGroups AS (   -- Use DENSE_RANK to create groups based on the RN added above.
                            -- How it works: RN set the row number 1 - 60 then repeats itself
                            -- but we dont want ever 60th row grouped together. We want blocks of 60 consecutive rows grouped together
                            -- DENSE_RANK accompolishes this by ranking within all the "1's", "2's"... and so on.
                            -- verify with the following query... SELECT * FROM cte_AssignGroups ag ORDER BY ag.TheDate
        SELECT 
            *,
            DateGroup = DENSE_RANK() OVER (PARTITION BY arn.RN ORDER BY arn.TheDate)
        FROM
            cte_AddRN arn
        ),
    cte_AggRange AS (   -- This is just a straight forward aggregation/rollup. It produces the results similar to the sample data you posed in your edit.
        SELECT 
            BegOfRange = MIN(ag.TheDate),
            EndOfRange = MAX(ag.TheDate),
            AverageMoodLevel = AVG(ag.MoodLevel),
            GorB = CASE WHEN AVG(ag.MoodLevel) >= 5 THEN 'Good' ELSE 'Bad' END,
            ag.DateGroup
        FROM
            cte_AssignGroups ag
        GROUP BY 
            ag.DateGroup
        ),
    cte_CompactGroup AS (   -- This time we're using dense rank to group all of the consecutive "Good" and "Bad" values so that they can be further aggregated below.
        SELECT 
            ar.BegOfRange, ar.EndOfRange, ar.AverageMoodLevel, ar.GorB, ar.DateGroup,
            DenseGroup = ar.DateGroup - DENSE_RANK() OVER (PARTITION BY ar.GorB ORDER BY ar.BegOfRange)
        FROM
            cte_AggRange ar
        )
-- The final aggregation step...
SELECT 
    BegOfRange = MIN(cg.BegOfRange),
    EndOfRange = MAX(cg.EndOfRange),
    cg.GorB
FROM
    cte_CompactGroup cg
GROUP BY 
    cg.DenseGroup,
    cg.GorB
ORDER BY 
    BegOfRange;

【讨论】:

  • 这似乎确实有效,但我仍在尝试弄清楚如何。它似乎取决于dense_rank(),那是什么?
  • docs.microsoft.com/en-us/sql/t-sql/functions/… 这只是创建分组分区的一种手段。给我几分钟,我将报告带有 som 内联 cmets 的第二个代码块,以帮助解释每个 CTE 级别发生的情况...
  • 已添加内联 cmets。注意:检查每个 CTE 的结果可以更容易地了解他们每个人在做什么。如果您仍有疑问,请告诉我。
  • SELECT TOP (DATEDIFF(dd, '2007-01-01', '2017-01-01'))的目的是什么??那不会总是评估为 3653 吗?为什么不直接说SELECT TOP 3653
  • 你是对的。我这样编码是为了清楚地表明我正在创建 10 年的约会。是的,我可以硬编码 3653 并使用内联注释...但我没有...
猜你喜欢
  • 2019-12-25
  • 1970-01-01
  • 2014-03-21
  • 2012-09-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-16
相关资源
最近更新 更多