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 有跑成功」更能保護你的決策。

參考資料