1. 项目背景与需求分析
最近在力扣(LeetCode)上刷到一道SQL实战题——1565号题"按月统计订单数与顾客数"。这道题看似简单,却涵盖了数据分析师日常工作中最基础的统计需求。作为电商数据分析的经典场景,按月统计的核心价值在于帮助业务方掌握经营趋势,识别销售淡旺季,优化库存和营销策略。
题目要求从订单表中提取两个关键指标:
- 每月订单总数(反映业务规模)
- 每月下单顾客数(反映用户活跃度)
这两个指标的组合分析能揭示重要业务洞察。例如:
- 订单数增长但顾客数持平:说明老客复购率提升
- 顾客数增长但订单数持平:可能新客转化效果不佳
- 两者同步增长:业务健康扩张
2. 数据模型解析
假设我们有一个标准的订单表orders,其典型结构如下:
CREATE TABLE orders ( order_id INT PRIMARY KEY, customer_id INT NOT NULL, order_date DATE NOT NULL, amount DECIMAL(10,2) );关键字段说明:
order_date:需要从中提取年月信息customer_id:用于统计独立顾客数- 注意:实际业务中可能还需要考虑订单状态(如已取消订单不应计入)
3. SQL解决方案详解
3.1 基础实现方案
最直接的实现方式是使用DATE_FORMAT或EXTRACT函数提取年月,然后进行分组统计:
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM orders GROUP BY DATE_FORMAT(order_date, '%Y-%m') ORDER BY month;3.2 进阶优化方案
对于大型数据集,我们可以优化查询性能:
SELECT EXTRACT(YEAR_MONTH FROM order_date) AS year_month, COUNT(*) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM orders WHERE order_status = 'completed' -- 只统计有效订单 GROUP BY year_month ORDER BY year_month;优化点说明:
- 使用
EXTRACT函数比DATE_FORMAT性能更好 - 添加
WHERE条件过滤无效订单 YEAR_MONTH直接返回整数格式(如202307),便于排序
3.3 处理边界情况
实际业务中还需考虑:
- 跨年数据:确保年份和月份组合正确
- 空值处理:使用
COALESCE保证结果完整 - 日期范围:可以添加
BETWEEN条件限制统计周期
改进后的健壮版本:
SELECT CONCAT( EXTRACT(YEAR FROM order_date), '-', LPAD(EXTRACT(MONTH FROM order_date), 2, '0') ) AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM orders WHERE order_date BETWEEN '2022-01-01' AND '2023-12-31' AND order_status = 'completed' GROUP BY EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date) ORDER BY EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date);4. 性能优化技巧
4.1 索引策略
为提高查询效率,建议创建复合索引:
CREATE INDEX idx_orders_date_status ON orders(order_date, order_status);对于顾客数统计,可以额外添加:
CREATE INDEX idx_orders_customer ON orders(customer_id);4.2 分区表方案
当数据量极大时(如亿级记录),考虑按月分区:
CREATE TABLE orders ( ... ) PARTITION BY RANGE (TO_DAYS(order_date)) ( PARTITION p202201 VALUES LESS THAN (TO_DAYS('2022-02-01')), PARTITION p202202 VALUES LESS THAN (TO_DAYS('2022-03-01')), ... );4.3 物化视图
对于频繁执行的统计查询,可以使用物化视图定期刷新:
CREATE MATERIALIZED VIEW monthly_stats AS SELECT ... (同上查询语句) REFRESH COMPLETE EVERY 1 DAY;5. 业务应用扩展
5.1 同比环比分析
在基础统计上增加业务分析维度:
SELECT month, order_count, customer_count, LAG(order_count, 12) OVER (ORDER BY month) AS prev_year_order, ROUND((order_count - LAG(order_count, 12) OVER (ORDER BY month)) / LAG(order_count, 12) OVER (ORDER BY month) * 100, 2) AS yoy_rate FROM ( -- 基础统计查询 ) t ORDER BY month;5.2 顾客分层统计
分析新老客贡献:
WITH first_orders AS ( SELECT customer_id, MIN(order_date) AS first_order_date FROM orders GROUP BY customer_id ) SELECT DATE_FORMAT(o.order_date, '%Y-%m') AS month, COUNT(DISTINCT CASE WHEN DATE_FORMAT(o.order_date, '%Y-%m') = DATE_FORMAT(f.first_order_date, '%Y-%m') THEN o.customer_id END) AS new_customers, COUNT(DISTINCT CASE WHEN DATE_FORMAT(o.order_date, '%Y-%m') > DATE_FORMAT(f.first_order_date, '%Y-%m') THEN o.customer_id END) AS repeat_customers FROM orders o JOIN first_orders f ON o.customer_id = f.customer_id GROUP BY DATE_FORMAT(o.order_date, '%Y-%m') ORDER BY month;6. 常见问题与解决方案
6.1 时区问题
当数据来自不同时区时:
SELECT DATE_FORMAT(CONVERT_TZ(order_date, '+00:00', '+08:00'), '%Y-%m') AS month, ...6.2 性能瓶颈
当统计速度变慢时检查:
- 是否使用了
DISTINCT导致临时表过大 - 是否缺少合适的索引
- 是否可以考虑采样统计代替全量统计
6.3 数据倾斜
某些月份数据量异常大的处理方案:
- 增加
WHERE条件分段查询 - 使用
/*+ MAX_EXECUTION_TIME(30000) */限制执行时间 - 考虑使用近似统计函数如
APPROX_COUNT_DISTINCT
7. 可视化建议
统计结果通常需要可视化呈现,推荐两种形式:
- 双轴折线图:左轴订单数,右轴顾客数,观察两者关系
- 热力图:横轴月份,纵轴年份,用颜色深浅表示增长幅度
SQL结果可以直接导出到可视化工具:
-- MySQL导出CSV SELECT ... INTO OUTFILE '/tmp/monthly_stats.csv' FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n';8. 不同数据库方言实现
8.1 PostgreSQL版本
SELECT TO_CHAR(order_date, 'YYYY-MM') AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM orders GROUP BY TO_CHAR(order_date, 'YYYY-MM') ORDER BY month;8.2 SQL Server版本
SELECT FORMAT(order_date, 'yyyy-MM') AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM orders GROUP BY FORMAT(order_date, 'yyyy-MM') ORDER BY month;8.3 Oracle版本
SELECT TO_CHAR(order_date, 'YYYY-MM') AS month, COUNT(order_id) AS order_count, COUNT(DISTINCT customer_id) AS customer_count FROM orders GROUP BY TO_CHAR(order_date, 'YYYY-MM') ORDER BY month;9. 实际业务思考
在真实电商场景中,按月统计只是最基础的维度。我们还需要考虑:
- 节假日影响:春节、双11等特殊月份需要单独分析
- 用户生命周期:统计不同注册时间的用户群体表现
- 商品维度:结合品类分析订单构成
- 地域维度:不同地区的销售趋势
一个完整的业务分析SQL可能包含多个CTE:
WITH monthly_stats AS (...), category_stats AS (...), region_stats AS (...) SELECT ... FROM monthly_stats JOIN category_stats ON ... JOIN region_stats ON ...10. 学习建议
要掌握这类时间序列统计,建议:
- 熟练使用各种日期函数
- 理解
GROUP BY的执行原理 - 掌握窗口函数用于同比环比分析
- 学习使用EXPLAIN分析查询计划
- 在力扣上练习相关题目:
- 餐馆营业额变化增长
- 净现值查询
- 最近的三笔订单