iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0

洛基推開茶室的門,看見大師正拿著一杯茶,盯著白板上的兩個方塊。

「我們昨天學到 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 的確定性 vs 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 的設計思維

大師做最後總結:

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年),程式碼範例為確保正確執行,使用對應的西元年份。


上一篇
Day 17:關聯式思維的現實撞擊
下一篇
Day 19:一對多關係的階層組織
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言