洛基推開茶室的門,看見大師正拿著一杯茶,盯著白板上的兩個方塊。
「我們昨天學到 ER 圖不能直譯,」洛基說,把筆記本放在桌上,「大師今天會從哪裡開始?」
大師轉過身,指著白板:「從最簡單的開始。」
軍官 (Officers) 1:1 個人檔案 (Profiles)
- 每個軍官有一份個人檔案
- 每份個人檔案屬於一個軍官
洛基看著這個一對一關係,和昨天那個複雜的多對多比起來,簡單太多了。他幾乎是反射性地開始思考:「在 SQL 中,這就是兩個表加外鍵...」話到一半,他停住了,想起昨天的教訓,「但在 DynamoDB 中,應該要先問查詢需求。」
大師點頭,示意他繼續說下去。
「在 SQL 裡,」洛基邊說邊在筆記本上寫,「一對一就是標準流程:」
-- SQL 的標準做法
CREATE TABLE Officers (
officer_id VARCHAR PRIMARY KEY,
username VARCHAR,
email VARCHAR
);
CREATE TABLE Profiles (
profile_id VARCHAR PRIMARY KEY,
officer_id VARCHAR UNIQUE, -- 外鍵,確保一對一
full_name VARCHAR,
rank VARCHAR,
homeplanet VARCHAR,
biography TEXT
);
「對,」大師說,「在 SQL 中,一對一幾乎是『無腦設計』——看到關係,開兩個表,加外鍵,完成。不需要思考太多。」
洛基抬起頭:「但在 DynamoDB 要怎麼設計?」
大師在白板上寫下對比:
SQL 思維:一對一 = 兩個表 + 外鍵(唯一解)
DynamoDB 思維:一對一 = ?(多種選擇)
洛基盯著那個問號。昨天學到 DynamoDB 要從查詢需求思考,那一對一的查詢需求...「等等,」他的手指在桌上輕敲,「一對一在查詢時,可能只需要其中一邊,也可能兩邊都要。」
「繼續,」大師沒有打斷。
「所以資料要不要放在一起,」洛基慢慢說,整理著思路,「取決於查詢模式?如果總是一起查,就合併;如果經常分開查,就分離?」
「正是這個方向,」大師微笑,「讓我給你看兩條完全不同的路。」
大師展示第一個方案:
// 策略一:嵌入設計(Embedded Design)
const officer = {
PK: 'OFFICER#alice',
SK: 'METADATA',
// 基本資料
officerId: 'alice',
username: 'alice_johnson',
email: 'alice@starfleet.gov',
// 個人檔案直接嵌入
profile: {
fullName: 'Alice Johnson',
rank: 'Captain',
homeplanet: 'Earth',
birthdate: '2185-06-15',
biography: '經驗豐富的星際探險家,專精於深空導航...'
}
};
洛基驚訝:「全部放在一起?這在 SQL 中通常不會這樣做...」
「沒錯,」大師說,「但看看查詢會發生什麼事。」
// 查詢軍官和個人檔案
async function getOfficerWithProfile(officerId) {
const result = await docClient.get({
TableName: 'IntergalacticEvents',
Key: {
PK: `OFFICER#${officerId}`,
SK: 'METADATA'
}
}).promise();
return result.Item;
// 一次查詢,所有資料都在這裡!
}
洛基看著程式碼,「一次查詢搞定!」他轉向大師,「這比 SQL 的 JOIN 還快,是不是我們所有的查詢都要設計成這樣?」
大師沒有回答,只是靜靜地看著他。
洛基察覺到大師沒有接話。這種沉默他很熟悉——不是等答案,而是等他自己想清楚。他深吸一口氣,重新看程式碼,腦中開始推演:「如果個人檔案很大...」他停頓,「不對,應該想想,如果檔案真的很大會發生什麼?」
大師這才開口:「讓我們看看『很大』是什麼意思。首先,DynamoDB 有個硬限制——」
「項目大小上限 400KB,」洛基接話。這是基礎知識。
「對,」大師說,「但問題不只是觸碰上限。即使遠低於 400KB,也可能造成浪費。」
// 問題情境:個人檔案可能很大
const officerWithLargeProfile = {
PK: 'OFFICER#bob',
SK: 'METADATA',
officerId: 'bob',
username: 'bob_martinez',
email: 'bob@starfleet.gov',
profile: {
fullName: 'Bob Martinez',
rank: 'Commander',
homeplanet: 'Mars',
biography: '...(5000字的詳細經歷)...',
commendations: [...], // 50個獎章記錄
trainingHistory: [...], // 100筆訓練記錄
medicalRecords: {...}, // 詳細醫療資料
photoGallery: [...] // 20張照片的 base64 編碼
}
};
// 項目大小
console.log('項目大小:', JSON.stringify(officerWithLargeProfile).length, 'bytes');
// 輸出:項目大小:156,789 bytes(約 153KB)
洛基看著數字:「153KB...還在 400KB 限制內,但...」他快速計算,「如果每秒查詢一千次,那就是每秒傳輸 153MB。」
「而且,」大師提示,「如果你只需要 username 和 email 呢?」
洛基的眉頭皺了起來:「即使我只需要 200 bytes 的基本資料...」他突然明白了,「我還是得讀取整個 153KB?包括那些照片和醫療記錄?」
// 常見場景:只需要基本資訊
async function getOfficerBasicInfo(officerId) {
const result = await docClient.get({
TableName: 'IntergalacticEvents',
Key: {
PK: `OFFICER#${officerId}`,
SK: 'METADATA'
}
}).promise();
// 只需要 username 和 email(約 200 bytes)
// 但讀取了整個 153KB 的項目(包含照片和醫療記錄)
return {
username: result.Item.username,
email: result.Item.email
};
}
洛基的手指在桌上輕輕敲了幾下:「這太浪費了。」他抬起頭,「所以第二條路是什麼?」
大師展示第二個方案:
// 策略二:分離設計(Separate Items)
// 基本資料
const officerBasic = {
PK: 'OFFICER#alice',
SK: 'METADATA',
officerId: 'alice',
username: 'alice_johnson',
email: 'alice@starfleet.gov'
};
// 個人檔案(分開存放)
const officerProfile = {
PK: 'OFFICER#alice',
SK: 'PROFILE',
fullName: 'Alice Johnson',
rank: 'Captain',
homeplanet: 'Earth',
birthdate: '2185-06-15',
biography: '...(詳細經歷)...',
commendations: [...],
trainingHistory: [...]
};
洛基觀察:「用相同的 PK,不同的 SK 來分離資料。」
「正是,」大師說,「現在看看查詢的差異。」
// 場景一:只需要基本資料(快速)
async function getOfficerBasic(officerId) {
const result = await docClient.get({
TableName: 'IntergalacticEvents',
Key: {
PK: `OFFICER#${officerId}`,
SK: 'METADATA'
}
}).promise();
return result.Item;
// 只讀取 200 bytes,很快!
}
// 場景二:需要完整資料(Query 取得所有相關項目)
async function getOfficerWithProfile(officerId) {
const result = await docClient.query({
TableName: 'IntergalacticEvents',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: {
':pk': `OFFICER#${officerId}`
}
}).promise();
return {
basic: result.Items.find(item => item.SK === 'METADATA'),
profile: result.Items.find(item => item.SK === 'PROFILE')
};
// 一次查詢,返回該 PK 下的所有項目,再從中篩選
}
洛基站起來,在兩段程式碼之間來回看:「嵌入很快,但全拿全丟。分離靈活,但要多寫幾行...」他轉向大師,「所以到底該選哪一個?」
「軍人最怕的是什麼?」大師突然問。
洛基愣了一下:「不明確的命令。」
「設計師最怕的也是,」大師走到白板前,「不明確的選擇標準。」他開始寫:
何時使用嵌入設計?
✅ 兩邊資料都很小(總和建議 < 50KB)
✅ 兩邊資料總是一起使用
✅ 更新頻率相同
✅ 查詢速度是首要考量
何時使用分離設計?
✅ 其中一邊資料很大(接近或可能超過 400KB 限制)
✅ 經常只需要其中一邊
✅ 更新頻率差異大
✅ 需要獨立存取控制
注意:DynamoDB 單一項目大小上限為 400KB
洛基盯著白板上的框架,若有所思。半晌,他開口:「所以...沒有『最佳解』,只有『最適解』?」
大師點頭:「就像戰場上沒有絕對的戰術,只有適合當下情境的選擇。」
「那給我一個情境,」洛基突然說,眼神變得銳利,「讓我試試。」
大師在白板角落寫下數據:
查詢頻率統計:
- 只查基本資料(username, email):每秒 1000 次
- 查完整檔案(basic + profile):每分鐘 10 次
洛基看著數字,眼睛瞇了起來。他在腦中快速計算,嘴裡喃喃自語:「每秒一千次基本查詢...每分鐘才十次完整查詢...比例是 6000:1...」
他沉默了三十秒,然後抬起頭:「分離設計。」
「理由?」大師沒有表情。
洛基走到白板前,拿起筆開始寫:
決策:分離設計
理由:
1. 高頻查詢(1000次/秒)只需基本資料
→ 嵌入設計會浪費頻寬,每次讀取 153KB 但只用 200 bytes
2. 完整檔案查詢頻率低(10次/分鐘)
→ 多一次查詢的成本可接受
3. 個人檔案可能很大
→ 分離可避免超過 400KB 限制
取捨:
✗ 犧牲:完整檔案查詢需要稍複雜的邏輯
✓ 獲得:高頻查詢的極致效能
寫完,他轉身面對大師,語氣堅定:「高頻查詢優先。犧牲低頻查詢的一點複雜度,換取主要場景的極致效能。」
大師的眼神閃過一絲讚許,但隨即又恢復平靜:「聽起來很合理。」
洛基察覺到大師的語氣有些保留。他回想剛才的分析,突然意識到一個問題。
「等等,」洛基皺眉,「第 6 天您不是教過我...資料重複是可以接受的?」他指著分離設計的程式碼,「但這裡的分離,不也是某種形式的...不對,這不是重複。」他停住,看向大師,「這兩個有什麼不同?」
大師這次真的笑了:「很好,你開始主動質疑了。」
// 第 6 天的多視角設計
const eventByPlanet = {
PK: 'PLANET#MARS',
SK: 'EVENT#science-conference',
// 活動資料重複存放
};
const eventByDate = {
PK: 'DATE#2210-03-15',
SK: 'EVENT#science-conference',
// 同樣的活動資料,再存一次
};
大師讓他自己觀察兩段程式碼。
洛基盯著看了一會兒,突然恍然大悟:「第 6 天是把同一筆資料,複製到不同的 PK 下——為了不同的查詢維度。」他指著今天的例子,「但今天是把一個實體,拆成多個 SK——為了優化讀取效能。」
「前者是橫向複製,後者是縱向分割,」大師說,「目的不同,手段也不同。」
「嘖,」Hippo 的聲音突然響起,「聽起來很複雜,但其實就是一句話:該合的合,該分的分。」
洛基被 Hippo 突然的介入嚇了一跳,但隨即笑了:「說得輕鬆。」
「因為我不用做決策啊,」Hippo 回答,「我只負責執行你們決定好的事。痛苦的選擇都是你們人類的專利。」
大師做最後總結:
SQL 的一對一設計:
1. 看 ER 圖上有一條線
2. 開兩個表
3. 加外鍵約束
4. 完成
DynamoDB 的一對一設計:
1. 看 ER 圖上有一條線
2. 分析查詢需求
3. 評估資料大小
4. 決策:嵌入 vs 分離
5. 設計 PK/SK 結構
6. 完成
洛基看著白板上的對比,點了點頭:「SQL 給你一條路,DynamoDB 讓你選路。」
「而選路的標準,」大師補充,「是你的查詢需求。」
洛基正要合上筆記本,大師在白板角落畫了新的圖:
軍官 (Officers) 1:N 任務 (Missions)
- 一個軍官可以參與多個任務
- 每個任務記錄包含參與時間、角色等資訊
洛基瞄了一眼:「一對多?」他想了想,「也是嵌入或分離的選擇?」
「如果我說不是呢?」大師反問。
洛基停住了。他看著那個「1:N」的符號,腦中開始推演各種可能性。
「『多』的那邊可能很多,」Hippo 幽幽地說,「如果一個軍官執行了一千個任務,你要怎麼辦?全塞在一個項目裡?還是分開?還是...」它停頓了一下,「還有第三種選擇?」
洛基皺眉。他意識到明天的挑戰,可能不是今天學到的任何一種模式。
「明天見,」大師說。
走出茶室,夜色已深。洛基回想今天的學習——從一開始的自信,到發現嵌入策略的問題,再到理解決策框架。
他突然明白了一件事:在 SQL 的世界裡,規範已經替你做好決策。但在 DynamoDB 的世界裡,你必須自己判斷——什麼時候該合,什麼時候該分。
這讓他想起轉到後勤開發工作後的適應過程。以前他習慣快速反應、立刻行動,但現在需要的是思考、權衡、選擇。
大師今天教的不只是技術,更是一種思維方式——在沒有標準答案的情況下,如何做出正確的判斷。
洛基把筆記本收進包裡,嘴角帶著一絲自嘲的笑:「看來我還需要學會慢下來。」
時間設定說明:故事中使用星際曆(SY210 = 西元2210年),程式碼範例為確保正確執行,使用對應的西元年份。