假設你正在設計一個訂單系統的資料庫,訂單表格裡有 is_paid、is_shipped、is_refunded 這些欄位。
這時你想到:「要不要再加一個 status 欄位,存『處理中』、『已完成』、『已退款』?」
但仔細想想,status 的值其實可以從那三個欄位推算出來。
既然算得出來,還需要額外存一份嗎?
這個問題背後,藏著一個資料庫設計的重要概念——Derived Attribute(衍生屬性)。
這篇文章會帶你搞懂什麼是 Derived Attribute,然後用訂單系統當例子,看看「存」跟「不存」各有什麼優缺點,最後介紹幾種實務上的做法,幫你判斷什麼情境該用哪一種。
Derived Attribute 是什麼?
Derived Attribute(衍生屬性)指的是:值可以從其他欄位推導出來的屬性。
它不是使用者直接輸入的資料,而是根據已經存在的欄位,透過某種規則算出來的結果。
最經典的例子就是「年齡」。
資料庫裡存的是 date_of_birth(出生日期),這是使用者實際填入的值,叫做 Stored Attribute(儲存屬性)。
而「年齡」(age)呢?它可以用「今天的日期 – 出生日期」算出來,所以 age 就是一個 Derived Attribute。
再舉一個例子:員工表格裡存了 joined_at(到職日期),而「年資」(years_of_experience)可以用「今天的日期 – 到職日期」推算出來,所以年資也是 Derived Attribute。
在 ER Model(實體關係模型)裡,Derived Attribute 會用虛線橢圓來表示,跟一般屬性的實線橢圓做區隔。
這個符號在告訴你:「這個值不是直接存的,是算出來的。」
用訂單系統理解 Derived Attribute
回到一開始的訂單系統。
假設訂單表格長這樣:
那 status(訂單狀態)可以用以下規則推導出來:
is_paid = true且is_shipped = false→ status 是「處理中」is_paid = true且is_shipped = true且is_refunded = false→ status 是「已完成」is_refunded = true→ status 是「已退款」
你會發現,status 的值完全由 is_paid、is_shipped、is_refunded 這三個欄位決定。
它沒有自己獨立的資料來源,就是一個標準的 Derived Attribute。
那問題來了:這個 status,到底該不該存進資料庫?
不存 status——查詢時即時計算
第一種做法是:不在資料庫裡存 status,每次需要的時候再算。
用 SQL 的 CASE WHEN 語法,就可以在查詢時即時推導出 status:
SELECT
id,
is_paid,
is_shipped,
is_refunded,
CASE
WHEN is_refunded = TRUE THEN '已退款'
WHEN is_paid = TRUE AND is_shipped = TRUE THEN '已完成'
WHEN is_paid = TRUE AND is_shipped = FALSE THEN '處理中'
ELSE '未知'
END AS status
FROM orders;這段 SQL 不會動到資料表的結構,純粹在查詢的時候把 status 算出來。
除了在 SQL 裡算,你也可以在應用層(後端程式碼)裡做這件事,例如在 API 回傳訂單資料時,用程式邏輯組出 status。
這種做法的好處:
- 資料不會不一致。
status永遠是從is_paid、is_shipped、is_refunded即時算出來的,不會出現「改了is_shipped但忘了更新status」的狀況。
這就是所謂的 Single Source of Truth(單一事實來源)——每筆資料只存在一個地方,沒有重複。 - 不需要額外的同步邏輯。 你不用擔心什麼時候該更新 status,因為它根本不存在資料表裡。
這種做法的缺點:
- 查詢語法變複雜。 每次需要 status 都得寫一段
CASE WHEN,如果很多地方都要用到,同樣的邏輯會散落在各處。 - 無法直接對 status 建 index。 因為 status 不是資料表裡的欄位,資料庫沒辦法對它建索引。
如果你需要「找出所有處理中的訂單」,資料庫得把每一筆訂單都算一遍 status,再篩選出來。
資料量小的時候沒感覺,資料量一大,效能就會變差。
存 status——用冗餘換效能
第二種做法是:明知 status 是算出來的,但還是在資料表裡多存一個欄位。
這種「故意多存一份可以推導的資料」,在資料庫設計裡叫做 Denormalization(反正規化)。
正規化(Normalization)的原則是「不存重複的資料」,反正規化就是反過來——為了效能,刻意存一份冗餘資料。
什麼時候適合這樣做?
- 需要頻繁用 status 做篩選或排序,例如後台管理頁面要列出「所有處理中的訂單」。
- 推導邏輯很複雜,可能要跨好幾張表 JOIN 才算得出來。
- 資料量大,即時計算的成本已經明顯拖慢查詢速度。
存了之後要注意什麼?
最大的風險就是資料一致性。
一旦你多存了 status,就代表同一個「訂單狀態」的資訊存在兩個地方:一個是 is_paid、is_shipped、is_refunded 的組合,另一個是 status 欄位本身。
如果有人改了 is_paid 卻忘了同步更新 status,資料就會打架。
所以存 status 的前提是:你必須建立同步機制,確保每次來源欄位變動時,status 也跟著更新。
常見的同步方式有兩種:
方式一:用資料庫 Trigger 自動同步
CREATE TRIGGER update_order_status
BEFORE UPDATE ON orders
FOR EACH ROW
BEGIN
SET NEW.status = CASE
WHEN NEW.is_refunded = TRUE THEN '已退款'
WHEN NEW.is_paid = TRUE AND NEW.is_shipped = TRUE THEN '已完成'
WHEN NEW.is_paid = TRUE AND NEW.is_shipped = FALSE THEN '處理中'
ELSE '未知'
END;
END;Trigger(觸發器)會在每次更新訂單時自動執行,幫你把 status 算好並寫入。
方式二:在應用層程式碼裡同步
在後端程式碼中,每次修改 is_paid、is_shipped、is_refunded 的邏輯旁邊,都同時更新 status。
這種做法比較直覺,但要小心「改了 A 忘了改 B」的情況,尤其在多人協作的專案裡,這個風險會更高。
Generated Column 與 View——讓資料庫幫你算
除了「完全不存」跟「自己存」,還有第三種選擇:讓資料庫自動幫你算,但你不用自己維護同步邏輯。
Generated Column:自動計算的欄位
MySQL 5.7 以上和 PostgreSQL 12 以上支援 Generated Column(生成欄位)。
簡單來說,你可以在資料表裡定義一個欄位,告訴資料庫「這個欄位的值請你用這個公式算出來」,之後資料庫會自動幫你維護它。
ALTER TABLE orders
ADD COLUMN status VARCHAR(20) GENERATED ALWAYS AS (
CASE
WHEN is_refunded = TRUE THEN '已退款'
WHEN is_paid = TRUE AND is_shipped = TRUE THEN '已完成'
WHEN is_paid = TRUE AND is_shipped = FALSE THEN '處理中'
ELSE '未知'
END
) STORED;這裡的 GENERATED ALWAYS AS (...) 就是在告訴資料庫:「這個欄位的值永遠由這個公式決定,你幫我算就好。」
最後面的 STORED 代表這個值會實際存進磁碟,所以可以建 index,查詢效能跟一般欄位一樣。
好處是:你不用自己寫 Trigger,也不用在程式碼裡同步,資料庫保證這個值永遠是對的。
限制是:Generated Column 的公式不能跨表(不能用 JOIN),也不能用 subquery。
也就是說,如果你的推導邏輯需要查其他表的資料,Generated Column 就不適用了。
View:把推導邏輯包成虛擬表格
View(檢視表)是另一種做法。
它不會在資料表裡新增欄位,而是建立一個「虛擬表格」,把推導邏輯包在裡面。
CREATE VIEW order_with_status AS
SELECT
id,
is_paid,
is_shipped,
is_refunded,
CASE
WHEN is_refunded = TRUE THEN '已退款'
WHEN is_paid = TRUE AND is_shipped = TRUE THEN '已完成'
WHEN is_paid = TRUE AND is_shipped = FALSE THEN '處理中'
ELSE '未知'
END AS status
FROM orders;建好之後,查詢時直接 SELECT * FROM order_with_status 就能拿到帶有 status 的結果,跟查一張真實的表格一樣。
好處是:查詢端看起來好像有 status 欄位,但底層其實沒有多存任何東西,資料一致性完全不用擔心。
限制是:每次查詢都會重新計算。
View 不會把結果存起來,它只是一段 SQL 的包裝。
所以如果資料量很大,頻繁查 View 的效能跟直接寫 CASE WHEN 差不多,不會比較快。
怎麼選?用情境判斷最適合的做法
一個實用的原則是:先從「不存」開始。
大部分的情境下,查詢時計算就夠用了。
等到你真的遇到效能瓶頸——例如某個查詢明顯變慢,而且瓶頸確實出在 status 的計算上——再考慮改成 Generated Column 或存 status。
不要一開始就為了「以防萬一」而做 Denormalization,因為多存一份資料帶來的同步成本,往往比你想像中更容易出問題。
Derived Attribute 重點整理
- Derived Attribute 就是「可以從其他欄位推算出來的屬性」,它本身不需要使用者輸入,值完全取決於其他欄位。
- 預設立場是能不存就不存。 讓資料保持 Single Source of Truth,避免同一個資訊存在多個地方導致不一致。
- 如果決定要存,就代表你在做 Denormalization(反正規化)。 這不是壞事,但必須搭配同步機制(Trigger 或應用層邏輯),否則遲早會出現資料打架的問題。
- 中間方案是 Generated Column 和 View。 Generated Column 讓資料庫幫你算並存起來,不用自己同步;View 讓查詢端看起來有這個欄位,但底層不多存任何東西。
- 記住這個順序:先不存 → 遇到瓶頸再考慮 Generated Column → 最後才考慮存 status。 從簡單開始,有需要再加複雜度。