1. 问题背景与需求分析
最近在力扣(LeetCode)上刷到一道SQL实战题——1565号"按月统计订单数与顾客数"。这道题看似简单,却隐藏着不少实际业务场景中常见的统计陷阱。作为电商数据分析的基础操作,按月统计的核心价值在于帮助运营团队掌握业务波动趋势,识别季节性消费特征。
在实际电商系统中,订单表和顾客表往往存在一对多关系(一个顾客可能有多个订单)。统计时容易犯的错误包括:
- 重复计算同一顾客
- 忽略月份边界条件
- 错误处理NULL值
- 性能问题(特别是大表关联时)
提示:这道题在2023年字节跳动和美团的后端面试中都被改编使用过,考察候选人处理时间序列数据的能力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据模型与测试用例构建
2.1 标准表结构设计
假设我们有以下两张核心表(以MySQL语法为例):
sql复制CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
amount DECIMAL(10,2)
);
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
name VARCHAR(100),
registration_date DATE
);
2.2 典型测试数据
插入示例数据时需要注意边界情况:
sql复制INSERT INTO customers VALUES
(1, '张三', '2022-01-01'),
(2, '李四', '2022-02-15'),
(3, '王五', '2022-03-20');
INSERT INTO orders VALUES
(101, 1, '2022-01-10', 100.00),
(102, 1, '2022-01-20', 200.00),
(103, 2, '2022-02-01', 150.00),
(104, 3, '2022-03-15', 300.00),
(105, 3, '2022-03-25', 250.00),
(106, 1, '2022-04-05', 180.00);
特别注意:
- 1月份有2个订单(同个顾客)
- 2月份有1个订单
- 3月份有2个订单(同个顾客)
- 4月份有1个订单
3. 基础解决方案实现
3.1 按月统计订单数
sql复制SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
COUNT(order_id) AS order_count
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;
这个查询会输出:
code复制2022-01 | 2
2022-02 | 1
2022-03 | 2
2022-04 | 1
3.2 按月统计顾客数(去重)
sql复制SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
COUNT(DISTINCT customer_id) AS customer_count
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;
输出结果:
code复制2022-01 | 1
2022-02 | 1
2022-03 | 1
2022-04 | 1
注意:COUNT(DISTINCT)操作在大数据量时性能较差,后续会讨论优化方案
4. 进阶统计与分析技巧
4.1 合并查询与性能优化
使用CTE (Common Table Expression)可以避免重复计算:
sql复制WITH monthly_stats AS (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
order_id,
customer_id
FROM orders
)
SELECT
month,
COUNT(order_id) AS order_count,
COUNT(DISTINCT customer_id) AS customer_count
FROM monthly_stats
GROUP BY month
ORDER BY month;
4.2 包含零订单的月份
实际业务中常需要显示没有订单的月份:
sql复制WITH date_series AS (
SELECT DATE_FORMAT(date, '%Y-%m') AS month
FROM (
SELECT '2022-01-01' + INTERVAL n MONTH AS date
FROM (
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2
UNION SELECT 3 UNION SELECT 4 UNION SELECT 5
) numbers
WHERE '2022-01-01' + INTERVAL n MONTH <= '2022-06-01'
) dates
)
SELECT
ds.month,
COUNT(o.order_id) AS order_count,
COUNT(DISTINCT o.customer_id) AS customer_count
FROM date_series ds
LEFT JOIN orders o ON ds.month = DATE_FORMAT(o.order_date, '%Y-%m')
GROUP BY ds.month
ORDER BY ds.month;
4.3 新增顾客占比分析
计算每月新增顾客占当月下单顾客的比例:
sql复制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 o.customer_id) AS total_customers,
COUNT(DISTINCT CASE
WHEN DATE_FORMAT(fo.first_order_date, '%Y-%m') = DATE_FORMAT(o.order_date, '%Y-%m')
THEN o.customer_id END) AS new_customers,
ROUND(COUNT(DISTINCT CASE
WHEN DATE_FORMAT(fo.first_order_date, '%Y-%m') = DATE_FORMAT(o.order_date, '%Y-%m')
THEN o.customer_id END) * 100.0 /
COUNT(DISTINCT o.customer_id), 2) AS new_customer_ratio
FROM orders o
JOIN first_orders fo ON o.customer_id = fo.customer_id
GROUP BY DATE_FORMAT(o.order_date, '%Y-%m')
ORDER BY month;
5. 生产环境优化实践
5.1 索引优化策略
对于大型电商平台,订单表可能包含数千万条记录。推荐索引方案:
sql复制ALTER TABLE orders ADD INDEX idx_order_date (order_date);
ALTER TABLE orders ADD INDEX idx_customer_date (customer_id, order_date);
5.2 分区表方案
按月分区可以显著提升查询性能:
sql复制CREATE TABLE orders_partitioned (
order_id INT,
customer_id INT,
order_date DATE,
amount DECIMAL(10,2),
PRIMARY KEY (order_id, order_date)
) 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')),
PARTITION p202203 VALUES LESS THAN (TO_DAYS('2022-04-01')),
PARTITION p202204 VALUES LESS THAN (TO_DAYS('2022-05-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
5.3 物化视图方案
对于频繁执行的统计查询,可以使用物化视图(MySQL 8.0+):
sql复制CREATE TABLE monthly_stats_mv (
month VARCHAR(7) PRIMARY KEY,
order_count INT,
customer_count INT,
last_refreshed TIMESTAMP
);
-- 刷新存储过程
DELIMITER //
CREATE PROCEDURE refresh_monthly_stats()
BEGIN
TRUNCATE TABLE monthly_stats_mv;
INSERT INTO monthly_stats_mv
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
COUNT(order_id) AS order_count,
COUNT(DISTINCT customer_id) AS customer_count,
NOW() AS last_refreshed
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m');
END //
DELIMITER ;
6. 跨数据库解决方案
6.1 PostgreSQL实现
PostgreSQL的日期函数略有不同:
sql复制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;
6.2 SQL Server实现
SQL Server使用不同的日期格式化函数:
sql复制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;
6.3 Oracle实现
Oracle使用TO_CHAR函数处理日期:
sql复制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;
7. 常见问题与解决方案
7.1 时区问题处理
当系统跨时区时,需要统一时区计算:
sql复制-- 假设存储的是UTC时间,需要转换为东八区
SELECT
DATE_FORMAT(CONVERT_TZ(order_date, '+00:00', '+08:00'), '%Y-%m') AS month,
COUNT(order_id) AS order_count
FROM orders
GROUP BY DATE_FORMAT(CONVERT_TZ(order_date, '+00:00', '+08:00'), '%Y-%m')
ORDER BY month;
7.2 大数据量分页优化
当月数据量极大时,避免使用OFFSET:
sql复制-- 使用游标分页
SELECT * FROM orders
WHERE order_date >= '2022-01-01'
AND order_date < '2022-02-01'
AND order_id > ? -- 上一页最后一条记录的ID
ORDER BY order_id
LIMIT 100;
7.3 NULL值处理
处理可能的NULL日期:
sql复制SELECT
DATE_FORMAT(COALESCE(order_date, '1970-01-01'), '%Y-%m') AS month,
COUNT(order_id) AS order_count
FROM orders
GROUP BY DATE_FORMAT(COALESCE(order_date, '1970-01-01'), '%Y-%m')
ORDER BY month;
8. 可视化与业务应用
8.1 月度趋势图
使用查询结果生成折线图:
- X轴:月份
- Y轴左:订单数(柱状图)
- Y轴右:顾客数(折线图)
8.2 关键业务指标
基于月度统计可以计算:
- 客单价 = 总金额 / 订单数
- 复购率 = (顾客数 - 新顾客数) / 顾客数
- 订单集中度 = 最大单日订单数 / 月订单数
8.3 预警机制
设置业务规则:
- 当月订单数环比下降超过20% → 触发预警
- 新顾客占比低于15% → 检查获客渠道
- 客单价波动超过2个标准差 → 分析产品组合
我在实际电商系统开发中发现,按月统计看似简单,但要做到生产环境可用的高性能实现需要综合考虑多种因素。特别是在大促期间,订单表写入压力大时,统计查询需要特别优化。一个实用的技巧是预先计算好上月的统计结果,与当月实时数据结合展示,既保证及时性又减轻数据库压力。
