如何在 SQL 中使用聚合函数统计分组数据

科技资讯10小时前发布 jdkjadmin
0 00
如何在 SQL 中使用聚合函数统计分组数据

如何在 SQL 中使用聚合函数统计分组数据

统计报表通常不是逐行展示数据,而是回答“每个部门有多少员工”“每个月销售额是多少”“每个用户平均下了几笔订单”等问题。SQL 聚合函数可以把多行数据归纳为一个结果;配合 GROUP BY,则可以按一个或多个维度分别计算。

聚合查询看起来简短,但容易在边界处出错:COUNT(*) 和 COUNT(列) 的结果可能不同,WHERE 与 HAVING 过滤的阶段不同,表连接还可能意外放大计数。本文使用 PostgreSQL 语法演示核心用法,也会说明需要留意的数据库差异。

一、先准备一组可运行的数据

下面的建表和插入语句可在 PostgreSQL 中直接运行。订单金额使用 NUMERIC,适合需要精确十进制表示的金额;实际项目还应根据业务确定精度和小数位数。

CREATE TABLE orders (
    order_id    INTEGER PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    department  TEXT NOT NULL,
    status      TEXT NOT NULL,
    amount      NUMERIC(10, 2),
    created_at  TIMESTAMP NOT NULL
);

INSERT INTO orders
    (order_id, customer_id, department, status, amount, created_at)
VALUES
    (1, 101, '书籍', 'paid',    120.00, '2025-01-03 10:00:00'),
    (2, 101, '书籍', 'paid',     80.00, '2025-01-10 12:00:00'),
    (3, 102, '数码', 'pending',  NULL,   '2025-01-11 09:30:00'),
    (4, 103, '数码', 'paid',    500.00, '2025-02-02 15:20:00'),
    (5, 104, '书籍', 'cancelled', 40.00, '2025-02-05 16:00:00'),
    (6, 102, '数码', 'paid',    300.00, '2025-02-18 08:10:00'),
    (7, 105, '家居', 'paid',    220.00, '2025-03-01 11:45:00');

二、常用聚合函数做什么

  • COUNT:统计行数或某列非 NULL 的值数量。
  • SUM:计算数值列的合计。
  • AVG:计算数值列的平均值。
  • MIN、MAX:取最小值和最大值。除数字外,也可用于日期或可排序类型;具体可用类型由数据库决定。

先看不分组的汇总:整个查询结果集被视作一组,通常只返回一行。

SELECT
    COUNT(*) AS order_rows,
    COUNT(amount) AS orders_with_amount,
    SUM(amount) AS amount_sum,
    AVG(amount) AS amount_average,
    MIN(amount) AS amount_min,
    MAX(amount) AS amount_max
FROM orders;

样例数据中,COUNT(*) 是 7,COUNT(amount) 是 6,因为有一行的 amount 为 NULL。其他几个金额聚合也会忽略这个空值。不要把 COUNT 的不同写法混为一谈:

  • COUNT(*) 统计输入结果中的行数,不根据某一列是否为空来排除行。
  • COUNT(amount) 统计 amount 非 NULL 的行数。
  • COUNT(DISTINCT customer_id) 统计非空且去重后的客户编号数。

在 SQL 中,NULL 表示缺失或未知值,不等同于零、空字符串或“没有这一行”。聚合函数一般忽略输入中的 NULL;COUNT(*) 是重要例外,因为它统计行而不是列值。

三、用 GROUP BY 按维度分组

GROUP BY 将指定列值相同的行归为一组,再对每组分别计算聚合结果。下面统计每个部门的订单数和金额:

SELECT
    department,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount,
    AVG(amount) AS average_amount
FROM orders
GROUP BY department
ORDER BY department;

结果中每个部门一行。这里 COUNT(*) 会把金额为空的订单也计入订单数,而 SUM(amount) 和 AVG(amount) 不会把空金额当成数值参与计算。

可以按多个字段分组。例如统计每个部门、每种状态的订单情况:

SELECT
    department,
    status,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount
FROM orders
GROUP BY department, status
ORDER BY department, status;

分组键是字段组合:只有 department 和 status 都相同的行才会归为一组。分组字段越多,通常结果组数越多;选择哪些字段,应由报表需要回答的问题决定。

四、WHERE、GROUP BY、HAVING 的分工

聚合查询的常见逻辑顺序可以概括为:先从 FROM 读取数据,经 WHERE 过滤行,再按 GROUP BY 分组,计算聚合,最后由 HAVING 筛选组。这个顺序有助于判断条件应该放在哪里。

WHERE:聚合前过滤明细行

例如只统计已支付订单,再按部门汇总:

SELECT
    department,
    COUNT(*) AS paid_order_count,
    SUM(amount) AS paid_amount
FROM orders
WHERE status = 'paid'
GROUP BY department
ORDER BY department;

WHERE 条件作用于单行,不能直接在同一查询层级写 WHERE SUM(amount) > 100,因为此时分组聚合还没有完成。

HAVING:聚合后过滤分组

若要保留已支付金额超过 300 的部门,应使用 HAVING:

SELECT
    department,
    SUM(amount) AS paid_amount
FROM orders
WHERE status = 'paid'
GROUP BY department
HAVING SUM(amount) > 300
ORDER BY paid_amount DESC;

这里先排除未支付订单,再对剩余记录分组,最后只保留合计金额大于 300 的组。没有聚合条件时,优先用 WHERE 筛选明细,通常更直观,也可减少后续需要处理的行。

五、NULL、空结果与计数边界

聚合值为空时的结果

对一组输入行计算 SUM、AVG、MIN 或 MAX 时,如果没有非空输入值,结果通常为 NULL。而在没有 GROUP BY 的聚合查询中,即使筛选后没有任何行,COUNT(*) 仍返回 0;其他上述聚合通常返回 NULL。

SELECT
    COUNT(*) AS row_count,
    SUM(amount) AS amount_sum
FROM orders
WHERE status = 'not_a_real_status';

若业务要求把空合计显示为零,可以显式使用 COALESCE:

SELECT
    COALESCE(SUM(amount), 0) AS amount_sum
FROM orders
WHERE status = 'not_a_real_status';

是否应该把 NULL 转成 0,需要根据含义决定。金额缺失不一定代表金额为零;过早替换可能掩盖数据质量问题。类似地,AVG(COALESCE(amount, 0)) 会把空金额当作零纳入平均值,通常与 AVG(amount) 的业务含义不同。

去重计数并不等于行数

COUNT(DISTINCT customer_id) 常用于统计去重后的实体数量,但它忽略 NULL。若要统计每个部门有多少名产生过订单的不同客户:

SELECT
    department,
    COUNT(DISTINCT customer_id) AS customer_count
FROM orders
GROUP BY department
ORDER BY department;

只有当“客户”由一个字段唯一标识时,这种写法才直接适用。若唯一键由多个字段组成,复合去重的语法和可用能力可能因数据库而异,不宜假定所有数据库都接受相同写法。

六、按日期分组和分桶

按时间统计时,通常先把时间戳转换为日期或月份,再分组。下面的 PostgreSQL 示例按月份统计已支付金额:

SELECT
    DATE_TRUNC('month', created_at) AS month_start,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount
FROM orders
WHERE status = 'paid'
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month_start;

DATE_TRUNC 是 PostgreSQL 函数,不是所有数据库通用的标准写法。MySQL 常见写法是按 YEAR(created_at) 和 MONTH(created_at) 分组,或用 DATE_FORMAT 生成月份键;SQL Server、Oracle 等也有各自的日期函数。迁移数据库时,应确认函数、时区处理和返回类型。

时间戳还涉及时区边界。若数据以 UTC 保存,而报表按当地自然月统计,应先明确转换时区的规则,否则接近午夜的记录可能被归入不同日期或月份。筛选时间范围时,推荐使用左闭右开区间,例如 created_at >= 起始时刻 AND created_at < 下一周期起始时刻,避免通过“结束日 23:59:59”漏掉更高精度的时间值。

七、条件聚合:一次查询统计多个指标

条件聚合适合在同一分组中并列计算多类指标。PostgreSQL 支持聚合函数的 FILTER 子句:

SELECT
    department,
    COUNT(*) AS all_orders,
    COUNT(*) FILTER (WHERE status = 'paid') AS paid_orders,
    SUM(amount) FILTER (WHERE status = 'paid') AS paid_amount,
    COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled_orders
FROM orders
GROUP BY department
ORDER BY department;

在其他数据库中,可以用 CASE 配合聚合函数实现类似效果。例如:

SELECT
    department,
    COUNT(CASE WHEN status = 'paid' THEN 1 END) AS paid_orders,
    SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS paid_amount
FROM orders
GROUP BY department;

这类写法的细节仍取决于数据和目标数据库。比如条件金额全部为空时,SUM 的结果可能是 NULL;需要零值时,可在外层使用 COALESCE。不要把 ELSE 0 机械套用到所有指标,因为它会改变空值参与计算的方式。

八、JOIN 后计数被放大的常见原因

聚合错误不一定来自聚合函数本身。假设一个订单可以有多条明细记录,订单表与明细表是一对多关系。连接后,一张订单会重复出现多次;此时直接 COUNT(orders.order_id) 统计的是连接结果中的行数,而非订单数。

SELECT
    o.department,
    COUNT(o.order_id) AS joined_rows
FROM orders AS o
JOIN order_items AS i
    ON i.order_id = o.order_id
GROUP BY o.department;

若一个订单有三条明细,它可能贡献三行。要统计订单数,常见做法是使用 COUNT(DISTINCT o.order_id),但还要确认连接条件和“订单数”的定义。若同时要汇总订单金额,连接造成的重复也可能让 SUM(o.amount) 被放大;简单改成 SUM(DISTINCT o.amount) 并不可靠,因为不同订单可能恰好金额相同。

更稳妥的做法通常是先分别聚合到合适的粒度,再连接汇总结果;或者先检查每张表的主键、关系基数和目标统计粒度。每个聚合指标都应问一句:“一条业务实体在当前结果中会出现几次?”

九、分组查询的语法约束与可读性

分组查询的 SELECT 列表通常只能包含分组键、聚合表达式,以及数据库能够确认由分组键函数依赖确定的列。为了跨数据库保持清晰,最稳妥的习惯是:非聚合列显式写进 GROUP BY;其余列放在聚合函数中。

例如,不能随意把 customer_id 放入只按 department 分组的查询,因为一个部门通常有多个客户,数据库无法知道应该返回哪一个客户编号。不同数据库对函数依赖和别名的接受程度不完全一致。尽量不要依赖某个数据库的宽松行为,也不要把未分组的明细字段混进汇总结果。

ORDER BY 用来排序最终结果,不决定聚合如何计算。聚合结果没有默认顺序;需要稳定展示时,应明确指定排序键,必要时添加第二个排序键以处理并列值。

十、数值类型、精度和参数限制

聚合函数的参数类型与结果类型由数据库的类型规则决定,不同产品之间可能不同。对金额、税率等精确十进制数据,优先选用适合业务精度的定点数类型,例如 PostgreSQL 的 NUMERIC,并核实总位数和小数位数;浮点类型是近似数值,直接比较或累计时可能出现舍入差异。

AVG 的结果不保证与输入列类型完全相同;类型转换、溢出范围和小数精度要查目标数据库的文档。大量整数求和也可能超过原列类型的范围。数据库是否自动扩展结果类型、何时溢出以及如何显式转换,都有产品差异,不能假定一种写法适用于所有数据库。

聚合函数也有各自的参数形式:例如常见的 COUNT(*) 不接收列参数,COUNT(expression) 统计表达式的非空值;SUM、AVG 通常用于数值表达式,MIN、MAX 则要求参数类型具备相应的比较能力。可接受的类型、是否支持多参数或特殊扩展,要以具体数据库及版本文档为准。不要为了绕过类型错误而随意强制转换,转换可能改变精度或语义。

十一、参数绑定与 SQL 安全

聚合查询同样应使用参数化查询处理外部输入。参数绑定可以避免把用户提供的值直接拼入 SQL 文本,从而降低 SQL 注入风险;它不负责验证业务含义,也不会让查询自动变快。

例如,下面是概念性示例。占位符写法会随驱动程序变化,不能把 $1 直接复制到所有语言或数据库连接库中:

SELECT department, SUM(amount) AS total_amount
FROM orders
WHERE status = $1
GROUP BY department;

应用程序应通过数据库驱动提供的绑定接口,将状态值作为参数传入,而不是用字符串拼接生成条件。参数通常用于数据值,不可直接代替表名、列名、排序方向等 SQL 标识符;这些动态结构应采用允许列表映射到固定 SQL 片段,而不是接受任意输入后拼接。占位符语法、参数类型推断和可绑定对象范围取决于驱动与数据库。

聚合函数本身不是安全机制。它不会清理输入、授权访问或防止注入;安全边界仍依赖参数化查询、权限控制、输入规则和正确的动态 SQL 构造。只读统计查询也应使用符合最小权限原则的数据库账号。

十二、性能与排查思路

  • 先限定数据范围。 能在分组前筛选的条件尽量放在 WHERE,例如状态和时间范围。筛选是否能利用索引,取决于查询、索引、数据分布和优化器,不能仅凭 SQL 外观保证。
  • 只取需要的列。 汇总查询通常无需 SELECT *。明确列出分组键和指标,更容易检查统计口径。
  • 检查连接粒度。 先确认连接前后行数和键的唯一性,再核对计数或求和是否被一对多关系重复放大。
  • 用小样本验证边界。 测试空表、全为 NULL、筛选后无行、重复键和极大数值等情况,确认返回值符合业务预期。
  • 用执行计划定位瓶颈。 聚合可能需要扫描、排序或哈希分组;具体计划由数据库决定。通过目标数据库的执行计划工具观察实际访问路径,不要为了“优化”而省略必要的筛选或改变统计口径。

总结

使用聚合函数统计分组数据,可以按以下步骤组织查询:

  1. 明确统计粒度:每行结果代表一个部门、一个月份,还是一个部门与状态的组合。
  2. 明确过滤阶段:筛选明细行用 WHERE,筛选聚合后的组用 HAVING。
  3. 明确空值口径:区分 COUNT(*)、COUNT(列) 和 COUNT(DISTINCT 列),确认金额缺失是否应参与计算。
  4. 检查连接是否改变行数,避免重复计数和重复求和。
  5. 确认目标数据库的类型、日期函数、参数占位符和聚合行为,并用参数绑定处理外部值。

只要统计口径、分组键和数据粒度定义清楚,COUNT、SUM、AVG、MIN、MAX 就能组成可靠的汇总查询。真正容易出错的,往往不是函数名称,而是对空值、连接关系、筛选时机和数值类型的误解。

© 版权声明

相关文章