昨晚 Hippo 拋出的問題——「存兩份怎麼保證一致性?」——讓洛基思考了很久,但真正讓他睡不著的是另一個問題:如果兩個方向都重要,具體要怎麼設計?
茶室裡,大師正泡著茶。
「昨天您說,如果兩個方向都重要,就需要存兩份,」洛基放下背包,直接進入主題,「我試著推演了一下...」他停頓,「但不確定對不對。」
大師倒了兩杯茶,示意他繼續。
「在 SQL 裡,多對多用中介表,」洛基邊說邊在筆記本上畫出結構,「一筆記錄,兩個外鍵,JOIN 可以往兩個方向查。」
他在筆記本上寫下:
SQL: Officers ←→ Registrations ←→ Events
一筆 Registration,兩個方向都能 JOIN
「但在 DynamoDB...」洛基抬起頭,「昨天學到查詢方向決定 PK 的組織。如果要從軍官查活動,PK 要是 OFFICER#...
;如果要從活動查軍官,PK 要是 EVENT#...
。」
他停頓,手指在筆記本上敲了兩下:「一個項目只能有一個 PK。所以...」
「所以?」大師問。
「所以需要兩個項目,」洛基緩緩說出結論,「一個 PK 是 OFFICER#alice
,另一個是 EVENT#science-conference
。」
大師沒有說話,啜了一口茶。
洛基在筆記本上開始畫:
項目 1: PK=OFFICER#alice, SK=EVENT#...
→ 支援「查軍官的活動」
項目 2: PK=EVENT#conference, SK=OFFICER#...
→ 支援「查活動的軍官」
他看著自己寫的結構,沉默了幾秒。「這就是第 6 天學的多視角,」他說,「只是這次是關聯關係的兩個方向。」
大師點頭:「本質相同。」
洛基想了想:「但這次有個我之前沒想清楚的問題——」
大師把茶杯放下:「說說看。」
洛基指著筆記本:「從軍官查活動時,我拿到的是 OFFICER#alice
下的項目。這個項目裡...應該有什麼?」
他停頓,開始列舉:
項目裡可以有:
1. 活動 ID(必須有,不然不知道是哪個活動)
2. 報名角色(speaker, attendee...)
3. 報名時間
4. 活動名稱?
5. 活動時間?
6. 活動地點?
7. 活動詳細描述?
8. 活動講者陣容?
9. 活動議程表?
洛基抬起頭:「我應該存到第幾項?」
大師沒有立刻回答,而是反問:「如果只存活動 ID,會發生什麼?」
洛基思考:「查詢軍官的活動列表時,我只能看到一堆 ID...」他停頓,「要知道活動名稱,還得再查一次每個活動的詳細資料。」
「如果全部都存呢?」
「那...」洛基皺眉,想起第 6 天學過的概念,「更新時要同步很多地方。而且項目會很大,可能超過 400KB 限制。」
「所以?」大師引導。
洛基沉默了幾秒,然後慢慢說:「所以要在中間找個平衡點。」
大師在白板上寫下三種方案:
策略一:完全規範化(只存 ID)
{
PK: 'OFFICER#alice',
SK: 'EVENT#science-conference',
eventId: 'science-conference', // 只有 ID
role: 'speaker'
}
查詢列表:需要再查 N 次活動詳情
更新成本:零(活動資訊改變不影響這裡)
---
策略二:淺層重複(基本資訊)
{
PK: 'OFFICER#alice',
SK: 'EVENT#2210-03-15#science-conference',
eventId: 'science-conference',
eventName: '星際科學大會', // 重複:顯示用
eventDate: '2210-03-15', // 重複:顯示用
eventLocation: 'Mars Center', // 重複:顯示用
role: 'speaker'
// 不包含:詳細描述、講者陣容、議程表
}
查詢列表:一次完成(顯示名稱、時間、地點)
查詢詳情:點進去時再查一次完整資料
更新成本:偶爾(活動基本資訊改變時)
---
策略三:深層重複(完整資料)
{
PK: 'OFFICER#alice',
SK: 'EVENT#science-conference',
eventId: 'science-conference',
eventName: '星際科學大會',
eventDate: '2210-03-15',
eventLocation: 'Mars Center',
eventDescription: '...(1000字)', // 完整描述
speakers: [...], // 講者陣容
agenda: [...] // 議程表
role: 'speaker'
}
查詢列表:一次完成(所有資訊)
查詢詳情:不需要再查
更新成本:高(任何活動資訊改變都要更新所有報名者)
洛基看著三種方案,手指在桌上輕輕敲著。他在思考。
大師等他想清楚,然後問:「用戶查看『我的活動列表』時,需要什麼資訊?」
洛基回想自己使用類似系統的經驗:「看到活動名稱、時間、地點...就能決定要不要點進去看詳情。」
「需要完整的活動描述嗎?」
「不需要,」洛基搖頭,「那是點進去詳情頁才看的。」
大師點頭:「那活動基本資訊多久會改一次?」
洛基思考:「名稱...可能有機會修改,但不常見。時間和地點...確定後很少改。」他停頓,「但活動狀態會常改——開放報名、額滿、結束。」
「所以狀態要不要重複?」
洛基立刻意識到:「不要。狀態改變太頻繁,如果重複存,每次改狀態要更新幾百個報名者的記錄...」
「那怎麼拿到活動狀態?」
「點進去看詳情時,」洛基說,「用活動 ID 查一次活動的完整資料,裡面就有最新狀態。」
大師微笑。洛基已經開始理解淺層重複的邏輯了。
洛基在筆記本上總結:
淺層重複的判斷標準:
包含的資訊:
✅ 列表顯示需要的基本資訊
✅ 更新頻率低的欄位
✅ 不會讓項目過大的資料
不包含的資訊:
❌ 只有詳情頁才需要的資訊
❌ 更新頻率高的欄位(如狀態)
❌ 大型資料(長文本、圖片、附件)
「這樣設計的好處是?」大師問。
洛基想了想:「列表查詢一次完成,不用 N+1 次查詢。但更新成本可控,因為只包含不常改的欄位。」
「而且,」大師補充,「項目大小保持合理,不會超過 400KB 限制。」
現在洛基理解了淺層重複的原則,大師展示完整的雙向查詢設計:
// 項目 1:從軍官查活動
{
PK: 'OFFICER#alice',
SK: 'EVENT#2210-03-15#science-conference',
// 關聯資訊
role: 'speaker',
registeredDate: '2210-03-10',
// 淺層重複:活動的基本資訊
eventName: '星際科學大會',
eventDate: '2210-03-15',
eventLocation: 'Mars Convention Center'
// 不包含:活動描述、講者陣容、議程、狀態
}
// 項目 2:從活動查軍官
{
PK: 'EVENT#science-conference',
SK: 'OFFICER#2210-03-10#alice',
// 關聯資訊
role: 'speaker',
registeredDate: '2210-03-10',
// 淺層重複:軍官的基本資訊
officerName: 'Alice Johnson',
officerRank: 'Captain',
officerEmail: 'alice@starfleet.gov'
// 不包含:完整履歷、獎章記錄、詳細經歷
}
洛基看著設計:「兩個方向各自包含對方的基本資訊,列表查詢一次完成。如果需要完整資料,再用 ID 查一次。」
「正是,」大師說,「這就是淺層重複策略。」
大師做最後總結:
策略一:完全規範化(只存 ID)
適用:資料高度可變,更新頻率遠高於查詢頻率
範例:訂單狀態、庫存數量
策略二:淺層重複(基本資訊)
適用:大多數場景,平衡查詢效能與更新成本
範例:活動報名、好友關係、訂閱關係
策略三:深層重複(完整資料)
適用:資料幾乎不變,查詢效能是首要考量
範例:歷史記錄、存檔資料
洛基看著總結,點頭:「不同場景選不同策略。」
「而且可以混合使用,」大師說,「同一個系統裡,有些關係用淺層重複,有些用完全規範化。」
洛基正要闔上筆記本,突然想到一個問題:「等等,淺層重複雖然更新頻率低,但還是要更新...」
他在筆記本上寫下場景:
場景:活動改名
「星際科學大會」→「星際科技論壇」
需要更新:
1. 活動的主記錄(EVENT#science-conference, SK: METADATA)
2. Alice 的報名記錄(OFFICER#alice, SK: EVENT#...)
3. Bob 的報名記錄(OFFICER#bob, SK: EVENT#...)
4. Charlie 的報名記錄...
...如果有 100 人報名,就是 101 次更新
他抬起頭看向大師:「如果更新到一半失敗...」
「資料不一致,」大師說。
洛基沉默了幾秒。他意識到這不只是淺層重複的問題,從一開始設計雙向查詢,就有這個風險。
「昨天 Hippo 提的問題,」洛基說,「現在我真正理解它的嚴重性了。」
大師點頭:「雙向查詢,兩次寫入。淺層重複,批量更新。都需要保證一致性。」
洛基想了想:「在 SQL 裡,這不就是 Transaction 嗎?BEGIN... COMMIT... ROLLBACK...」
「DynamoDB 也有 Transaction,」大師說,「但使用方式不太一樣。」
「怎麼不一樣?」
Hippo 的聲音響起:「SQL 的 Transaction 就像呼吸,你幾乎感覺不到它存在。」Hippo停頓一下接著說,「DynamoDB 的 Transaction 就像手動檔汽車——你得明確告訴它什麼時候要換檔。」
洛基笑了:「聽起來需要更謹慎的設計。」
「還有限制,」大師補充,「明天你會理解這些限制背後的原因。」
走出茶室,洛基回想今天的發現。
今天學到的不是「要不要重複資料」——那在第 6 天就學過了。今天學的是重複多深的策略選擇。
完全規範化?查詢慢但更新快。
深層重複?查詢快但更新慢。
淺層重複?在兩者之間取得平衡。
他想起大師說的:「可以混合使用。」這讓他理解到,設計不是找一個「最佳解」,而是針對每個關係做出最適合的選擇。
一對一教他判斷何時合何時分,一對多教他選擇查詢方向,多對多則讓他理解重複的深度策略。
但還有一個問題沒解決——一致性。
洛基看著夜空,想起 Hippo 的比喻。手動檔汽車。明天,他要學會怎麼「換檔」。
時間設定說明:故事中使用星際曆(SY210 = 西元2210年),程式碼範例為確保正確執行,使用對應的西元年份。