<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Lsi on Tarragon</title><link>https://tarrragon.github.io/blog/tags/lsi/</link><description>Recent content in Lsi on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 27 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/lsi/index.xml" rel="self" type="application/rss+xml"/><item><title>DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>single-table design 上線後第三個月、PM 提了三個新 query 需求：「依商品分類查訂單」、「依 status 查 user」、「依時間 range 取最近活動」。team 第一反應是加 GSI、結果 GSI 從 1 個變 6 個、cost 跟 latency 一起上升。打開 AWS Cost Explorer 一看、GSI 的 storage + WCU 合計已經超過 base table。這時 team 開始懷疑「single-table 是不是錯了」— 那是 &lt;em>誤判&lt;/em>。GSI 多到 cost 超過 base table 通常是 &lt;em>主 PK 沒設計好&lt;/em>、不是 single-table 錯。本文展開 GSI / LSI 的正確補位、projection 的三型選擇、sparse index、以及 DAX 作為讀峰值補位的觸發條件。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DynamoDB workload 適配判讀（基本 4 軸）&lt;/strong>：PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定 — 判讀軸詳見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>。本文聚焦 GSI / LSI 補位操作層、是 &lt;em>已選 DynamoDB + access pattern 已穩定&lt;/em> 的 schema 設計議題。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制gsi-vs-lsi-的工程差異">核心機制：GSI vs LSI 的工程差異&lt;/h2>
&lt;p>DynamoDB 的兩種 secondary index 解的問題不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>屬性&lt;/th>
 &lt;th>GSI（Global Secondary Index）&lt;/th>
 &lt;th>LSI（Local Secondary Index）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Partition&lt;/td>
 &lt;td>獨立 partition、可選新 PK + SK&lt;/td>
 &lt;td>同 base table partition、同 PK&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>建立時機&lt;/td>
 &lt;td>隨時可加 / 移除&lt;/td>
 &lt;td>只能在 create table 時定義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consistency&lt;/td>
 &lt;td>只支援 eventual read&lt;/td>
 &lt;td>支援 strongly consistent read&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Capacity&lt;/td>
 &lt;td>獨立 RCU/WCU、按 base 主表 write 同步收&lt;/td>
 &lt;td>共享 base table capacity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>數量上限&lt;/td>
 &lt;td>vendor 規格、需 cross-verify AWS doc&lt;/td>
 &lt;td>vendor 規格、需 cross-verify&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適用場景&lt;/td>
 &lt;td>跨 PK 查詢、需求變動&lt;/td>
 &lt;td>同 PK 內不同 SK + 需 strong read&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「LSI 數量上限 5 個」、「GSI 數量上限 20」這些具體數字屬 vendor 規格、需在實作時 cross-verify AWS doc 當前數字、本文 case（Disney+ / Capcom / Lemino）沒揭露具體 index 數量。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB</a> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>single-table design 上線後第三個月、PM 提了三個新 query 需求：「依商品分類查訂單」、「依 status 查 user」、「依時間 range 取最近活動」。team 第一反應是加 GSI、結果 GSI 從 1 個變 6 個、cost 跟 latency 一起上升。打開 AWS Cost Explorer 一看、GSI 的 storage + WCU 合計已經超過 base table。這時 team 開始懷疑「single-table 是不是錯了」— 那是 <em>誤判</em>。GSI 多到 cost 超過 base table 通常是 <em>主 PK 沒設計好</em>、不是 single-table 錯。本文展開 GSI / LSI 的正確補位、projection 的三型選擇、sparse index、以及 DAX 作為讀峰值補位的觸發條件。</p>
<blockquote>
<p><strong>DynamoDB workload 適配判讀（基本 4 軸）</strong>：PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定 — 判讀軸詳見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>。本文聚焦 GSI / LSI 補位操作層、是 <em>已選 DynamoDB + access pattern 已穩定</em> 的 schema 設計議題。</p></blockquote>
<h2 id="核心機制gsi-vs-lsi-的工程差異">核心機制：GSI vs LSI 的工程差異</h2>
<p>DynamoDB 的兩種 secondary index 解的問題不同：</p>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>GSI（Global Secondary Index）</th>
          <th>LSI（Local Secondary Index）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Partition</td>
          <td>獨立 partition、可選新 PK + SK</td>
          <td>同 base table partition、同 PK</td>
      </tr>
      <tr>
          <td>建立時機</td>
          <td>隨時可加 / 移除</td>
          <td>只能在 create table 時定義</td>
      </tr>
      <tr>
          <td>Consistency</td>
          <td>只支援 eventual read</td>
          <td>支援 strongly consistent read</td>
      </tr>
      <tr>
          <td>Capacity</td>
          <td>獨立 RCU/WCU、按 base 主表 write 同步收</td>
          <td>共享 base table capacity</td>
      </tr>
      <tr>
          <td>數量上限</td>
          <td>vendor 規格、需 cross-verify AWS doc</td>
          <td>vendor 規格、需 cross-verify</td>
      </tr>
      <tr>
          <td>適用場景</td>
          <td>跨 PK 查詢、需求變動</td>
          <td>同 PK 內不同 SK + 需 strong read</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>Scope warning</strong>：「LSI 數量上限 5 個」、「GSI 數量上限 20」這些具體數字屬 vendor 規格、需在實作時 cross-verify AWS doc 當前數字、本文 case（Disney+ / Capcom / Lemino）沒揭露具體 index 數量。</p></blockquote>
<p><strong>Projection type</strong> 決定 GSI 儲存哪些 attribute：</p>
<ul>
<li><code>KEYS_ONLY</code>：只存 PK + SK + base key、最省 storage、但讀取後通常還要回 base table 撈 attribute</li>
<li><code>INCLUDE</code>：除了 key、再存指定的 attribute；常用 sweet spot、storage 跟 query 效率平衡</li>
<li><code>ALL</code>：複製 base table 所有 attribute；最方便、最貴</li>
</ul>
<p>讀路徑差異：</p>
<ul>
<li>GSI eventual read：跨 partition、不支援 strong；base table write → GSI replication 通常 &lt; 1s 但無 SLA</li>
<li>LSI strong read：同 partition quorum 內成立、read-your-write 場景適用</li>
</ul>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a>、<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>。</p>
<h2 id="dax-作為讀峰值補位">DAX 作為讀峰值補位</h2>
<p>DAX（DynamoDB Accelerator）不是 GSI / LSI 同層方案、不是 DynamoDB 預設配置、是「讀峰值持續高時的補位」。寫進你的設計前先看觸發條件：</p>
<p><strong><code>9.C29 Lemino</code> 揭露</strong>（case fact）：「DAX 是 DynamoDB 讀 cache 的標準解法」、觸發條件是「當讀峰值持續高、加 DAX 減少 DynamoDB 讀次數、降低成本」（熱門節目首播時段、共用 metadata）。Lemino 是 case 直接揭露使用 DAX。</p>
<p><strong><code>9.C19 Capcom</code> 是判讀層 derive、不是 case fact</strong>：原 finding 從「single-digit ms」latency 反推 Capcom 必須用 sub-region cache + DynamoDB DAX、不能單靠 DynamoDB；但 <code>9.C19</code> case <em>沒有公開揭露</em> 使用 DAX。引用 Capcom 時要明示「DAX 是作者判讀層推論、Capcom 沒公開使用」、避免把推論寫成 case 揭露。</p>
<p><strong>跟 GSI / LSI 的職責分離</strong>：</p>
<ul>
<li>GSI / LSI 解「無法用主 PK 查」的問題（access pattern 補位）</li>
<li>DAX 解「同 query 重複打 DynamoDB 太貴或太慢」的問題（讀路徑加速）</li>
<li>兩者不互斥、但解不同問題；不要把 DAX 當 GSI 替代品</li>
</ul>
<p><strong>DAX 適用觸發條件</strong>：</p>
<ul>
<li>讀峰值持續高（熱門節目 / 共用 leaderboard / 全平台共享 metadata / read:write ratio &gt; 10:1）</li>
<li>cache 命中率可預期高（重複讀同一組 key）</li>
</ul>
<p><strong>DAX 不適用情境</strong>：</p>
<ul>
<li>寫密集 workload（cache invalidation 開銷 &gt; cache 收益）</li>
<li>每次讀都不同 key（cache hit rate &lt; 30%、加 DAX 等於白花錢）</li>
<li>read-your-write 場景（DAX 仍是 eventual cache、staleness 視 cache TTL 而定）</li>
</ul>
<h2 id="設計流程">設計流程</h2>
<p>從 access pattern 補位到 DAX 評估的 6 步流程。</p>
<h4 id="step-1標記最小成本路徑">Step 1：標記最小成本路徑</h4>
<p>每個 access pattern 標記能用最便宜路徑解：</p>
<ul>
<li>能用主表 PK/SK 直接 <code>GetItem</code> / <code>Query</code> → 主表（最便宜）</li>
<li>同 PK 內不同 SK 排序 + 需要 strong read → LSI（同 partition、strong）</li>
<li>跨 PK 或 base table 已建好 → GSI（額外 storage + WCU）</li>
</ul>
<h4 id="step-2選-lsi-還是-gsi">Step 2：選 LSI 還是 GSI</h4>
<p>LSI 只能在 create table 時定義、不能後加。team 經常踩雷：上線後想加 strongly consistent 索引、發現只能重建 table。建 table 前列完 access pattern、不確定走 GSI 不走 LSI 是保守選擇（GSI 隨時可加可移）。</p>
<h4 id="step-3projection-設計">Step 3：projection 設計</h4>
<p>每個 GSI 單獨設 projection、不要全用 <code>ALL</code>：</p>
<ul>
<li>query 只要回 key → <code>KEYS_ONLY</code></li>
<li>query 需要常見 3-5 個欄位 → <code>INCLUDE</code>（列出實際 column、storage 跟 query 效率平衡）</li>
<li>用 GSI 直接顯示資料（不回 base table） → <code>ALL</code>（storage 跟 WCU 都翻倍、慎用）</li>
</ul>
<h4 id="step-4sparse-index-pattern">Step 4：sparse index pattern</h4>
<p>GSI PK 只在某 attribute 存在時填、自動「只索引子集」、節省 storage：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">write_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">status</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">item</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ORDER#</span><span class="si">{</span><span class="n">order_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">,</span> <span class="s2">&#34;status&#34;</span><span class="p">:</span> <span class="n">status</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1"># sparse index: 只有 active order 進 GSI</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">if</span> <span class="n">status</span> <span class="o">==</span> <span class="s2">&#34;active&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">item</span><span class="p">[</span><span class="s2">&#34;GSI1PK&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;STATUS#active&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">item</span><span class="p">[</span><span class="s2">&#34;GSI1SK&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">order_id</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">table</span><span class="o">.</span><span class="n">put_item</span><span class="p">(</span><span class="n">Item</span><span class="o">=</span><span class="n">item</span><span class="p">)</span></span></span></code></pre></div><p>GSI1 只索引 active order、archive order 不進 GSI。當 active order 是 10%、storage 節省約 90%。</p>
<blockquote>
<p><strong>Scope warning</strong>：「50-90% storage 節省」具體節省比例屬通用工程估算、依 active subset 比例變動、case 未揭露 sparse index 具體數字。</p></blockquote>
<h4 id="step-5驗證點">Step 5：驗證點</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">query</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">KeyConditionExpression</span><span class="o">=</span><span class="n">Key</span><span class="p">(</span><span class="s2">&#34;GSI1PK&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">eq</span><span class="p">(</span><span class="s2">&#34;STATUS#active&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">IndexName</span><span class="o">=</span><span class="s2">&#34;GSI1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ReturnConsumedCapacity</span><span class="o">=</span><span class="s2">&#34;INDEXES&#34;</span>  <span class="c1"># 看每個 query 走 GSI 還是主表</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;ConsumedCapacity&#34;</span><span class="p">])</span></span></span></code></pre></div><p>CloudWatch GSI metric：看每個 GSI 的 WCU usage 跟主表的比例；GSI WCU &gt; base table WCU 通常是設計訊號。</p>
<h4 id="step-6dax-評估">Step 6：DAX 評估</h4>
<p>讀峰值持續高 + cache hit rate 可預期、才加 DAX；不要把 DAX 當預設配置（Lemino 揭露的觸發條件）。先觀察 base 路徑的 read pattern、判斷 cache hit rate 預期值、再決定加 DAX。</p>
<p><strong>Rollback boundary</strong>：GSI 可隨時刪、但 deletion 是 async 且不可逆；建議先 application 切回 base table query、觀察 1 週再刪 GSI。DAX 可隨時 detach、application 端把 DAX endpoint 換回 DynamoDB endpoint 即可。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>7 個 production 常見踩雷：</p>
<h4 id="case-1gsi-寫入-throttle-拖累主表-write">Case 1：GSI 寫入 throttle 拖累主表 write</h4>
<p>GSI 用了集中型 PK（如 <code>STATUS#active</code> 所有 active order 集中）、單 partition 上限 1000 WCU 撞牆、GSI replication 失敗、主表 write retry、整體 latency 上升。修法：GSI PK 設計獨立 review、不可繼承主表 PK 的均勻假設（base PK 均勻 ≠ GSI PK 均勻）；GSI PK 也要做 <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition key 均勻度判讀</a>。</p>
<h4 id="case-2gsi-eventual-read-餵錯資料">Case 2：GSI eventual read 餵錯資料</h4>
<p>application 用 GSI 讀「user 最新 status」、code 假設 strong 一致；實際 100-500ms staleness 導致 UI 顯示舊狀態。修法：read-your-write 場景改回主表 query（主表支援 strong）、或加 application-side write-through cache。</p>
<blockquote>
<p><strong>Scope warning</strong>：「100-500ms staleness」具體數字屬通用工程估算、case 未揭露 GSI replication latency 具體 p99 數字。</p></blockquote>
<h4 id="case-3projection-all-把-cost-翻倍">Case 3：projection ALL 把 cost 翻倍</h4>
<p>圖省事所有 GSI 用 <code>ALL</code>、實際 query 只需要 3 個 column；storage + WCU 都浪費。修法：每個 GSI 單獨設 projection、<code>INCLUDE</code> 列出實際 column；只在「用 GSI 直接顯示資料、不回主表」場景才用 <code>ALL</code>。</p>
<blockquote>
<p><strong>Scope warning</strong>：「cost 翻 3 倍」具體數字屬通用工程估算、case 未揭露具體 cost ratio。</p></blockquote>
<h4 id="case-4lsi-用完了才發現要的是-gsi">Case 4：LSI 用完了才發現要的是 GSI</h4>
<p>LSI 上限受 vendor 規格限制（建議 cross-verify AWS doc 當前數字）且建 table 時定、半年後想加 strongly consistent 索引發現要重建 table。修法：建 table 前列完 access pattern、不確定就走 GSI（隨時可加可移）；LSI 留給「明確需要同 PK + strong read」場景。</p>
<h4 id="case-5gsi-反向-scan-取代-query">Case 5：GSI 反向 scan 取代 query</h4>
<p>application 用 GSI 做 <code>Scan</code> 而非 <code>Query</code>、全 GSI 掃過去、cost 跟 latency 都炸。修法：<code>Scan</code> 是 <em>程式碼錯誤訊號</em>、不是 capacity 不夠；review code 看 GSI 為什麼沒被當 query 路徑用、通常是 GSI PK 設計沒對齊 access pattern。</p>
<h4 id="case-6把-dax-當預設配置">Case 6：把 DAX 當預設配置</h4>
<p>寫密集 workload / cache hit rate 低的場景加 DAX、cache invalidation 成本超過 cache 收益、cost 上升 latency 沒降。修法：DAX 是「讀峰值持續高」的補位、不是預設（Lemino 揭露的觸發條件、Capcom 是 derive 不是 case fact）；先觀察 read pattern + 評估 cache hit rate 預期、再決定。</p>
<h4 id="case-7gsi-capacity-mode-跟-base-table-不一致">Case 7：GSI capacity mode 跟 base table 不一致</h4>
<p>GSI 的 capacity mode 跟 base table 是 <em>獨立</em> 設定、不會自動繼承 — base table 是 provisioned + auto-scaling、開新 GSI 預設仍是 provisioned 但 WCU / RCU 預設值跟 base table 不同步、或誤把某個 GSI 切 on-demand 而 base table 維持 provisioned、實際 production 寫入 throttle / 成本失衡都會出現。屬通用工程議題、case 未直接揭露具體 mode 錯配狀況。</p>
<p>徵兆：</p>
<ul>
<li>Base table <code>ConsumedWriteCapacityUnits</code> 健康、卻看到 GSI <code>WriteThrottleEvents</code> 持續觸發、application 端寫入 latency p99 拉高</li>
<li>GSI 切 on-demand 後成本「不知為何」翻 X 倍、查 Cost Explorer 才發現 GSI WCU 計費跟 base table 的 provisioned 是完全不同帳單路徑</li>
<li>Auto-scaling policy 只設了 base table、GSI 沒設、流量上來時 base table 自動擴、GSI 卻 throttle</li>
</ul>
<p>修法：</p>
<ul>
<li>建 GSI 時把 capacity mode 當成獨立決策、不要假設「base 怎麼設、GSI 跟著走」</li>
<li>流量穩定 workload 同時把 base + GSI 都設 provisioned + auto-scaling、auto-scaling target 對齊</li>
<li>Spiky workload 改 on-demand 時整批切（base table + 全部 GSI 同時切）、避免單側切換造成 partial throttle</li>
<li>CloudWatch alarm 對每個 GSI 獨立設 <code>WriteThrottleEvents</code> / <code>ReadThrottleEvents</code>、不要只盯 base table</li>
<li>詳細 mode 切換時機看 sibling <a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/" data-link-title="DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover" data-link-desc="capacity mode 選擇不是單軸 peak/avg ratio；本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 暫時 vs 永久 baseline / predictable-peak vs flash-sale / DBA 工時釋放 / vendor vs 自管 cost crossover），含 Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload 等 case 分軸 anchor">on-demand vs provisioned</a></li>
</ul>
<p><strong>Anti-recommendation</strong>：access pattern &lt; 3 個、主表 PK 已能覆蓋 → 不要預先建 GSI；GSI 從少到多容易、從多到少要 application 端配合 cutover。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li>每個 GSI 獨立 <code>ConsumedReadCapacityUnits</code> / <code>ConsumedWriteCapacityUnits</code></li>
<li><code>ReplicationLatency</code>：GSI async replication 延遲、p99 通常 &lt; 1s（無 SLA）</li>
<li>DAX：<code>CacheHits</code> / <code>CacheMisses</code> / <code>CacheHitRate</code>、<code>ItemCacheHits</code> / <code>QueryCacheHits</code></li>
</ul>
<p><code>ReturnConsumedCapacity</code> flag：query 時帶 <code>INDEXES</code> 看 GSI consumption；<code>TOTAL</code> 看 base + GSI 合計、debug 時切換用。</p>
<p><strong>Cost monitoring</strong>：</p>
<ul>
<li>每個 GSI 都重複收 storage + WCU；GSI 多時 cost 容易超過 base table</li>
<li>用 AWS Cost Explorer 按 GSI 維度看、不是只看 table-level 總 cost</li>
<li>DAX cost 是 instance-hour 計、不是 per-request；只在 read peak 持續高才划算</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「GSI 多時 cost 超過 base table」屬通用工程知識、<code>9.C27 Disney+</code> / <code>9.C19 Capcom</code> case 沒揭露具體 GSI cost ratio。</p></blockquote>
<p><strong>DAX 觀測重點</strong>（新增）：</p>
<ul>
<li><code>CacheHitRate</code> &lt; 70% 應重新評估 DAX 是否該存在</li>
<li>cache size utilization 看 DAX instance class 是否足夠</li>
<li>觀察 cache miss 後 fallback 到 DynamoDB 的 latency、確認 DAX 真的減少 base 路徑壓力</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「70% hit rate 閾值」屬通用工程估算、case 未揭露具體閾值。</p></blockquote>
<p>接回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 的 NoSQL index cost section、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="disney--capcom-的-access-pattern-對照">Disney+ / Capcom 的 access pattern 對照</h3>
<p><code>9.C27 Disney+</code> 跟 <code>9.C19 Capcom</code> 是兩種 GSI 用法：</p>
<ul>
<li>Disney+ watchlist + 播放進度 + cross-device sync 全用主表 + 少量 GSI、避免 GSI 爆炸；cross-device sync 透過 <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">Global Tables</a> 處理、不是 GSI</li>
<li>Capcom 玩家 leaderboard / 戰績用 GSI 反向查詢（跨遊戲共用平台、player_id 為 base PK、game_id 為 GSI PK）；leaderboard 是否該走 GSI 還是 Redis sorted set 是另一個取捨</li>
</ul>
<p>兩個 case 都 <em>沒有公開揭露</em> 具體 GSI 數量、projection 配置、DAX 是否使用。引用 case 時要分層 — 概念是 case 揭露、實作數字是通用工程估算。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — GSI 是 single-table 沒覆蓋的 access pattern 補位</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — GSI 自己也會 hot partition、GSI PK 設計獨立 review</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — GSI 強制 eventual、對應 consistency 軸</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/" data-link-title="DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover" data-link-desc="capacity mode 選擇不是單軸 peak/avg ratio；本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 暫時 vs 永久 baseline / predictable-peak vs flash-sale / DBA 工時釋放 / vendor vs 自管 cost crossover），含 Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload 等 case 分軸 anchor">on-demand-vs-provisioned</a> — GSI 多時 cost 跟 mode 互動</li>
<li>替代路由：access pattern 變動頻繁 → 考慮 OpenSearch / Aurora、單純 search 不要拿 GSI 當 inverted index</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom 9.C19</a> 互引：leaderboard 用 GSI vs Redis sorted set 的選擇；DAX 是 derive 不是 case fact、引用要明示</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino 9.C29</a> 互引：DAX 作為讀峰值補位的 case 揭露</li>
</ul>
]]></content:encoded></item></channel></rss>