9

我在 SO 上看到了很多关于如何在 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  |
---------------------------       ------------------------

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

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

4

7 回答 7

3

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

日期维度

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

使用上面的示例,以下是获取结果的示例查询:

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

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

这是SQL 小提琴

日期维度人口脚本取自此处

于 2012-07-17T19:56:26.433 回答
2

通常情况下,此 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

演示

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

于 2012-07-17T17:19:39.060 回答
1

在这种类型中,我最喜欢的情况是交易必须按财政季度或财政年度分组。各种企业的财政季度或财政年度界限可能近乎离奇。

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

当您需要按会计季度对交易数据进行分组时,您只需将这个表加入日期,然后按会计季度分组。

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

于 2012-07-17T16:38:48.247 回答
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 中创建维度的基础。

于 2012-07-17T16:48:53.017 回答
1

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

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

于 2012-07-17T16:23:50.670 回答
1

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

  1. 我们有两个表:Sessions 和 Ranges。范围具有引用 Session 的外键。
  2. 当我们需要执行 SQL 时,我们在 Sessions 中创建了一条新记录,并在 Ranges 中创建了几条引用该会话的新记录。
  3. 我们的 SQL 通过 Session 过滤加入 Ranges:
    选择总和(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)
于 2012-07-17T16:38:41.773 回答
1

看一下OVER 子句及其相关子句:PARTITION BY、ROW、RANGE...

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

于 2012-07-17T16:25:06.203 回答