拆表把成本集中在一次 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

寫入這邊正規化是完勝,不是打平;只有讀取那一格它輸,而且輸得有限,還能用索引補。

判準順序見 反正規化要排在索引與物化視圖後面。「開銷不比好處大」這種說法如果沒有附上索引狀況與列數,就跟 同一個寫法在互動式列表沒事,在全量掃描必爆 一樣是把慣例當結論。