拆表把成本集中在一次 join,不拆表把成本攤到每一列
「正規化拆一堆小表,查詢時要 join,不是很貴嗎?」——這個問題只算了一邊的帳。不拆表也有成本,只是換了地方收。
正規化拆出來的 join 是最便宜的那種
JOIN 貴在每一列都要去另一張表配對 的判準是:join 欄位有沒有索引、參與配對的列數多大。FK join 維度表剛好兩個條件都站在好的那一邊:
orders.customer_id → customers.id,右邊是主鍵、天生有索引。customers只有幾萬列,整張塞得進記憶體,做成 hash table 之後每列查一次 hash。
真正會爆的是另外兩種:join 欄位沒有索引(每列都掃一次對面整張表),或一對多/多對多把中間結果放大到比任何一張原表都大。FK join 維度表兩種都不是。
不拆表的成本:每一列都變胖
一千萬列的 orders,寬表每列多帶 customer_name + store_name + address 約 80 bytes:
- 多出 800MB。
- 資料庫是按 page 讀的,每列變胖 → 一個 page 裝得下的列變少 → 讀同樣多的列要讀更多 page。
- 所以不是「省下一次 join」,是讓每一次查詢都多搬幾百 MB。
三種操作分開看
| 正規化(拆表) | 反正規化(寬表) | |
|---|---|---|
| UPDATE | 改一列 | 改一萬列,還要處理改到一半 |
| INSERT | 只寫該寫的欄位,寫入量小;代價是 FK 檢查 | 每筆都重寫一遍重複資料 |
| READ | 多一次 join,但 join 的是有索引的小表 | 不用 join,但每列更胖、掃更多 page |
寫入這邊正規化是完勝,不是打平;只有讀取那一格它輸,而且輸得有限,還能用索引補。
判準順序見 反正規化要排在索引與物化視圖後面。「開銷不比好處大」這種說法如果沒有附上索引狀況與列數,就跟 同一個寫法在互動式列表沒事,在全量掃描必爆 一樣是把慣例當結論。