JOIN 后数字暴增,多半是两张表的粒度(grain)不一致:左表一列代表一张订单,右表一列代表一项明细,订单金额便会随明细列数被重复。AI 能写出可执行的 SQL,却未必知道哪个粒度才符合你的决策问题;不能只靠它自行判定,执行前仍要检查 key、cardinality 与聚合位置。
Fan-out 是什么?为什么 SQL 没报错
假设 orders 每张订单一列,order_items 则每项商品一列。一张 1,000 元、含三项商品的订单,JOIN 后会出现三列:
| order_id | order_total | item_id |
|---|---|---|
| A001 | 1,000 | P01 |
| A001 | 1,000 | P02 |
| A001 | 1,000 | P03 |
此时 SUM(order_total) 得到 3,000 元。SQL 在语法与关联条件上都可能完全合法,只是输出已从「订单粒度」变成「订单明细粒度」。Google Cloud 的 BigQuery 文件也把 primary key 定义为每列唯一的键,并提醒某些系统的 key constraint 未必强制执行,资料是否真的符合唯一性仍由使用者负责。BigQuery primary/foreign key 文件
Fan-out 不只放大营收,也会污染:
- 不加
DISTINCT的用户数与订单数; - 已付款订单占比等分母;
- 平均客单价与每户订单数;
- 再接第三张一对多表后的所有聚合。
最危险的地方是结果常常「看起来很像真的」。因此,query succeeded 不是分析通过。
JOIN 前先回答四个问题
在请 AI 跨表查询前,先把这四项写进分析规格:
- 决策指标的目标粒度是什么? 一列应代表订单、用户、商品,还是用户 × 月?
- 两侧 join key 是否唯一? 不能只看栏位叫
order_id,要查实际 distinct count。 - 关系是一对一、一对多,还是多对多? 多对多不一定错,但需要先定义如何聚合。
- 哪张表的 measure 可以在哪个粒度加总? 订单总额不能在明细粒度重复相加;商品小计则可以在明细层加总。
这就是 cardinality 检查:不是问「能不能 JOIN」,而是问「一列进去,最多会变成几列」。
可重用的 Fan-out 诊断 SQL
先量左右表的列数与键唯一性:
SELECT
COUNT(*) AS rows,
COUNT(DISTINCT order_id) AS distinct_orders
FROM "orders.csv";
SELECT
COUNT(*) AS rows,
COUNT(DISTINCT order_id) AS distinct_orders
FROM "order_items.csv";
再检查一个 key 在右表最多出现几次:
SELECT order_id, COUNT(*) AS item_rows
FROM "order_items.csv"
GROUP BY order_id
HAVING COUNT(*) > 1
ORDER BY item_rows DESC
LIMIT 20;
最后比较 JOIN 前后列数与 distinct key:
SELECT
COUNT(*) AS joined_rows,
COUNT(DISTINCT o.order_id) AS distinct_orders
FROM "orders.csv" o
LEFT JOIN "order_items.csv" i USING (order_id);
如果分析要保留订单粒度,可先把明细预聚合成每张订单一列,再 JOIN:
WITH item_by_order AS (
SELECT
order_id,
SUM(quantity * unit_price) AS item_revenue,
COUNT(*) AS item_lines
FROM "order_items.csv"
GROUP BY order_id
)
SELECT o.order_id, o.customer_id, i.item_revenue, i.item_lines
FROM "orders.csv" o
LEFT JOIN item_by_order i USING (order_id);
不要把 SUM(DISTINCT order_total) 当万用解法;两张不同订单可能恰好同额,反而被合并。正确修复点是粒度与键,而不是在最后加一个 DISTINCT 掩盖症状。
Lantide Data 如何把 grain 变成可审阅契约
在 Lantide Data 的 Project Analysis 中,Plan 会先写明分母、grain、join key 与验证 checkpoint;使用者审阅并按下 Approve & Execute 后,Agent 才进入正式取数。SQL 会成为可回看的 artifact,而不是只剩聊天中的总额。这种 Plan → Execute → Report 工作流 的价值,不是保证 AI 不犯错,而是让 fan-out 能在数字进入 Report 前被看见。
适合的 checkpoint 可以直接写成:
JOIN 后
COUNT(DISTINCT order_id)应等于订单母体;若列数增加,说明增加来自哪个一对多关系,所有订单层指标须在 JOIN 前聚合或以订单 grain 重算。
若只是一次性的简单 JOIN,未必需要建立完整 DAG;一段清楚、经验证的 SQL 就足够。当中间结果会重用、需要交接,才值得用持久 SQL 分页、快取与 Source Run 留下依赖,详见 统一查询层。Lantide 的查询管线以唯读分析为主,也不是用来取代企业数仓约束或资料品质平台。
结语
审 AI 写的 JOIN,先看粒度,再看总额。最小可行做法是保留四个数字:左右表 row count、左右 key distinct count、JOIN 后 row count、JOIN 后 distinct key。这组证据通常比「SQL 有跑成功」更能保护你的决策。