本章目标:拿到项目里的任意一张表,能说出字段、主键、和谁关联;能把 GMV、AOV、复购率等核心指标算到标准答案。 前置知识:完成项目 00 章的数据准备,读过 SQL 基础 的前几节。 用途:只服务项目线,做项目、对口径、面试前回查时使用。理论章节不依赖这张字典。
1. 为什么需要数据字典
数据字典回答三个问题:这张表存什么、每个字段什么意思、表之间怎么关联。没有它,你看到 order_delivered_customer_date 为空时,不知道该删还是该留;看到 customer_unique_id 和 customer_id 时,不知道哪个才是用户主键。
数据开发不仅要会写 SQL,还要能维护口径。同一个“GMV”,含不含运费、算不算取消单、按订单去重还是按明细求和,结果可能差很多。口径必须写下来,代码才能复查,面试才讲得清。
2. 核心概念
2.1 八张表一览
| 表名 | 业务含义 | 行数 | 粒度 | 主键 |
|---|---|---|---|---|
orders |
订单主表 | 99,441 | 一个订单 | order_id |
order_items |
订单明细 | 112,650 | 订单 × 商品行 | order_id + order_item_id |
payments |
支付流水 | 103,886 | 订单 × 支付笔 | order_id + payment_sequential |
customers |
客户与收货地址 | 99,441 | 一个 customer_id |
customer_id |
products |
商品 | 32,951 | 一个商品 | product_id |
sellers |
卖家 | 3,095 | 一个卖家 | seller_id |
order_reviews |
订单评价 | 99,224 | 一条评价 | review_id(有重复) |
category_translation |
品类翻译 | 71 | 一个品类 | product_category_name |
合计 550,759 行。数据来自 Olist 巴西电商公开数据集,时间范围 2016-09-04 ~ 2018-10-17,分析窗口取 2017-01 ~ 2018-08。
2.2 表关系与关联键
customers 1 ───< orders 1 ───< order_items >─── 1 products
│ │
│ └────────── 1 sellers
├──< payments
└──< order_reviews
products.product_category_name ─── 1 category_translation
orders.customer_id → customers.customer_id
order_items.order_id → orders.order_id
order_items.product_id → products.product_id
order_items.seller_id → sellers.seller_id
payments.order_id → orders.order_id
order_reviews.order_id → orders.order_id
products.product_category_name → category_translation.product_category_name
orders 和 order_items 是一对多;payments、order_reviews 都可能一单多行,直接 JOIN 明细会 fan-out。customers.customer_unique_id 才是自然人 ID,一个自然人可能对应多个 customer_id(多个收货地址),复购必须用它。
2.3 字段清单
| 表 | 字段 | 类型 | 说明 |
|---|---|---|---|
| orders | order_id | string | 订单 ID,主键 |
| orders | customer_id | string | 收货地址维度 ID,关联 customers |
| orders | order_status | string | delivered/shipped/canceled/unavailable 等 |
| orders | order_purchase_timestamp | string | 下单时间,ODS 保留字符串 |
| orders | order_approved_at | string | 审核时间,160 条为空 |
| orders | order_delivered_carrier_date | string | 发货时间,1,783 条为空 |
| orders | order_delivered_customer_date | string | 签收时间,2,965 条为空 |
| orders | order_estimated_delivery_date | string | 预计送达时间 |
| order_items | order_id | string | 订单 ID |
| order_items | order_item_id | int | 明细序号 |
| order_items | product_id | string | 商品 ID |
| order_items | seller_id | string | 卖家 ID |
| order_items | shipping_limit_date | string | 卖家发货截止时间 |
| order_items | price | decimal | 商品单价 |
| order_items | freight_value | decimal | 运费,383 条为 0(包邮) |
| payments | order_id | string | 订单 ID |
| payments | payment_sequential | int | 支付序号 |
| payments | payment_type | string | credit_card/boleto/voucher/debit_card/not_defined |
| payments | payment_installments | int | 分期数 |
| payments | payment_value | decimal | 支付金额 |
| customers | customer_id | string | 收货地址维度 ID,主键 |
| customers | customer_unique_id | string | 自然人 ID,复购口径用它 |
| customers | customer_zip_code_prefix | string | 邮编前缀 |
| customers | customer_city | string | 城市 |
| customers | customer_state | string | 州,两字母缩写 |
| products | product_id | string | 商品 ID,主键 |
| products | product_category_name | string | 葡萄牙语品类,610 条为空 |
| products | product_weight_g | int | 重量(克) |
| products | product_length_cm / height_cm / width_cm | int | 尺寸 |
| sellers | seller_id | string | 卖家 ID,主键 |
| sellers | seller_zip_code_prefix | string | 邮编前缀 |
| sellers | seller_city | string | 城市 |
| sellers | seller_state | string | 州 |
| order_reviews | review_id | string | 评价 ID,789 个 ID 重复、多出 814 行 |
| order_reviews | order_id | string | 订单 ID |
| order_reviews | review_score | int | 1–5 分 |
| order_reviews | review_comment_title / message | string | 评论标题和正文,大量为空 |
| order_reviews | review_creation_date | string | 评价创建时间 |
| category_translation | product_category_name | string | 葡萄牙语品类名 |
| category_translation | product_category_name_english | string | 英文品类名 |
2.4 真实脏数据
| # | 表 | 问题 | 数量 | 处理 |
|---|---|---|---|---|
| 1 | order_reviews | review_id 重复 |
789 个 ID / 多出 814 行 | 按 order_id 聚合均分,不直接 DISTINCT |
| 2 | order_reviews | 一单多条评价 | 547 | 先聚合到订单粒度 |
| 3 | orders | delivered 状态但无签收时间 | 8 | 不进履约分母,单独标记 |
| 4 | orders | 有效订单缺签收时间 | 1,737 | 未签收,正常,不进履约分母 |
| 5 | orders | 发货早于下单 / 签收早于发货 | 166 / 23 | 保留,质量报告披露 |
| 6 | products | 品类为空 | 610 | 关联翻译表后归为 unknown |
| 7 | orders | 取消/不可用订单仍有支付 | 1,234 | 告警,不计入有效 GMV |
| 8 | payments | 支付与明细差额 | 441 单差 >0.01 | 这些订单净差 -1,851.52,披露不修改 |
| 9 | orders | 全部订单无明细 / 有效订单无明细 | 775 / 8 | 保留在订单表,后者不进 AOV 分母 |
| 10 | order_reviews | 评价时间早于签收 | 8,320 | 保留,不参与评分趋势 |
order_delivered_customer_date 为空多数不是脏数据。它表示订单还没签收,要和“delivered 状态却没有签收时间”的 8 条分开处理。
2.5 清洗口径与规模
标准清洗顺序:
- 订单:排除
canceled和unavailable,得到有效订单。 - 明细:只保留属于有效订单的
order_items行。 - 关联:LEFT JOIN 客户、商品、品类翻译、卖家,补上可读维度。
- 评价:先按
order_id聚合均分,再 JOIN 明细,避免 fan-out。 - 时间:字符串
CAST成DATE/TIMESTAMP,派生order_date、is_late、delivery_days。
| 指标 | 数值 | 说明 |
|---|---|---|
| 原始订单 | 99,441 | 8 张表里的 orders |
| 有效订单 | 98,207 | 排除 canceled 625 + unavailable 609 |
| 有效明细行 | 112,101 | 属于有效订单的明细 |
| 有明细的有效订单 | 98,199 | 8 个有效订单没有明细 |
2.6 核心指标标准答案
以 data/metrics.json 为唯一基线。
| 指标 | 口径 | 标准答案 |
|---|---|---|
| GMV(含运费) | SUM(price + freight_value),仅有效订单 |
15,740,216.93 |
| GMV(不含运费) | SUM(price),仅有效订单 |
13,494,400.74 |
| 客单价 AOV | GMV(含运费)÷ 有明细的有效订单 98,199 | 160.29 |
| 下单买家 | 有效订单中 COUNT(DISTINCT customer_unique_id) |
94,990 |
| 复购买家 | 有效订单数 ≥ 2 的买家 | 2,888 |
| 复购率 | 2,888 ÷ 94,990 | 3.04% |
| 迟到率 | 迟到订单 ÷ 有签收和预计时间的有效订单 | 8.11% |
| 评价均分 | 全部 99,224 条评价的 AVG(review_score) |
4.09 |
| 平均 / 中位履约 | 下单日到签收日 | 12.5 / 10.0 天 |
| 分析窗口 GMV | 2017-01 ~ 2018-08 | 15,688,396.64 |
品类 GMV Top5(窗口内,含运费):health_beauty 1,432,395.00、watches_gifts 1,294,824.43、bed_bath_table 1,239,779.55、sports_leisure 1,146,420.59、computers_accessories 1,049,201.49。Top10 累计占比 62.37%。
AOV 的分母容易错:不是 99,441(原始订单),不是 98,207(有效订单),也不是 112,101(明细行),而是 98,199(有明细的有效订单)。
2.7 签收时间空值
orders 有 2,965 条 order_delivered_customer_date 为空,其中大部分是未签收订单,属于正常业务空值;只有 8 条 order_status = 'delivered' 却没有签收时间,才是异常。
判断空值是否脏,看三点:业务上是否允许为空;空值是否和业务状态一致;会不会影响当前指标。计算 GMV 不需要该字段,做履约分析时只取签收和预计时间都不为空的有效订单。
3. 动手示例
先跑数仓和体检:
python project/build_warehouse.py
python scripts/check_data.py
构建完成后,用 DuckDB 查询数仓文件:
python - <<'PY'
import duckdb
con = duckdb.connect('project/warehouse.duckdb', read_only=True)
print(con.sql("""
SELECT
COUNT(*) AS total_orders,
COUNT(*) FILTER (WHERE order_delivered_customer_date IS NULL) AS null_delivered,
COUNT(*) FILTER (
WHERE order_status = 'delivered'
AND order_delivered_customer_date IS NULL
) AS delivered_missing_date
FROM ods_orders
""").df())
PY
预期结果:
total_orders null_delivered delivered_missing_date
99441 2965 8
空值数量和业务状态对得上,说明缺失主要来自未签收;8 条 delivered 缺签收时间才是异常。再用 check_data.py 的输出对照 98,207 / 112,101 / 98,199 和 GMV,能对上就说明清洗口径正确。
4. 常见坑
| 坑 | 后果 | 处理 |
|---|---|---|
| 把签收时间为空当脏数据全删 | 未签收订单被删,订单数和 GMV 对不上 | 先看订单状态,只把 8 条 delivered 缺时间标为异常 |
用 customer_id 算复购 |
同一人多个地址被拆成多用户,复购率偏低 | 复购统一用 customer_unique_id |
| 评价或支付直接 JOIN 明细 | fan-out,GMV 和评分被重复计算 | 先聚合到订单粒度再 JOIN |
| AOV 分母用有效订单数 | 结果偏小 | 用有明细的有效订单 98,199 |
| 拿全部支付减有效明细对账 | 多出约 26.9 万,误判数据错误 | 先对齐口径,再比“同时有明细和支付”的订单 |
| 重新下载数据后不回看基线 | 标准答案可能变化 | 重新跑 check_data.py,对比 metrics.json |
5. 在线练习
| 题目 | 考点 |
|---|---|
| 175. 组合两个表 | LEFT JOIN |
| 183. 从不订购的客户 | 反连接 |
| 1084. 销售分析 III | 时间窗口聚合 |
| 1321. 餐馆营业额变化增长 | 移动窗口 |
自测:
- 能用
LEFT JOIN和NOT EXISTS各写一条“没有下过有效订单的客户”。 - 能在固定时间窗口内按品类聚合 GMV,并说清分子和分母。
- 能用窗口函数算 7 日移动平均,并解释为什么要先固定日期范围。
6. 面试延伸
面试题:什么是数据口径?为什么数据开发要负责口径?
是什么:口径是指标的业务定义和计算规则,包括统计对象、过滤条件、时间范围、分母和单位。
解决什么问题:同一份数据,不同口径会算出不同结果。把口径写进数据字典和 SQL 注释,报表、分析和财务才能对得上;出问题时也能定位是数据错还是定义不同。
我的实操边界:我在项目里统一定义了有效订单、GMV 含运费、AOV 分母、复购 ID 和迟到判定;没有在公司级指标平台上维护过跨部门口径。
面试题:空值和脏数据有什么区别?
是什么:空值是字段没有值;脏数据是值不符合业务规则,例如重复评价 ID、支付与明细不一致、时间倒挂。
解决什么问题:空值可能合法也可能非法,要看业务含义。未签收订单没有签收时间合法,delivered 状态却没有签收时间才异常。清洗前先分类,避免误删正常数据。
我的实操边界:我在项目里保留未签收订单的空时间,只把 8 条 delivered 缺时间标为异常;商品缺失品类归为 unknown;没有处理过线上数据质量告警。
7. 本章验收
- 能不看文档说出 8 张表各存什么、行数大概多少。
- 能画出表关系图,写出 7 条关联键。
- 能说出 5 类真实脏数据的数量和处理方式,并解释签收时间空值为什么多数不算脏数据。
- 能复述 GMV、AOV、复购率、迟到率的标准答案和各自口径。
- 能运行本章 SQL,验证 99,441 / 2,965 / 8 和 98,207 / 112,101 / 98,199。
重新下载数据:python scripts/fetch_olist.py;重新体检:python scripts/check_data.py。下一章:速查表。