<?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>Single-Table-Design on Tarragon</title><link>https://tarrragon.github.io/blog/tags/single-table-design/</link><description>Recent content in Single-Table-Design 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/single-table-design/index.xml" rel="self" type="application/rss+xml"/><item><title>DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/</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>team 用 RDBMS 設計思維建多個 DynamoDB table（&lt;code>user&lt;/code> / &lt;code>order&lt;/code> / &lt;code>order_item&lt;/code>）跑了一季、第二季開始撞「每個 query 要打 2-3 個 table、application 端拼接邏輯爆炸、latency 跟 cost 線性上升」。最直覺的補救是再加 GSI、結果 GSI 數量超過 5 個還是抓不到 access pattern。這時 team 通常開始問「DynamoDB 怎麼 join」— 那是 &lt;em>誤問&lt;/em>。DynamoDB 不做 join，要嘛把相關 entity 放同 PK 用 SK 前綴區分（single-table design），要嘛這個 workload 根本不該用 DynamoDB。本文先回答後者（DynamoDB 適用度前置判讀），再展開前者（single-table 設計流程）。&lt;/p>
&lt;h2 id="dynamodb-適用度前置判讀4-軸">DynamoDB 適用度前置判讀（4 軸）&lt;/h2>
&lt;p>進到 single-table 設計細節之前要先判讀 workload 是否在 DynamoDB 適用區。下面 4 個維度同時成立、single-table 才有意義；任一條不成立、改回 SQL / 多 vendor 組合可能更便宜。9 個 production case（Zoom / Disney+ / Capcom / PayPay / Tixcraft / Lemino / Amazon Ads / Genesys / Zomato）跨 case 重複揭露這 4 軸是適用度的真實邊界。&lt;/p>
&lt;h3 id="軸-1partition-key-是否天然均勻">軸 1：Partition key 是否天然均勻&lt;/h3>
&lt;p>DynamoDB 容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 就是 workload 的天花板。&lt;code>meeting_id&lt;/code>（Zoom）/ &lt;code>player_id&lt;/code>（Capcom）/ &lt;code>message_id&lt;/code>（PayPay）/ &lt;code>user_id&lt;/code>（Disney+）這類 ID 天然散布、不會集中在少數 partition；反之 &lt;code>event_id&lt;/code>（Tixcraft 售票）/ &lt;code>date&lt;/code>（時間序）/ &lt;code>status&lt;/code>（少數枚舉值）這類 PK 天然不均勻、要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key&lt;/a> 修補才能 single-table。修補成本見 &lt;a href="https://tarrragon.github.io/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 &amp;#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns&lt;/a>。&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>team 用 RDBMS 設計思維建多個 DynamoDB table（<code>user</code> / <code>order</code> / <code>order_item</code>）跑了一季、第二季開始撞「每個 query 要打 2-3 個 table、application 端拼接邏輯爆炸、latency 跟 cost 線性上升」。最直覺的補救是再加 GSI、結果 GSI 數量超過 5 個還是抓不到 access pattern。這時 team 通常開始問「DynamoDB 怎麼 join」— 那是 <em>誤問</em>。DynamoDB 不做 join，要嘛把相關 entity 放同 PK 用 SK 前綴區分（single-table design），要嘛這個 workload 根本不該用 DynamoDB。本文先回答後者（DynamoDB 適用度前置判讀），再展開前者（single-table 設計流程）。</p>
<h2 id="dynamodb-適用度前置判讀4-軸">DynamoDB 適用度前置判讀（4 軸）</h2>
<p>進到 single-table 設計細節之前要先判讀 workload 是否在 DynamoDB 適用區。下面 4 個維度同時成立、single-table 才有意義；任一條不成立、改回 SQL / 多 vendor 組合可能更便宜。9 個 production case（Zoom / Disney+ / Capcom / PayPay / Tixcraft / Lemino / Amazon Ads / Genesys / Zomato）跨 case 重複揭露這 4 軸是適用度的真實邊界。</p>
<h3 id="軸-1partition-key-是否天然均勻">軸 1：Partition key 是否天然均勻</h3>
<p>DynamoDB 容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 就是 workload 的天花板。<code>meeting_id</code>（Zoom）/ <code>player_id</code>（Capcom）/ <code>message_id</code>（PayPay）/ <code>user_id</code>（Disney+）這類 ID 天然散布、不會集中在少數 partition；反之 <code>event_id</code>（Tixcraft 售票）/ <code>date</code>（時間序）/ <code>status</code>（少數枚舉值）這類 PK 天然不均勻、要 <a href="/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key</a> 修補才能 single-table。修補成本見 <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>。</p>
<p><code>9.C18 Zoom</code>、<code>9.C19 Capcom</code>、<code>9.C26 PayPay</code>、<code>9.C27 Disney+</code> 4 個 case 都揭露 partition key 天然均勻是 DynamoDB 「能撐」的前提之一。</p>
<h3 id="軸-2workload-是-control-plane-還是-data-plane">軸 2：Workload 是 control plane 還是 data plane</h3>
<p>DynamoDB 適合存 metadata / state，實際大流量（影音串流 / 大型 BLOB / 全文搜尋）走 CDN / WebRTC / object store。<code>9.C18 Zoom</code> 把媒體串流放 P2P + edge servers、DynamoDB 只承擔會議 metadata；<code>9.C27 Disney+</code> 把 content 放 S3 + CDN、DynamoDB 只承擔 watchlist + 播放進度；<code>9.C19 Capcom</code> 把即時遊戲邏輯放 EKS、DynamoDB 處理持久狀態。讀者該問的不是「DynamoDB 能撐多大流量」、是「我的系統哪一層該放 DynamoDB」。</p>
<p>如果 workload 是 data plane（單筆 payload 上 MB、要做全文搜尋、要存 BLOB），用 DynamoDB 是反模式 — single item 上限 400KB 直接擋掉 BLOB 場景。</p>
<h3 id="軸-3consistency-需求是否可接受-eventual">軸 3：Consistency 需求是否可接受 eventual</h3>
<p>DynamoDB 預設 eventually consistent read、strong read 也只在同 region quorum 內成立。最終一致性可接受的 workload 才適合；strong consistency 必要（跨 entity 原子寫入 / 跨 region 強一致 / 全局單調遞增 ID）必須走 SQL / NewSQL。本軸屬通用工程判讀、case 沒有揭露具體 staleness 閾值；判讀工具是 <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> 的 per-call site review。</p>
<h3 id="軸-4access-pattern-是否穩定">軸 4：Access pattern 是否穩定</h3>
<p>access pattern 數量穩定且窮舉可列（典型 10-30 個）single-table 才能精準設計 PK/SK 跟 GSI；查詢仍在探索期、pattern 頻繁變動，SQL 多 table 較容易演化、改 query 不用改 schema。本軸也屬通用工程判讀、case 沒明示 access pattern 數量閾值，但 9 個 case 寫進 production 的 access pattern 多半是 <em>業務契約已凍結</em> 的場景（會議 metadata、watchlist、玩家戰績、訊息推送）。</p>
<p>任一軸不成立、回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a> 或考慮多 vendor 組合。4 軸都成立、再進 single-table 設計。</p>
<h2 id="核心概念access-pattern-先於-schema">核心概念：access pattern 先於 schema</h2>
<p>Single-table design 的 first-class concept 是 <em>access pattern 先於 schema</em>：先列 15-30 個 query 才開始設 key、不是先設 schema 再想怎麼 query。</p>
<p>DynamoDB 的 key 結構：</p>
<ul>
<li><strong>PK（partition key）</strong>：決定資料散布到哪個 partition；同 PK 的 item 物理共置（item collection）</li>
<li><strong>SK（sort key）</strong>：決定同 partition 內排序與範圍查詢；composite SK 用 <code>#</code> 分隔層級（如 <code>ORDER#2026-05-27#001</code>）</li>
<li><strong>同 PK 不同 SK 前綴</strong>：把相關 entity 物理共置、用一次 <code>Query</code> 拿回多個 entity；對應 RDB 的 JOIN</li>
</ul>
<p>實際範例（Disney+ 9.C27 揭露的 access pattern）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">PK             SK                          Entity
</span></span><span class="line"><span class="ln">2</span><span class="cl">USER#u123      PROFILE                     用戶資料
</span></span><span class="line"><span class="ln">3</span><span class="cl">USER#u123      WATCHLIST#m456              觀看清單項目
</span></span><span class="line"><span class="ln">4</span><span class="cl">USER#u123      PROGRESS#device-iPad#m456   播放進度
</span></span><span class="line"><span class="ln">5</span><span class="cl">USER#u123      PROGRESS#device-TV#m456     播放進度（跨裝置）</span></span></code></pre></div><p>一次 <code>Query PK=USER#u123</code> 拿回該 user 的所有資料、不需要 join。SK 前綴 <code>PROFILE</code> / <code>WATCHLIST#</code> / <code>PROGRESS#</code> 區分 entity type、range query 還能限定「只取 watchlist」（<code>begins_with(SK, &quot;WATCHLIST#&quot;)</code>）。</p>
<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/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">workload model</a>。</p>
<h2 id="設計流程">設計流程</h2>
<p>從 access pattern 反推 PK/SK 跟 GSI 的 5 步流程。</p>
<h4 id="step-1access-pattern-表窮舉">Step 1：access pattern 表窮舉</h4>
<p>每個 user story 寫成一條 query：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">| #  | User story                          | Query                                 | Latency | Consistency |
</span></span><span class="line"><span class="ln">2</span><span class="cl">| -- | ----------------------------------- | ------------------------------------- | ------- | ----------- |
</span></span><span class="line"><span class="ln">3</span><span class="cl">| 1  | 顯示用戶 profile                    | GetItem PK=USER#{id} SK=PROFILE       | p99 5ms | eventual    |
</span></span><span class="line"><span class="ln">4</span><span class="cl">| 2  | 取用戶所有觀看清單                  | Query PK=USER#{id} begins_with(SK, &#34;WATCHLIST#&#34;) | p99 10ms | eventual |
</span></span><span class="line"><span class="ln">5</span><span class="cl">| 3  | 跨裝置同步播放進度（最新）          | GetItem PK=USER#{id} SK=PROGRESS#{movie}#latest | p99 15ms | strong |</span></span></code></pre></div><p>15-30 條 query 全列出，這是 single-table 的契約。漏列等於設計時看不到、上線後撞。</p>
<h4 id="step-2entity-relationship--pksk-映射">Step 2：entity-relationship → PK/SK 映射</h4>
<p>常見模式：</p>
<ul>
<li>主 entity 用 <code>{ENTITY}#{id}</code> 當 PK（USER / ORDER / PRODUCT）</li>
<li>子 entity 用同 PK + 不同 SK 前綴（<code>PROFILE</code> / <code>ORDER#{timestamp}</code> / <code>ITEM#{id}</code>）</li>
<li>1-N 關係（user 有多個 watchlist）用同 PK + 不同 SK</li>
<li>N-N 關係（user 跟 friend）用兩條 item（A→B 與 B→A）或單獨 relationship entity</li>
</ul>
<h4 id="step-3gsi-補反向查詢">Step 3：GSI 補反向查詢</h4>
<p>主 PK 覆蓋不到的 access pattern 用 GSI 補：</p>
<ul>
<li>「依 status 查所有 order」→ GSI PK = <code>status</code>、SK = <code>created_at</code></li>
<li>「依 product 查所有買家」→ GSI PK = <code>product_id</code>、SK = <code>user_id</code></li>
</ul>
<p>GSI 數量上限 20、實務 &lt; 5；過多時表示主 PK 設計沒覆蓋夠多 access pattern、應重新設計。詳見 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a>。</p>
<h4 id="step-4cloudformation--terraform-ddl">Step 4：CloudFormation / Terraform DDL</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">Resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">SingleTable</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::DynamoDB::Table</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">BillingMode</span><span class="p">:</span><span class="w"> </span><span class="l">PAY_PER_REQUEST</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">AttributeDefinitions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">PK</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">          </span><span class="nt">AttributeType</span><span class="p">:</span><span class="w"> </span><span class="l">S</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">SK</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">          </span><span class="nt">AttributeType</span><span class="p">:</span><span class="w"> </span><span class="l">S</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1PK</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">          </span><span class="nt">AttributeType</span><span class="p">:</span><span class="w"> </span><span class="l">S</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1SK</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">          </span><span class="nt">AttributeType</span><span class="p">:</span><span class="w"> </span><span class="l">S</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span><span class="nt">KeySchema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">PK</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">KeyType</span><span class="p">:</span><span class="w"> </span><span class="l">HASH</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">SK</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">KeyType</span><span class="p">:</span><span class="w"> </span><span class="l">RANGE</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">GlobalSecondaryIndexes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">        </span>- <span class="nt">IndexName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">          </span><span class="nt">KeySchema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1PK</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">              </span><span class="nt">KeyType</span><span class="p">:</span><span class="w"> </span><span class="l">HASH</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">            </span>- <span class="nt">AttributeName</span><span class="p">:</span><span class="w"> </span><span class="l">GSI1SK</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">              </span><span class="nt">KeyType</span><span class="p">:</span><span class="w"> </span><span class="l">RANGE</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">          </span><span class="nt">Projection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">            </span><span class="nt">ProjectionType</span><span class="p">:</span><span class="w"> </span><span class="l">INCLUDE</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">            </span><span class="nt">NonKeyAttributes</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">status, created_at]</span></span></span></code></pre></div><h4 id="step-5驗證點">Step 5：驗證點</h4>
<ul>
<li>每個 access pattern 對應一個 <code>Query</code> / <code>GetItem</code>、沒有 <code>Scan</code>、沒有 application-side join</li>
<li>Contributor Insights 看 top-N PK 訪問是否均勻</li>
<li>CloudWatch <code>ConsumedReadCapacityUnits</code> / <code>ConsumedWriteCapacityUnits</code> 按 partition 分布觀察</li>
</ul>
<p><strong>Rollback boundary</strong>：access pattern 改動可加 GSI 補；entity 拆 table 比合 table 容易，先合再拆。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>5 個 production 常見踩雷：</p>
<h4 id="case-1late-binding-access-pattern">Case 1：late-binding access pattern</h4>
<p>production 上線半年後 PM 要新 query「按地區列訂單」、PK 沒包 region、只能 <code>Scan</code> 或加 GSI。根因是 access pattern 沒在設計階段窮舉，這是 single-table design 的核心責任。修法：access pattern 表列完整、不可省略；新需求進來先回 access pattern 表 review、再決定加 GSI 還是重設計 PK。</p>
<h4 id="case-2sk-排序衝突">Case 2：SK 排序衝突</h4>
<p>同 PK 下兩種 entity（<code>ORDER#{timestamp}</code> 與 <code>PAYMENT#{timestamp}</code>）混用同 SK 空間、range query 拿 <code>BETWEEN '2026-01-01' AND '2026-12-31'</code> 時 entity 邊界錯亂。修法：SK 前綴必須能 <em>用 <code>begins_with</code> 完全區隔</em> entity（<code>ORDER#2026-...</code> vs <code>PAYMENT#2026-...</code>）。</p>
<h4 id="case-3item-collection-超過-10gb">Case 3：item collection 超過 10GB</h4>
<p>單 PK 下所有 item 加起來超過 10GB 上限、DynamoDB 拒絕新寫入。常見於「user 為 PK + user 有大量歷史 event」場景。修法：歷史 event 改用 <code>USER#{id}#YYYYMM</code> 當 PK 把時間 bucket 切開、或把歷史 event 寫進另一張 archive table（cold path）。</p>
<h4 id="case-4gsi-反向變主表">Case 4：GSI 反向變主表</h4>
<p>開始 GSI 只補 1-2 個 query，半年後 GSI 流量超過主表、cost 翻倍。根因是主 PK 沒設計好、GSI 變成 <em>實質的主存取路徑</em>。修法：重新設計 PK、把 GSI 流量主要的 access pattern 升為主表 query；GSI 從多到少要 application 端配合 cutover。</p>
<h4 id="case-5dynamodb-當-rdbms-用">Case 5：DynamoDB 當 RDBMS 用</h4>
<p>把 normalize 過的 schema 直接搬、每個 business query 要 2-3 個 <code>GetItem</code>、latency 從 5ms 變 30ms。修法：normalize 適合 SQL、不適合 KV；single-table 是把 normalize 拍平、用 denormalize 換 read latency。</p>
<p><strong>Anti-recommendation</strong>：access pattern &lt; 5 個、entity 間關聯弱、查詢仍在探索期 → 用 SQL 或 multi-table 先寫、access pattern 穩定再 single-table。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>ConsumedReadCapacityUnits</code> / <code>ConsumedWriteCapacityUnits</code>：按 partition 分布看是否均勻</li>
<li><code>ThrottledRequests</code>：早期 hot partition 訊號（provisioned 模式立即可見）</li>
<li><code>SuccessfulRequestLatency</code> p99：on-demand 模式下 hot partition 表現為 latency spike（見 <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> 的 mode × partition 交叉判讀）</li>
</ul>
<p>Contributor Insights：top-N partition key 訪問頻率，揭露 single-table 設計後是否仍均勻；每月 cost ~$0.02 per million event、值得開。</p>
<p>GSI 觀測：每個 GSI 獨立 RCU/WCU、projection type（<code>KEYS_ONLY</code> / <code>INCLUDE</code> / <code>ALL</code>）決定 storage cost。</p>
<p>TTL 是 storage cost 防爆的標配（特別在 message-class workload）— PayPay <code>9.C26</code> 揭露 3 億 / 天 × 30 天 = 90 億筆記錄、不清理會撐死 storage 預算；設 TTL attribute 讓 DynamoDB 自動刪過期 item、消耗 0 WCU。</p>
<p>接回 <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> 跟 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 Bottleneck localization</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="frame-3dynamodb-在-fleet-治理-frame-的退化">Frame 3：DynamoDB 在 fleet 治理 frame 的退化</h3>
<p>跨 vendor 共通 frame：production scale 走 <em>fleet of clusters</em>（Aurora 200 cluster / CockroachDB 380+ cluster / MongoDB Atlas 20 DB 都是這個 frame）。DynamoDB 在這 frame 退化得最徹底 — <em>不走 fleet of clusters</em>、是用 partition 內部自動切。</p>
<p>對照其他 vendor：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Scale-out 拓樸</th>
          <th>容量決策層</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DynamoDB</td>
          <td>單 table、partition 自動 split / merge</td>
          <td>mode 選擇 + PK 均勻 + GSI 補位</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>Fleet of clusters（business / microservice / 合規）</td>
          <td>Cluster boundary + replica 數量</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>Fleet of clusters or 邏輯一個 cluster + locality</td>
          <td>Per-app vs shared cluster 決策</td>
      </tr>
      <tr>
          <td>MongoDB</td>
          <td>Sharded cluster + 多 cluster（blast radius）</td>
          <td>Shard key + cluster ownership boundary</td>
      </tr>
  </tbody>
</table>
<p><strong>DynamoDB 退化點</strong>：partition 是 <em>vendor 內部物理層</em>、不暴露給應用 — application 看到的永遠是「一張 table」、不需要規劃 cluster boundary。代價是 <em>partition key 設計責任全壓在 schema 上</em>（<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>）、不能用「拆 cluster 解 blast radius」當逃避路徑。</p>
<p><strong>例外情境</strong>：DynamoDB 在 <em>合規場景</em> 仍可能走「多 table per market」拓樸（見 Frame 5 <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-conflict</a> region-pinned 段）— 但動機是合規 boundary 而非 capacity scale、跟 Aurora fleet driver 結構不同。</p>
<h3 id="dynamodb-在系統中的角色control-plane--metadata--state">DynamoDB 在系統中的角色：control plane / metadata / state</h3>
<p>DynamoDB 不是 universal store、不是 SQL 替代品。3 個 case 重複揭露同一定位：</p>
<ul>
<li><strong>9.C18 Zoom</strong>：媒體串流走 P2P + edge servers、DynamoDB 只承擔會議 / 用戶 metadata。control plane 跟 data plane 分離是 30x DAU surge 能撐的工程前提（不是 DynamoDB 自己魔法）。</li>
<li><strong>9.C27 Disney+</strong>：content 走 S3 + CDN、DynamoDB 只承擔 metadata / watchlist / cross-device 進度。</li>
<li><strong>9.C19 Capcom</strong>：EKS 跑 game server / 處理即時遊戲邏輯、DynamoDB 處理持久狀態。</li>
</ul>
<h3 id="durable-queue--write-buffer-作為正向非-oltp-access-pattern">Durable queue / write-buffer 作為正向非 OLTP access pattern</h3>
<p><code>9.C15 Tixcraft</code> 揭露 DynamoDB 的另一種正向用法 — <em>寫入緩衝層</em>、不是 OLTP：</p>
<ul>
<li>拓元用 DynamoDB 接「訂單」寫入、不是即時生效、是讓 traditional server（金流 / 票庫）用自己能承受的速度消費</li>
<li>架構上 DynamoDB 扮演 durable queue、不是傳統 OLTP DB；這層解耦讓「前端可擴 130 倍、後端不用同步擴」</li>
<li>對比 RDBMS：RDB 寫入要即時可讀、即時索引、即時 transaction commit；DynamoDB 寫入可以「先 durable、之後處理」</li>
<li>寫進你的設計時要明示：這是 <em>非預設</em> access pattern、是 flash-sale / 高峰寫入解耦的工程選擇、不是 DynamoDB 預設定位</li>
</ul>
<p>這個 access pattern 跟 single-table 設計兼容 — PK 仍是 <code>event_id#shard</code>、SK 是 <code>ORDER#{user_id}#{timestamp}</code>、寫入時直接寫，後端傳統 server 慢消費；只是讀路徑是 <em>後端服務 batch 取</em> 而非 user-facing query。</p>
<h3 id="rdb-connection-limit-機制對照">RDB connection limit 機制對照</h3>
<p><code>9.C29 Lemino</code> 揭露為什麼 DynamoDB 在 surge 下不會踩 RDB 的隱性天花板：</p>
<ul>
<li>「connection limits became bottlenecks when experiencing a rapid increase in access」— PostgreSQL/MySQL 每連線吃記憶體 / process、pool 上限 1K-5K、connection 是 RDB 在 surge 下 <em>第一個爆點</em>（不是 CPU / disk）</li>
<li>DynamoDB 的 HTTP API（無 long-lived connection state）天然解這個問題；client 不需要維護 connection pool、AWS SDK 用 connection-less HTTP request</li>
</ul>
<p>選 DynamoDB 不只是 schema 選擇、是 connection model 選擇。single-table 設計 <em>外部</em> 的容量優勢、寫進邊界判讀條件。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<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> — 軸 1 不天然均勻時的 composite key 補救</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a> — 主 PK 覆蓋不到的 access pattern 補位</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> — access pattern 影響 capacity mode 選擇</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> — 軸 3 的 per-call site review</li>
<li><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-conflict</a> — 跨 region 多寫入時 single-table 仍適用、但 conflict resolution 加一層</li>
<li>反向路由：access pattern 探索期 / strong consistency 必要 / data plane workload → 回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a></li>
</ul>
]]></content:encoded></item></channel></rss>