一、什么是聚合查询
聚合查询是数据库中最常见的统计分析方式,它可以把表中的多行数据按照一定规则汇总成一行结果。例如,统计用户总数、计算订单金额总和、求某门课程的平均分等,都离不开聚合查询。
在 SQL 中,聚合查询主要依赖两类关键字:
- 聚合函数 :用于对某一列或多列的数据执行计算,如
COUNT、SUM、AVG、MAX、MIN。 - GROUP BY:用于把数据按指定字段分组,再对每个分组分别执行聚合计算。
下面先准备一张示例表,后续所有 SQL 都基于这份数据展开。
sql
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_name VARCHAR(50),
product_name VARCHAR(50),
category VARCHAR(50),
amount DECIMAL(10, 2),
status VARCHAR(20),
created_at DATE
);
INSERT INTO orders VALUES
(1, '张三', '手机', '电子产品', 3999.00, '已完成', '2024-01-05'),
(2, '李四', '键盘', '电脑配件', 299.00, '已完成', '2024-01-08'),
(3, '张三', '耳机', '电子产品', 599.00, '已完成', '2024-01-10'),
(4, '王五', '显示器', '电脑配件', 1599.00, '已取消', '2024-01-12'),
(5, '李四', '鼠标', '电脑配件', 129.00, '已完成', '2024-01-15'),
(6, '赵六', '平板', '电子产品', 2599.00, '已完成', '2024-01-18'),
(7, '张三', '充电器', '电子产品', 89.00, '已退货', '2024-01-20'),
(8, '王五', '机械键盘', '电脑配件', 799.00, '已完成', '2024-01-22');
二、五个核心聚合函数
聚合函数接收一列值,返回单个统计结果。最常用的五个聚合函数如下:
| 函数 | 作用 | 说明 |
|---|---|---|
COUNT |
统计行数 | 可以统计所有行,也可以使用 DISTINCT 去重 |
SUM |
求和 | 通常用于数值列 |
AVG |
求平均值 | 通常用于数值列 |
MAX |
求最大值 | 可用于数字、日期和字符串 |
MIN |
求最小值 | 可用于数字、日期和字符串 |
2.1 COUNT:统计行数
COUNT(*) 会统计所有行的数量,包括包含 NULL 的行;COUNT(column_name) 只统计该列不为 NULL 的行。
sql
-- 统计订单总条数
SELECT COUNT(*) AS total_orders
FROM orders;
-- 统计指定列不为 NULL 的行数
SELECT COUNT(status) AS status_count
FROM orders;
-- 统计去重后的客户数量
SELECT COUNT(DISTINCT customer_name) AS customer_count
FROM orders;
2.2 SUM:计算总和
SUM 用于对数值列求和,常用于金额、销量、积分等场景。
sql
-- 统计所有订单金额总和
SELECT SUM(amount) AS total_amount
FROM orders;
-- 统计已完成订单的金额总和
SELECT SUM(amount) AS completed_amount
FROM orders
WHERE status = '已完成';
2.3 AVG:计算平均值
AVG 返回指定数值列的平均值。需要注意,AVG 默认会忽略该列中的 NULL 值。
sql
-- 计算订单平均金额
SELECT AVG(amount) AS avg_amount
FROM orders;
-- 计算电子产品类目的平均金额
SELECT AVG(amount) AS avg_electronics_amount
FROM orders
WHERE category = '电子产品';
2.4 MAX 与 MIN:获取最大值和最小值
MAX 和 MIN 不仅适用于数字,也可以用于日期和字符串,返回最大或最小的值。
sql
-- 查询最高和最低订单金额
SELECT MAX(amount) AS max_amount,
MIN(amount) AS min_amount
FROM orders;
-- 查询最早和最晚的订单日期
SELECT MAX(created_at) AS latest_order,
MIN(created_at) AS earliest_order
FROM orders;
三、GROUP BY:分组聚合
如果只是对整个表执行聚合,结果只有一行。实际业务中,我们更希望按维度分组统计,例如按客户、按类目、按月份统计。GROUP BY 就是实现这一目标的关键。
3.1 按单字段分组
sql
-- 统计每个类目的订单数量和总金额
SELECT category,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY category;
执行后会得到每个类目的统计结果,每个类目一行。
3.2 按多字段分组
当需要更细的统计粒度时,可以同时按多个字段分组。
sql
-- 统计每个客户在每个类目下的订单数量和总金额
SELECT customer_name,
category,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_name, category;
3.3 注意 SELECT 列表的约束
使用 GROUP BY 时,SELECT 中出现的非聚合字段通常必须出现在 GROUP BY 中,否则数据库会报错。下面这条 SQL 在大多数数据库中是非法的:
sql
-- 错误示例:customer_name 未出现在 GROUP BY 中,MySQL 较老版本可能不报错但结果不可控
SELECT customer_name, category, COUNT(*)
FROM orders
GROUP BY category;
正确写法是把 customer_name 一并加入 GROUP BY,或者把它作为聚合函数的参数。
四、HAVING:过滤分组结果
WHERE 用于在分组前过滤行,HAVING 用于在分组后过滤聚合结果。这是聚合查询中最常见也最容易混淆的知识点。
sql
-- 统计每个客户的订单总金额,只保留订单总金额大于 1000 的客户
SELECT customer_name,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_name
HAVING SUM(amount) > 1000;
4.1 WHERE 与 HAVING 的执行顺序
SQL 的逻辑执行顺序大致为:FROM 找到数据源,WHERE 过滤行,GROUP BY 分组,然后执行聚合函数,最后 HAVING 过滤分组,再执行 SELECT。因此:
- 如果过滤条件作用于原始行,使用
WHERE。 - 如果过滤条件作用于聚合结果,使用
HAVING。
sql
-- 同时使用 WHERE 和 HAVING:
-- 先排除已取消订单,再统计每个客户订单数,仅保留订单数大于 1 的客户
SELECT customer_name,
COUNT(*) AS order_count
FROM orders
WHERE status <> '已取消'
GROUP BY customer_name
HAVING COUNT(*) > 1;
五、进阶聚合技巧
5.1 条件聚合
业务中经常需要在一行里同时展示多个统计口径,例如同时统计总订单数、已完成订单数和已取消订单数。可以结合 CASE WHEN 与聚合函数实现条件聚合。
sql
-- 按客户统计总订单数、已完成订单数和取消/退货订单数
SELECT customer_name,
COUNT(*) AS total_orders,
SUM(CASE WHEN status = '已完成' THEN 1 ELSE 0 END) AS completed_orders,
SUM(CASE WHEN status IN ('已取消', '已退货') THEN 1 ELSE 0 END) AS invalid_orders
FROM orders
GROUP BY customer_name;
5.2 分组汇总:ROLLUP
如果希望在分组结果的基础上再增加一行总计,可以使用 WITH ROLLUP(MySQL)或 ROLLUP(PostgreSQL、SQL Server 等)。
sql
-- MySQL 示例:按类目统计,最后额外输出一行所有类目的总计
SELECT category,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY category
WITH ROLLUP;
在上面的结果中,类目为 NULL 的那一行就是汇总行。如果希望显示为"总计",可以使用 COALESCE 处理。
sql
SELECT COALESCE(category, '总计') AS category,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY category
WITH ROLLUP;
5.3 使用 DISTINCT 去重统计
当某列存在重复值时,如果希望统计不同值的数量,可以使用 COUNT(DISTINCT column_name)。
sql
-- 统计购买过商品的客户数量
SELECT COUNT(DISTINCT customer_name) AS unique_customers
FROM orders;
-- 统计每个类目下购买过商品的客户数量
SELECT category,
COUNT(DISTINCT customer_name) AS unique_customers
FROM orders
GROUP BY category;
六、窗口函数:保留明细的聚合
普通聚合查询会把多行合并成一行,而窗口函数可以在保留每一行明细的同时计算聚合值,例如计算移动平均、排名、累计和等。
sql
-- 保留每一笔订单,同时计算每个客户的累计订单金额
SELECT customer_name,
id,
amount,
SUM(amount) OVER (PARTITION BY customer_name ORDER BY created_at) AS cumulative_amount
FROM orders
ORDER BY customer_name, created_at;
上面的查询使用 PARTITION BY 按客户分区,再按日期排序计算累计金额,适合查看用户消费增长趋势。
七、实战案例:销售数据统计
根据示例订单表,假设产品同学希望看到一个简单的销售概览,包含每条统计口径,可以这样写:
sql
SELECT
COUNT(*) AS total_orders,
COUNT(DISTINCT customer_name) AS unique_customers,
SUM(amount) AS total_sales,
AVG(amount) AS avg_order_value,
MAX(amount) AS max_order_amount,
MIN(amount) AS min_order_amount
FROM orders
WHERE status = '已完成';
如果需要按类目生成销售报表:
sql
SELECT
category AS 类目,
COUNT(*) AS 订单数,
COUNT(DISTINCT customer_name) AS 购买人数,
SUM(amount) AS 销售总额,
AVG(amount) AS 平均客单价,
MAX(amount) AS 最高订单金额,
MIN(amount) AS 最低订单金额
FROM orders
WHERE status = '已完成'
GROUP BY category
ORDER BY SUM(amount) DESC;
八、常见错误与优化建议
8.1 常见错误
- SELECT 列未出现在 GROUP BY 中 :非聚合列必须包含在
GROUP BY里,否则在严格模式下会报错。 - 混淆 WHERE 和 HAVING :聚合函数不能直接放在
WHERE中,例如WHERE SUM(amount) > 1000是错误的。 - 忽略 NULL 的影响 :
COUNT(column)、AVG(column)会忽略NULL,而COUNT(*)不会,统计行数时要注意。 - 使用中文别名时缺少引号:如果使用中文别名,建议用反引号包裹,避免兼容性问题。
8.2 性能优化建议
- 提前用 WHERE 缩小数据范围 :把能在原始行上过滤的条件放在
WHERE中,减少分组数据量。 - 为分组字段建立索引 :
GROUP BY和JOIN的字段加索引,可以显著提升查询速度。 - 避免大范围模糊统计:如果只需要部分统计结果,优先考虑分页、时间范围过滤或提前聚合的统计表。
- 谨慎使用 DISTINCT :在大表上执行
COUNT(DISTINCT column)可能消耗较多资源,必要时可考虑用近似统计或用索引优化。
九、总结
聚合查询是数据分析型 SQL 的核心能力。掌握以下要点,可以应对绝大多数统计需求:
- 用
COUNT、SUM、AVG、MAX、MIN完成基础统计。 - 用
GROUP BY按维度分组,用HAVING过滤分组后的结果。 - 理解
WHERE与HAVING的执行顺序,正确放置过滤条件。 - 使用
CASE WHEN条件聚合、ROLLUP汇总和窗口函数应对复杂统计场景。 - 注意
NULL值、分组字段约束和索引优化,避免常见错误和性能问题。
建议读者先在本地数据库执行一遍示例表,再逐步套用到自己的业务数据上,通过实际查询加深理解。