洛基看著白板上並排寫著兩個詞:
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 為什麼存在、什麼時候用、以及為什麼大部分情況下我們不用它。」
大師在白板上寫下一個場景:
// 需求:查詢特定使用者的訂單
// 主表設計:
{
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 還有一個 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 的三大限制:
限制 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 是完全獨立的表,可以隨時建立。」
大師繼續講解第二個限制:
「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
大師講解第三個限制:
「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 前要考慮的。」
Hippo 在白板上顯示完整的對比表:
洛基盯著這個表格,問:「看起來 GSI 比 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 的真實定位,不是 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 年),程式碼範例為確保正確執行,使用對應的西元年份。