Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

資料庫的 Derived Attribute:status 欄位到底該不該存?

最後更新:2026年7月15日資料庫

假設你正在設計一個訂單系統的資料庫,訂單表格裡有 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

回到一開始的訂單系統。

假設訂單表格長這樣:

欄位型別說明
idINT訂單編號
is_paidBOOLEAN是否已付款
is_shippedBOOLEAN是否已出貨
is_refundedBOOLEAN是否已退款
型別INT
說明訂單編號
型別BOOLEAN
說明是否已付款
型別BOOLEAN
說明是否已出貨
型別BOOLEAN
說明是否已退款

那 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 差不多,不會比較快。

怎麼選?用情境判斷最適合的做法

情境建議做法
推導邏輯簡單,資料量小查詢時計算(CASE WHEN 或程式碼)
推導邏輯簡單,來源欄位都在同一張表Generated Column
需要頻繁篩選 status,資料量大存 status + 同步機制(Trigger 或應用層)
推導邏輯涉及跨表 JOINView 或存 status
建議做法查詢時計算(CASE WHEN 或程式碼)
建議做法Generated Column
建議做法存 status + 同步機制(Trigger 或應用層)
建議做法View 或存 status

一個實用的原則是:先從「不存」開始。

大部分的情境下,查詢時計算就夠用了。

等到你真的遇到效能瓶頸——例如某個查詢明顯變慢,而且瓶頸確實出在 status 的計算上——再考慮改成 Generated Column 或存 status。

不要一開始就為了「以防萬一」而做 Denormalization,因為多存一份資料帶來的同步成本,往往比你想像中更容易出問題。

Derived Attribute 重點整理

  • Derived Attribute 就是「可以從其他欄位推算出來的屬性」,它本身不需要使用者輸入,值完全取決於其他欄位。
  • 預設立場是能不存就不存。 讓資料保持 Single Source of Truth,避免同一個資訊存在多個地方導致不一致。
  • 如果決定要存,就代表你在做 Denormalization(反正規化)。 這不是壞事,但必須搭配同步機制(Trigger 或應用層邏輯),否則遲早會出現資料打架的問題。
  • 中間方案是 Generated Column 和 View。 Generated Column 讓資料庫幫你算並存起來,不用自己同步;View 讓查詢端看起來有這個欄位,但底層不多存任何東西。
  • 記住這個順序:先不存 → 遇到瓶頸再考慮 Generated Column → 最後才考慮存 status。 從簡單開始,有需要再加複雜度。
目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

資料庫

目錄

  • Derived Attribute 是什麼?
  • 用訂單系統理解 Derived Attribute
  • 不存 status——查詢時即時計算
  • 存 status——用冗餘換效能
  • 方式一:用資料庫 Trigger 自動同步
  • 方式二:在應用層程式碼裡同步
  • Generated Column 與 View——讓資料庫幫你算
  • Generated Column:自動計算的欄位
  • View:把推導邏輯包成虛擬表格
  • 怎麼選?用情境判斷最適合的做法
  • Derived Attribute 重點整理