當我們把資料正規化做到極致時,每個 table 都會變得超精簡,裡面的資訊只遵循一個原則。但現實查詢時(像是「有哪些學生有報名 SQL 課程?」)你可能得 JOIN 一堆 table。table 越多,query 越複雜,DBMS 就越「辛苦」。
你應該已經在前面的課程碰過 JOIN 了。這裡有個例子,展示在一個設計良好的資料庫裡可能會用到的查詢:
SELECT students.name, courses.title
FROM students
JOIN enrollments ON students.id = enrollments.student_id
JOIN courses ON enrollments.course_id = courses.id
WHERE courses.title = 'SQL';
看起來很簡單,但底層其實 server 做了超多事:讀每個 table、資料合併、過濾……如果 table 超大怎麼辦?很合理地,效能就會掉下來。
大戰:正規化 vs. 速度
幸好(還是不幸?),現實世界的資料庫就是個妥協。完全正規化可以保證資料一致性,但複雜查詢會變慢。如果你的資料庫主要拿來做 analytics 或報表,有時候反而該做點 denormalization。這就像把 10 個小盒子換成一個大箱子:拿資料快多了,但要重新整理就麻煩。
什麼時候可以「放鬆」正規化?
有些情境下,denormalization 會比較划算:
常用的 aggregate
比如說,假設系統每天都要查詢每個課程有多少學生。用正規化的結構就得一直JOIN 跟
COUNT()。這時可以直接在 "Courses" table 加一個
student_count 欄位,每次新增/刪除時自動更新。
-- 已經 denormalized 的欄位
UPDATE courses
SET student_count = (
SELECT COUNT(*)
FROM enrollments
WHERE enrollments.course_id = courses.id
);
常做的報表
如果你的 client 每天都要「誰、在哪、什麼時候買了什麼?」的報表,直接存一個 denormalized 的 table,像「客戶名、商品、日期」這樣的 row。table 會變大,但查詢速度會快很多。
大量讀取、很少寫入
如果資料庫主要是讀取(像 analytics),可以為了速度犧牲正規化。
複雜關聯時減少 JOIN
如果 table 之間有很多層(nested)關聯,JOIN 變成惡夢時,就可以少做幾層正規化。
範例:denormalization 怎麼加速?
假設我們有一個網路商店的正規化 table:
Table products |
Table orders |
Table order_items |
|---|---|---|
| id | id | id |
| name | date | order_id |
| price | customer_id | product_id |
| quantity |
每個訂單(orders)都包含多個訂單項目(order_items)。來算一下商店賺了多少錢:
SELECT SUM(order_items.quantity * products.price) AS total_revenue
FROM order_items
JOIN products ON order_items.product_id = products.id;
當資料量很大時,order_items 跟 products 的 JOIN 會拖慢查詢速度。
Denormalized 結構
現在假設 order_items table 多了一個「多餘」的 total_price 欄位(denormalization):
Table order_items |
|---|
| id |
| order_id |
| product_id |
| quantity |
| total_price |
這樣查詢就超簡單:
SELECT SUM(total_price) AS total_revenue
FROM order_items;
這樣就不用 JOIN,查詢速度直接提升。
實作練習:優化「銷售」資料庫
給定:正規化的 table
Table products |
Table sales |
|---|---|
| id | id |
| name | product_id |
| price | date |
| quantity |
任務:加速「每個產品總共賺了多少?」這種常見查詢。
步驟 1: 在 sales table 加一個 total_price 欄位:
ALTER TABLE sales ADD COLUMN total_price NUMERIC;
步驟 2: 用現有資料填好這個欄位:
UPDATE sales
SET total_price = quantity * (
SELECT price
FROM products
WHERE products.id = sales.product_id
);
步驟 3: 查詢就快多了:
SELECT product_id, SUM(total_price) AS total_revenue
FROM sales
GROUP BY product_id;
但!denormalization 也是有代價的
你應該知道,「更快」不代表「更好」。denormalization 會帶來一些問題:
多餘的儲存空間
total_price 欄位就是資料的複本,需要額外空間。
更新變複雜
如果 products table 裡的價格改了,total_price 欄位也要手動更新。不然就會不一致。
新增、更新、刪除時的異常
如果忘記同步 denormalized 的資料,很容易「不同步」。比如產品價格變了,但這不會自動反映到其他地方。
平衡:怎麼找到黃金中間值?
先決定什麼比較重要:效能還是結構? 如果資料庫主要是讀取,就多考慮查詢速度。
只針對關鍵數據或報表做 denormalization。 不要全都 denormalize。
自動化 denormalized 資料的更新。 用 trigger 或定時任務,避免資料不一致。
GO TO FULL VERSION