指标不是给字段换一个名字。每个指标都包含分析范围、数据粒度和计算公式。这一节先把订单明细聚合到订单粒度,再计算订单数、客户数、销售额、客单价、毛利和毛利率。
开始写 SQL 前,先把指标口径写成一张小字典。这样做看似多了一步,实际能避免“同名指标、不同算法”在 SQL 和 Excel 之间来回出现。
“统计对象”就是指标粒度。订单数的对象是订单,销售额的原始对象是订单明细;如果不先说明这一点,同一个订单含三件商品时,就很容易被误算成三笔订单。
WITH order_metrics AS (
SELECT
order_id,
customer_id,
order_date,
SUM(quantity) AS units,
ROUND(SUM(revenue), 2) AS order_revenue,
ROUND(SUM(gross_profit), 2) AS order_profit
FROM v_sales_detail
GROUP BY order_id, customer_id, order_date
)
SELECT *
FROM order_metrics
ORDER BY order_date, order_id
LIMIT 10;CTE order_metrics 的粒度是一行一订单。后面算客单价时,分母直接使用 COUNT(*),不会被订单中的商品行数干扰。
可以先做一个小检查,确认 CTE 真的是一行一订单:
WITH order_metrics AS (
SELECT order_id
FROM v_sales_detail
GROUP BY order_id, customer_id, order_date
)
SELECT
COUNT(*) AS rows_in_cte,
COUNT(DISTINCT order_id) AS distinct_orders
FROM order_metrics;两列都应是 509。若 rows_in_cte 更大,说明分组列让同一订单被拆开了;这时不要继续算客单价,先回头检查订单是否跨客户、跨日期,或是否误把 product_id 放进了分组。
WITH order_metrics AS (
SELECT
order_id,
customer_id,
SUM(revenue) AS order_revenue,
SUM(gross_profit) AS order_profit
FROM v_sales_detail
GROUP BY order_id, customer_id
)
SELECT
COUNT(*) AS orders,
COUNT(DISTINCT customer_id) AS customers,
ROUND(SUM(order_revenue), 2
客单价是订单销售额的平均值:
毛利率使用总毛利除以总销售额,不是逐行毛利率的平均值:
如果先计算每件商品的毛利率再取平均,小额商品和大额商品会得到相同权重,结果会偏离整体经营情况。
举个极端例子:商品 A 销售额 100 元、毛利率 10%,商品 B 销售额 900 元、毛利率 50%。直接平均得到 30%,但真实总毛利是 460 元,总销售额是 1000 元,因此整体毛利率是 46%。经营指标需要回答“每 1 元销售额留下多少毛利”,所以必须按金额加权。
查询结果中的 0.4928 是小数比例。SQL 中保留小数,交给 Excel 设置为 49.28%;不要在 SQL 中先乘 100 又在 Excel 中设为百分比,否则会显示成 4928%。
SELECT
city,
COUNT(DISTINCT order_id) AS orders,
COUNT(DISTINCT customer_id) AS customers,
ROUND(SUM(revenue), 2) AS revenue,
ROUND(
SUM(revenue) / COUNT(DISTINCT order_id),
2
) AS avg_order_value
FROM v_sales_detail
GROUP BY city
ORDER BY上海销售额最高,主要原因是订单更多;广州客单价更高,但订单规模较小。一个总量指标和一个效率指标放在一起,解释才不会停在“谁排名第一”。
这里可以用一个恒等式检查解释是否成立:
例如用表格中的显示值计算,上海约为 150 × 438.33 = 65749.50,与 65749.75 相差 0.25 元。这个小差异来自表格中客单价只显示两位小数,而 SQL 内部仍使用完整精度,不是数据错误。若差异很大,再检查订单数是否用了 DISTINCT、客单价是否和销售额使用同一筛选范围。
WITH customer_channel AS (
SELECT
channel,
customer_id,
COUNT(DISTINCT order_id) AS orders,
SUM(revenue) AS revenue
FROM v_sales_detail
GROUP BY channel, customer_id
)
SELECT
channel,
COUNT(*) AS customers,
ROUND(AVG(orders), 2) AS orders_per_customer,
这里先算“每位客户在一个渠道中的表现”,再按渠道求平均。如果直接对销售明细求平均,购买商品多的客户会产生更多行,从而获得更高权重。
这段 SQL 有两个粒度:customer_channel 是“一行一渠道一客户”,外层查询才是“一行一渠道”。阅读多层 SQL 时,可以在每个 CTE 旁边写一句粒度说明。只要某一层的粒度说不清,就先不要继续往下聚合。
还要注意,这里的客户数不能跨渠道相加。一位客户可能同时在小程序和门店购买,渠道客户数相加会重复;如果要汇报全局客户数,仍应单独使用 COUNT(DISTINCT customer_id)。
分组分析完成后,把各城市销售额相加,应该仍然等于 229909.45。
WITH city_metrics AS (
SELECT city, SUM(revenue) AS revenue
FROM v_sales_detail
GROUP BY city
)
SELECT ROUND(SUM(revenue), 2) AS checked_revenue
FROM city_metrics;这类对账很简单,却能及时发现连接重复、筛选范围变化或分组遗漏。下一节把指标放到时间轴上,观察月度变化和趋势。
完成这一节前,按下面的顺序验收:
order_metrics 的行数与去重订单数都等于 509。只有这些检查都通过,下一节的月度环比才有可信的基准。