Lantide Data
返回博客
数据分析实践

JOIN 后数字暴增?AI 数据分析最常漏掉的 Fan-out 陷阱

JOIN 后总额变大,通常不是加总函数坏了,而是表格粒度不一致造成 fan-out。本文用订单案例与 SQL 检查法,教你在 AI 查询交付前找出重复计算。

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 跨表查询前,先把这四项写进分析规格:

  1. 决策指标的目标粒度是什么? 一列应代表订单、用户、商品,还是用户 × 月?
  2. 两侧 join key 是否唯一? 不能只看栏位叫 order_id,要查实际 distinct count。
  3. 关系是一对一、一对多,还是多对多? 多对多不一定错,但需要先定义如何聚合。
  4. 哪张表的 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 有跑成功」更能保护你的决策。

参考资料