数据粒度与 mart:计算每位客户的消费
目标:生成一位客户一行的消费汇总,并保留没有成交订单的客户。
目前你有四位客户、五笔订单,但报表要的是“每位客户消费多少”。先回答一行代表什么,这叫数据粒度:订单表一行代表一笔订单,客户汇总一行代表一位客户。连接两个表时如果忘了粒度,很容易把金额重复相加。
本课先将 completed 订单按客户聚合,再连接客户表。这样连接两侧都至多是一位客户一行。未来如果引入“一笔订单多次付款”,不能直接把付款表与订单表连接后再对订单总金额求和;要先把付款聚合到合适粒度。文件划分服务于这个含义,不是目录越多越专业。
写出完整的报表模型
创建 models/marts/customer_summary.sql:
with completed_orders as (
select customer_id, amount_cents
from {{ ref('stg_orders') }}
where status = 'completed'
),
customer_totals as (
select
customer_id,
count(*) as completed_order_count,
sum(amount_cents) as revenue_cents
from completed_orders
group by customer_id
)
select
c.customer_id,
c.customer_name,
coalesce(t.completed_order_count, 0) as completed_order_count,
coalesce(t.revenue_cents, 0) as revenue_cents,
cast(coalesce(t.revenue_cents, 0) / 100.0 as decimal(12,2)) as revenue
from {{ ref('stg_customers') }} as c
left join customer_totals as t
on c.customer_id = t.customer_id
with 给中间查询起名字,叫 CTE;它们只在本条 SQL 内可见,不会自动成为独立 dbt 模型。第一个 CTE 保留成交订单,第二个通过 group by 把同一客户的订单归在一起,计算笔数与金额。
主查询中的 c、t 是表的简写。left join 保留左边所有客户,即使右边没匹配行。没有匹配时右侧列是 null,表示缺失,不能简单当作数字 0;这里业务上明确“没有成交就是零消费”,所以用 coalesce(x, 0) 把缺失转换成零。把筛选放在第一个 CTE,可以避免在连接后用 where 意外删掉无订单客户。
先手算 Ada、Bo、Chen、Dee 的结果,再运行:
uv run dbt seed --profiles-dir .
uv run dbt run --profiles-dir . --select +customer_summary
uv run dbt show --profiles-dir . --inline "select * from {{ ref('customer_summary') }} order by customer_id" --limit 10
预期结果的数值如下:
customer_id | customer_name | completed_order_count | revenue_cents | revenue
1 | Ada | 1 | 1200 | 12.00
2 | Bo | 2 | 4000 | 40.00
3 | Chen | 0 | 0 | 0.00
4 | Dee | 0 | 0 | 0.00
Chen 有订单但没有 completed 订单,Dee 根本没有订单,两人都会保留。总金额 52.00 元。+customer_summary 包含两个 staging 祖先;第 5 课之后 seed 不在这条祖先链上,所以先装载输入仍然必要。
亲眼看到连接方式的影响
- 将模型中的
left join改为inner join,其他部分不动。 - 重新 run,再 show,应只剩 Ada、Bo 两行,总额仍是 52 元。
- 解释为什么“总金额没错”仍不能证明报表正确:它丢失了没有成交的客户。
- 恢复
left join,重新 run,应回到四行。
独立练习:只用一条预览 SQL,计算报表中的客户总数,以及成交订单笔数之和。不要修改模型。预期应该是多少?
参考答案
uv run dbt show --profiles-dir . --inline "select count(*) as customers, sum(completed_order_count) as orders from {{ ref('customer_summary') }}"
应为 customers = 4、orders = 3。这个检查同时观察客户粒度和订单数量,能发现只看总金额会遗漏的问题。
若 Ada 是 20 元,先查 returned 是否被计入;若结果缺少 Dee,检查连接类型和连接后的过滤;若同一客户出现多行,检查客户表是否重复,或分组时是否多放了一个字段。不要用 distinct 无条件掩盖重复,它可能只是让错误更难发现。
本课保留 customer_summary.sql 的 left join 主版本。现在项目已经产出可读的报表,接下来的测试会把刚才的手工判断变成可重复检查。