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;
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;
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 子句的执行顺序,有助于写出正确且高效的查询。一个包含聚合、分组、过滤的完整查询,其逻辑执行顺序如下:
- FROM:确定数据源表
- WHERE:过滤原始行(排除不满足条件的行)
- GROUP BY:将剩余行按指定列分组
- HAVING:过滤分组(排除不满足条件的分组)
- SELECT:计算并输出列(包括聚合函数)
- ORDER BY:对结果排序
- 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%的端点”,这些过滤后的结果可以直接推送至告警看板。
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,查看分组后的原始聚合结果,确认分组逻辑正确后再添加过滤和排序条件。