Course Builder个人学习工作台 · 本地模式
dbt 入门:把 SQL 变成可信的数据流水线

数据粒度与 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 不在这条祖先链上,所以先装载输入仍然必要。

亲眼看到连接方式的影响

  1. 将模型中的 left join 改为 inner join,其他部分不动。
  2. 重新 run,再 show,应只剩 Ada、Bo 两行,总额仍是 52 元。
  3. 解释为什么“总金额没错”仍不能证明报表正确:它丢失了没有成交的客户。
  4. 恢复 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 主版本。现在项目已经产出可读的报表,接下来的测试会把刚才的手工判断变成可重复检查。

参考资料