导读:本期聚焦于公主创作的《如何在SQL嵌套查询中实现多级分组统计?利用GROUPING SETS配合子查询的完整思路》,敬请观看详情。为什么同一张报表里要同时看到区域、省份、城市三级汇总?一条GROUP BY只能产出最细粒度,UNION ALL拼接又会重复扫描数据。SQL标准里的GROUPING SETS正好解决这个矛盾,它允许在一次分组扫描中同时生成多个层级的结果集。但当业务口径复杂,例如要先过滤订单状态、关联维度表、扣除退款金额,再把清洗后的中间结果拿去做多级分组,就必须借助嵌套查询。本文以销售统计为场景,先介绍GROUPING SETS的基础语法和输出特征,再演示如何把子查询作为输入,在外层使用GROUPING SETS完成大区、省份、城市以及整体汇总。进一步配合GROUPING函数和GROUPING_ID识别哪些行是汇总行,避免真实NULL与汇总NULL混淆。最后对比UNION ALL、ROLLUP、CUBE等方式的性能差异,并给出索引设计和兼容性注意事项。

多级分组统计在报表开发中非常常见,比如既要看全国总额,又要看每个大区、每个省份、每个城市的销售额。普通 GROUP BY 一次只能按照固定的一组列做聚合,如果直接按大区、省份、城市分组,结果里不会出现大区小计或全国总计。为了拿到这些不同层级的汇总,过去通常需要写多个分组查询再用 UNION ALL 拼起来,代码重复且执行效率不理想。SQL 标准中的 GROUPING SETS 提供了一种更优雅的方案,它可以在一次扫描中生成多个分组维度。不过真实业务里往往不会直接对原始表做分组,而是先经过过滤、关联、退款扣除等一系列处理,再把中间结果交给 GROUPING SETS。这就涉及嵌套查询与多级分组统计的配合。

如何在SQL嵌套查询中实现多级分组统计?利用GROUPING SETS配合子查询的完整思路

一、GROUPING SETS 解决什么问题

先看一个最基础的需求:销售表 sales 中包含 region、province、city、amount 四个字段,需要同时输出城市级、省份级、区域级和全国级四个汇总结果。如果只写 GROUP BY region, province, city,结果只有城市粒度;如果只写 GROUP BY region,结果只有区域粒度。为了在同一个结果集中看到不同层级,传统写法是四个查询用 UNION ALL 连接,每个查询都要扫描一次 sales 表,SQL 会变得很长。

GROUPING SETS 可以直接在 GROUP BY 子句中列出多个分组组合,语法如下:

SELECT region,
       province,
       city,
       SUM(amount) AS total_amount
FROM sales
GROUP BY GROUPING SETS (
    (region, province, city),
    (region, province),
    (region),
    ()
);

这段 SQL 会把四个分组组合一次性计算出来。没有参与分组的列在结果中会显示为 NULL,例如按大区汇总时,province 和 city 都是 NULL;整体汇总那一行三个维度列全是 NULL。它等价的 UNION ALL 写法至少要扫描四次销售表,而 GROUPING SETS 在多数数据库中可以只扫描一次底层数据,再根据分组键做分发聚合,IO 和 CPU 消耗通常更低。

不过 GROUPING SETS 并不是银弹。当业务口径复杂时,分组表达式不能直接写在原始表上。例如订单金额需要扣除退款金额,退款数据在另一张表;城市名称需要通过维度表映射;订单状态需要过滤掉未支付和已取消的记录。如果把这些逻辑全部塞进一个很长的 GROUP BY 查询里,可读性会明显下降。更合理的做法是先用子查询完成数据口径处理,再让外层查询专注于分组统计。

二、嵌套查询与 GROUPING SETS 的配合方式

嵌套查询配合 GROUPING SETS 的思路很简单:内层子查询负责把数据清理成口径统一、字段明确的中间结果,外层查询再对中间结果做多级聚合。这样职责分离后,内层可以自由使用 WHERE、JOIN、CASE WHEN、子查询甚至窗口函数,外层只需要关心分组维度和聚合指标。

下面是一个销售统计的完整例子。假设订单表 orders 保存订单金额和城市 ID,维度表 dim_region 保存城市、省份、区域名称,退款表 refunds 保存订单级退款金额。现在要按大区、省份、城市输出净销售额,同时也要大区小计和全国总计:

SELECT region_name,
       province_name,
       city_name,
       SUM(net_amount) AS total_net_amount
FROM (
    SELECT d.region_name,
           d.province_name,
           d.city_name,
           o.amount - COALESCE(r.refund_amount, 0) AS net_amount
    FROM orders o
    JOIN dim_region d
      ON o.city_id = d.city_id
    LEFT JOIN refunds r
      ON o.order_id = r.order_id
    WHERE o.status = 'paid'
      AND o.created_at >= TIMESTAMP '2024-01-01 00:00:00'
) t
GROUP BY GROUPING SETS (
    (region_name, province_name, city_name),
    (region_name, province_name),
    (region_name),
    ()
);

内层查询完成了三件重要的事:通过 JOIN dim_region 把城市编码转换成可读的区域、省份、城市名称;通过 LEFT JOIN refunds 计算退款,并用 COALESCE 把没有退款记录的订单处理为 0;通过 WHERE 过滤掉非支付状态和指定日期之前的数据。外层拿到的 t 表已经是一张口径干净的明细表,它只负责按照 GROUPING SETS 定义的四个层级做 SUM 聚合。

这种写法的另一个好处是调试方便。当结果金额不对时,可以先单独运行内层查询,确认每一行的净销售额是否符合预期;确认无误后再打开外层的分组逻辑。如果所有逻辑都堆在一个查询里,定位问题会更困难。对于需要多套分组维度的报表,也可以复用同一个内层查询,只修改外层的 GROUPING SETS 组合,降低维护成本。

三、用 GROUPING 和 GROUPING_ID 识别汇总层级

GROUPING SETS 输出结果中,未参与分组的列会显示为 NULL。但 NULL 本身存在歧义:如果维度列数据本身可能为空,那么无法区分这一行到底是因为没有参与分组而产生的汇总 NULL,还是数据本身就缺失。比如城市名称在维度表清洗后通常不会为空,但如果原始数据质量差,城市 ID 无法匹配时可能保留 NULL,这时直接看结果就容易混淆。

SQL 提供了 GROUPING 函数来解决这个问题。GROUPING 接收一个分组列作为参数,当该列未参与当前分组时返回 1,否则返回 0。结合 CASE WHEN 可以把 NULL 转换成更容易阅读的标签。示例中还用多个 GROUPING 函数相加生成 group_level,用于排序和标识汇总层级:

SELECT CASE WHEN GROUPING(region_name) = 1 THEN '全部区域'
            ELSE region_name END AS region_label,
       CASE WHEN GROUPING(province_name) = 1 THEN '全部省份'
            ELSE province_name END AS province_label,
       CASE WHEN GROUPING(city_name) = 1 THEN '全部城市'
            ELSE city_name END AS city_label,
       SUM(net_amount) AS total_net_amount,
       GROUPING(region_name) + GROUPING(province_name) + GROUPING(city_name) AS group_level
FROM (
    SELECT d.region_name,
           d.province_name,
           d.city_name,
           o.amount - COALESCE(r.refund_amount, 0) AS net_amount
    FROM orders o
    JOIN dim_region d
      ON o.city_id = d.city_id
    LEFT JOIN refunds r
      ON o.order_id = r.order_id
    WHERE o.status = 'paid'
) t
GROUP BY GROUPING SETS (
    (region_name, province_name, city_name),
    (region_name, province_name),
    (region_name),
    ()
)
ORDER BY group_level, region_label, province_label, city_label;

GROUPING_ID 是另一种快捷函数,它接收多个列并返回一个十进制位掩码。例如对于 (region_name, province_name, city_name) 三个列,城市级明细返回 0,省份级返回 1,区域级返回 3,全国汇总返回 7。不过 GROUPING_ID 在 Oracle、SQL Server 等数据库中可用,MySQL 8.0 只支持 GROUPING 函数,PostgreSQL 也只有 GROUPING。为了兼容更多数据库,示例中使用了 GROUPING 求和的方式得到 group_level,0 表示城市明细,1 表示省份汇总,2 表示区域汇总,3 表示全国汇总。这个 group_level 同样可以用于排序,先展示最细粒度,再展示各级小计和总计。

在实际开发中,建议把 GROUPING 函数放在外层 SELECT 中处理,不要在内层子查询里处理。因为内层数据还处于明细阶段,每条记录都有真实的维度值,还没有产生汇总 NULL。只有外层经过 GROUPING SETS 之后,才会出现需要识别的 NULL。如果在内层使用 GROUPING,函数会因为缺少分组上下文而报错,或者返回固定值。

四、GROUPING SETS 与其他多级聚合方案的对比

除了 GROUPING SETS,SQL 还提供了 ROLLUP 和 CUBE 两种分组扩展。ROLLUP 适合有层级关系的维度,例如区域、省份、城市,它会自动生成从最细粒度到整体汇总的逐级上卷,语法上比 GROUPING SETS 更简洁。CUBE 则生成所有维度的所有可能组合,包括跨维度组合,比如只按省份而不按区域汇总。GROUPING SETS 的优势在于可以精确指定需要的组合,避免 ROLLUP 固定层级和 CUBE 组合爆炸的问题。

从性能角度看,GROUPING SETS 通常优于手工 UNION ALL。UNION ALL 的每个分支都是一个独立查询,即使数据库优化器识别出多个分支访问同一张表,合并扫描也需要额外的优化能力。GROUPING SETS 则是语法层面的直接声明,优化器可以更早地进行扫描共享和分组下推。但也要注意,GROUPING SETS 生成的分组组合越多,内存和临时表空间占用越大,尤其在分组键基数很高时,多层汇总会导致结果集迅速膨胀。因此不要把所有维度组合都无脑列出来,只保留业务真正需要的层级。

兼容性方面,GROUPING SETS 在 PostgreSQL、SQL Server、Oracle、MySQL 8.0 及以上版本中都有支持,但 MySQL 5.7 及更早版本不支持。迁移老系统时需要先确认数据库版本。如果遇到不支持的数据库,一种替代方案是使用多个 GROUP BY 加 UNION ALL,并尽量把公共过滤条件下推到每个分支中,减少重复扫描。另一种方案是在应用层做多级汇总,把明细数据取到内存中,按不同维度分别聚合,但这会带来较大的网络传输和内存压力,只适合数据量较小的场景。

最后还要注意索引设计。嵌套查询中的内层过滤条件如果选择性较高,应在 orders.status、orders.created_at、orders.city_id 等列上建立合适索引;如果退款表参与 JOIN,refunds.order_id 也需要索引。外层 GROUPING SETS 的分组列来自内层结果,这些列通常无法直接走索引,因为中间结果可能已经物化到临时表,重点应放在减少内层扫描行数和输出行数上。

SQL GROUPING SETS嵌套查询多级分组统计修改时间:2026-09-28 03:05:10

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0928/62812.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。