4. SQL 基础回顾(二):聚合函数与分组查询

在实时监控大屏的数据集成中,我们经常需要从原始数据中提取统计指标,例如“过去1小时的平均响应时间”、“今日订单总量”或“各区域的最高并发数”。SQL 聚合函数与分组查询正是实现这类统计的核心工具。本节将系统回顾 COUNT、SUM、AVG、MAX、MIN 五大聚合函数,以及 GROUP BY 分组与 HAVING 过滤分组的完整用法。

💡 核心概念:聚合函数将多行数据压缩为单个统计值;GROUP BY 将数据按列分组后分别聚合;HAVING 则用于过滤分组后的结果,类似于 WHERE 但作用于分组。

4.1 五大聚合函数详解

聚合函数(Aggregate Functions)对一组值执行计算并返回单个值。它们通常与 SELECT 语句配合使用,是生成 KPI 指标的基石。

函数 语法示例 返回值 常见应用场景(监控大屏)
COUNT COUNT(column) 或 COUNT(*) 非 NULL 值的行数 / 总行数 总请求数、活跃用户数、错误日志条数
SUM SUM(column) 数值列的总和 总销售额、累计带宽消耗、总交易金额
AVG AVG(column) 数值列的平均值 平均响应时间、平均 CPU 使用率、平均订单金额
MAX MAX(column) 数值/日期/字符串列的最大值 峰值并发数、最高延迟、最新时间戳
MIN MIN(column) 数值/日期/字符串列的最小值 最低响应时间、最早错误时间、最小库存量
⚠️ 注意 NULL 值处理:除 COUNT(*) 外,所有聚合函数都会忽略 NULL 值。例如 AVG(price) 只计算非 NULL 的价格行,而 COUNT(price) 也只统计 price 不为 NULL 的行数。

基础查询示例:假设有一张 api_metrics 表,记录了每次 API 调用的耗时与状态。

-- 查询总请求数、平均耗时、最大耗时、最小耗时
SELECT
    COUNT(*) AS total_requests,
    AVG(duration_ms) AS avg_duration,
    MAX(duration_ms) AS max_duration,
    MIN(duration_ms) AS min_duration
FROM api_metrics
WHERE status_code = 200;
聚合函数数据流:从原始行到单一统计值 原始数据行 id=1, dur=120 id=2, dur=95 id=3, dur=150 id=4, dur=80 id=5, dur=200 ... 共 N 行 聚合函数处理 COUNT → 计数 SUM → 求和 AVG → 平均 MAX → 最大值 MIN → 最小值 输出统计值 total_requests = 5 avg_duration = 129 max_duration = 200 min_duration = 80

4.2 GROUP BY 分组聚合

GROUP BY 子句将查询结果按一个或多个列进行分组,然后对每个分组独立应用聚合函数。这是实现“按维度统计”的核心语法,例如“按区域统计销售额”或“按服务名统计平均响应时间”。

语法结构:

SELECT
    group_column,
    aggregate_function(measure_column) AS alias
FROM table_name
[WHERE condition]
GROUP BY group_column
[ORDER BY alias];

实战案例:从 api_metrics 表中,按 service_name 分组,统计每个服务的调用次数与平均耗时。

SELECT
    service_name,
    COUNT(*) AS call_count,
    ROUND(AVG(duration_ms), 2) AS avg_duration_ms
FROM api_metrics
WHERE recorded_at >= NOW() - INTERVAL '1 hour'
GROUP BY service_name
ORDER BY call_count DESC;
📌 关键规则:SELECT 子句中出现的非聚合列(如 service_name)必须出现在 GROUP BY 子句中。否则数据库会报错,因为无法确定该列取哪个分组内的值。

多列分组:可以按多个维度进行分组,例如按“服务名 + 状态码”统计。

SELECT
    service_name,
    status_code,
    COUNT(*) AS count,
    AVG(duration_ms) AS avg_duration
FROM api_metrics
GROUP BY service_name, status_code
ORDER BY service_name, status_code;
GROUP BY 分组聚合流程 原始数据 svc=A, dur=100 svc=B, dur=200 svc=A, dur=150 svc=C, dur=80 svc=B, dur=180 svc=A, dur=120 svc=C, dur=90 svc=B, dur=220 GROUP BY 分组 分组键: service_name 组 A: {100, 150, 120} 组 B: {200, 180, 220} 组 C: {80, 90} 聚合结果(每分组一行) service_name | count | avg_dur A | 3 | 123.33 B | 3 | 200.00 C | 2 | 85.00

4.3 HAVING 过滤分组

HAVING 子句用于过滤 GROUP BY 之后的分组结果。它与 WHERE 的区别在于:WHERE 在分组前过滤行,HAVING 在分组后过滤分组。HAVING 中可以使用聚合函数,而 WHERE 中不能。

特性 WHERE HAVING
执行时机 分组之前,过滤原始行 分组之后,过滤分组
能否使用聚合函数 ❌ 不能 ✅ 可以(如 COUNT(*) > 10)
作用对象 表中的每一行 GROUP BY 产生的每个分组
典型场景 过滤时间范围、状态码等 过滤调用次数过少的服务

完整查询示例:查询过去1小时内,平均耗时超过150ms且调用次数大于5的服务。

SELECT
    service_name,
    COUNT(*) AS call_count,
    ROUND(AVG(duration_ms), 2) AS avg_duration_ms
FROM api_metrics
WHERE recorded_at >= NOW() - INTERVAL '1 hour'   -- WHERE 过滤原始行
GROUP BY service_name
HAVING COUNT(*) > 5                              -- HAVING 过滤分组
   AND AVG(duration_ms) > 150
ORDER BY avg_duration_ms DESC;
🚨 常见错误:初学者常将聚合条件误放在 WHERE 中,例如 WHERE COUNT(*) > 5。这会导致语法错误,因为 WHERE 子句在聚合之前执行,此时 COUNT(*) 尚未计算。务必使用 HAVING 来过滤分组。

4.4 执行顺序与综合案例

理解 SQL 子句的执行顺序,有助于写出正确且高效的查询。一个包含聚合、分组、过滤的完整查询,其逻辑执行顺序如下:

  1. FROM:确定数据源表
  2. WHERE:过滤原始行(排除不满足条件的行)
  3. GROUP BY:将剩余行按指定列分组
  4. HAVING:过滤分组(排除不满足条件的分组)
  5. SELECT:计算并输出列(包括聚合函数)
  6. ORDER BY:对结果排序
  7. LIMIT/OFFSET:限制输出行数

综合实战案例:从 orders 订单表中,统计每个城市中总金额超过10000元的客户数量,并只显示客户数大于3的城市。

SELECT
    city,
    COUNT(DISTINCT customer_id) AS high_value_customers,
    SUM(order_amount) AS total_revenue
FROM orders
WHERE order_status = 'completed'          -- 只统计已完成订单
  AND order_date >= '2024-01-01'          -- 只统计2024年后的数据
GROUP BY city
HAVING SUM(order_amount) > 10000          -- 城市总金额超过10000
   AND COUNT(DISTINCT customer_id) > 3    -- 且高价值客户数大于3
ORDER BY total_revenue DESC;
📊 监控大屏应用提示:在实时大屏中,GROUP BY 常用于按“时间窗口”(如每分钟、每小时)和“维度标签”(如服务名、区域、错误类型)进行聚合。HAVING 则用于过滤出异常分组,例如“平均延迟超过阈值的服务”或“错误率超过5%的端点”,这些过滤后的结果可以直接推送至告警看板。
SQL 子句逻辑执行顺序(聚合查询) ① FROM 确定数据源 ② WHERE 过滤行 ③ GROUP BY 分组 ④ HAVING 过滤分组 ⑤ SELECT 计算输出列 ⑥ ORDER BY 排序 ⑦ LIMIT 限制行数 💡 记忆口诀:FWGHSOL FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT HAVING 必须出现在 GROUP BY 之后,且可以使用聚合函数

4.5 常见陷阱与最佳实践

  • 陷阱1:SELECT 中非聚合列未包含在 GROUP BY 中。解决方案:确保所有非聚合列都出现在 GROUP BY 子句中。
  • 陷阱2:在 WHERE 中使用聚合函数。解决方案:将聚合条件移至 HAVING 子句。
  • 陷阱3:COUNT(column) 与 COUNT(*) 混淆。COUNT(*) 统计所有行,包括 NULL;COUNT(column) 只统计该列非 NULL 的行数。
  • 最佳实践1:为聚合列起别名,如 COUNT(*) AS total,提高可读性。
  • 最佳实践2:先使用 WHERE 过滤掉大量无关数据,减少 GROUP BY 的处理量,提升查询性能。
  • 最佳实践3:使用 DISTINCT 与 COUNT 结合,如 COUNT(DISTINCT user_id),统计唯一值数量。
🔧 调试技巧:在编写复杂分组查询时,可以先去掉 HAVING 和 ORDER BY,查看分组后的原始聚合结果,确认分组逻辑正确后再添加过滤和排序条件。