iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Vibe Coding

夢幻甜品師闖工程世界:Vibe Coding vs 專業開發的 0→1 冒險攻略系列 第 6

Day 6|食譜寫得再美,也擋不住有人把鹽當成糖:約束是資料庫自己的把關機制,索引負責找得快

  • 分享至 

  • xImage
  •  

昨天,我們把資料庫那張看起來像摩斯密碼的圖拆開了第一層:哪些東西該成為實體、彼此之間到底是一對一、一對多,還是多對多。

最後還留下了兩個沒解完的東西——約束(Constraint)和索引(Index)

一個負責守住資料規則,一個會影響資料查得快不快。

今天,就把這兩個一起打開來看。

老實說,我一開始看到資料表旁邊那些 PKFK 時,第一個反應其實是:

「嗯?PK?是要跟誰 PK 嗎?」

再加上 NOT NULLUNIQUECHECK……

每一個都看得懂英文字母,但放在一起就又變回工程世界的摩斯密碼。

後來才發現,它們其實不是隨便散落在 Schema 裡的小字,而是在替資料庫守不同的規則。

如果把應用程式想成一道食譜,前端和後端可以一路提醒你「這裡不可以這樣填」、「那裡不能重複」。

但真的走到資料庫門口,還需要有人做最後一次確認:

這筆資料,到底合不合法?

這就是今天先要拆的——約束(Constraint)


約束:資料進來以前,資料庫會自己再檢查一次

先看一小段 SQL:

CREATE TABLE users (
  id       UUID PRIMARY KEY,
  email    TEXT NOT NULL UNIQUE,
  age      INTEGER CHECK (age >= 0)
);

CREATE TABLE activities (
  id         UUID PRIMARY KEY,
  creator_id UUID NOT NULL,

  FOREIGN KEY (creator_id)
    REFERENCES users(id)
);

這裡真正屬於資料庫約束的,有:

  • PRIMARY KEY
  • NOT NULL
  • UNIQUE
  • CHECK
  • FOREIGN KEY

它們做的事情不太一樣,但核心目的很接近:

把「不可以發生的資料狀態」直接寫進資料庫規則裡。

例如:

email NOT NULL

代表:

email 這一格不准沒有值。

email UNIQUE

代表:

這張表裡不能出現兩筆一樣的 email。

CHECK (age >= 0)

代表:

age 必須符合這個條件。

而:

FOREIGN KEY (creator_id)
REFERENCES users(id)

則是在說:

你填進來的 creator_id,一定要真的找得到那個使用者。

以前我會覺得,這些判斷寫在程式裡不就好了嗎?

但後來才慢慢理解,應用程式負責的是「怎麼操作」,資料庫自己也可以守住最後一道底線:

真的存進來的資料,不能違反這些規則。

就像食譜寫得再漂亮,最後還是得有人站在出餐口確認——這匙到底是糖,還是鹽。


約束不是一張扁平清單,它們守的範圍不一樣

我一開始以為約束就是一串要背起來的單字。

但真的拆開之後才發現,如果照「它到底在守哪一層資料完整性」去看,反而清楚很多。

https://ithelp.ithome.com.tw/upload/images/20260829/201834843VrQxChMFB.png

家族 管的範圍 白話說
網域約束 Domain 單一欄位的值 這一格裡可以放什麼
鍵約束 Key 一張表裡的唯一性 這一筆/這一組不能重複
參照完整性 Referential Integrity 跨表的參照 你指的那筆資料必須真的存在

範圍大概就是:

一格 → 一張表 → 跨表

接下來就照這個順序走一遍。


網域約束:這一格裡可以放什麼

https://ithelp.ithome.com.tw/upload/images/20260829/20183484BwUvoas6EO.png

最小的一層,是單一欄位本身。

這裡最常一起看到的是 資料型別(Data Type)NOT NULLCHECK

資料型別(Data Type)

資料型別決定這個欄位可以放哪一類資料。

例如:

Data Type 可以裝什麼
TEXT 文字
INTEGER 整數
BOOLEAN true / false
UUID UUID 識別碼
DATE 日期
TIMESTAMP / TIMESTAMPTZ 日期+時間

像:

age INTEGER

就是在說:

age 這個欄位要存整數。

不過要注意,Data Type 會限制欄位能接受的值域,但它本身不是 SQL Constraint。

它比較像先決定:

這一格本來可以裝哪一類東西。

真正的 Constraint,還會再往下限制。

NOT NULL:這一格不能空著

email TEXT NOT NULL

NOT NULL 很直接:

這個欄位不能沒有值。

這是最基本、也最常見的欄位限制之一。

CHECK:值必須符合某個條件

如果規則不只是「能不能空」,就可以用 CHECK Constraint

例如:

age INTEGER CHECK (age >= 0)

意思是:

age 必須符合 age >= 0 這個條件。

也可以用在同一列裡的多個欄位:

CHECK (start_at < end_at)

意思就是:

開始時間要早於結束時間。

以前看到 CHECK,我只會把它當成又一個要記的單字。

現在反而會先問:

「這是不是一條資料本身永遠都應該成立的規則?」

如果答案是,那它就很值得考慮交給資料庫一起守住。


鍵約束:這一組東西不能重複

https://ithelp.ithome.com.tw/upload/images/20260829/20183484FSw3nQakoX.png

再往上一層,問題不再只是:

這一格能不能放這個值?

而是:

這張表裡,怎麼把每一筆資料明確分開?

這裡最常見的就是 Primary KeyUNIQUE

主鍵(Primary Key):每一筆資料自己的身分證

id UUID PRIMARY KEY

主鍵(Primary Key, PK) 可以把它想成每一筆資料的身分證字號。

名字可以一樣,但身分證字號不能重複,而且不能沒有。

所以 Primary Key 的核心就是:

讓每一筆資料都可以被唯一識別。

知道這件事之後,我再看到 PK,終於不會再想著「到底要跟誰 PK」了。

這也是 實體完整性(Entity Integrity) 很核心的一件事。

UNIQUE:這個值或這組值,不准重複

有些欄位不是 Primary Key,但一樣有不能重複的需求。

例如:

email TEXT UNIQUE

代表同一張表裡,不能有兩筆一樣的 email。

UNIQUE 也可以套在一組欄位上。

像昨天多對多的例子:

UNIQUE (activity_id, user_id)

意思不是 activity_id 不能重複,也不是 user_id 不能重複。

而是:

這兩個值組合起來不能重複。

所以:

activity A + user 1
activity A + user 2
activity B + user 1

都可以。

但:

activity A + user 1

不能再出現第二次。


參照完整性:你指的那筆資料必須真的存在

https://ithelp.ithome.com.tw/upload/images/20260829/20183484MWYs0nGTHc.png

再往外一層,問題開始跨表。

這裡最重要的角色,就是昨天已經看過的 外鍵(Foreign Key)

外鍵(Foreign Key)

FOREIGN KEY (creator_id)
  REFERENCES users(id)

外鍵(Foreign Key, FK) 的工作,是保證你跨表指向的資料真的存在。

例如:

creator_id = abc123

資料庫會去 users.id 裡確認:

真的有 abc123 這個使用者嗎?

有,才讓你存。

沒有,就拒絕。

這就是 參照完整性(Referential Integrity)

不能指向一筆根本不存在的資料。


那被指向的資料刪掉之後呢?

建立 Foreign Key 之後,還會碰到另一個問題:

如果被參照的那筆資料被刪掉,現在這筆資料怎麼辦?

這時候就會用到 參照動作(Referential Action)

例如:

ON DELETE CASCADE

意思是:

父資料刪掉時,相關的子資料也一起刪掉。

另外還有常見的:

Referential Action 大概意思
CASCADE 父資料變動時,相關資料一起處理
SET NULL 把外鍵欄位改成 NULL
RESTRICT 還有人參照時,不允許操作
NO ACTION 不允許破壞參照完整性的變更

除了:

ON DELETE

也可以設定:

ON UPDATE

決定被參照的 Key 更新時,下面的資料要怎麼處理。

不過要注意:

ON DELETE / ON UPDATE 是 Foreign Key 的參照動作,不是另一種 Constraint。

Constraint 在守:

這段關係是否合法?

Referential Action 則是在決定:

關聯的資料被刪除或更新時,要怎麼處理?


索引:不管對不對,只管快不快

講完前面幾種資料完整性的規則,還有昨天留下來的另一塊拼圖——索引(Index)

它很常跟約束一起出現在 Schema 裡,但其實管的是完全不同的問題。

索引不會決定資料合不合法,也不會替你擋掉錯誤資料。

它影響的是另一件很重要的事:

效能(Performance)。

這也是 Vibe Coding 很容易忽略的地方。

資料量還小的時候,就算沒有 Index,功能通常還是能跑、資料也查得到,所以很容易覺得:

「能跑了,那這裡應該完成了吧?」

但「查得到」和「查得快」其實是兩回事。

像 BuJo 的通知表,就有替 user_idis_read 建一組 Index:

CREATE INDEX idx_notifications_user_read
ON notifications (user_id, is_read);

它主要是幫助這類查詢:

SELECT *
FROM notifications
WHERE user_id = ?
  AND is_read = false;

也就是:

「查某個使用者的未讀通知。」

所以這裡先記住一個最簡單的差別就好:

約束(Constraint)管資料對不對;索引(Index)影響資料查得快不快。

至於哪些欄位值得加 Index、怎麼知道 Index 有沒有真的帶來效能提升,就先留到第三階段的效能優化,再回來拆這一關。


寫一次,就不用期待每個入口都記得

回頭看這一關,我最有感的是:約束可以把一條重要規則寫進資料庫,之後就不用期待每一個入口都「記得」守住它。

程式裡的判斷邏輯,你得確保每一個會寫入資料的入口都真的有走到它。換一支 API、補一段腳本,甚至直接操作資料庫,只要有一個入口沒有守住規則,不合法的資料就可能進去。

但像:

UNIQUE (activity_id, user_id)

這條規則一旦交給資料庫守,不管是哪一支正常的寫入流程,都不能出現兩筆一模一樣的「這個人參加這個活動」紀錄。

我以前會覺得,多一條約束就是多一個麻煩,因為它會讓我在測試的時候撞到報錯。

現在我反而知道,那個報錯是好事。

它在正式環境幫我擋下的東西,我永遠不會知道有多少。

回頭再看那些曾經像摩斯密碼一樣散落在 Schema 裡的 PKFKNOT NULLUNIQUECHECK,現在終於一個一個開始有了意思。

原來摩斯密碼沒有變簡單,只是我終於開始看得懂它在說什麼。

摩斯密碼解完了,明天,我們要從資料庫最深的地基,一口氣跳回使用者真正會看到的第一眼—— 這個網站,到底長什麼樣子?


上一篇
Day 5|那串摩斯密碼,先從方框開始解:實體與關聯基數,決定你的產品裝得下什麼
下一篇
Day 7|食材備好了,那擺盤呢?介面設計的三層階梯
系列文
夢幻甜品師闖工程世界:Vibe Coding vs 專業開發的 0→1 冒險攻略9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言