「早安,大師!」洛基神情愉悅地說,「我昨天練習了資料型別的處理,成功建立了一個包含上百個活動的測試資料庫!」
諾斯克大師看了看洛基的清單:「很好!看起來你已經可以處理大量資料了。不過,我想問你一個問題:當你的應用程式需要顯示這上百個活動時,你打算怎麼做?」
洛基想了想:「就...一次全部查出來顯示?」
Hippo 的聲音響起:「哦不,菜鳥要犯大錯了!想像一下用戶打開你的應用,等了十秒才看到畫面,最後因為記憶體不足而當機...」
洛基恍然大悟:「我需要分頁!但...要怎麼實現?」
諾斯克大師在白板上畫了三個圓圈:
1. 狀態管理:如何記住「我在哪一頁」?
2. 導航設計:如何前進、後退、跳頁?
3. 使用體驗:用戶期待什麼樣的互動?
「大多數開發者只關注第一個問題,」大師說,「但真正的挑戰在於如何整合這三者。」
洛基若有所思:「在傳統資料庫中,我可以用 OFFSET 和 LIMIT...」
「但 DynamoDB 沒有 OFFSET,」大師提醒,「這反而是件好事。讓我解釋為什麼。」
大師在白板上畫出兩種分頁方式的對比:
傳統 OFFSET 方式:
- 第1頁:SELECT * FROM events LIMIT 10 OFFSET 0 (讀 10 筆)
- 第2頁:SELECT * FROM events LIMIT 10 OFFSET 10 (讀 20 筆,丟棄前 10 筆)
- 第10頁:SELECT * FROM events LIMIT 10 OFFSET 90 (讀 100 筆,丟棄前 90 筆)
問題:資料庫必須掃描並跳過前面所有資料!
DynamoDB 方式:
- 第1頁:從頭開始讀 10 筆,回傳 LastEvaluatedKey A
- 第2頁:從 ExclusiveStartKey=A 繼續讀 10 筆,回傳 LastEvaluatedKey B
- 第10頁:從 ExclusiveStartKey=I 繼續讀 10 筆
優勢:每次只讀取需要的資料,效能穩定!
「原來如此!」洛基說,「LastEvaluatedKey 是最後一筆資料的完整 key,下次查詢會從這個 key 之後開始。」
「正是,」大師點頭,「但這也帶來新的挑戰:你無法直接跳到第 10 頁,必須一頁一頁地翻。」
洛基皺眉:「那用戶想要回到上一頁怎麼辦?」
大師微笑:「這就需要你自己管理分頁歷史了。想像你在讀一本書...」
// 概念示意
class PaginationConcept {
constructor() {
// 關鍵:保存每一頁的起始 key
this.pageStartKeys = [null]; // null 代表第一頁
this.currentPage = 0;
}
nextPage(lastEvaluatedKey) {
// 記住下一頁的起始位置
this.pageStartKeys.push(lastEvaluatedKey);
this.currentPage++;
}
previousPage() {
// 回到上一頁的起始位置
if (this.currentPage > 0) {
this.currentPage--;
return this.pageStartKeys[this.currentPage];
}
}
}
「我懂了!」洛基興奮地說,「我需要保存每一頁的起始 key,這樣才能實現前後導航!」
Hippo 插話:「聰明!但別忘了考慮記憶體。如果用戶翻了 100 頁,你要保存 100 個起始 key 嗎?」
「讓我告訴你一個重要的事實,」大師正色道,「DynamoDB 的 LastEvaluatedKey
是單向的。」
洛基疑惑:「單向?什麼意思?」
DynamoDB 分頁機制的限制:
1. 只提供 LastEvaluatedKey(下一頁的起點)
2. 沒有 PreviousEvaluatedKey
3. LastEvaluatedKey 只能用來「繼續往下讀」
4. 無法反向使用這個 key 回到上一頁
「如果真的要實作『上一頁』功能,」大師在白板上寫道:
// 實際的實作方式
class RealPagination {
constructor() {
// 必須自己記錄每一頁的進入點
this.pageHistory = [
null, // 第 1 頁:從頭開始
{ PK: "EVENT#123", SK: "..." }, // 第 2 頁的起點
{ PK: "EVENT#456", SK: "..." }, // 第 3 頁的起點
];
}
// 回到第 2 頁
goToPage(pageNumber) {
const params = {
TableName: "IntergalacticEvents",
Limit: 10,
ExclusiveStartKey: this.pageHistory[pageNumber - 1],
};
// 重新查詢該頁(適用於 Query,Scan 也可以使用相同機制)
return docClient.query(params);
}
}
「但是,」大師提出關鍵問題,「這個設計有幾個嚴重問題。」
大師列出了實務考量:
技術挑戰:
1. 記憶體負擔:保存所有起始 key 佔用空間
2. 狀態管理複雜:需要在前端或後端維護歷史
3. 資料變動問題:如果中間有資料被刪除或新增怎麼辦?
4. Key 過期:某些場景下 ExclusiveStartKey 可能失效
使用者體驗考量:
1. 現代 UX 趨勢:無限滾動 > 傳統分頁
2. 使用者習慣:很少真的需要「回到第 3 頁」
3. 替代方案:提供搜尋和篩選功能更實用
「所以現代應用的選擇是?」洛基問。
「接受這個限制,」大師說,「選擇更適合的 UX 模式:」
// 實務上的三種替代方案
const practicalApproaches = {
1: "無限滾動:只往下,不回頭",
2: "載入更多:累積顯示,無需返回",
3: "重新查詢:提供篩選條件而非分頁",
};
洛基恍然大悟:「原來不是每個技術限制都需要克服,有時候改變設計思維更聰明!」
大師在白板上列出三種常見的分頁模式:
特點:顯示 [1] [2] [3] ... [10] [下一頁]
適用:用戶需要跳頁、知道總頁數
挑戰:DynamoDB 不提供總數,需要額外設計
洛基思考:「如果我要顯示總頁數,是不是得先掃描整個表?」
「那會很慢,」大師說,「更好的方法是維護一個計數器,或者接受『不顯示總頁數』這個限制。」
特點:滾動到底部自動載入更多
適用:社交媒體、動態內容
優勢:最符合 DynamoDB 的設計哲學
「這個最簡單!」洛基說,「只要持續使用 LastEvaluatedKey 就好。」
大師點頭:「但要注意效能和記憶體管理。你需要決定何時清理舊資料。」
特點:用戶主動控制載入時機
適用:資料密集型應用
平衡:介於傳統分頁和無限滾動之間
「原則我大概知道了,」洛基說,「但實際整合到前端時,有什麼要注意的?」
大師列出幾個關鍵點:
1. 載入狀態管理
- 顯示載入中的提示
- 防止重複請求
- 處理載入失敗
2. 資料快取策略
- 是否快取已載入的頁面?
- 如何處理資料更新?
- 何時清理快取?
3. 使用體驗優化
- 預載入下一頁
- 平滑的滾動體驗
- 返回時記住位置
大師再分享了一些實戰經驗:
「第一,合理設定 Limit 參數。」大師說,「太小會增加請求次數,太大會增加延遲。通常 20-50 是個好範圍。」
// 動態調整頁面大小的概念
function determinePageSize(context) {
if (context.isMobile) return 20;
if (context.isSlowNetwork) return 10;
return 50; // 桌面端預設
}
「第二,考慮使用投影(Projection)減少資料傳輸:」
// 只取需要的欄位
{
ProjectionExpression: "id, title, date, summary",
// 而不是取回整個 item
}
「第三,預載入策略:」
洛基問:「預載入下一頁會不會浪費?」
「取決於用戶行為,」大師說,「如果 80% 的用戶會看第二頁,預載入就值得。你需要收集資料來決定。」
大師畫了一個決策流程圖:
問:資料集大小?
├─ < 100 筆:考慮一次載入全部
└─ > 100 筆:需要分頁
│
問:用戶需要跳頁嗎?
├─ 是:實作頁碼管理系統
└─ 否:
│
問:內容是否連續瀏覽?
├─ 是:使用無限滾動
└─ 否:使用載入更多按鈕
洛基問:「實作分頁時最容易犯什麼錯誤?」
大師列出了幾個常見問題:
「當用戶在第 2 頁時,第 1 頁的資料可能已經變了。」大師說,「DynamoDB 的分頁是即時的,不是快照。」
解決方案:
「如果在分頁過程中有新資料插入到已讀取的位置之前,下一頁可能會看到重複。」
解決方案:
「無限滾動很容易造成記憶體問題。」
解決方案:
經過一天的學習,洛基在筆記本上總結:
分頁不只是技術問題,更是使用體驗設計:
1. 理解 LastEvaluatedKey 的本質
- 它是最後一筆資料的完整 key(包含 PK 和 SK)
- 下次查詢用 ExclusiveStartKey 從這裡繼續
2. 選擇適合的分頁模式
- 考慮用戶需求,不是技術偏好
- 接受 DynamoDB 的限制
3. 管理好狀態和效能
- 分頁歷史要有上限
- 合理使用快取和預載入
4. 永遠從用戶角度思考
- 他們不在乎你用什麼資料庫
- 他們只在乎速度和體驗
「很好的總結,」大師讚許道,「記住,DynamoDB 的分頁設計反映了它的哲學:為規模化而生。犧牲一些靈活性,換取可預測的效能。」
大師接著預告:「明天我們要學習批次操作,批次操作的藝術在於平衡效能與可靠性,希望你也可以像今天一樣順利地領悟。」