总销售额只能说明整个期间卖了多少,不能说明增长发生在什么时候。时间分析要先统一周期,再比较相邻周期,最后用移动平均减少单月波动。
销售视图已经准备了 order_month,可以直接按月分组:
SELECT
order_month,
COUNT(DISTINCT order_id) AS orders,
COUNT(DISTINCT customer_id) AS customers,
ROUND(SUM(revenue), 2) AS revenue,
ROUND(SUM(gross_profit), 2) AS gross_profit,
ROUND(
SUM(revenue) / COUNT(DISTINCT order_id),
2
) AS avg_order_value
FROM v_sales_detail
GROUP BY order_month
ORDER BY order_month;结果从 2024-01 连续到 2025-06,共 18 个月。先检查月份是否缺失。如果某月没有订单,这个查询不会自动生成该月,折线图会直接从前一个月连到后一个月。正式报表通常要准备日历表补齐空周期。
本项目恰好每个月都有完成订单,所以可以直接继续。如果检查结果出现断月,使用递归 CTE 生成连续月份,再左连接月度结果:
WITH RECURSIVE
bounds AS (
SELECT
MIN(order_month) AS min_month,
MAX(order_month) AS max_month
FROM v_sales_detail
), calendar(month_start, max_month) AS (
SELECT date(min_month || '-01'), date(max_month || '-01')
FROM bounds
UNION ALL
SELECT date(month_start, '+1 month'), max_month
FROM
这里将无订单月份补为 0,是因为“该月没有完成订单”本身就是一个可解释的经营事实。若月份缺失是因为数据尚未同步,则不能补 0,应先修复数据源,否则图表会把“未知”误画成“没有销售”。
LAG 能取得排序后上一行的值。月度数据一行一月,所以“上一行”就是上一个月。
WITH monthly AS (
SELECT
order_month,
COUNT(DISTINCT order_id) AS orders,
ROUND(SUM(revenue), 2) AS revenue
FROM v_sales_detail
GROUP BY order_month
), compared AS (
SELECT
*,
LAG(revenue) OVER (
ORDER BY order_month
) AS previous_revenue
第一个月没有上月数据,所以环比为空。不要把它填成 0;0 表示“与上月持平”,而这里是“无法比较”。
窗口函数不会减少行数。它先按 order_month 排序,再把上一行的销售额放到当前行。以 2024-02 为例:
为了防止上月销售额为 0,生产查询可以把分母写成 NULLIF(previous_revenue, 0)。结果为空比报出无意义的无穷大更诚实。同比则不是上一行,而是上一年同月,可以用 LAG(revenue, 12);只有数据至少覆盖 13 个月时,同比才开始有值。
三个月移动平均使用当前月和前两个月:
WITH monthly AS (
SELECT
order_month,
ROUND(SUM(revenue), 2) AS revenue
FROM v_sales_detail
GROUP BY order_month
)
SELECT
order_month,
revenue,
ROUND(
AVG(revenue) OVER (
ORDER BY order_month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
),
窗口开头不足三个月时,SQLite 会用已有月份计算平均值。如果报表要求“满三个月才显示”,可以再用 ROW_NUMBER() 判断行号。
WITH monthly AS (
SELECT order_month, SUM(revenue) AS revenue
FROM v_sales_detail
GROUP BY order_month
), numbered AS (
SELECT
*,
ROW_NUMBER() OVER (ORDER BY order_month) AS month_index,
AVG(revenue) OVER (
ORDER BY order_month
ROWS BETWEEN 2 PRECEDING AND
选择三个月不是统计定律,而是为了让短期波动变平,同时不把趋势拖得太迟。季节性强的业务还可以比较 12 个月移动平均;窗口越长,曲线越平滑,反应也越慢。
销售额上升可能来自更多订单,也可能来自更高客单价。因为“销售额 = 订单数 × 客单价”,看到异常月份后要同时比较两个驱动因素:
WITH monthly AS (
SELECT
order_month,
COUNT(DISTINCT order_id) AS orders,
SUM(revenue) / COUNT(DISTINCT order_id) AS aov
FROM v_sales_detail
GROUP BY order_month
), compared AS (
SELECT
*,
LAG(orders) OVER (ORDER BY order_month) AS previous_orders,
如果销售额上升、订单数上升而客单价下降,可以解释为“更多订单拉动了增长,但每单金额变小”;下一步应检查新增客户、渠道或促销。不要仅凭一条销售额折线就写“经营改善”。
后面会把月度结果导入 Excel。折线图使用“月份”为横轴、“销售额”为主线;订单数的量级较小,适合放在次坐标轴。
图表的用途是让变化更容易看见,不替代 SQL 结果。看到某个月上升或下降后,仍要回到订单、品类和客户维度查原因。
交给 Excel 的月度表保持“一行一月”,至少包含 order_month、orders、customers、revenue、gross_profit。客单价、毛利率、环比和移动平均会在 Excel 中再算一次:这不是重复劳动,而是让学习者看到数据库聚合结果如何变成可审阅的表格公式,并能与 SQL 口径互相对账。
用绝对值筛出环比变化超过 8% 的月份:
WITH monthly AS (
SELECT order_month, SUM(revenue) AS revenue
FROM v_sales_detail
GROUP BY order_month
), compared AS (
SELECT
*,
LAG(revenue) OVER (ORDER BY order_month) AS previous_revenue
FROM monthly
)
SELECT
order_month,
ROUND(revenue, 2) AS revenue,
8% 不是通用标准,只是本项目的观察阈值。真实业务应结合历史波动、预算容忍度和促销日历定义。下一节会转向商品,判断销售额由哪些品类和商品组合构成。
排查异常月时按“先确认、再拆分、后解释”的顺序:先确认数据是否完整,再拆订单数与客单价,然后按城市、渠道和品类下钻。只有当多个维度的证据能够解释总额变化时,才把原因写进结论;促销、节假日等外部事件若没有数据佐证,只能写成待验证假设。