【问题标题】:Is there an established pattern for SQL queries which group by a range?是否有按范围分组的 SQL 查询的既定模式?
【发布时间】:2012-07-16 14:50:21
【问题描述】:

我在 SO 上看到了 lot of questions,关于如何在 SQL 查询中按范围对数据进行分组。

具体情况各不相同,但每种情况的一般潜在问题是按一系列值而不是GROUP BY 列中的每个离散值进行分组。换句话说,按比您存储在数据库表中的粒度更精确的粒度进行分组。

这在现实世界中经常出现在生成直方图、日历表示、数据透视表和其他定制报告输出等内容时。

一些示例数据(不相关的表格):

|      OrderHistory       |       |         Staff        |                
---------------------------       ------------------------
|    Date    |  Quantity  |       |   Age     |   Name   |
---------------------------       ------------------------       
|01-Jul-2012 |     2      |       |    19     |   Barry  |
|02-Jul-2012 |     5      |       |    53     |   Nigel  |
|08-Jul-2012 |     1      |       |    29     |   Donna  |
|10-Jul-2012 |     3      |       |    26     |   James  |
|14-Jul-2012 |     4      |       |    44     |   Helen  |
|17-Jul-2012 |     2      |       |    49     |   Wendy  |
|28-Jul-2012 |     6      |       |    62     |   Terry  |
---------------------------       ------------------------

现在假设我们要使用OrderHistory 表的Date 列按周分组,即7 天范围。或者也许将Staff 分组为 10 岁年龄段:

|       Week      |  QtyCount  |        |  AgeGroup | NameCount |         
--------------------------------        -------------------------
|01-Jul to 07-Jul |     7      |        |   10-19   |    1      |
|08-Jul to 14-Jul |     8      |        |   20-29   |    2      | 
|15-Jul to 21-Jul |     2      |        |   30-39   |    0      |
|22-Jul to 28-Jul |     6      |        |   40-49   |    2      |
--------------------------------        |   50-59   |    1      |
                                        |   60-69   |    1      |
                                        -------------------------

GROUP BY DateGROUP BY Age 自己不会这样做。

我看到的最常见的答案(没有一个始终被评为“正确”)是使用以下一项或多项:

  • 一堆CASE 语句,每个分组一个
  • 一堆UNION 查询,每个分组有不同的WHERE 子句
  • 当我使用 SQL Server 时,PIVOT() and UNPIVOT()
  • 使用子选择、临时表或视图构造的两阶段查询

是否存在处理此类查询的既定通用模式?

【问题讨论】:

  • CTE 通常用于即时生成范围表。然后将结果连接到正在汇总的数据中。
  • 出于多种原因,您不应该存储像“年龄”这样的派生值 - 毕竟,它需要每年更新,在一年中的某个(可能未知)时间点.更好地存储birthday 并即时计算一个人的年龄。
  • @X-Zero 我们确实这样做了,这只是为了消除从“生日”计算“年龄”的步骤(这本身就是一个问题!)。
  • ... 因为人实际上是在某个特定的时刻出生的,但通常引用它时没有时间信息;人们不担心时区,他们只处理“观察到的”(太阳)日。

标签: sql sql-server design-patterns group-by


【解决方案1】:

您不能将年龄(或日期)视为一个新的、只有年龄(或日期)及其相应范围的小表中的外键吗?连接语句可以为新表提供包含 AgeGroups 的列。对于新表,您可以使用标准的分组方法。

为分组创建一个新表似乎很鲁莽,但以编程方式创建很容易,我认为它比 case 语句或 where 子句更容易维护(或删除并重新创建)。如果这个查询的结果是一次性的,一次性的 sql 语句可能效果最好,但我认为我的方法最适合长期使用。

【讨论】:

  • 这对于日期示例来说不是很方便 - 世界上有很多日期!如果我们稍后将“周”重新定义为从每个星期五而不是每月的第一天开始,会发生什么?
  • 在重要时期(1950-2050)没有那么多日期 - 只有 36500 :)
  • @DmitryOsinovskiy 好的,公平点。虽然我只是感觉在 2051 年维护我的代码的人手上都会遇到千年虫问题!
  • @Widor 您可以有一个计划的作业,它会自动检查未来 10 年(或 50 年)内的所有日期是否已经在表中,并会添加缺失的日期。这并不难。我们也这样做了。
【解决方案2】:

查看OVER clause 及其相关子句:PARTITION BY、ROW、RANGE...

确定行集之前的分区和排序 应用关联的窗口函数。即 OVER 子句 在查询结果中定义一个窗口或用户指定的一组行 放。然后一个窗口函数计算每一行的值 窗户。您可以将 OVER 子句与函数一起使用来计算 聚合值,例如移动平均线、累积聚合、 运行总计,或每组前 N 个结果。

【讨论】:

  • 谢谢 - 您能否举例说明如何在我的任一示例表上使用 OVER 子句来生成示例输出?
  • 分析查询功能经常被忽视。这似乎是一个主要情况..使用窗口来定义每条记录所属的范围
【解决方案3】:

嗯,几年前,我们使用 Oracle DB 的方式如下:

  1. 我们有两个表:Sessions 和 Ranges。范围具有引用 Session 的外键。
  2. 当我们需要执行 SQL 时,我们在 Sessions 中创建了一条新记录,并在 Ranges 中创建了几条引用该会话的新记录。
  3. 我们的 SQL 使用按会话过滤器加入范围:
选择总和(t.Value),r.Name 从数据表 t 加入 Ranges r on (r.Session = ? and r.Start t.MyDate) 按 r.Name 分组
  1. 得到结果后,我们从 Sessions 中删除了该记录,并从 Ranges 中删除了被级联删除的记录。
  2. 我们有一个守护程序作业,可以从垃圾记录中清除会话,这些记录在异常情况(终止进程等)的情况下泄露。

这非常有效。从那时起,Oracle 添加了新的 SQL 子句,也许可以使用它们。但在其他 RDBMS 上,这仍然是一种有效的方式。

另一种方法是创建一些函数,例如 GET_YEAR_BY_DATE 或 GET_QUARTER_BY_DATE 或 GET_WEEK_BY_DATE(它们将返回相应的开始日期 期间,例如,对于任何日期返回一年的开始日期)。然后按他们分组:

select sum(Value), GET_YEAR_BY_DATE(MyDate) from DataTable
group by GET_YEAR_BY_DATE(MyDate)

【讨论】:

    【解决方案4】:

    我最喜欢这种类型的案例是交易必须按财政季度或财政年度分组。各种企业的财季或财年边界可以说是离奇的。

    我最喜欢的实现方式是为日期的属性创建一个单独的表。让我们称表为“年历”。此表中的一列是会计季度,另一列是会计年度。这张表的关键当然是日期。十年的数据填充了 3,650 行,加上一些闰年。然后,您需要一个可以从头开始填充此表的程序。所有企业日历规则都内置在这个程序中。

    当您需要按会计季度对交易数据进行分组时,您只需加入此表,然后按会计季度分组即可。

    我认为这种模式可以扩展到按其他范围进行分组,但我自己从未这样做过。

    【讨论】:

    • PS:Almanac 表的另一个用途是有一个名为“Holiday”的布尔列。
    • 一般情况下,假期因国家而异。你应该有一个单独的表,按日期+州/国家的唯一键。
    【解决方案5】:

    在您的第一个示例中,您的时间间隔是有规律的,因此您只需使用函数即可获得所需的结果。下面是一个根据需要获取数据的示例。第一个查询将第一列保持为日期格式(我最好如何处理它,在 SQL 之外进行任何格式化),第二个查询为您进行字符串转换。

    DECLARE @OrderHistory TABLE (Date DATE, Quantity INT)
    INSERT @OrderHistory VALUES 
        ('20120701', 2), ('20120702', 5), ('20120708', 1), ('20120710', 3), 
        ('20120714', 4), ('20120717', 2), ('20120728', 6)
    
    SET DATEFIRST 7
    
    SELECT  DATEADD(DAY, 1 - DATEPART(WEEKDAY, Date), Date) AS WeekStart,
            SUM(Quantity) AS Quantity
    FROM    @OrderHistory
    GROUP BY DATEADD(DAY, 1 - DATEPART(WEEKDAY, Date), Date)
    
    SELECT  WeekStart,
            SUM(Quantity) AS Quantity
    FROM    @OrderHistory
            CROSS APPLY 
            (   SELECT  CONVERT(VARCHAR(6), DATEADD(DAY, 1 - DATEPART(WEEKDAY, Date), Date), 6) + ' to ' + 
                        CONVERT(VARCHAR(6), DATEADD(DAY, 7 - DATEPART(WEEKDAY, Date), Date), 6) AS WeekStart
            ) ws
    GROUP BY WeekStart
    

    可以使用以下方法对您的年龄分组进行类似的操作:

    SELECT  CAST(FLOOR(Age / 10.0) * 10 AS INT)
    

    但是,对于 30-39,这会失败,因为没有该组的数据。

    我对此事的立场是,如果您一次性进行查询,使用临时表、cte 或 case 语句应该可以正常工作,这也应该扩展到在小数据集上重用相同的查询.

    但是,如果您可能会重复使用该组,或者您要引用大量数据,则创建一个永久表,其中定义了范围并将索引应用于所需的任何列。这是在 OLAP 中创建维度的基础。

    【讨论】:

      【解决方案6】:

      通常情况下,此 SQL 问题需要在组合中使用多个模式。

      在这种情况下,您可以使用的两个是

      • NTILE
      • 数字表

      您可以使用NTITLE 创建一定数量的组。但是,由于您没有代表组中的每个成员,因此您还需要使用 数字表 由于您使用的是 SQL Server,因此您可以轻松进行,因为您也不必模拟。

      这是员工问题的一个例子

      WITH g as (
      SELECT 
           NTILE(6) OVER (ORDER BY number) grp, 
           NUMBER
      FROM 
          master..spt_values
      WHERE 
          TYPE = 'P'
      and number >=10 and number <=69
      )
      SELECT 
            CAST(min(g.number) as varchar) + ' - ' + 
            CAST(max(g.number) as varchar) AgeGroup ,
            COUNT(s.age) NameCount
      FROM 
           g
           LEFT JOIN Staff s
           ON g.NUMBER = s.Age
      GROUP BY 
          grp
      

      DEMO

      您也可以将其应用于日期,它只需要一些日期操作

      【讨论】:

        【解决方案7】:

        您可以使用一些维度建模技术,例如fact tablesdimension tables。订单历史记录可以充当与 DateKey 外键关系到 Date 维度的事实表。 日期维度可以有如下模式:

        请注意,日期表已预先填充了最多 N 年的数据。

        使用上面的示例,这是一个获取结果的示例查询:

        select CalendarWeek, sum(Quantity)
        from OrderHistory a
        join DimDate b
            on a.DateKey = b.DateKey
        group by CalendarWeek
        

        对于 Staff 表,您可以存储生日键而不是年龄,并让查询计算年龄和范围。

        这里是SQL Fiddle

        日期维度填充脚本取自here

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-04-21
          • 2013-08-14
          • 2021-05-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-10-11
          相关资源
          最近更新 更多