iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0

「早安,大師!」洛基神情愉悅地說,「我昨天練習了資料型別的處理,成功建立了一個包含上百個活動的測試資料庫!」

諾斯克大師看了看洛基的清單:「很好!看起來你已經可以處理大量資料了。不過,我想問你一個問題:當你的應用程式需要顯示這上百個活動時,你打算怎麼做?」

洛基想了想:「就...一次全部查出來顯示?」

Hippo 的聲音響起:「哦不,菜鳥要犯大錯了!想像一下用戶打開你的應用,等了十秒才看到畫面,最後因為記憶體不足而當機...」

洛基恍然大悟:「我需要分頁!但...要怎麼實現?」

分頁的三個核心問題

諾斯克大師在白板上畫了三個圓圈:

1. 狀態管理:如何記住「我在哪一頁」?
2. 導航設計:如何前進、後退、跳頁?
3. 使用體驗:用戶期待什麼樣的互動?

「大多數開發者只關注第一個問題,」大師說,「但真正的挑戰在於如何整合這三者。」

洛基若有所思:「在傳統資料庫中,我可以用 OFFSET 和 LIMIT...」

「但 DynamoDB 沒有 OFFSET,」大師提醒,「這反而是件好事。讓我解釋為什麼。」

LastEvaluatedKey 的智慧

大師在白板上畫出兩種分頁方式的對比:

傳統 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. 傳統分頁(頁碼導航)

特點:顯示 [1] [2] [3] ... [10] [下一頁]
適用:用戶需要跳頁、知道總頁數
挑戰:DynamoDB 不提供總數,需要額外設計

洛基思考:「如果我要顯示總頁數,是不是得先掃描整個表?」

「那會很慢,」大師說,「更好的方法是維護一個計數器,或者接受『不顯示總頁數』這個限制。」

2. 無限滾動(Infinite Scroll)

特點:滾動到底部自動載入更多
適用:社交媒體、動態內容
優勢:最符合 DynamoDB 的設計哲學

「這個最簡單!」洛基說,「只要持續使用 LastEvaluatedKey 就好。」

大師點頭:「但要注意效能和記憶體管理。你需要決定何時清理舊資料。」

3. 載入更多按鈕

特點:用戶主動控制載入時機
適用:資料密集型應用
平衡:介於傳統分頁和無限滾動之間

前端整合的關鍵考量

「原則我大概知道了,」洛基說,「但實際整合到前端時,有什麼要注意的?」

大師列出幾個關鍵點:

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 筆:需要分頁
   │
   問:用戶需要跳頁嗎?
   ├─ 是:實作頁碼管理系統
   └─ 否:
      │
      問:內容是否連續瀏覽?
      ├─ 是:使用無限滾動
      └─ 否:使用載入更多按鈕

常見陷阱與解決方案

洛基問:「實作分頁時最容易犯什麼錯誤?」

大師列出了幾個常見問題:

陷阱 1:資料一致性

「當用戶在第 2 頁時,第 1 頁的資料可能已經變了。」大師說,「DynamoDB 的分頁是即時的,不是快照。」

解決方案:

  • 接受即時資料(最簡單,也是 DynamoDB 預設行為)
  • 使用時間戳作為 SK,確保資料不會被更新
  • 在應用層實作快照機制

陷阱 2:重複資料

「如果在分頁過程中有新資料插入到已讀取的位置之前,下一頁可能會看到重複。」

解決方案:

  • 在前端去重複
  • 使用唯一識別碼追蹤
  • 接受少量重複(某些場景可接受)

陷阱 3:記憶體膨脹

「無限滾動很容易造成記憶體問題。」

解決方案:

  • 虛擬滾動(只渲染可見部分)
  • 定期清理舊資料
  • 設定最大快取數量

洛基的領悟

經過一天的學習,洛基在筆記本上總結:

分頁不只是技術問題,更是使用體驗設計:

1. 理解 LastEvaluatedKey 的本質
   - 它是最後一筆資料的完整 key(包含 PK 和 SK)
   - 下次查詢用 ExclusiveStartKey 從這裡繼續

2. 選擇適合的分頁模式
   - 考慮用戶需求,不是技術偏好
   - 接受 DynamoDB 的限制

3. 管理好狀態和效能
   - 分頁歷史要有上限
   - 合理使用快取和預載入

4. 永遠從用戶角度思考
   - 他們不在乎你用什麼資料庫
   - 他們只在乎速度和體驗

「很好的總結,」大師讚許道,「記住,DynamoDB 的分頁設計反映了它的哲學:為規模化而生。犧牲一些靈活性,換取可預測的效能。」

大師接著預告:「明天我們要學習批次操作,批次操作的藝術在於平衡效能與可靠性,希望你也可以像今天一樣順利地領悟。」


上一篇
Day 13:資料型別的真實世界
下一篇
Day 15:批次操作的效能藝術
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言