iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0

洛基看著白板上並排寫著兩個詞:

Global Secondary Index (GSI)
Local Secondary Index (LSI)

「昨天學了 GSI,」大師說,沒有轉頭,「今天學 LSI。它們都叫索引,但本質完全不同。」

「LSI 也是用來解決查詢問題的嗎?」洛基問。

「是,但解決的是不同類型的問題,」大師轉身,「我先問你一個問題——GSI 和 LSI 最大的差異是什麼?」

洛基想了想:「一個是 Global,一個是 Local?」

「這只是名字,」大師說,「我們要更深入本質差異。」

他在白板上畫出兩個表結構:

// GSI:全新的查詢路徑
主表:
PK: LOCATION#MARS
SK: EVENT#SY210-03-15-001

GSI:
GSI_PK: TOPIC#Physics      // ← 不同的 PK
GSI_SK: EVENT#SY210-03-15-001

// LSI:相同實體的不同排序
主表:
PK: USER#alice
SK: ORDER#2210-03-15#001

LSI:
PK: USER#alice             // ← 相同的 PK
LSK: AMOUNT#299#ORDER#001  // ← 不同的排序鍵

洛基盯著這個對比,慢慢說:「GSI 改變了 PK,建立全新的查詢路徑。LSI 保持相同的 PK,只是改變排序方式。」

「正確,」大師說,「這就是本質差異。」

他用紅筆在 GSI 旁邊寫下「新的查詢維度」,用藍筆在 LSI 旁邊寫下「相同實體的不同視角」。

「今天,」大師說,「我們要理解 LSI 為什麼存在、什麼時候用、以及為什麼大部分情況下我們不用它。」


LSI 的存在理由

大師在白板上寫下一個場景:

// 需求:查詢特定使用者的訂單

// 主表設計:
{
  PK: 'USER#alice',
  SK: 'ORDER#2210-03-15#001',  // 按建立時間排序
  orderDate: '2210-03-15',
  amount: 299,
  status: 'completed'
}

// 查詢 1:按建立時間查詢
await docClient.query({
  KeyConditionExpression: 'PK = :pk',
  ExpressionAttributeValues: { ':pk': 'USER#alice' }
});
// → 結果按 SK 排序,也就是按建立時間排序 ✓

// 查詢 2:按訂單金額排序
// ???

「現在問題來了,」大師看著洛基,「要按金額排序,你會怎麼做?」

洛基思考:「amount 不在 SK 裡...只能取得所有訂單後,在應用層排序。」

「對,」大師說,「或者?」

「建立 GSI,」洛基說,「用 amount 作為 GSI_PK。」

大師在白板上寫下這個方案:

// 方案:用 GSI

GSI_AmountIndex: GSI_PK: "AMOUNT#299";
GSI_SK: "ORDER#001";

// 問題:
// 要查詢「Alice 的訂單,按金額排序」
// GSI_PK 是 AMOUNT,不是 USER
// 無法限定在 Alice 的訂單範圍內

洛基看出問題了:「GSI 改變了 PK,所以無法保留『特定使用者』這個限制。」

「正確,」大師說,「這就是 GSI 的局限——它建立的是全新的查詢路徑,無法保留原本 PK 的限制範圍。」

他繼續:「但 LSI 可以。」

// LSI 設計:

主表:
PK: 'USER#alice'
SK: 'ORDER#2210-03-15#001'

LSI_AmountIndex:
PK: 'USER#alice'                // ← 保持相同
LSK: 'AMOUNT#299#ORDER#001'     // ← 改變排序

// 查詢:Alice 的訂單,按金額排序
await docClient.query({
  TableName: 'Orders',
  IndexName: 'LSI-Amount',
  KeyConditionExpression: 'PK = :pk',
  ExpressionAttributeValues: { ':pk': 'USER#alice' }
});

// → 結果按 LSK 排序,也就是按金額排序 ✓
// → 且限定在 Alice 的訂單範圍內 ✓

洛基理解了:「所以 LSI 是『在同一個 PK 範圍內,提供不同的排序方式』。」

「完全正確,」大師說,「這就是 LSI 存在的理由。」


LSI 的獨特價值:強一致性

大師繼續:「LSI 還有一個 GSI 沒有的特性。」

他在白板上寫下:

GSI:最終一致性(Eventually Consistent)
LSI:支援強一致性(Strongly Consistent)

洛基想起第 23 天學到的:「GSI 的更新有延遲,剛寫入的資料可能查不到。」

「對,」大師說,「但 LSI 可以選擇強一致性讀取。」

他展示場景:

// 場景:下單後立即查詢「我的最新訂單」

// 1. 建立訂單
await docClient.put({
  TableName: "Orders",
  Item: {
    PK: "USER#alice",
    SK: "ORDER#2210-03-15#001",
    amount: 299,
  },
});

// 2. 查詢 LSI(強一致性)
const result = await docClient.query({
  TableName: "Orders",
  IndexName: "LSI-Amount",
  KeyConditionExpression: "PK = :pk",
  ExpressionAttributeValues: { ":pk": "USER#alice" },
  ConsistentRead: true, // ← LSI 支援!GSI 不支援
});

// → 一定能查到剛才建立的訂單 ✓

洛基驚訝:「所以 LSI 沒有 GSI 的最終一致性問題?」

「在強一致性讀取模式下,沒有,」大師說,「這是 LSI 的第二個獨特價值。」

他在白板上總結:

LSI 的兩個獨特價值:

1. 保留 PK 的範圍限制
   - 只改變排序,不改變查詢範圍
   - 適合「同一實體的不同排序需求」

2. 支援強一致性讀取
   - 可選 ConsistentRead: true
   - 無延遲問題

洛基看著這些優點,問:「那為什麼不都用 LSI?」

大師微笑:「因為 LSI 有非常嚴格的限制。」


LSI 的嚴格限制

大師翻到白板的另一面,寫下:

LSI 的三大限制:

限制 1:必須在建表時定義
限制 2:10GB per partition key
限制 3:共用主表容量

「限制 1,」大師說,「LSI 必須在建表時定義,之後無法新增或刪除。」

洛基皺眉:「所以如果我忘記建 LSI...」

「就必須刪除表重建,」大師說,「這是 LSI 最大的限制。」

他展示對比:

// GSI:靈活
// ✓ 隨時新增
// ✓ 隨時刪除
// ✓ 需求變化時可調整

// LSI:僵化
// ✗ 只能在 CreateTable 時定義
// ✗ 之後無法修改
// ✗ 必須事前完全確定需求

const createTableWithLSI = {
  TableName: "Orders",
  KeySchema: [
    { AttributeName: "PK", KeyType: "HASH" },
    { AttributeName: "SK", KeyType: "RANGE" },
  ],
  LocalSecondaryIndexes: [
    // ← 建表時定義
    {
      IndexName: "LSI-Amount",
      KeySchema: [
        { AttributeName: "PK", KeyType: "HASH" },
        { AttributeName: "LSK_Amount", KeyType: "RANGE" },
      ],
      Projection: { ProjectionType: "ALL" },
    },
  ],
  // 之後無法新增 LSI!
};

洛基問:「為什麼 LSI 有這個限制,GSI 沒有?」

「因為 LSI 和主表存在同一個分區,」大師解釋,「它們的資料結構緊密綁定。GSI 是完全獨立的表,可以隨時建立。」


限制 2:10GB 的鐵律

大師繼續講解第二個限制:

「LSI 和主表共用相同的 partition key,」他在白板上畫圖,「所有資料都在同一個分區。」

PK: USER#alice
│
├─ 主表資料
│  ├─ SK: ORDER#2210-03-15#001
│  ├─ SK: ORDER#2210-03-14#002
│  └─ SK: ORDER#2210-03-13#003
│
├─ LSI1 資料(按金額排序)
│  ├─ LSK: AMOUNT#100#ORDER#003
│  ├─ LSK: AMOUNT#200#ORDER#002
│  └─ LSK: AMOUNT#299#ORDER#001
│
├─ LSI2 資料(按狀態排序)
│  ├─ LSK: STATUS#completed#ORDER#001
│  └─ LSK: STATUS#pending#ORDER#002
│
總計:所有這些資料都在同一個分區
DynamoDB 限制:10GB per partition key

洛基算了算:「如果每筆訂單 3KB,主表有 1000 筆,LSI1 也有 1000 筆,LSI2 也有 1000 筆...」

「3000 筆 × 3KB = 9MB,」大師說,「還在限制內。但如果使用者有 5000 筆訂單呢?」

洛基繼續算:「5000 × 3 × 3KB = 45MB...還可以。但如果是 10000 筆?」

「90MB,」大師說,「如果使用者數據持續增長,最終會碰到 10GB 的上限。」

他展示後果:

// 當達到 10GB 限制時:

// 寫入操作
await docClient.put({
  TableName: "Orders",
  Item: {
    PK: "USER#alice", // ← 這個 PK 已經達到 10GB
    SK: "ORDER#2210-03-16#004",
  },
});

// → 錯誤:ItemCollectionSizeLimitExceededException
// → 無法寫入!

洛基驚訝:「所以 LSI 不適合高增長的資料?」

「正確,」大師說,「這是 LSI 的第二個嚴格限制。」

他在白板上寫下判斷準則:

LSI 適用性評估:

✓ 每個 PK 的資料量可控(< 10GB)
  - 例如:使用者訂單(通常不會超過)
  - 例如:文章評論(可能超過,不適合)

✗ 資料會持續增長
  - 例如:感測器資料(時間序列)
  - 例如:日誌記錄

評估方法:
- 預估單筆資料大小
- 預估 PK 的最大項目數
- 計算:大小 × 數量 × (1 + LSI 數量)
- 確保 < 10GB

限制 3:共用主表容量

大師講解第三個限制:

「GSI 有獨立的 RCU/WCU 配置,」他說,「但 LSI 共用主表的容量。」

// GSI:獨立容量
主表:100 RCU, 100 WCU
GSI1:50 RCU, 50 WCU    // ← 獨立配置
GSI2:30 RCU, 30 WCU    // ← 獨立配置

// LSI:共用容量
主表:100 RCU, 100 WCU
LSI1:共用主表的 100 RCU, 100 WCU
LSI2:共用主表的 100 RCU, 100 WCU

洛基問:「共用容量是好事還是壞事?」

「兩面性,」大師說,「優點是不需要額外配置容量,成本管理簡單。缺點是 LSI 的查詢會和主表搶容量。」

他展示場景:

場景:主表和 LSI 的容量競爭

主表查詢:50 RCU/秒
LSI 查詢:60 RCU/秒
總需求:110 RCU/秒
配置:100 RCU

結果:
→ 主表或 LSI 的查詢會被 Throttle
→ 無法單獨擴展 LSI 的容量
→ 只能提升整個主表的容量

洛基理解了:「所以如果 LSI 的查詢量很大,會影響主表的查詢效能。」

「正確,」大師說,「這是使用 LSI 前要考慮的。」


GSI vs LSI 完整對比

Hippo 在白板上顯示完整的對比表:
https://ithelp.ithome.com.tw/upload/images/20251008/20178813SlkVqFyJz3.jpg

洛基盯著這個表格,問:「看起來 GSI 比 LSI 更好用?」

「大部分情況是的,」大師說,「但 LSI 有它獨特的價值——當你需要『在同一實體範圍內,用不同方式排序,且需要強一致性』時,LSI 是唯一選擇。」


決策框架:何時用 LSI

大師在白板上寫下決策流程:

選擇 GSI 或 LSI?

問題 1:查詢路徑 vs 排序方式?
├─ 需要不同的 PK(新查詢維度)→ 只能用 GSI
└─ 相同 PK,不同排序 → 可考慮 LSI

問題 2:需要強一致性嗎?
├─ 是 → LSI
└─ 否 → GSI 或 LSI 都可

問題 3:資料量會超過 10GB/partition 嗎?
├─ 會 → 只能用 GSI
└─ 不會 → LSI 可行

問題 4:需求可能變化嗎?
├─ 可能 → 選 GSI(可隨時調整)
└─ 確定不變 → LSI 可行

問題 5:查詢量會很大嗎?
├─ 是 → GSI(獨立容量)
└─ 否 → LSI 可行

結論:
- 大部分情況 → 選 GSI(靈活、無限制)
- 特殊情況 → 選 LSI(強一致性、同實體排序)

洛基看著這個決策流程,總結:「所以 LSI 適合的場景很窄:同一實體、需要不同排序、資料量可控、需求明確不變、需要強一致性。」

「完全正確,」大師說,「這就是為什麼在真實系統中,LSI 使用較少。」


實戰案例分析

大師提出幾個場景讓洛基判斷:

// 場景 1:使用者訂單系統
const scenario1 = {
  需求: "查詢特定使用者的訂單,按不同欄位排序",
  資料: {
    PK: "USER#alice",
    預估訂單數: "平均 100 筆/使用者",
    單筆大小: "2KB",
    總大小: "100 × 2KB × 3 (主表+2個LSI) = 600KB",
  },
  判斷: "可用 LSI(資料量遠低於 10GB)",
};

// 場景 2:社群媒體貼文評論
const scenario2 = {
  需求: "查詢特定貼文的評論,按不同方式排序",
  資料: {
    PK: "POST#viral-video-001",
    預估評論數: "可能達到 100,000 筆",
    單筆大小: "1KB",
    總大小: "100,000 × 1KB × 2 = 200MB",
  },
  判斷: "可用 LSI(仍在 10GB 內)",
};

// 場景 3:IoT 感測器資料
const scenario3 = {
  需求: "查詢特定感測器的數據,按時間或數值排序",
  資料: {
    PK: "SENSOR#temperature-01",
    預估資料數: "每小時 1 筆 × 24 × 365 × 5年 = 43,800 筆",
    單筆大小: "0.5KB",
    總大小: "43,800 × 0.5KB × 3 = 65MB",
    但是: "資料會持續增長,5 年後可能超過 10GB",
  },
  判斷: "不適合 LSI(持續增長會超限)→ 用 GSI",
};

// 場景 4:按地點查詢活動
const scenario4 = {
  需求: "查詢特定地點的活動,按主題分類",
  資料: {
    PK: "LOCATION#MARS",
    需要: "按主題查詢",
  },
  問題: "主題是新的查詢維度,不是排序",
  判斷: "需要 GSI(改變 PK)",
};

洛基逐一分析後說:「場景 1 和 2 可以用 LSI,場景 3 雖然目前符合但長期會超限所以不適合,場景 4 需要改變 PK 所以只能用 GSI。」

「完全正確,」大師滿意地點頭。


LSI 的真實定位

大師走回白板前,總結:

「LSI 的真實定位,不是 GSI 的替代品,而是特殊場景的專用工具。」

他畫出一個圖:

查詢需求的分類:

1. 新查詢維度(不同 PK)
   → 只能用 GSI
   → 例如:按主題查詢、按狀態查詢

2. 同實體不同排序(相同 PK)
   ├─ 資料量大 或 需求可能變化
   │  → 用 GSI(雖然失去 PK 限制,但更靈活)
   │
   └─ 資料量小 且 需求明確 且 需要強一致性
      → 用 LSI(唯一選擇)

實務統計:
- 90% 的場景用 GSI
- 10% 的場景用 LSI
- LSI 主要用於:使用者訂單、文章評論、小型階層資料

洛基理解了:「所以預設選 GSI,只有在真正需要『同實體不同排序 + 強一致性』時才考慮 LSI。」

「對,」大師說,「而且由於 LSI 必須在建表時定義,如果不確定是否需要,就不要建——因為 GSI 隨時可以加,但 LSI 錯過了就只能重建表。」


洛基看著白板上的分類圖,突然問:「既然 90% 場景都用 GSI,為什麼 DynamoDB 還要提供 LSI?」

大師笑了:「好問題。因為那 10% 的場景,LSI 是唯一解。」

他舉例:「用戶訂單查詢——按訂單時間排序、按金額排序、按狀態排序。都是同一個用戶的資料,需要強一致性,而且單一用戶訂單量通常不會超過 10GB。這就是 LSI 的舞台。」

「如果用 GSI 呢?」洛基問。

「可以,但失去了強一致性,」大師說,「而且 GSI 的 PK 會變成 userId,失去了主表 PK 的資料共存優勢,查詢成本反而更高。」

洛基理解了。LSI 不是弱化版的 GSI,而是針對特定場景的最佳解。

「明天,」大師說,「我們會整合這三天學的內容——從索引存在的原因,到 GSI 的成本權衡,到 LSI 的特殊定位——建立完整的索引設計決策樹。」

洛基點點頭,開始期待能看到完整的策略全貌。


時間設定說明:故事中使用星際曆(SY210 = 西元 2210 年),程式碼範例為確保正確執行,使用對應的西元年份。


上一篇
Day 23:GSI 設計原則與陷阱
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言