iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0

第六天,洛基準時走進茶室,神態比前幾天更為專注。昨天大師提到的「重新思考資料組織方式」讓他一整晚都在思考。

「大師,」洛基坐下後說道,「關於您昨天提到的挑戰,我想我準備好了。」

諾斯克微笑:「很好。那我就給你一個實際的任務。」他在白板上寫下一個系統需求:

星際活動管理系統需求:
1. 用戶可以查看某個星球的所有活動
2. 用戶可以搜尋特定日期的活動
3. 用戶可以找到特定主題的活動(如「防禦」、「科技」)
4. 用戶可以查看某個講者的所有活動
5. 用戶可以找到容量大於 N 人的活動
6. 系統管理員需要統計每個星球的活動數量

「這是一個典型的多維度查詢場景,」大師說,「請用你目前學到的知識,設計一個 DynamoDB 方案來支援這些查詢。」

洛基審視著需求,軍人的習慣讓他先進行分析:「看起來很全面...讓我試試現有的方法。」他想起了之前學習的複合主鍵結構:

{
  "PK": "LOCATION#MARS",
  "SK": "DATE#2024-06-15#EVENT#001",
  "name": "火星防禦研討會",
  "speaker": "戰神將軍",
  "capacity": 500,
  "topic": "防禦",
  "status": "ACTIVE"
}

「這個結構應該可以處理大部分查詢,」洛基謹慎地說,「讓我逐一驗證。」

第一個查詢:按星球查找

# 查找火星的所有活動 - 這個可以!
aws dynamodb query \
  --table-name IntergalacticEvents \
  --key-condition-expression "PK = :pk" \
  --expression-attribute-values '{":pk": {"S": "LOCATION#MARS"}}' \
  --endpoint-url http://localhost:8000

「第一個查詢成功,」洛基點頭,「按星球查找確實可行。」

第二個查詢:按日期查找

「接下來試試按日期查找,」洛基繼續,「找 2024-07-20 這天的所有活動...」

# 嘗試按日期查找
aws dynamodb query \
  --table-name IntergalacticEvents \
  --key-condition-expression "SK CONTAINS :date" \
  --expression-attribute-values '{":date": {"S": "2024-07-20"}}' \
  --endpoint-url http://localhost:8000

執行後出現錯誤:Query condition missed key schema element

洛基皺眉:「咦?為什麼不行?」

「因為你無法用 Sort Key 單獨查詢。」大師說,「Query 必須指定 Partition Key。」

洛基皺眉:「那我該怎麼辦?難道沒有辦法搜尋整個表格嗎?」

「有的,這就是 Scan 操作。」大師說,「但在使用之前,讓我先解釋它的工作原理。Hippo 麻煩開一下說明。」

Query:針對特定分區的精確查詢
[分區A] [分區B] [分區C] [分區D]
    ↑
  只查這個

Scan:掃描整個表格
[分區A] [分區B] [分區C] [分區D]
  ↑        ↑        ↑        ↑
 掃描    掃描     掃描     掃描

「Scan 會檢查表格中的每一個 item,」大師解釋,「讓我們試試:」

# 用 Scan + Filter
aws dynamodb scan \
  --table-name IntergalacticEvents \
  --filter-expression "contains(SK, :date)" \
  --expression-attribute-values '{":date": {"S": "2024-07-20"}}' \
  --endpoint-url http://localhost:8000

洛基執行後看著結果:「確實找到了那天的活動!」

然後他檢視著回傳的 ConsumedCapacity,軍人的直覺讓他察覺到問題:「但是...這個成本好像很高?」

大師點頭:「沒錯。Scan 會檢查整張表的每一個 item,即使最終只找到一筆資料,你也要為檢查的所有 item 付費。」

「這效率太低了...」洛基意識到問題的嚴重性。

第三個查詢:按主題查找

「既然學會了 Scan,我繼續試試其他查詢。」洛基說,「找所有『防禦』主題的活動...」

# 按主題查找
aws dynamodb scan \
  --table-name IntergalacticEvents \
  --filter-expression "topic = :topic" \
  --expression-attribute-values '{":topic": {"S": "防禦"}}' \
  --endpoint-url http://localhost:8000

洛基執行後,看著結果陷入沉思:「又是 Scan...」他的表情變得嚴肅,「如果有一萬個活動,我需要掃描一萬個才能找到三個防禦主題的活動?這在戰場上是不可接受的效率。」

第四個查詢:按講者查找

洛基深吸一口氣,繼續驗證他的猜測:

# 找戰神將軍的所有活動
aws dynamodb scan \
  --table-name IntergalacticEvents \
  --filter-expression "speaker = :speaker" \
  --expression-attribute-values '{":speaker": {"S": "戰神將軍"}}' \
  --endpoint-url http://localhost:8000

洛基放下指令,沉思片刻:「果然還是 Scan...我開始看出問題的根源了。」

設計思維的第一次衝擊

洛基放下指令,表情變得凝重:「大師,我發現了一個嚴重的問題。除了第一個查詢,其他的都只能用 Scan。不過 Scan 效能很差...這意味著我的設計根本不實用。」

「沒錯。」大師在白板上畫了一個表格:

查詢需求         能用 Query?    只能用 Scan?    效能
──────────────────────────────────────────────────────
按星球查找        ✓              -               優秀
按日期查找        ✗              ✓               差
按主題查找        ✗              ✓               差
按講者查找        ✗              ✓               差
按容量查找        ✗              ✓               差

「我的設計有什麼問題嗎?」洛基問。

「問題不在於你的設計技巧,」大師說,「而在於你的設計思維。告訴我,你是如何設計這個資料結構的?」

洛基回想:「我先想『活動有哪些屬性』,然後把它們放進 item 裡。PK 設為 LOCATION 是因為...因為位置很重要?」

「你是從『資料有什麼』的角度思考,」大師指出,「但 DynamoDB 的設計原則是從『如何查詢』的角度思考。」

大師在白板上寫下兩種思維方式:

傳統資料庫思維(你現在的方式):
1. 分析實體和屬性
2. 設計表格結構
3. 考慮如何查詢

DynamoDB 思維:
1. 分析查詢需求(Access Patterns)
2. 設計支援這些查詢的結構
3. 儲存資料

Access Pattern 分析

「什麼是 Access Patterns?」洛基問。

「就是你的系統需要回答的所有問題。」大師說,「我們重新整理一下你的需求:」

Access Patterns (存取模式):
AP1: 找某個星球的所有活動
AP2: 找特定日期的所有活動(跨星球)
AP3: 找特定主題的所有活動(跨星球)
AP4: 找某個講者的所有活動(跨星球)
AP5: 找容量大於 N 的所有活動(跨星球)
AP6: 統計每個星球的活動數量

「現在,讓我問你一個關鍵問題:」大師停頓,「你能用一個資料結構同時高效地支援所有這些查詢嗎?」

洛基思考了很久:「似乎...不行?每個查詢都需要不同的主鍵設計。」

「正是如此!」大師讚許,「這就是 NoSQL 設計的核心挑戰。」

重新思考資料組織

「那我該怎麼辦?」洛基問,「建立六個不同的表格?」

大師回答,「有時候,我們需要用不同的方式組織同一份資料。」

諾斯克大師在白板上寫下:

傳統想法:一個活動 = 一個 item
{PK: "LOCATION#MARS", SK: "DATE#2024-06-15#EVENT#001", ...}

重新思考:一個活動 = 多個 items
{PK: "LOCATION#MARS", SK: "DATE#2024-06-15#EVENT#001", ...}    // 按位置查找
{PK: "DATE#2024-06-15", SK: "EVENT#001", ...}                  // 按日期查找
{PK: "TOPIC#防禦", SK: "EVENT#001", ...}                        // 按主題查找
{PK: "SPEAKER#戰神將軍", SK: "EVENT#001", ...}                   // 按講者查找

「等等,」洛基驚訝,「您是說同一個活動要存儲四次?」

「為什麼不?」大師反問,「儲存成本很便宜,但查詢效能很昂貴。而且,這些不是『重複』,而是『不同的視角』。」

「但是...更新的時候不是要更新四個地方?」

「這就是 NoSQL 的權衡,」大師說,「我們用儲存空間和更新複雜度,換取查詢效能。在 DynamoDB 中,快速查詢比資料正規化更重要。」

第一次設計實驗

「讓我們試試這個新思路。」大師說,「我們先實作前兩個 Access Patterns。」

# 建立按日期查找的資料
aws dynamodb put-item \
  --table-name IntergalacticEvents \
  --item '{
    "PK": {"S": "DATE#2024-06-15"},
    "SK": {"S": "EVENT#001"},
    "name": {"S": "火星防禦研討會"},
    "location": {"S": "MARS"},
    "speaker": {"S": "戰神將軍"},
    "capacity": {"N": "500"},
    "topic": {"S": "防禦"}
  }' \
  --endpoint-url http://localhost:8000

# 現在可以按日期查詢了!
aws dynamodb query \
  --table-name IntergalacticEvents \
  --key-condition-expression "PK = :pk" \
  --expression-attribute-values '{":pk": {"S": "DATE#2024-06-15"}}' \
  --endpoint-url http://localhost:8000

洛基看著結果:「真的可以快速找到這一天的所有活動!而且是用 Query,不是 Scan!」

「這就是 Access Pattern 驅動設計的威力。」大師說。

設計思維的轉變

「但這樣設計好複雜...」洛基有些擔心。

「複雜性是設計師的責任,」大師說,「而不是使用者的負擔。你寧願花時間思考設計,還是讓用戶忍受緩慢的查詢?」

洛基點頭:「我明白了。設計不是為了讓程式碼簡單,而是為了讓系統高效。」

「正是如此。在 DynamoDB 中,我們的設計哲學是:」

DynamoDB 設計原則:
1. Access Patterns 決定資料結構
2. 查詢效能優於儲存正規化
3. 一個邏輯實體可以有多個物理表示
4. 設計複雜度換取查詢簡單度

新的挑戰

「我想試試更複雜的設計。」洛基說,「如果我要支援所有六個 Access Patterns,該怎麼做?」

大師微笑:「這是一個很好的挑戰。但在此之前,你需要學習更多工具。」

「什麼工具?」

「明天,我們會遇到一個真實的複雜場景,」大師說,「你會發現,有些問題需要更系統性的方法來解決。」

「更複雜?」洛基既興奮又緊張。

「不要擔心,」大師站起身,「今天你已經完成了最重要的思維轉變:從『資料導向』到『查詢導向』的設計思維。這是使用 DynamoDB 最關鍵的第一步。」

大師停頓了一下,眼中閃過一絲神秘的光芒:「你今天探索的這種設計方法,其實有一個正式的名稱和完整的方法論。等你對這些基礎技巧更熟練後,我會教你這個被業界認為的最佳實踐設計模式。」

今日的領悟

走出茶室時,洛基回想著今天的學習。原來他一直以為的「資料庫設計」只是冰山一角。真正的挑戰不是如何儲存資料,而是如何組織資料以支援高效查詢。

在他的筆記本上,他寫下了今天領悟。

明天,又會有什麼新的挑戰在等著他呢?而大師提到的那個「正式的設計模式」又是什麼呢?

Hippo 的課外教學

Access Pattern 分析方法

1. 需求收集模板

## Access Pattern 分析表

| AP# | 查詢需求         | 頻率 | 效能要求 | 資料量     | 成長性   |
| --- | ---------------- | ---- | -------- | ---------- | -------- |
| AP1 | 查某星球所有活動 | 高   | <100ms   | ~100 items | 線性成長 |
| AP2 | 查特定日期活動   | 中   | <100ms   | ~50 items  | 季節性   |
| AP3 | 查特定主題活動   | 低   | <200ms   | ~20 items  | 穩定     |

2. 設計權衡分析

// 設計選項評估
const designOptions = {
  singleTable: {
    pros: ["單一表格管理", "一致性容易"],
    cons: ["大部分查詢需要Scan", "效能差"],
    score: 3,
  },
  multipleViews: {
    pros: ["每個查詢都用Query", "效能優秀"],
    cons: ["資料重複", "更新複雜"],
    score: 8,
  },
  hybridApproach: {
    pros: ["平衡效能和複雜度"],
    cons: ["部分查詢仍需Scan"],
    score: 6,
  },
};

3. 實務設計模式

# 模式1: 實體複製模式
# 同一活動的不同視角
PK="LOCATION#MARS",    SK="EVENT#001"     # 按位置查找
PK="DATE#2024-06-15",  SK="EVENT#001"     # 按日期查找
PK="TOPIC#防禦",        SK="EVENT#001"     # 按主題查找

# 模式2: 階層式組織
# 利用 Sort Key 的排序特性
PK="MARS",  SK="2024-06-15#EVENT#001"     # 位置 > 時間
PK="MARS",  SK="TOPIC#防禦#EVENT#001"      # 位置 > 主題

# 模式3: 複合查詢支援
# 支援多條件查詢
PK="MARS#防禦",  SK="2024-06-15#EVENT#001"   # 位置+主題

4. 更新策略

// 實體更新的事務處理
async function updateEvent(eventId, updates) {
  const transactItems = [];

  // 更新所有視角
  const viewTypes = ["LOCATION", "DATE", "TOPIC", "SPEAKER"];

  for (const viewType of viewTypes) {
    const key = generateKeyForView(viewType, eventId);
    transactItems.push({
      Update: {
        TableName: "IntergalacticEvents",
        Key: key,
        UpdateExpression: "SET #name = :name, capacity = :capacity",
        ExpressionAttributeNames: { "#name": "name" },
        ExpressionAttributeValues: {
          ":name": updates.name,
          ":capacity": updates.capacity,
        },
      },
    });
  }

  // 使用事務確保一致性
  await docClient
    .transactWrite({
      TransactItems: transactItems,
    })
    .promise();
}

常見設計錯誤

  1. 過度正規化

    錯誤:試圖避免任何資料重複
    正確:接受重複以換取查詢效能
    
  2. 忽略查詢頻率

    錯誤:為罕見查詢過度設計
    正確:優先設計高頻查詢路徑
    
  3. 設計時沒考慮擴展

    錯誤:只考慮目前的資料量
    正確:預估未來 2-3 年的成長
    

Hippo:「好的 DynamoDB 設計是藝術,不是科學。需要在複雜度、效能、成本間找到最佳平衡點!」


上一篇
Day 5:Query 與 Get 的差異
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言