是否有一个已建立的SQL查询模式,它按范围分组?

时间:2012-07-17 16:14:35

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

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

确切的情景会有所不同,但每个方面的一般基本问题是按一系列值而不是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  |
---------------------------       ------------------------

现在假设我们要使用Date表的OrderHistory列按周分组,即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()
  • 使用子选择,临时表或视图构造
  • 的两阶段查询

是否存在用于处理此类查询的已建立的通用模式?

7 个答案:

答案 0 :(得分:3)

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

Date Dimesion

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

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

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

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

这是SQL Fiddle

日期维度填充脚本取自here

答案 1 :(得分:2)

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

在这种情况下,你可以使用的是

  • NTILE
  • 数字表

您可以使用NTITLE创建一定数量的群组。但是,由于您没有代表组的每个成员,因此您还需要使用数字表因为您使用的是SQL Server,所以您可以轻松实现,因为您无需模拟。< / p>

以下是员工问题的示例

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

您可以将此应用于日期,只需要一些日常操作

答案 2 :(得分:1)

难道你不能将年龄(或日期)视为一个新的小表,只是年龄(或日期)及其相应的范围吗? join语句可以为新表提供包含AgeGroups的列。使用新表,您可以使用标准的分组方法。

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

答案 3 :(得分:1)

查看OVER clause及其相关条款:PARTITION BY,ROW,RANGE ......

  

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

答案 4 :(得分:1)

好吧,几年前在Oracle DB中,我们采用以下方式实现:

  1. 我们有两个表:Sessions和Ranges。 Ranges具有引用Session的外键。
  2. 当我们需要执行SQL时,我们在Sessions中创建了一条新记录,并在Ranges中创建了几条引用该会话的新记录。
  3. 我们的SQL通过会话过滤Ranges:
  4.     select sum(t.Value), r.Name 
        from DataTable t 
        join Ranges r on (r.Session = ? and r.Start  t.MyDate)
        group by r.Name
    
    1. 我们得到结果后,我们从Sessions中删除了该记录,并从Ranges中删除了级联记录。
    2. 我们有一个守护进程,可以清除Sessions中的垃圾记录,这些记录在特殊情况下被泄露(被杀死的进程等)。
    3. 这完美无缺。从那时起,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)
      

答案 5 :(得分:1)

此类型中我最喜欢的案例是交易必须按财政季度或财政年度分组。各个企业的财政季度或财政年度界限可以接近奇怪。

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

当您需要按会计季度对交易数据进行分组时,您只需加入此表格的日期,然后按财务季度分组。

我认为这种模式可以通过其他类型的范围扩展到分组,但我自己从未做过。

答案 6 :(得分:1)

在您的第一个示例中,您的间隔是常规的,因此您只需使用功能即可获得所需的结果。下面是根据您的需要获取数据的示例。第一个查询使第一列保持日期格式(我最好如何处理它在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中创建维度的基础。