大師諾斯克對洛基說:「今天我會帶你看一下 Query 操作本身的邊界。即使有完美的優先級分析,有些技術限制是無法迴避的。」
洛基不禁想著今天又會經過什麼樣的腦內大爗炸。
大師在白板上寫下一個看似簡單的需求:
新需求:查詢所有星球在 SY210-09-15 到 SY210-09-30 期間的活動
洛基思考:「這聽起來和第5天學到的很相似。我記得那時我用了時間範圍查詢。」
「很好,你記得之前的學習,」大師說,「但讓我們仔細比較一下:」
第5天的場景:查詢火星基地在特定時間範圍的活動
第9天的場景:查詢所有星球在特定時間範圍的活動
洛基皺眉:「差別在於...第5天是『特定星球』,今天是『所有星球』?」
大師點頭:「讓我們回顧第5天的成功方案:」
{
"PK": "LOCATION#MARS",
"SK": "DATE#SY210-03-15#EVENT#001",
"name": "火星防禦研討會"
}
「這個設計能夠查詢火星基地的時間範圍活動,」洛基說,「我用了:」
# Day05 的成功查詢
aws dynamodb query \
--table-name IntergalacticEvents \
--key-condition-expression "PK = :pk AND SK BETWEEN :start AND :end" \
--expression-attribute-values '{
":pk": {"S": "LOCATION#MARS"},
":start": {"S": "DATE#SY210-09-15"},
":end": {"S": "DATE#SY210-09-30"}
}' \
--endpoint-url http://localhost:8000
「現在,」大師問,「如果要查詢『所有星球』的活動,你的 PK 要寫什麼?」
洛基想了想:「我需要...不指定星球?但 PK 是必須的!」
「正確,」大師說,「PK 必須是精確的值匹配。你無法寫:」
# ❌ 這是不可能的
PK = "任何星球" AND SK BETWEEN ...
# ❌ 這也是不可能的
PK LIKE "LOCATION#%" AND SK BETWEEN ...
洛基恍然大悟:「所以如果我要查詢所有星球,我必須...」
「你有三個選擇,」大師在白板上寫下:
解決方案比較:
方案 A:多次 Query
- 對每個已知星球執行一次 Query
- MARS: Query PK="LOCATION#MARS" AND SK BETWEEN ...
- VENUS: Query PK="LOCATION#VENUS" AND SK BETWEEN ...
- 在應用程式中合併結果
方案 B:重新設計 PK 結構
- PK: "GLOBAL", SK: "DATE#SY210-09-15#MARS#EVENT#001"
- 但失去按星球查詢的優化能力
方案 C:使用 Scan
- 全表掃描 + FilterExpression
- 成本高但實作簡單
洛基思考:「所以第5天的成功是建立在『已知星球』這個前提上。如果查詢範圍擴大到『所有星球』,就遇到了 PK 精確匹配的限制。」
「完全正確,」大師說,「這就是 Query 的第一個技術邊界。」
大師繼續:「讓我們看另一個場景。如果用戶想要『查詢火星基地的 Foundation 主題活動』呢?」
洛基回想起第6天學到的多視角設計:
按星球的視角:
{PK: "LOCATION#MARS", SK: "DATE#SY210-09-15#EVENT#001", topic: "Foundation"}
按主題的視角:
{PK: "TOPIC#Foundation", SK: "DATE#SY210-09-15#EVENT#001", planet: "MARS"}
「如果我用按星球的視角查詢,」洛基分析,「我能找到火星的所有活動,然後...在結果中過濾 Foundation 主題?」
「正確的思路,」大師說,「但這帶來什麼問題?」
洛基想了想:「我需要取回所有火星活動,即使我只需要其中的 Foundation 主題活動。這增加了 RCU 消耗。」
大師點頭:「更重要的是,你無法在 Query 操作中直接結合多個維度。」
# ❌ Query 無法直接做到
PK = "LOCATION#MARS" AND topic = "Foundation" AND SK BETWEEN ...
# ✓ 只能是
PK = "LOCATION#MARS" AND SK BETWEEN ...
# 然後在應用程式中過濾 topic
洛基理解了:「所以 Query 的設計理念是『一次查詢,一個存取路徑』。如果我需要結合多個條件,就超出了 Query 的設計邊界。」
大師提出新的挑戰:「假設你需要支援這些查詢:」
查詢 A:按星球+時間範圍查詢活動
查詢 B:按星球+活動 ID 查詢特定活動
查詢 C:按星球+主題查詢活動
洛基思考:「對於查詢 A,我需要 SK 包含時間信息,像 DATE#SY210-09-15#EVENT#001
」
「對於查詢 B 呢?」大師問。
洛基皺眉:「如果 SK 是日期開頭,我就無法直接用活動 ID 查詢...除非我把活動 ID 放在前面,但那樣又無法做時間範圍查詢。」
大師在白板上寫下衝突:
SK 設計的兩難:
設計 A: SK = "DATE#SY210-09-15#EVENT#001"
✓ 支援時間範圍查詢
❌ 無法直接按活動 ID 查詢
設計 B: SK = "EVENT#001#DATE#SY210-09-15"
✓ 支援按活動 ID 查詢
❌ 無法做時間範圍查詢
設計 C: 建立多個視角
✓ 支援所有查詢類型
❌ 維護成本大幅增加
洛基發現:「所以我必須在設計 SK 時就決定優先支援哪種查詢模式。Query 不允許我事後靈活地改變查詢方式。」
大師最後提出:「如果小行星5020前哨站的管理員想要查詢『火星基地在九月份的 Foundation 主題活動,且報名人數超過 50 人』,你要怎麼設計?」
洛基分析這個需求的複雜性:
查詢條件分析:
- 星球:火星 (可以用 PK)
- 時間:九月份 (可以用 SK 範圍查詢)
- 主題:Foundation (???)
- 報名人數:>50 (???)
「我發現這個查詢涉及四個不同的維度,」洛基說,「但 Query 的 Key Condition 只能處理 PK 和 SK 兩個維度。那其他條件要怎麼辦?」
大師點頭:「這就是問題所在。」
洛基困惑:「所以我只能先用 PK 和 SK 查詢出火星九月份的所有活動,然後...再想辦法過濾主題和報名人數?但這樣可能會取得很多不需要的資料。」
「正確,」大師說,「而且更重要的是:」
複合查詢的根本問題:
- Query 只為『單一存取路徑』優化
- 複雜的多維度查詢不是 Query 的設計目標
- 額外的過濾步驟可能會帶來效率問題
洛基沉默了一會兒,然後說:「我現在明白了。我一直在想如何讓 Query 做更多事情,但其實應該理解 Query 被設計來做什麼。」
大師微笑:「這就是今天最重要的認知轉變。Query 的力量在於它在特定存取模式下的極高效率,不是在於靈活性。」
洛基點頭:「所以優秀的設計是『接受 Query 的邊界』,然後在這個邊界內做出最佳決策?」
「完全正確,」大師說,「這就是 DynamoDB 設計藝術的精髓。」
大師在白板上總結:
面對 Query 邊界的務實策略:
1. 明確識別邊界
- PK 只能精確匹配
- 一次查詢一個存取路徑
- SK 設計必須預先決定查詢模式
2. 在邊界內優化
- 為核心級查詢設計專用視角
- 接受重要級查詢的適度妥協
3. 邊界外的解決方案
- 低頻複雜查詢:使用 Scan
- 極複雜查詢:考慮外部搜尋引擎
- 混合策略:Query + 後處理
「明天我們就會學習如何在這些技術邊界下,仍然能夠明智地使用一些輔助工具來改善查詢體驗。」大師作了結語。
走出茶室時,洛基感到思維大爆炸的快感與疲憊。不再試圖讓單一工具解決所有問題,而是開始思考如何為不同類型的問題選擇合適的工具,是他今天的心得,也許這就是大師一直在強調網狀思考的意義。
Query 邊界內:
✓ 單一 PK 的精確查詢
✓ PK + SK 範圍查詢
✓ 簡單的 FilterExpression
Query 邊界外但 DynamoDB 可行:
→ Scan + FilterExpression
→ GSI 的額外存取路徑
→ 多次 Query + 合併結果
DynamoDB 邊界外:
→ ElasticSearch 用於複雜搜尋
→ Data Pipeline 用於分析查詢
→ 快取層用於熱點資料