Logo
Published on

停止在資料庫中使用布林值?探討日期時間欄位的替代方案

AI 產生的圖片,粗暴直覺又有力,我喜歡 XD

最近在 YouTube 上看到一個有趣的技術討論:「你應該停止在資料庫中使用布林值」。這個看似簡單卻引發廣泛爭議的觀點,讓我重新思考了資料庫設計中的型別選擇問題。

影片觀點:日期時間欄位的優勢

傳統布林值方式

在傳統的資料庫設計中,我們習慣使用布林值來表示二元狀態:

CREATE TABLE todos (
    id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    is_completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT NOW()
);

這種設計簡單直觀,但只能告訴我們「是否完成」,無法提供更多上下文資訊。

日期時間欄位的替代方案

影片提出的替代方案是使用日期時間欄位:

CREATE TABLE todos (
    id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    completed_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

這種設計的優勢在於:

  • 豐富的上下文資訊:當 completed_at 有值時,表示已完成,且能知道確切的完成時間
  • 節省欄位空間:一個欄位同時表達「是否完成」和「何時完成」
  • 更好的數據洞察:可以分析完成時間的分佈、趨勢等
-- 查詢已完成的任務
SELECT * FROM todos WHERE completed_at IS NOT NULL;

-- 查詢未完成的任務
SELECT * FROM todos WHERE completed_at IS NULL;

-- 分析完成時間分佈
SELECT
    DATE(completed_at) as completion_date,
    COUNT(*) as completed_tasks
FROM todos
WHERE completed_at IS NOT NULL
GROUP BY DATE(completed_at);

我的觀點:不是萬靈丹

雖然這個想法很有趣,但我認為這兩種資料型別本質上服務於不同的目的。日期時間欄位雖然可以運作,但我不會全盤採用這種做法。

布林值的適用場景

某些情況下,布林值仍然是最佳選擇:

CREATE TABLE users (
    id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    is_email_verified BOOLEAN DEFAULT FALSE,
    is_admin BOOLEAN DEFAULT FALSE,
    email_verified_at TIMESTAMP NULL
);

在這個例子中:

  • is_admin 是一個純粹的權限狀態,不需要時間資訊
  • email_verified_at 提供驗證時間,is_email_verified 提供快速查詢

Enum:更好的替代選擇

如果真的要取代布林值,我更傾向於使用 Enum 型別:

CREATE TYPE task_status AS ENUM ('pending', 'in_progress', 'completed', 'cancelled');

CREATE TABLE tasks (
    id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    status task_status DEFAULT 'pending',
    completed_at TIMESTAMP NULL,
    cancelled_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

這種設計的優勢:

  • 語意明確:狀態值具有明確的業務含義
  • 擴展性佳:可以輕鬆添加新狀態
  • 型別安全:資料庫層面的約束確保資料完整性
  • 查詢效率:Enum 型別通常比字串比較更高效

複雜場景的考量

狀態變更追蹤

當需要追蹤狀態變更歷史時,單純的日期時間欄位會遇到限制:

-- 如果任務從完成變回進行中,completed_at 該如何處理?
UPDATE tasks SET completed_at = NULL WHERE id = 1;

這種情況下,影片提到的事件溯源(Event Sourcing)模式更適合:

CREATE TABLE task_events (
    id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    task_id INTEGER REFERENCES tasks(id),
    event_type VARCHAR(50) NOT NULL, -- 'created', 'started', 'completed', 'reopened'
    occurred_at TIMESTAMP DEFAULT NOW(),
    metadata JSONB
);

混合方案

在實務中,我經常採用混合方案:

CREATE TABLE orders (
    id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    status order_status DEFAULT 'pending',
    is_paid BOOLEAN DEFAULT FALSE,
    paid_at TIMESTAMP NULL,
    shipped_at TIMESTAMP NULL,
    delivered_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

這種設計結合了各種方案的優點:

  • status Enum 提供清晰的狀態管理
  • is_paid 布林值提供快速查詢
  • 各種時間戳記提供詳細的歷史資訊

效能與索引考量

不同的資料型別對查詢效能也有影響:

-- 布林值索引
CREATE INDEX idx_todos_completed ON todos (is_completed);

-- 日期時間欄位的部分索引(PostgreSQL)
CREATE INDEX idx_todos_completed_at ON todos (completed_at)
WHERE completed_at IS NOT NULL;

-- Enum 索引
CREATE INDEX idx_tasks_status ON tasks (status);

布林值索引通常具有較低的選擇性,但在某些場景下仍然有效。日期時間欄位可以利用部分索引來優化 NULL 值的處理。

實務建議

根據我的經驗,選擇合適的資料型別應該考慮以下因素:

1. 業務需求分析

  • 是否需要追蹤時間資訊?
  • 狀態是否會多次變更?
  • 是否需要狀態變更的歷史記錄?

2. 查詢模式

  • 最常見的查詢是什麼?
  • 是否需要複雜的狀態組合查詢?
  • 效能要求如何?

3. 未來擴展性

  • 狀態是否可能增加?
  • 業務邏輯是否會變得更複雜?

4. 團隊慣例

  • 團隊對不同方案的熟悉程度
  • 現有系統的一致性考量

結論

「停止使用布林值」這個觀點雖然新穎,但確實提醒我們重新思考資料庫設計的選擇。我的建議是:

  1. 不要一刀切:每種資料型別都有其適用場景
  2. 理解業務需求:選擇最符合業務邏輯的方案
  3. 考慮混合使用:不同型別可以互補,發揮各自優勢
  4. 重視可維護性:選擇團隊能夠理解和維護的方案

正如影片最後所說:「這取決於具體情況。」最重要的是理解每一個技術選擇背後的「為什麼」,而不是盲目地追隨某種趨勢。

在資料庫設計中,沒有銀彈,只有在特定情境下的最佳解答。作為開發者,我們需要在簡潔性、效能、可維護性和業務需求之間找到平衡點。


支持創作

如果這篇文章對您有幫助,歡迎透過 贊助連結 支持我持續創作優質內容。您的支持是我前進的動力!


圖片來源:AI 產生