1. 问题背景与需求分析
最近在刷力扣数据库题目时,遇到了1565题"按月统计订单数与顾客数"。这道题考察的是SQL中的日期处理和聚合统计能力,看似简单但有几个关键点需要注意。作为电商数据分析的常见场景,这类查询在实际工作中非常实用。
核心需求是从订单表中按月统计满足特定条件的:
- 唯一订单数(order_count)
- 唯一顾客数(customer_count)
筛选条件是invoice金额大于20美元。题目特别强调要按月份分组,且结果中的月份格式应为"YYYY-MM"。
2. 数据表结构与理解
首先我们来看题目提供的表结构:
sql复制+---------------+---------+
| Column Name | Type |
+---------------+---------+
| order_id | int |
| order_date | date |
| customer_id | int |
| invoice | int |
+---------------+---------+
关键字段说明:
- order_id:主键,唯一标识每个订单
- order_date:订单日期,格式为YYYY-MM-DD
- customer_id:顾客标识
- invoice:订单金额(美元)
注意:题目明确order_id是唯一值列,这意味着不会有重复订单,因此我们可以放心使用COUNT(*)或COUNT(order_id)来统计订单数。
3. 解题思路拆解
3.1 日期格式化处理
原始数据中的order_date是完整的日期格式(YYYY-MM-DD),但我们需要按月统计,因此需要提取年月部分。不同数据库系统的处理方式略有差异:
MySQL(力扣默认环境):
sql复制DATE_FORMAT(order_date, '%Y-%m') -- 标准格式化函数
-- 或
LEFT(order_date, 7) -- 简单截取前7位
Oracle/PostgreSQL:
sql复制TO_CHAR(order_date, 'YYYY-MM') -- Oracle/PostgreSQL的标准函数
SQL Server:
sql复制FORMAT(order_date, 'yyyy-MM') -- SQL Server 2012+版本
-- 或
CONVERT(varchar(7), order_date, 120) -- 兼容旧版本
3.2 数据筛选条件
题目要求只统计invoice大于20的订单,这需要在聚合前过滤:
sql复制WHERE invoice > 20
重要提示:WHERE子句必须在GROUP BY之前执行,这是SQL的执行顺序决定的。如果放在HAVING中,虽然语法正确但效率较低。
3.3 聚合统计逻辑
这里有两个统计维度需要特别注意:
-
订单数统计:
- 可以直接使用COUNT(*)统计所有满足条件的记录数
- 也可以使用COUNT(order_id),由于order_id是唯一的,结果相同
-
顾客数统计:
- 必须使用COUNT(DISTINCT customer_id),因为同一顾客可能在一个月内有多个订单
- 如果使用COUNT(customer_id)会重复计算同一顾客
4. 完整SQL实现
基于PostgreSQL语法(题目示例中使用的是TO_CHAR函数):
sql复制SELECT
TO_CHAR(order_date, 'YYYY-MM') AS month,
COUNT(*) AS order_count,
COUNT(DISTINCT customer_id) AS customer_count
FROM
orders
WHERE
invoice > 20
GROUP BY
TO_CHAR(order_date, 'YYYY-MM')
ORDER BY
month;
5. 执行结果验证
让我们用题目提供的示例数据验证查询结果:
输入数据:
code复制| order_id | order_date | customer_id | invoice |
|----------|------------|-------------|---------|
| 1 | 2020-09-15 | 1 | 30 |
| 2 | 2020-09-17 | 2 | 90 |
| 3 | 2020-10-06 | 3 | 20 |
| 4 | 2020-10-20 | 3 | 21 |
| 5 | 2020-11-10 | 1 | 10 |
| 6 | 2020-11-21 | 2 | 15 |
| 7 | 2020-12-01 | 4 | 55 |
| 8 | 2020-12-03 | 4 | 77 |
| 9 | 2021-01-07 | 3 | 31 |
| 10 | 2021-01-15 | 2 | 20 |
预期输出:
code复制| month | order_count | customer_count |
|---------|-------------|----------------|
| 2020-09 | 2 | 2 |
| 2020-10 | 1 | 1 |
| 2020-12 | 2 | 1 |
| 2021-01 | 1 | 1 |
验证过程:
- 2020-09月:订单1(30)和订单2(90)都满足>20,来自顾客1和2 → 2订单,2顾客
- 2020-10月:只有订单4(21)满足>20(订单3金额=20不满足),来自顾客3 → 1订单,1顾客
- 2020-11月:订单5(10)和6(15)都不满足 → 不显示
- 2020-12月:订单7(55)和8(77)都满足>20,但都来自顾客4 → 2订单,1顾客
- 2021-01月:只有订单9(31)满足>20(订单10金额=20不满足),来自顾客3 → 1订单,1顾客
6. 常见问题与优化建议
6.1 边界条件处理
问题1:invoice=20的订单是否应该包含?
- 题目明确要求"大于20",所以不包含等于20的情况
- 实际业务中要特别注意这种边界条件
问题2:如果某个月没有满足条件的订单,是否显示?
- 当前查询不会显示零记录的月份
- 如果需要显示所有月份(包括零记录),需要构建月份维度表进行LEFT JOIN
6.2 性能优化建议
-
索引设计:
- 为order_date和invoice字段创建复合索引,加速WHERE条件过滤
- 例如:CREATE INDEX idx_orders_date_invoice ON orders(order_date, invoice)
-
大数据量处理:
- 对于历史数据量大的表,可以考虑按月分区表
- 使用物化视图预计算统计结果
-
执行计划检查:
- 使用EXPLAIN ANALYZE查看查询计划
- 确保使用了正确的索引,避免全表扫描
6.3 业务扩展思考
实际业务中,类似的统计需求可能更复杂:
-
多维度分析:
sql复制-- 按年月和顾客等级统计 SELECT TO_CHAR(order_date, 'YYYY-MM') AS month, customer_level, COUNT(*) AS order_count, SUM(invoice) AS total_amount FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE invoice > 20 GROUP BY TO_CHAR(order_date, 'YYYY-MM'), customer_level; -
同比环比分析:
sql复制-- 计算月度环比增长率 WITH monthly_stats AS ( SELECT TO_CHAR(order_date, 'YYYY-MM') AS month, COUNT(*) AS order_count FROM orders WHERE invoice > 20 GROUP BY TO_CHAR(order_date, 'YYYY-MM') ) SELECT current.month, current.order_count, prev.order_count AS prev_month_count, (current.order_count - prev.order_count) * 100.0 / NULLIF(prev.order_count, 0) AS growth_rate FROM monthly_stats current LEFT JOIN monthly_stats prev ON current.month = TO_CHAR( TO_DATE(prev.month, 'YYYY-MM') + INTERVAL '1 month', 'YYYY-MM' ) ORDER BY current.month;
7. 不同数据库的语法差异
在实际工作中,我们需要适配不同的数据库系统。以下是主要数据库的实现差异:
7.1 MySQL实现
sql复制SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
COUNT(*) AS order_count,
COUNT(DISTINCT customer_id) AS customer_count
FROM
orders
WHERE
invoice > 20
GROUP BY
DATE_FORMAT(order_date, '%Y-%m')
ORDER BY
month;
7.2 SQL Server实现
sql复制SELECT
FORMAT(order_date, 'yyyy-MM') AS month,
COUNT(*) AS order_count,
COUNT(DISTINCT customer_id) AS customer_count
FROM
orders
WHERE
invoice > 20
GROUP BY
FORMAT(order_date, 'yyyy-MM')
ORDER BY
month;
7.3 BigQuery实现
sql复制SELECT
FORMAT_DATE('%Y-%m', order_date) AS month,
COUNT(*) AS order_count,
COUNT(DISTINCT customer_id) AS customer_count
FROM
orders
WHERE
invoice > 20
GROUP BY
FORMAT_DATE('%Y-%m', order_date)
ORDER BY
month;
8. 实际业务中的应用场景
这类按月统计查询在实际业务中非常常见,例如:
-
电商运营分析:
- 监控高价值订单(金额>20可视为过滤条件)的月度趋势
- 分析优质顾客(高频高额)的留存情况
-
财务报表生成:
- 月度销售报表的基础统计
- 客户贡献度分析
-
营销效果评估:
- 促销活动后的订单增长分析
- 不同顾客分群的响应情况
我在实际工作中发现,这类查询经常需要进一步扩展:
- 添加更多筛选条件(如产品类别、地区)
- 计算衍生指标(如客单价、复购率)
- 与其它表关联(如顾客属性、产品信息)
9. 性能优化实战技巧
经过多次性能调优,我总结出几个实用技巧:
-
避免在WHERE条件中使用函数:
sql复制-- 不推荐:索引无法生效 WHERE YEAR(order_date) = 2020 AND MONTH(order_date) = 9 -- 推荐:使用范围查询 WHERE order_date >= '2020-09-01' AND order_date < '2020-10-01' -
使用CTE提高可读性:
sql复制WITH filtered_orders AS ( SELECT order_id, customer_id, TO_CHAR(order_date, 'YYYY-MM') AS month FROM orders WHERE invoice > 20 ) SELECT month, COUNT(*) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM filtered_orders GROUP BY month ORDER BY month; -
分区表策略:
- 对于大型订单表,可以按range分区(按月)
- 这样查询特定月份数据时只需扫描对应分区
10. 常见错误与排查方法
在解决这类问题时,新手常犯的错误包括:
-
忘记DISTINCT导致顾客数统计错误:
sql复制-- 错误:会重复计算同一顾客的多个订单 COUNT(customer_id) AS customer_count -- 正确:使用DISTINCT去重 COUNT(DISTINCT customer_id) AS customer_count -
错误理解日期格式化:
sql复制-- 错误:在不同数据库中语法可能不兼容 SUBSTRING(order_date, 1, 7) AS month -- 正确:使用数据库特定的日期函数 TO_CHAR(order_date, 'YYYY-MM') AS month -
筛选条件位置错误:
sql复制-- 错误:在HAVING中过滤,效率低 GROUP BY month HAVING invoice > 20 -- 正确:在WHERE中提前过滤 WHERE invoice > 20 GROUP BY month
排查方法:
- 先验证基础查询结果是否正确
- 检查聚合函数是否按预期工作
- 使用LIMIT或子查询逐步调试复杂查询