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