SQL Server温故系列(5):SQL 查询之分组查询 GROUP BY

SQL Server温故系列(5):SQL 查询之分组查询 GROUP BY

1、GROUP BY 与聚合函数

GROUP BY 是一种能将查询结果划分为多个行组的查询语句的子句,其目的通常是为了在每个组上执行一个或多个聚合运算,所以 GROUP BY 通常会与聚合函数一块儿出现在查询语句中。

GROUP BY 的标准分组方式是按所有分组字段的值依次来分组。假如字段 A 的值有 3 种,字段 B 的值有 2 种;如果是GROUP BY A,那么就会被分为 3 组;而如果是GROUP BY A,B,那么就会先被 A 分为 3 组,然后这 3 组又会被 B 再各自分为 2 组,最终会被分为 3×2 等于 6 组。

显然,GROUP BY B,A最终也会被分为 6 组,换而言之,标准分组时的字段的顺序不会对分组结果产生影响。但分组字段的顺序会影响查询结果的排序,如果想要改变结果集的排序,可以通过 ORDER BY 子句来实现。

示例一、查询统计学生 1、2、3 的第 1 次考试成绩,且按各科总分降序排列:

SELECT t.StudentId,COUNT(1) 科目数,
    SUM(t.Scores) 总分,MAX(t.Scores) 最高分,MIN(t.Scores) 最低分,AVG(t.Scores) 平均分 
FROM T_ExamResults t 
WHERE t.Counts = 1 AND t.StudentId IN(1,2,3) 
GROUP BY t.StudentId 
ORDER BY 总分 DESC;

示例二、查询统计学生 1、2、3 的第 1 次考试成绩,且按班级名称和学生名称来升序排列:

SELECT t1.Code,t1.Name,t3.Name,COUNT(1) 科目数,
    SUM(t2.Scores) 总分,MAX(t2.Scores) 最高分,MIN(t2.Scores) 最低分,AVG(t2.Scores) 平均分 
FROM T_Students t1 
JOIN T_ExamResults t2 ON t1.Id = t2.StudentId AND t2.Counts = 1 
JOIN T_Classes t3 ON t1.ClassId = t3.Id 
WHERE t1.Id IN(1,2,3) 
GROUP BY t1.Code,t1.Name,t3.Name 
ORDER BY t3.Name,t1.Name DESC;

注意:在含有 GROUP BY 子句的查询语句中,每组只会返回一行数据,且查询选择列表中的列只能是 GROUP BY 中的字段或聚合函数表达式。

2、GROUP BY 与 HAVING

HAVING 子句的作用有点类似于 WHERE 子句,说到底它们都是过滤数据用的,但不同的是,WHERE 子句过滤的最小单位是数据行,而 HAVING 子句过滤的最小单位是行组。相较于 WHERE 子句,HAVING 子句最大的优势就是支持聚合函数。

HAVING 子句只能在查询语句中使用,且通常与 GROUP BY 子句一起使用。如果查询语句中没有 GROUP BY 子句,那么就会有隐式的单一行组,但这通常是没有意义的。例如要查询统计平均分达到 80 分的学生第 1 次考试成绩,且按总分倒序排列,示例如下:

SELECT t.StudentId,COUNT(1) 科目数,
    SUM(t.Scores) 总分,MAX(t.Scores) 最高分,MIN(t.Scores) 最低分,AVG(t.Scores) 平均分 
FROM T_ExamResults t 
WHERE t.Counts = 1 
GROUP BY t.StudentId 
HAVING AVG(t.Scores) >= 80
ORDER BY SUM(t.Scores) DESC;

3、GROUP BY 扩展分组

在实际的开发工作中,尤其是开发数据报表,往往需要统计多维度的小计和合计。大多数情况下用 UNION 也能达到类似效果,但实现起来比较繁琐,灵活性较差,性能往往也比较低。针对这类需求,SQL Server 提供了几个实用的扩展分组,以便能更好的实现这些需求。

3.1、GROUP BY ROLLUP

ROLLUP 是对 GROUP BY 子句的一种扩展,它允许计算标准分组及部分维度的小计及合计。ROLLUP 的计算结果与分组字段的顺序有关,因为它的分组过程具有方向性,先计算标准分组,然后从右到左递减计算更高一级的小计,直到所有字段被计算完,最后计算合计。

对于GROUP BY ROLLUP(a,b,c),结果具有 (a,b,c)、(a,b,NULL)、(a,NULL,NULL)、(NULL,NULL,NULL) 唯一值的组。换而言之,GROUP BY ROLLUP(a,b,c)的结果集就等价于GROUP BY a,b,c的结果集,加上GROUP BY a,b的结果集,再加上GROUP BY a的结果集,最后加上不带GROUP BY的总计结果集。

示例一、查询统计 1、2、3 班的学生个数及年龄:

WITH t AS(
    SELECT t.Code,t.Name,DATEDIFF(YEAR,t.Birthday,GETDATE()) Age,t.ClassId 
    FROM T_Students t 
    WHERE t.ClassId IN(1,2,3)
)
SELECT t.ClassId,COUNT(1) 学生个数,MAX(t.Age) 最大年龄,MIN(t.Age) 最小年龄 
FROM t 
GROUP BY ROLLUP(t.ClassId);

查询结果如下:

ClassId     学生个数        最大年龄        最小年龄
----------- ----------- ----------- -----------
1           10          20          15
2           9           20          16
3           9           21          15
NULL        28          21          15

示例二、查询统计 1、2 班的学生个数及年龄:

WITH t AS(
    SELECT t.Code,t.Name,DATEDIFF(YEAR,t.Birthday,GETDATE()) Age,t.ClassId,t.Gender 
    FROM T_Students t 
    WHERE t.ClassId IN(1,2)
)
SELECT t.ClassId,t.Gender,COUNT(1) 学生个数,MAX(t.Age) 最大年龄,MIN(t.Age) 最小年龄 
FROM t 
GROUP BY ROLLUP(t.ClassId,t.Gender);

查询结果如下:

ClassId     Gender      学生个数        最大年龄        最小年龄
----------- ----------- ----------- ----------- -----------
1           0           6           19          15
1           1           4           20          18
1           NULL        10          20          15
2           0           4           20          17
2           1           5           20          16
2           NULL        9           20          16
NULL        NULL        19          20          15

示例三、查询统计 1、2 班的学生个数及年龄:

WITH t AS(
    SELECT t.Code,t.Name,DATEDIFF(YEAR,t.Birthday,GETDATE()) Age,t.ClassId,t.Gender 
    FROM T_Students t 
    WHERE t.ClassId IN(1,2)
)
SELECT t.ClassId,t.Gender,COUNT(1) 学生个数,MAX(t.Age) 最大年龄,MIN(t.Age) 最小年龄 
FROM t 
GROUP BY t.ClassId,ROLLUP(t.Gender);

查询结果如下:

ClassId     Gender      学生个数        最大年龄        最小年龄
----------- ----------- ----------- ----------- -----------
1           0           6           19          15
1           1           4           20          18
1           NULL        10          20          15
2           0           4           20          17
2           1           5           20          16
2           NULL        9           20          16

3.2、GROUP BY CUBE

CUBE 是对 GROUP BY 子句的一种扩展,它允许计算标准分组及所有维度的小计及合计。CUBE 会对所有可能的分组进行统计,从而生成交叉报表。CUBE 比 ROLLUP 的分组更多,完全包含了 ROLLUP 的统计结果,且计算结果与分组字段的顺序无关,但如果字段顺序不同,默认的结果集排序会有不同。

对于GROUP BY CUBE(a,b),结果具有 (a,b)、(a,NULL)、(NULL,b)、(NULL,NULL) 唯一值的组。换而言之,GROUP BY CUBE(a,b)的结果集就等价于GROUP BY a,b的结果集,加上GROUP BY a的结果集,再加上GROUP BY b的结果集,最后加上不带GROUP BY的总计结果集。

示例一、查询统计 1、2、3 班的学生个数及年龄:

WITH t AS(
    SELECT t.Code,t.Name,DATEDIFF(YEAR,t.Birthday,GETDATE()) Age,t.ClassId 
    FROM T_Students t 
    WHERE t.ClassId IN(1,2,3)
)
SELECT t.ClassId,COUNT(1) 学生个数,MAX(t.Age) 最大年龄,MIN(t.Age) 最小年龄 
FROM t 
GROUP BY CUBE(t.ClassId);

查询结果如下:

ClassId     学生个数        最大年龄        最小年龄
----------- ----------- ----------- -----------
1           10          20          15
2           9           20          16
3           9           21          15
NULL        28          21          15

示例二、查询统计 1、2 班的学生个数及年龄:

WITH t AS(
    SELECT t.Code,t.Name,DATEDIFF(YEAR,t.Birthday,GETDATE()) Age,t.ClassId,t.Gender 
    FROM T_Students t 
    WHERE t.ClassId IN(1,2)
)
SELECT t.ClassId,t.Gender,COUNT(1) 学生个数,MAX(t.Age) 最大年龄,MIN(t.Age) 最小年龄 
FROM t 
GROUP BY CUBE(t.ClassId,t.Gender);

查询结果如下:

ClassId     Gender      学生个数        最大年龄        最小年龄
----------- ----------- ----------- ----------- -----------
1           0           6           19          15
2           0           4           20          17
NULL        0           10          20          15
1           1           4           20          18
2           1           5           20          16
NULL        1           9           20          16
NULL        NULL        19          20          15
1           NULL        10          20          15
2           NULL        9           20          16

示例三、查询统计 1、2 班的学生个数及年龄:

WITH t AS(
    SELECT t.Code,t.Name,DATEDIFF(YEAR,t.Birthday,GETDATE()) Age,t.ClassId,t.Gender 
    FROM T_Students t 
    WHERE t.ClassId IN(1,2)
)
SELECT t.ClassId,t.Gender,COUNT(1) 学生个数,MAX(t.Age) 最大年龄,MIN(t.Age) 最小年龄 
FROM t 
GROUP BY t.ClassId,CUBE(t.Gender);

查询结果如下:

ClassId     Gender      学生个数        最大年龄        最小年龄
----------- ----------- ----------- ----------- -----------
1           0           6           19          15
1           1           4           20          18
1           NULL        10          20          15
2           0           4           20          17
2           1           5           20          16
2           NULL        9           20          16

3.3、GROUP BY GROUPING SETS

GROUPING SETS 是对 GROUP BY 子句的一种扩展,它允许一次计算多个标准分组的小计。GROUPING SETS 的功能相当于将多个 GROUP BY 子句组合到一个 GROUP BY 子句中,类似于用 UNION ALL 合并多个 GROUP BY 的结果集,所以它的计算结果与排序字段的顺序无关,而且不会合并重复组。

例如GROUP BY GROUPING SETS(ROLLUP(A))GROUP BY ROLLUP(A)的结果集相同,GROUP BY GROUPING SETS(A,B)GROUP BY AGROUP BY B的结果集相同。示例如下:

WITH t AS(
    SELECT t.Code,t.Name,DATEDIFF(YEAR,t.Birthday,GETDATE()) Age,t.ClassId,t.Gender 
    FROM T_Students t 
    WHERE t.ClassId IN(1,2)
)
SELECT t.ClassId,COUNT(1) 学生个数,MAX(t.Age) 最大年龄,MIN(t.Age) 最小年龄 
FROM t 
GROUP BY GROUPING SETS(ROLLUP(t.ClassId),CUBE(t.ClassId));

查询结果如下:

ClassId     学生个数        最大年龄        最小年龄
----------- ----------- ----------- -----------
1           10          20          15
2           9           20          16
NULL        19          20          15
1           10          20          15
2           9           20          16
NULL        19          20          15

GROUPING SETS 中还支持GROUP BY (),用于指定生成总计的空组。例如要查询统计浙江地区各级别行政区个数,及总计个数,示例如下:

SELECT t.Level 级别,COUNT(1) 个数 
FROM T_Districts t 
WHERE SUBSTRING(t.Code,1,2) = '33' 
GROUP BY GROUPING SETS(t.Level,());

查询结果如下:

级别          个数
----------- -----------
1           1
2           6
3           10
NULL        17

4、GROUP BY 扩展函数

4.1、GROUPING 函数

GROUPING 函数用于指示当前行是否为聚合行,如果它返回 1 则表示聚合,相反,返回 0 则表示未聚合。仅当指定了 GROUP BY 时,GROUPING 才能在 SELECT 子句、HAVING 或 ORDER BY 子句中使用。

通常将一个分组字段作为该函数的参数,然后通过判断它的返回值来区分聚集行与常规行,从而进一步对结果集美化或过滤。示例如下:

SELECT GROUPING(t.Level) 标志,t.Level 级别,COUNT(1) 个数 
FROM T_Districts t 
WHERE SUBSTRING(t.Code,1,2) = '33' 
GROUP BY GROUPING SETS(t.Level,());

查询结果如下:

标志   级别          个数
---- ----------- -----------
0    1           1
0    2           6
0    3           10
1    NULL        17

4.2、GROUPING_ID 函数

GROUPING 函数用于计算分组级别,它将返回与行相关联的 GROUPING 位向量对应的数值。GROUPING_ID 按从左到右的顺序计算,如果是分组字段,则为 0,如果是小计或合计则为 1,然后按字段的顺序将计算结果组成二进制序列(位向量),最后将位向量转化为十进制数。仅当指定了 GROUP BY 时,GROUPING 才能在 SELECT 子句、HAVING 或 ORDER BY 子句中使用。

GROUPING_ID 函数在功能上等效于多个 GROUPING 函数,当查询结果有多个聚合级别时,使用该函数会更容易表达行过滤条件。示例如下:

SELECT t.CourseId,t.StudentId,t.Counts,MAX(t.Scores) Scores,
    GROUPING_ID(t.CourseId) gc,GROUPING_ID(t.StudentId) gs,GROUPING_ID(t.Counts) gt,
    GROUPING_ID(t.CourseId,t.StudentId) gcs,
    GROUPING_ID(t.StudentId,t.Counts) gst,
    GROUPING_ID(t.CourseId,t.Counts) gct,
    GROUPING_ID(t.CourseId,t.StudentId,t.Counts) gcst 
FROM T_ExamResults t 
WHERE t.StudentId = 1 AND t.Counts = 2 
GROUP BY ROLLUP(t.CourseId,t.StudentId,t.Counts);

查询结果如下:

CourseId   StudentId  Counts    Scores    gc      gs      gt      gcs     gst     gct     gcst
---------- ---------- --------- --------- ------- ------- ------- ------- ------- ------- -------
1          1          2         63.0      0       0       0       0       0       0       0
1          1          NULL      63.0      0       0       1       0       1       1       1
1          NULL       NULL      63.0      0       1       1       1       3       1       3
2          1          2         98.0      0       0       0       0       0       0       0
2          1          NULL      98.0      0       0       1       0       1       1       1
2          NULL       NULL      98.0      0       1       1       1       3       1       3
3          1          2         73.0      0       0       0       0       0       0       0
3          1          NULL      73.0      0       0       1       0       1       1       1
3          NULL       NULL      73.0      0       1       1       1       3       1       3
NULL       NULL       NULL      98.0      1       1       1       3       3       3       7

5、本文小结

本文主要讲述了 SQL Server 中分组查询的相关知识点,包括 GROUP BY 与聚合函数、HAVING 联合使用及 GROUP BY 的标准分组、扩展分组、扩展函数等。

另外,不知道会不会有读者感到疑惑,为什么扩展分组返回的结果集中经常会出现 NULL 值?其实这是 NULL 的一个特殊应用,它在 ROLLUP、CUBE 或 GROUPING SETS 操作的结果集内作为字段的占位符,表示全体(数据)。

本文参考链接:

评论

0条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注