第 4 週 · 關聯、登入與佈署
關聯、JOIN、索引
把使用者和留言分成兩張表,用編號對起來,並為常被搜尋的欄位加索引。
正在打開這一關…
學習目標
- 說明為什麼不要把作者名字直接重複寫在每一則留言裡當唯一真相
- 寫出 INNER JOIN
- 知道索引是加速查找的目錄,不是另一份必讀的資料
現在每一則留言自己帶著 author 文字。若同一個人改名,你要改很多列,也無法確定兩個「Ada」是不是同一個人。關聯的做法是:人放一張表,留言放另一張表,留言只記人的編號。
用生活例子理解
通訊錄是一張表,借書紀錄是另一張表。借書紀錄不抄一整份地址,只寫證號。地址改了,只要改通訊錄。
生活比喻:JOIN 是把兩本帳攤在一起,用證號對齊。你仍然有兩張表,只是查詢時合成一張看。
INNER JOIN 只留下兩邊都對得到的列。留言的 user_id 在使用者表沒有這個人時,這則留言不會出現。這很適合「顯示名字」的清單。
索引像書後面的關鍵詞頁。沒有索引,資料庫可能從第一列看到最後一列。有 created_at 的索引,依時間排序會快些。索引會占空間,也會讓寫入多做一點事。先加在你真的拿來搜尋或排序的欄位,不要每一欄都加。
主鍵本身就有索引。外部鍵是「這個編號必須在另一張表存在」的規則。D1 會檢查外部鍵。
動手做
大約 25 分鐘。在筆記寫 SQL,並放進新的 migration 0002_users.sql。不要改已經套用的 0001。若你的 0001 只有 author 文字,學習專案可以直接在範例 examples/04-guestbook/ 看最終兩張表。今天先把句子讀懂。
- 寫
users表:id、username(唯一)、password_hash、salt、created_at。密碼欄位先留空位,明天才寫雜湊。 - 寫新的
messages:id、user_id、body、created_at,user_id參考users(id)。 - 寫 JOIN,列出留言內容、使用者名字、時間,時間新的在前。
- 為
messages(created_at)寫一個索引。 - 用兩列假資料在紙上走一次 JOIN,確認名字是從使用者表來的,不是從留言表抄來的。
若你要在已經有舊留言的本機資料庫改格式,學習時最簡單的做法是刪掉本機 D1 狀態再重跑遷移(.wrangler 裡的本機狀態),不要在含有正式資料的資料庫上試刪表。
案例
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_messages_created_at ON messages (created_at);SELECT
messages.id,
messages.body,
users.username,
messages.created_at
FROM messages
JOIN users ON users.id = messages.user_id
ORDER BY messages.created_at DESC;期末專案把這兩張表放在同一份 0001 遷移,因為那個範例從一開始就用登入後的留言。你的舊練習庫與期末範例可以是兩個資料庫,不必硬把白板時期的列搬過去。
常見錯誤
- 只在留言表加
username,使用者表也有username,兩邊都當成可以各自修改的真相。之後一定不一致。留言表留user_id就好。 JOIN條件寫錯欄位,例如把messages.id = users.id對在一起。編號各自發號,不能這樣對。- 以為索引會自動讓
SELECT *變快。它幫助的是你拿來過濾或排序的欄位。 - 在有正式資料時直接
DROP TABLE。學習庫可以重建,正式庫要寫會保留資料的遷移。
自我檢查
- 使用者改名時,為什麼留言表不該各自存一份舊名字當唯一來源?
JOIN users ON users.id = messages.user_id這句在對什麼?- 索引的代價是什麼?