<?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>模組一：資料庫與持久化 on Tarragon</title><link>https://tarrragon.github.io/blog/backend/01-database/</link><description>Recent content in 模組一：資料庫與持久化 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 22 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/backend/01-database/index.xml" rel="self" type="application/rss+xml"/><item><title>1.1 高併發下的 SQL 讀寫邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/</guid><description>&lt;p>高併發服務處理 SQL 的核心原則是共用資料庫 client、並讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 管理連線生命週期。當並發升高時、真正要控制的是連線數、交易範圍、查詢時間與下游壓力；每個 request 各自建立連線會放大握手、排隊與資源回收成本。&lt;/p>
&lt;p>本章是 01 模組的基礎章節之一、之後章節（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰&lt;/a>）都會回引這層的概念。跨模組對接 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程&lt;/a>。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後、讀者能夠：&lt;/p>
&lt;ol>
&lt;li>理解資料庫 client 為什麼應該共用&lt;/li>
&lt;li>分辨 query、exec、rows 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 的不同邊界&lt;/li>
&lt;li>了解連線池參數對高併發的影響&lt;/li>
&lt;li>設計多層 connection pool 架構（app + middleware + DB）&lt;/li>
&lt;li>識別 hot row / lock contention 並選擇對策&lt;/li>
&lt;li>用 read replica 擴 read traffic、注意 replication lag&lt;/li>
&lt;li>用 &lt;code>context&lt;/code> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 控制慢查詢&lt;/li>
&lt;li>判斷什麼情況該換 KV / 緩衝模式而非繼續硬擴 SQL&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察資料庫-client-通常代表連線池入口">【觀察】資料庫 client 通常代表連線池入口&lt;/h2>
&lt;p>多數後端語言的資料庫 client 都會包住連線池或連線管理能力。一般情況下、服務會在啟動時建立可重用的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a> handle、讓 request handler、worker 或 service layer 共用它、並在需要時從池子裡取出可用連線。&lt;/p>
&lt;p>這種模型的好處是：&lt;/p>
&lt;ul>
&lt;li>呼叫端不用自己管理每個連線的生命週期&lt;/li>
&lt;li>多個 request 或 worker 可以同時發出資料庫操作&lt;/li>
&lt;li>連線回收與重用由 &lt;code>sql.DB&lt;/code> 處理&lt;/li>
&lt;/ul>
&lt;h2 id="判讀高併發需要有界連線">【判讀】高併發需要有界連線&lt;/h2>
&lt;p>高併發時的核心風險是把 application concurrency 誤解成 database concurrency。語言端的 thread、task、coroutine 或 goroutine 可能很容易建立、但資料庫有自己的容量上限；連線池只是把壓力從應用端平滑地送到下游、無法消滅壓力。&lt;/p></description><content:encoded><![CDATA[<p>高併發服務處理 SQL 的核心原則是共用資料庫 client、並讓 <a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 管理連線生命週期。當並發升高時、真正要控制的是連線數、交易範圍、查詢時間與下游壓力；每個 request 各自建立連線會放大握手、排隊與資源回收成本。</p>
<p>本章是 01 模組的基礎章節之一、之後章節（<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a> / <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>）都會回引這層的概念。跨模組對接 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</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 瓶頸定位流程</a>。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後、讀者能夠：</p>
<ol>
<li>理解資料庫 client 為什麼應該共用</li>
<li>分辨 query、exec、rows 與 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 的不同邊界</li>
<li>了解連線池參數對高併發的影響</li>
<li>設計多層 connection pool 架構（app + middleware + DB）</li>
<li>識別 hot row / lock contention 並選擇對策</li>
<li>用 read replica 擴 read traffic、注意 replication lag</li>
<li>用 <code>context</code> 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 控制慢查詢</li>
<li>判斷什麼情況該換 KV / 緩衝模式而非繼續硬擴 SQL</li>
</ol>
<hr>
<h2 id="觀察資料庫-client-通常代表連線池入口">【觀察】資料庫 client 通常代表連線池入口</h2>
<p>多數後端語言的資料庫 client 都會包住連線池或連線管理能力。一般情況下、服務會在啟動時建立可重用的 <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a> handle、讓 request handler、worker 或 service layer 共用它、並在需要時從池子裡取出可用連線。</p>
<p>這種模型的好處是：</p>
<ul>
<li>呼叫端不用自己管理每個連線的生命週期</li>
<li>多個 request 或 worker 可以同時發出資料庫操作</li>
<li>連線回收與重用由 <code>sql.DB</code> 處理</li>
</ul>
<h2 id="判讀高併發需要有界連線">【判讀】高併發需要有界連線</h2>
<p>高併發時的核心風險是把 application concurrency 誤解成 database concurrency。語言端的 thread、task、coroutine 或 goroutine 可能很容易建立、但資料庫有自己的容量上限；連線池只是把壓力從應用端平滑地送到下游、無法消滅壓力。</p>
<p>連線池調校的核心觀念是：</p>
<ul>
<li><code>SetMaxOpenConns</code> 太低、request 會在應用端排隊。</li>
<li><code>SetMaxOpenConns</code> 太高、可能把 DB 直接打滿。</li>
<li><code>SetMaxIdleConns</code> 影響高峰與尖峰之間的重用效率。</li>
<li><code>SetConnMaxLifetime</code> / <code>SetConnMaxIdleTime</code> 影響長連線與資源回收節奏。</li>
</ul>
<h3 id="第一個爆的通常是連線不是-cpu-或-disk">第一個爆的通常是連線、不是 CPU 或 disk</h3>
<p>SQL DB 在 surge 場景的 <em>first bottleneck</em> 不是 CPU、也不是 disk I/O、是 <em>連線數量</em>。原因：傳統 RDB（PostgreSQL、MySQL）每個連線吃記憶體 + 一個 process / thread、connection pool 上限通常 1K-5K。流量湧入時、application 想開更多連線、DB 直接拒絕（PostgreSQL：<code>FATAL: too many connections</code>）、看起來像 DB 故障、實際是連線數限制。</p>
<p>對應 <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%">9.C29 Lemino</a> — NTT DOCOMO 串流平台選 DynamoDB 而非 RDB 的原因之一是「connection limit 在快速流量增加時變成 bottleneck」。DynamoDB 的 HTTP API 模型沒有 connection state、天然解決這個瓶頸。</p>
<p><strong>判讀順序</strong>：surge 期間 DB 看起來慢、先 <code>SHOW PROCESSLIST</code> / <code>pg_stat_activity</code> 看連線數、再看 CPU / disk。連線數已經滿、再加 CPU 沒用；要加 middleware pool（pgBouncer / ProxySQL）或換 HTTP-based DB。</p>
<h2 id="多層-connection-pool-架構">多層 Connection Pool 架構</h2>
<p>實務上 production-grade 服務的 connection pool 通常分三層：</p>
<h3 id="layer-1application-pool每個-instance-內">Layer 1：Application pool（每個 instance 內）</h3>
<ul>
<li>每個 application instance 維護自己的 driver-level pool</li>
<li>典型大小：30-50 connection / instance</li>
<li>工具：HikariCP（Java）、SQLAlchemy pool（Python）、<code>sql.DB</code>（Go）</li>
</ul>
<h3 id="layer-2middleware-pool共享層">Layer 2：Middleware pool（共享層）</h3>
<ul>
<li>PostgreSQL：<a href="https://www.pgbouncer.org/">pgBouncer</a>（最常見、transaction pooling）、<a href="https://github.com/postgresml/pgcat">PgCat</a>（rust、支援 sharding）</li>
<li>MySQL：<a href="https://proxysql.com/">ProxySQL</a>（query routing + pool）</li>
<li>為什麼需要：多個 application instance 同時打 DB、總 connection 數會爆</li>
<li>pgBouncer 把 1000 application connection mux 到 50 個 DB connection、應用感覺有 1000 connection、DB 只看到 50</li>
</ul>
<h3 id="layer-3database-端-max_connections">Layer 3：Database 端 max_connections</h3>
<ul>
<li>PostgreSQL default 100、實務常設 200-500</li>
<li>MySQL default 151、實務常設 1000-5000</li>
<li>每個 connection 吃記憶體（PG ~10MB、MySQL ~3MB）、設太高會 OOM</li>
</ul>
<p><strong>典型配置範例</strong>（中型網路服務）：</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">50 application instance × 30 connection (app pool)
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → pgBouncer transaction pool (4 instance × 100 connection)
</span></span><span class="line"><span class="ln">3</span><span class="cl">  → PostgreSQL primary (max_connections = 200)</span></span></code></pre></div><p>1500 application connection mux 到 200 DB connection、4 倍 multiplexing。</p>
<p><strong>反模式</strong>：</p>
<ul>
<li>跳過 middleware pool、application 直連 DB</li>
<li>應用 instance 50 個 × 30 connection = 1500 connection、PostgreSQL 直接拒絕</li>
</ul>
<p>對應 <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%">9.C29 Lemino case</a> — RDB connection limit 是 surge 場景的隱性 bottleneck、Lemino 選擇遷移到 DynamoDB 而不是擴 connection pool（因為 HTTP-based KV 沒這個問題）。</p>
<h3 id="query-反模式如何放大連線池壓力">Query 反模式如何放大連線池壓力</h3>
<p>連線池被占滿的根本原因不只是「連線數不夠」、還有「單一連線被占用的時間太長」。Query 反模式直接放大每筆 request 的連線占用時間：</p>
<ul>
<li><strong>N+1 query</strong> 讓一個 request 占用連線從 1 個 round trip 拉長到 N+1 個。同樣的 throughput、需要 N+1 倍的連線數來 sustain</li>
<li><strong>Long-running transaction</strong> 把一個連線從幾毫秒占用變成幾秒，相當於把連線池的有效容量除以幾百倍</li>
<li><strong>缺索引的 query</strong> 在熱表上跑 full scan、單筆 query 從 10ms 變成 1-5 秒、連線占用時間放大兩個數量級</li>
<li><strong><code>SELECT *</code> 載入大欄位</strong>：reader 在反序列化大物件期間連線一直 hold、不是 query 本身慢、是 serialization overhead 拉長占用</li>
</ul>
<p>這些反模式單獨看是「query 寫法問題」、但放到連線池語境就是「連線池容量被間接削減」。先用 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a> 的清單收回連線占用時間、再考慮加 <a href="/blog/backend/09-performance-capacity/connection-pool-amplification/" data-link-title="9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）" data-link-desc="水平擴展應用層時 DB 連線池放大問題的具體解法、connection pooler 三大選項對比、解 9.13 提出但未深入的隱性成本">9.14 connection pooler</a> 中介層 — 順序顛倒會讓 pooler 治標不治本。</p>
<h2 id="策略讀取與寫入要分開看">【策略】讀取與寫入要分開看</h2>
<p>讀取的核心風險通常是慢查詢、掃描過大、N+1、熱點資料與連線被占住太久。寫入的核心風險則常常是 transaction 太大、衝突太高、鎖時間太長、重試邏輯不清楚。</p>
<h3 id="讀取">讀取</h3>
<ul>
<li>用索引支援常見查詢條件。</li>
<li>避免一次載入過多資料。</li>
<li>需要分頁時、先考慮游標或穩定排序。</li>
<li>熱讀資料可以在上層加 cache、同時保留資料庫作為正式狀態來源。</li>
</ul>
<h3 id="寫入">寫入</h3>
<ul>
<li>transaction 只包住真正需要一致性的範圍。</li>
<li>transaction 範圍只保留必要資料操作、外部 API 呼叫、使用者等待或長迴圈應放在交易外。</li>
<li>高衝突寫入要搭配重試、唯一鍵或明確去重策略。</li>
<li>需要高吞吐時、先評估批次化、分段處理與有界並發。</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 對 transaction 設計的深度討論。</p>
<h2 id="hot-row--lock-contention-識別與處理">Hot Row / Lock Contention 識別與處理</h2>
<p>當多個 request 同時想 update 同一筆資料、會在 DB 層出現 lock contention。這跟 KV 的 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a> 是同類問題、但 <em>機制不同</em>。</p>
<p><strong>典型 hot row 場景</strong>：</p>
<ul>
<li>inventory counter：所有用戶搶同一個 product 庫存</li>
<li>counter / metrics：實時計數器（view count、like count）</li>
<li>queue / job ledger：所有 worker 競爭同一個 job table</li>
<li>session：高頻 session 更新</li>
</ul>
<p><strong>識別訊號</strong>：</p>
<ul>
<li><code>pg_stat_activity</code> / SHOW PROCESSLIST 顯示大量 <code>lock waiting</code></li>
<li>整體 QPS 沒滿、但某些 endpoint p99 飆</li>
<li><code>pg_locks</code> / INFORMATION_SCHEMA.INNODB_LOCK_WAITS 有大量等待</li>
</ul>
<p><strong>對策</strong>：</p>
<p><strong>1. 分散熱點</strong>：</p>
<ul>
<li>counter shard：把 1 個 counter 拆成 N 個 sub-counter、寫入時隨機選一個、讀取時 SUM</li>
<li>例：<code>view_count_0</code> ~ <code>view_count_9</code> → 10 倍寫入吞吐</li>
<li>對應 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a> 在 SQL DB 的對應做法</li>
</ul>
<p><strong>2. Asynchronous batching</strong>：</p>
<ul>
<li>不要每次點擊就 update counter、先進 in-memory buffer、定期 flush</li>
<li>應用層 Redis INCR + 定期同步回 SQL</li>
</ul>
<p><strong>3. Optimistic concurrency control</strong>：</p>
<ul>
<li>用 <code>WHERE version = ?</code> 樂觀鎖、避免 SELECT FOR UPDATE</li>
<li>衝突時應用層 retry</li>
</ul>
<p><strong>4. 換 KV / cache</strong>：</p>
<ul>
<li>counter workload 本來就不適合 SQL transaction</li>
<li>用 Redis INCR、DynamoDB 的 atomic counter</li>
</ul>
<p><strong>5. Queue + worker 序列化</strong>：</p>
<ul>
<li>把搶資源的 request 排隊、worker 序列化處理</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 案例</a> — 售票把 inventory 搶購塞進 DynamoDB queue、legacy server 慢慢消費、避免 SQL hot row</li>
</ul>
<h2 id="read-replica-scaling">Read Replica Scaling</h2>
<p>當 read traffic 超過 primary 吞吐、用 read replica 擴 read。</p>
<p><strong>Read replica 機制</strong>：</p>
<ul>
<li>PostgreSQL：streaming replication（async / sync）</li>
<li>MySQL：async replication（binlog）</li>
<li>Aurora：storage-level replication（lag 10-30ms）</li>
</ul>
<p><strong>Routing 策略</strong>：</p>
<p><strong>1. Read / write split（application-level）</strong>：</p>
<ul>
<li>應用層判斷 query 類型、寫走 primary、讀走 replica</li>
<li>工具：ProxySQL（MySQL）、application 自管</li>
</ul>
<p><strong>2. Routing 自動化（middleware）</strong>：</p>
<ul>
<li>pgBouncer + 路由規則</li>
<li>HAProxy + health check</li>
</ul>
<p><strong>3. Stale read 容忍策略</strong>：</p>
<ul>
<li>「能容忍秒級 stale」的 read → replica（用戶 profile、報表）</li>
<li>「不能 stale」的 read → primary（剛寫入後的查詢、餘額確認）</li>
<li>read-after-write consistency：用 session token 標記「剛寫過」、N 秒內讀走 primary</li>
</ul>
<p><strong>Replication lag 監控</strong>：</p>
<ul>
<li>PostgreSQL：<code>pg_stat_replication.replay_lag</code></li>
<li>MySQL：<code>SHOW SLAVE STATUS\G</code> 的 <code>Seconds_Behind_Master</code></li>
<li>Aurora：CloudWatch <code>AuroraReplicaLag</code></li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a> — replication lag 從 30 秒降到 10-30ms、是切換到 Aurora 的關鍵改善</li>
</ul>
<p><strong>注意事項</strong>：</p>
<ul>
<li>replica 數量不是無限、Aurora 最多 15 個、PostgreSQL 通常 3-5 個（chain replication 更多但複雜）</li>
<li>跨 region replica 通常 async、不能保證 read-after-write</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> Super Bowl 5-10x peak、需要動態加 replica</li>
</ul>
<h3 id="儲存層-replication-vs-compute-層-replication">儲存層 replication vs compute 層 replication</h3>
<p>Aurora / Cosmos DB / Spanner 的 replication 跟傳統 PostgreSQL streaming replication 是兩種本質不同的設計、決定 read replica 怎麼擴、replication lag 落在什麼量級、容量規劃要顧哪些瓶頸。</p>
<p><strong>傳統 RDB（compute 層 replication）</strong>：</p>
<ul>
<li>primary 寫入後、把 WAL / binlog 流到 replica</li>
<li>replica 自己 replay log、消耗 CPU 跟 disk</li>
<li>primary 寫入量大、replica 跟不上、replication lag 飆</li>
<li>加 replica 增加 primary 的 <em>replication 負擔</em>、不能無限加</li>
</ul>
<p><strong>Aurora / Cosmos DB（storage 層 replication）</strong>：</p>
<ul>
<li>compute 跟 storage 分離、storage 是分散式 log-based</li>
<li>replication 在 <em>storage 層</em> 處理、不經過 compute</li>
<li>replica 不用自己 replay、直接讀同一份 storage</li>
<li>加 read replica 不增加 primary 寫入負擔</li>
<li>replication lag 從 30 秒級降到 10-30ms（Aurora）</li>
</ul>
<p><strong>為什麼這層差異反映在應用層設計</strong>：compute 層 replication 的 replication lag 通常在秒級、應用層必須處理「剛寫的資料 N 秒內讀不到」的情境 — 常見補丁是 read-after-write consistency（session token 標記「剛寫過」、N 秒內走 primary）、cache invalidation 延遲、或刻意走 primary 的關鍵查詢路徑。Storage 層 replication 的 lag 在毫秒級、這些補丁多半不需要、read 可以幾乎無條件走 replica。對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 從 30 秒到 10-30ms 不只是「快」、是讓整個應用層 cache invalidation 跟 session routing 邏輯大幅簡化。對應 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a> — Aurora 75% performance improvement 主要來自 storage layer 設計、不是 CPU 改善。</p>
<p><strong>選型含義</strong>：如果應用層 <em>依賴 read-after-write</em>（餘額確認、剛寫的查詢、session 狀態）、storage 層 replication 比 compute 層 replication 大幅簡化設計。代價是 vendor lock-in 加深、應用層綁定特定雲商。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent Azure SQL Hyperscale</a> 跟 Aurora 是同類設計（log-structured 分散式 storage）、選哪家看 application 已在哪個 cloud、技術哲學一致。Sharding 觸發點（managed DB 容量上限）跟業務一致性需求決定 sharding 粒度的討論、見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 Sharding 粒度跟業務一致性需求</a>。</p>
<h2 id="執行查詢與-rows-的生命週期要收乾淨">【執行】查詢與 rows 的生命週期要收乾淨</h2>
<p>查詢回傳 rows 後、呼叫端要負責把它關掉、並檢查迭代錯誤。這不只是記憶體管理問題、也會影響連線何時能回到池子裡。</p>
<p>典型模式是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">rows</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">QueryContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;SELECT id, name FROM users WHERE status = ?&#34;</span><span class="p">,</span> <span class="nx">status</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">defer</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">for</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Next</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="kd">var</span> <span class="nx">id</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="kd">var</span> <span class="nx">name</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">id</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">name</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Err</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="策略慢查詢要靠-timeout-與上層限流處理">【策略】慢查詢要靠 timeout 與上層限流處理</h2>
<p>在高併發服務裡、database timeout 應由 request timeout、client timeout 與資料庫 timeout 共同定義。語言端需要能把取消、<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 或 timeout 往資料庫 client 傳遞、讓慢查詢在合理時間內釋放資源。</p>
<p>如果下游開始變慢、通常要搭配：</p>
<ul>
<li>request-level timeout</li>
<li><a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a> 或 semaphore</li>
<li><a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 長度限制</li>
<li>降級或拒絕策略</li>
</ul>
<p>這樣做的目標是避免應用自己堆出大量等待中的工作、最後把問題放大成整個服務卡死。</p>
<h2 id="什麼時候該換-kv--緩衝模式而非繼續硬擴-sql">什麼時候該換 KV / 緩衝模式而非繼續硬擴 SQL</h2>
<p>SQL 的 transactional 模型有結構性限制、超過某個規模硬擴 SQL 不如換工具。</p>
<p><strong>換工具的訊號</strong>：</p>
<ol>
<li>
<p><strong>Connection saturate 但 CPU / RAM 還閒</strong>：connection 是 SQL 的早期 bottleneck。對應 <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%">9.C29 Lemino</a> — RDB connection limit 是 surge 場景的瓶頸、換 DynamoDB（HTTP-based、無 connection 概念）解決。</p>
</li>
<li>
<p><strong>Hot row contention 無法分散</strong>：應用層改不了 schema、無法把 counter shard、SQL 就是 contention 源頭。換 Redis atomic counter / DynamoDB atomic update。</p>
</li>
<li>
<p><strong>Write throughput &gt; 50K WPS 單機</strong>：sharding 工程成本變高、不如換 KV 或分散式 SQL。詳見 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 或 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>。</p>
</li>
<li>
<p><strong>Flash-sale spiky workload</strong>：用 SQL 接搶購、connection 跟 lock 都會爆。對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 用 DynamoDB 當 durable queue、legacy SQL 慢慢消費。</p>
</li>
<li>
<p><strong>跨 region 強一致 OLTP</strong>：傳統 PostgreSQL / MySQL 跨 region 是 async、滿足不了強一致。換 Spanner / Aurora DSQL / CockroachDB（<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11</a>）。</p>
</li>
</ol>
<p>不要因為「現在 SQL 慢」就跳結論換 NoSQL — 先確認問題是 <em>結構性的</em>（connection、contention、跨 region）、不只是 <em>調校問題</em>（index、query、cache）。</p>
<h2 id="延伸語言端的責任是邊界">【延伸】語言端的責任是邊界</h2>
<p>這一章不討論 PostgreSQL、MySQL、SQLite 的語法差異、也不討論 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> 工具本身。語言端需要掌握的是：怎麼共用 database client、怎麼控制並發、怎麼縮小 transaction、怎麼把 timeout 和取消傳下去。</p>
<p>具體 schema、index、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a> 與 migration 寫法、會放在這個模組的其他資料庫教材中。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>高併發場景重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day 2025</a></td>
          <td>DynamoDB 1.51 億 RPS + Aurora 5000 億 txn、可預期峰值的 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">dogfood baseline</a>（vendor 自家 production-critical workload 是 selection signal）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></td>
          <td>1M ops/min、200 個獨立 cluster、replication lag 30s → 10-30ms</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered Aurora</a></td>
          <td>4000 TPS、7 個受監管市場、各自獨立 cluster</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora</a></td>
          <td>DB 統一後 +75% 效能、storage / compute 分離釋放 read replica</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>Super Bowl 5-10x peak、Aurora MySQL + read replica scaling</td>
      </tr>
      <tr>
          <td><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%">9.C29 Lemino</a></td>
          <td>RDB connection limit 是 surge 瓶頸、改用 DynamoDB</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent Azure SQL Hyperscale</a></td>
          <td>5 億 txn/年、storage / compute 分離跟 Aurora 同類設計</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a> 是高併發章節的 <em>上限參考點</em>：Amazon 自家 Prime Day 在 24 小時內、DynamoDB 服務 1.51 億 RPS 毫秒級回應、Aurora 處理 5000 億次 transaction。這份數字的意義不是「要達到這個量級」、而是給定 <em>可預期峰值</em> 跟 <em>無限預算</em> 時、AWS 自家服務的設計上限長這樣。讀本章其他內部 baseline（connection pool、replica lag、isolation level）時、要記得最終物理上限遠高於大部分服務日常會碰到的水位。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>資料庫高併發邊界會受語言 runtime 影響。Thread-based runtime 要管理 thread pool 與 connection pool 的比例；async runtime 要確認 database driver 是否真正非阻塞（很多老 driver 只是包了 sync 在 thread pool 上、會吃 thread limit）；輕量 task runtime（Go、Erlang）要限制同時查詢數量、避免把大量 task 轉成下游連線壓力。強型別語言可以用型別保護 row mapping 與錯誤分類；動態語言則需要用 migration、runtime validation、<a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test 與 fixture 保護 schema 邊界。</p>
<h2 id="小結">小結</h2>
<p>高併發下處理 SQL 的核心原則：</p>
<ol>
<li><strong>database client 共用</strong>、不要每 request 新建</li>
<li><strong>連線池可控</strong> — 三層架構（app pool + middleware + DB max_connections）</li>
<li><strong>transaction 要短</strong> — 詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3</a></li>
<li><strong>rows 要關</strong>、避免連線被占住</li>
<li><strong>timeout 要傳遞</strong> — 從 request 一路到 DB</li>
<li><strong>Hot row 要識別</strong> — counter shard、optimistic concurrency、async batching、或換 KV</li>
<li><strong>Read replica 要會用</strong> — 但注意 lag、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 容忍度</li>
<li><strong>下游壓力要限流</strong> — request timeout、worker pool、queue 長度、降級拒絕</li>
<li><strong>知道什麼時候換工具</strong> — connection saturation、hot contention、flash-sale、跨 region 強一致都是 SQL 結構性限制的訊號</li>
</ol>
<p>應用端並發可以很多、但資料庫連線必須受控、這兩者的邊界要分開管理。</p>
<h2 id="讀峰值數字的工程細節">讀「峰值」數字的工程細節</h2>
<p>容量規劃時看到「100 萬 ops/分鐘」、「150 萬 RPS」這類數字、要拆三個維度看、否則容量規劃會錯位。</p>
<h3 id="容量數字的三個口徑">容量數字的三個口徑</h3>
<table>
  <thead>
      <tr>
          <th>口徑</th>
          <th>含義</th>
          <th>用於規劃</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>最大瞬時</td>
          <td>某一秒的最高峰（單秒）</td>
          <td>不能拿這個訂 baseline、是 outlier</td>
      </tr>
      <tr>
          <td>99 百分位平均</td>
          <td>99% 時間在這個水位以下</td>
          <td>訂 capacity 上限的依據</td>
      </tr>
      <tr>
          <td>常態流量</td>
          <td>平均的日常水位</td>
          <td>訂 cost baseline、auto-scaling 起點</td>
      </tr>
  </tbody>
</table>
<p><strong>最大瞬時</strong> 是觀測得到的最高峰值、通常是年度某秒、不能拿來訂 baseline。在 Grafana / CloudWatch / Datadog 上看 <code>max</code> 指標就是這個數字 — 用來知道系統 <em>曾經</em> 撐過多少、不是 <em>日常</em> 要撐多少。</p>
<p><strong>99 百分位平均</strong> 是 capacity 規劃的主要依據。在監控工具看的是 <code>p99</code> 隨時間的平均值（rolling 30 天或 90 天）— 代表 99% 的時間流量低於這個水位。Auto-scaling 上限通常訂在這個值的 1.5-2 倍、確保 99% 時間有足夠 headroom。</p>
<p><strong>常態流量</strong> 是 average / median、訂 cost baseline 跟 auto-scaling 的下限。在 PaaS（Aurora Serverless、Cosmos DB serverless）這是「最低保留容量」的依據；在 IaaS 是「永遠開著的 instance 數量」。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 揭露這個議題：「9000 萬 reads / 秒」通常是年度峰值最高一秒、不是平均。讀案例時要區分這三個口徑、否則容量規劃會錯位。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 「100 萬 ops/分鐘」≈ 17K ops/秒、跨 200 個獨立 cluster 平均下來每 DB 約 80 ops/秒。讀峰值要看 <em>分散到多少 shard</em>、不只看總數。</p>
<h3 id="延遲改善要看-percentile不是平均">延遲改善要看 percentile、不是平均</h3>
<p>「延遲降 90%」這類敘述要追問：是 p50 還是 p99？兩者改善幅度通常差很多、平均值會掩蓋尾巴問題。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> — 「90% 延遲降」實際可能是 p50、p99 / p999 改善幅度通常較小。判讀重點：用戶體驗主要受 <em>p99 / p999</em> 影響、不是 p50。看到「平均 50ms 降到 5ms」要追問「p99 從多少降到多少」、否則可能用戶感受沒改善。</p>
<p>延遲監控的必要 percentile：p50、p95、p99、p99.9。p99.9 對 1000 個 request 才偵測一次、但通常代表系統最差表現、是 SLO breach 的早期訊號。</p>
<h2 id="headroom-budget事件型-vs-突發型峰值">Headroom budget：事件型 vs 突發型峰值</h2>
<p>Headroom budget 是 <em>提前預留的容量空間</em>、給可預期或不可預期的峰值用。讀「Super Bowl +50% no sweat」這種敘述、工程意義是團隊事前預留了 headroom、不是 vendor 神奇。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — Super Bowl 是已知事件、+50% 是歷史經驗、所以可以提前 pre-scale。整個 system headroom 預留至少 50%、加上 read replica 動態加減、才能讓 50% 增幅變成「不流汗」。</p>
<p>兩種峰值的 headroom budget 規劃完全不同：</p>
<p><strong>事件型峰值</strong>（已知時間 + 已知幅度）：</p>
<ul>
<li>例：Super Bowl、Black Friday、票券開賣、財報日</li>
<li>規劃做法：歷史 peak × 預期成長 × headroom（通常 1.5-2x）= baseline、事件前 scheduled scale-up</li>
<li>headroom 預算可以較低（20-30%）、因為峰值可預測、可在事件前測試</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
</ul>
<p><strong>突發型峰值</strong>（未知時間或未知幅度）：</p>
<ul>
<li>例：突發新聞、KOL 推廣、競爭對手出包導致流量湧入、病毒式擴散</li>
<li>規劃做法：常態 baseline 預留高 headroom（50-100%）、加 auto-scaling 跟動態 capacity</li>
<li>headroom 預算要高、因為事故發生前沒時間 scale</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech AI 預測式擴容</a></li>
</ul>
<p>判讀重點：事件型 headroom 適合可預測峰值、突發型 headroom 適合不可預測峰值；兩者預算邏輯不同。把事件型 headroom 套用在突發型場景、突發事件發生時容量會不足；把突發型的高 headroom 套用在事件型、會付大量浪費成本。</p>
<h2 id="讀寫峰值錯位dual-peak-workload">讀寫峰值錯位：dual peak workload</h2>
<p>部分業務有 <em>讀峰值跟寫峰值不同時段</em> 的特性、容量規劃要按 <em>peak 之和</em> 而非 <em>單一 peak</em>。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 「write workloads spike up significantly around payout events, but opening the app during the game also activates a lot of balance queries」。比賽進行時讀爆量（用戶看餘額、看下注狀態）、比賽結束 payout 時寫爆量（賠付寫進帳本）、兩個 peak 錯位。</p>
<p>容量規劃含義：</p>
<ul>
<li>不能只規劃「讀 peak + 寫常態」或「寫 peak + 讀常態」</li>
<li>要規劃「讀 peak 跟寫 peak 各自的容量」、即使不同時發生、底層 DB 都要撐</li>
<li>read replica 動態增減可以平滑讀 peak、但寫 peak 要靠 primary capacity 撐住</li>
</ul>
<p><strong>類似 dual peak 業務</strong>：</p>
<ul>
<li>體育博彩：比賽中讀、payout 時寫（DraftKings）</li>
<li>票券：開賣前 30 分鐘讀爆量（用戶看座位）、開賣瞬間寫爆量（搶票）</li>
<li>電商促銷：促銷前讀爆量（用戶看價格）、促銷瞬間寫爆量（下單）</li>
<li>股票交易：開盤前讀爆量（看開盤價）、開盤瞬間寫爆量（送單）</li>
</ul>
<p>判讀重點：dual peak workload 是業務 <em>天然</em> 特性、不是異常。容量規劃要識別這層、否則尖峰時段會踩到沒預期的瓶頸。</p>
<h2 id="關鍵路徑切分低頻流量保護">關鍵路徑切分：低頻流量保護</h2>
<p>當系統有「高頻流量（如選位、瀏覽）」跟「低頻但關鍵流量（如付款、結算）」共存時、必須切分、否則高頻流量會塞爆低頻路徑、讓低頻關鍵業務無法完成。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 拓元把 Payment EC2 拉出來、直連傳統金流 server、不放在搶票流量會打到的 ELB / DB 後面。讓「選位 + 下單」的高頻流量塞爆時、「付款」的低頻流量仍能跑。</p>
<p><strong>切分策略</strong>：</p>
<ul>
<li><strong>資料路徑切分</strong>：高頻 query 走 DynamoDB / read replica、低頻關鍵 query 走 primary</li>
<li><strong>連線池切分</strong>：高頻 service 跟低頻 service 用不同 connection pool、避免高頻吃光連線</li>
<li><strong>runtime 切分</strong>：低頻關鍵 service 部署到獨立 instance、不跟高頻共用 CPU / memory</li>
<li><strong>限流切分</strong>：高頻 endpoint 設高限流、低頻關鍵 endpoint 設保護性低限流（避免 cascading failure）</li>
</ul>
<p>判讀重點：切分前要先盤「哪些流量是業務關鍵但量小」、這些路徑要事先保護、不能等爆了再分開。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool 卡片</a></li>
<li>上游：<a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式與 Query 預算</a>（connection saturation 常因 N+1 / long transaction 放大、先檢查 query 寫法）</li>
<li>平行：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 Schema Design</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>（SQL 不夠用時的替代）/ <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（換 DB engine 的決策跟流程）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</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 瓶頸定位流程</a>、<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>、<a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a>（hot row 是不可分散瓶頸的 application 層表現）</li>
<li>Vendor：<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</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a>、<a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora</a></li>
<li>規模成長路線下一站 → <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a>（連線池 / replica 擴完後、進入應用層快取設計）</li>
<li>MongoDB connection storm 深入：<a href="/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/" data-link-title="MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling" data-link-desc="MongoDB 大規模 OLTP 撞牆不是單一 driver 議題、是 driver × 部署模型 × cache × scaling trigger 三層協作；含 Coinbase mongobetween / freshness token / ML 預測擴容三件套 &#43; 適用範圍紀律">MongoDB connection 管理與 cache 層</a> / <a href="/blog/backend/01-database/vendors/mongodb/replica-set-read-preference/" data-link-title="MongoDB Replica Set Read Preference：DB 層 causal session vs cache 層 freshness token" data-link-desc="MongoDB read preference 五擇一 &#43; read concern &#43; causal consistency session 機制；DB 層機制解 cluster 內 read-your-own-write、cache 層 freshness token 解跨層 read-after-write、大規模 OLTP 必須兩層合用">replica set read preference</a></li>
<li>Aurora read replica 擴展：<a href="/blog/backend/01-database/vendors/aurora/read-replica-scaling/" data-link-title="Aurora Read Replica Scaling：15 replica 上限、lag profile、headroom 預留與 fleet 治理" data-link-desc="Aurora 15 replica 上限、共享 storage 為什麼能養大量 replica、事件型容量分級表、DraftKings headroom 預留判讀、FanDuel 雙 SLO 並行、fleet 治理 3 條 driver（business sharding / microservice / 合規）">Aurora read replica scaling</a>（reader endpoint / lag 治理）</li>
<li>Freshness token 卡片：<a href="/blog/backend/knowledge-cards/freshness-token/" data-link-title="Freshness Token" data-link-desc="DB write 後返回的版本 token、後續 read 帶 token、保證 read 看到的資料 ≥ token 版本、解 DB &#43; cache 跨層 read-after-write">Freshness Token</a>（read-after-write 保證選項）</li>
</ul>
]]></content:encoded></item><item><title>1.2 Schema Design 與資料建模</title><link>https://tarrragon.github.io/blog/backend/01-database/schema-design/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/schema-design/</guid><description>&lt;p>資料綱要設計（schema design）的核心責任是把業務狀態轉成可維護、可查詢、可演進的資料結構。資料建模做得好、交易邊界、查詢效率、migration 成本與事故修復路徑都會更穩定。&lt;/p>
&lt;p>本章是 01 模組的基礎章節之一、結合 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary&lt;/a>（交易範圍）、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence&lt;/a>（演進證據）與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃&lt;/a>（partition key 設計）一起讀。讀完後能回答：table 怎麼切、index 怎麼選、什麼時候 denormalize、partition 怎麼設、命名怎麼治理。&lt;/p>
&lt;h2 id="先定義狀態責任">先定義狀態責任&lt;/h2>
&lt;p>資料模型第一步是定義狀態責任：哪些欄位代表正式狀態、哪些欄位是派生值、哪些欄位只為追蹤與審計。這個分層會直接決定 table 邊界與 relation 方向。&lt;/p>
&lt;p>在訂單服務中、訂單主檔、付款狀態、庫存扣減屬於正式狀態；展示排序欄位、快取摘要屬於派生值；版本號、更新時間與來源欄位屬於可追蹤證據。把三類混在同一模型裡、後續查詢與演進成本會持續上升。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership 與 Query Boundary&lt;/a>。&lt;/p>
&lt;h2 id="table-與-relation">Table 與 Relation&lt;/h2>
&lt;p>table 切分要對齊業務聚合邊界。聚合內需要交易一致性的欄位、放在同一交易可控範圍；跨聚合流程透過事件或引用關係接續。relation 的責任是表達資料約束、不是替代流程編排。&lt;/p>
&lt;p>主鍵策略要先回答「如何穩定識別」與「如何支援查詢」。自然鍵可讀性高但變動風險高；代理鍵穩定且易擴展、常搭配業務唯一鍵一起使用。外鍵策略則要平衡完整性與演進自由度：正式核心域可強約束、跨域整合可由應用層保護並保留遷移彈性。&lt;/p>
&lt;p>&lt;strong>主鍵選擇實務&lt;/strong>：&lt;/p>
&lt;p>ID 設計不只是「選個格式」，而是在五個維度做取捨。先理解取捨、再按場景選型。&lt;/p>
&lt;h3 id="id-設計的五個取捨維度">ID 設計的五個取捨維度&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>唯一性&lt;/strong>&lt;/td>
 &lt;td>跨機器、跨時間不碰撞&lt;/td>
 &lt;td>分散式系統的核心需求&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>有序性&lt;/strong>&lt;/td>
 &lt;td>是否可按生成順序排序&lt;/td>
 &lt;td>B-tree 插入效能、時間軸查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>隱私性&lt;/strong>&lt;/td>
 &lt;td>是否洩漏業務資訊（量級、時間、機器）&lt;/td>
 &lt;td>外部可見的 ID 不應洩漏用戶數量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>儲存成本&lt;/strong>&lt;/td>
 &lt;td>佔多少 byte、index 體積&lt;/td>
 &lt;td>高 TPS 場景每 byte 都乘以百萬筆&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>產生效能&lt;/strong>&lt;/td>
 &lt;td>需要鎖？需要 crypto/rand？需要 network call？&lt;/td>
 &lt;td>熱路徑上的 ID 產生 ns 級差異有影響&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="id-類型選型矩陣">ID 類型選型矩陣&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>ID 類型&lt;/th>
 &lt;th>大小&lt;/th>
 &lt;th>唯一性&lt;/th>
 &lt;th>有序性&lt;/th>
 &lt;th>隱私性&lt;/th>
 &lt;th>產生效能&lt;/th>
 &lt;th>適合場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Bigint sequence&lt;/strong>&lt;/td>
 &lt;td>8 byte&lt;/td>
 &lt;td>單機唯一&lt;/td>
 &lt;td>嚴格有序&lt;/td>
 &lt;td>低（可猜量級）&lt;/td>
 &lt;td>最快（DB 自增）&lt;/td>
 &lt;td>單機、內部 ID&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>UUID v4&lt;/strong>&lt;/td>
 &lt;td>16 byte&lt;/td>
 &lt;td>全域唯一&lt;/td>
 &lt;td>無序&lt;/td>
 &lt;td>高（不可預測）&lt;/td>
 &lt;td>中（crypto/rand）&lt;/td>
 &lt;td>外部可見 ID、隱私敏感&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>UUID v7&lt;/strong>&lt;/td>
 &lt;td>16 byte&lt;/td>
 &lt;td>全域唯一&lt;/td>
 &lt;td>時間有序&lt;/td>
 &lt;td>中（時間可推）&lt;/td>
 &lt;td>中（timestamp + crypto/rand）&lt;/td>
 &lt;td>內部 ID、事件追蹤、DB 主鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>ULID&lt;/strong>&lt;/td>
 &lt;td>16 byte&lt;/td>
 &lt;td>全域唯一&lt;/td>
 &lt;td>時間有序&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>類 UUID v7（先於 v7 標準化）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Snowflake&lt;/strong>&lt;/td>
 &lt;td>8 byte&lt;/td>
 &lt;td>需要 machine_id 協調&lt;/td>
 &lt;td>時間有序&lt;/td>
 &lt;td>低（含 machine_id）&lt;/td>
 &lt;td>快（無 crypto）&lt;/td>
 &lt;td>高 TPS + 分散式 + 空間敏感&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>NanoID&lt;/strong>&lt;/td>
 &lt;td>可變（預設 21 字元）&lt;/td>
 &lt;td>依長度&lt;/td>
 &lt;td>無序&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>快（PRNG 即可）&lt;/td>
 &lt;td>URL-safe 短 ID（用於外部可見的短連結、邀請碼）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="選型決策流程">選型決策流程&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">需要跨機器唯一？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> └─ 否 → Bigint sequence（最簡單、效能最好）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └─ 是 → ID 對外部可見？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> └─ 是 → 隱私敏感？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> └─ 是 → UUID v4（不可預測）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> └─ 否 → UUID v7（有序、DB 友好）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> └─ 否 → 空間敏感（8 byte vs 16 byte）？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> └─ 是 → Snowflake（需要 machine_id 協調）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl"> └─ 否 → UUID v7（簡單、標準）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="有序-id-的-db-效能影響">有序 ID 的 DB 效能影響&lt;/h3>
&lt;p>B-tree 索引的插入效能和 key 的分布有直接關係。UUID v4 的隨機分布導致每次插入都可能落在 B-tree 的不同 leaf page，造成大量隨機 I/O（page split、cache miss）。UUID v7 的時間戳前綴讓插入集中在 B-tree 的尾端，接近 sequential insert。&lt;/p></description><content:encoded><![CDATA[<p>資料綱要設計（schema design）的核心責任是把業務狀態轉成可維護、可查詢、可演進的資料結構。資料建模做得好、交易邊界、查詢效率、migration 成本與事故修復路徑都會更穩定。</p>
<p>本章是 01 模組的基礎章節之一、結合 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a>（交易範圍）、<a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence</a>（演進證據）與 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a>（partition key 設計）一起讀。讀完後能回答：table 怎麼切、index 怎麼選、什麼時候 denormalize、partition 怎麼設、命名怎麼治理。</p>
<h2 id="先定義狀態責任">先定義狀態責任</h2>
<p>資料模型第一步是定義狀態責任：哪些欄位代表正式狀態、哪些欄位是派生值、哪些欄位只為追蹤與審計。這個分層會直接決定 table 邊界與 relation 方向。</p>
<p>在訂單服務中、訂單主檔、付款狀態、庫存扣減屬於正式狀態；展示排序欄位、快取摘要屬於派生值；版本號、更新時間與來源欄位屬於可追蹤證據。把三類混在同一模型裡、後續查詢與演進成本會持續上升。</p>
<p>詳見 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership 與 Query Boundary</a>。</p>
<h2 id="table-與-relation">Table 與 Relation</h2>
<p>table 切分要對齊業務聚合邊界。聚合內需要交易一致性的欄位、放在同一交易可控範圍；跨聚合流程透過事件或引用關係接續。relation 的責任是表達資料約束、不是替代流程編排。</p>
<p>主鍵策略要先回答「如何穩定識別」與「如何支援查詢」。自然鍵可讀性高但變動風險高；代理鍵穩定且易擴展、常搭配業務唯一鍵一起使用。外鍵策略則要平衡完整性與演進自由度：正式核心域可強約束、跨域整合可由應用層保護並保留遷移彈性。</p>
<p><strong>主鍵選擇實務</strong>：</p>
<p>ID 設計不只是「選個格式」，而是在五個維度做取捨。先理解取捨、再按場景選型。</p>
<h3 id="id-設計的五個取捨維度">ID 設計的五個取捨維度</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>說明</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>唯一性</strong></td>
          <td>跨機器、跨時間不碰撞</td>
          <td>分散式系統的核心需求</td>
      </tr>
      <tr>
          <td><strong>有序性</strong></td>
          <td>是否可按生成順序排序</td>
          <td>B-tree 插入效能、時間軸查詢</td>
      </tr>
      <tr>
          <td><strong>隱私性</strong></td>
          <td>是否洩漏業務資訊（量級、時間、機器）</td>
          <td>外部可見的 ID 不應洩漏用戶數量</td>
      </tr>
      <tr>
          <td><strong>儲存成本</strong></td>
          <td>佔多少 byte、index 體積</td>
          <td>高 TPS 場景每 byte 都乘以百萬筆</td>
      </tr>
      <tr>
          <td><strong>產生效能</strong></td>
          <td>需要鎖？需要 crypto/rand？需要 network call？</td>
          <td>熱路徑上的 ID 產生 ns 級差異有影響</td>
      </tr>
  </tbody>
</table>
<h3 id="id-類型選型矩陣">ID 類型選型矩陣</h3>
<table>
  <thead>
      <tr>
          <th>ID 類型</th>
          <th>大小</th>
          <th>唯一性</th>
          <th>有序性</th>
          <th>隱私性</th>
          <th>產生效能</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Bigint sequence</strong></td>
          <td>8 byte</td>
          <td>單機唯一</td>
          <td>嚴格有序</td>
          <td>低（可猜量級）</td>
          <td>最快（DB 自增）</td>
          <td>單機、內部 ID</td>
      </tr>
      <tr>
          <td><strong>UUID v4</strong></td>
          <td>16 byte</td>
          <td>全域唯一</td>
          <td>無序</td>
          <td>高（不可預測）</td>
          <td>中（crypto/rand）</td>
          <td>外部可見 ID、隱私敏感</td>
      </tr>
      <tr>
          <td><strong>UUID v7</strong></td>
          <td>16 byte</td>
          <td>全域唯一</td>
          <td>時間有序</td>
          <td>中（時間可推）</td>
          <td>中（timestamp + crypto/rand）</td>
          <td>內部 ID、事件追蹤、DB 主鍵</td>
      </tr>
      <tr>
          <td><strong>ULID</strong></td>
          <td>16 byte</td>
          <td>全域唯一</td>
          <td>時間有序</td>
          <td>中</td>
          <td>中</td>
          <td>類 UUID v7（先於 v7 標準化）</td>
      </tr>
      <tr>
          <td><strong>Snowflake</strong></td>
          <td>8 byte</td>
          <td>需要 machine_id 協調</td>
          <td>時間有序</td>
          <td>低（含 machine_id）</td>
          <td>快（無 crypto）</td>
          <td>高 TPS + 分散式 + 空間敏感</td>
      </tr>
      <tr>
          <td><strong>NanoID</strong></td>
          <td>可變（預設 21 字元）</td>
          <td>依長度</td>
          <td>無序</td>
          <td>高</td>
          <td>快（PRNG 即可）</td>
          <td>URL-safe 短 ID（用於外部可見的短連結、邀請碼）</td>
      </tr>
  </tbody>
</table>
<h3 id="選型決策流程">選型決策流程</h3>





<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">需要跨機器唯一？
</span></span><span class="line"><span class="ln">2</span><span class="cl">  └─ 否 → Bigint sequence（最簡單、效能最好）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  └─ 是 → ID 對外部可見？
</span></span><span class="line"><span class="ln">4</span><span class="cl">           └─ 是 → 隱私敏感？
</span></span><span class="line"><span class="ln">5</span><span class="cl">                    └─ 是 → UUID v4（不可預測）
</span></span><span class="line"><span class="ln">6</span><span class="cl">                    └─ 否 → UUID v7（有序、DB 友好）
</span></span><span class="line"><span class="ln">7</span><span class="cl">           └─ 否 → 空間敏感（8 byte vs 16 byte）？
</span></span><span class="line"><span class="ln">8</span><span class="cl">                    └─ 是 → Snowflake（需要 machine_id 協調）
</span></span><span class="line"><span class="ln">9</span><span class="cl">                    └─ 否 → UUID v7（簡單、標準）</span></span></code></pre></div><h3 id="有序-id-的-db-效能影響">有序 ID 的 DB 效能影響</h3>
<p>B-tree 索引的插入效能和 key 的分布有直接關係。UUID v4 的隨機分布導致每次插入都可能落在 B-tree 的不同 leaf page，造成大量隨機 I/O（page split、cache miss）。UUID v7 的時間戳前綴讓插入集中在 B-tree 的尾端，接近 sequential insert。</p>
<table>
  <thead>
      <tr>
          <th>測試場景（PostgreSQL、1000 萬筆）</th>
          <th>UUID v4</th>
          <th>UUID v7</th>
          <th>Bigint</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>INSERT 吞吐</td>
          <td>~5,000/sec</td>
          <td>~15,000/sec</td>
          <td>~20,000/sec</td>
      </tr>
      <tr>
          <td>Index 大小</td>
          <td>~400 MB</td>
          <td>~350 MB</td>
          <td>~200 MB</td>
      </tr>
      <tr>
          <td>範圍查詢延遲</td>
          <td>要額外建 timestamp index</td>
          <td>UUID 本身有序</td>
          <td>天然有序</td>
      </tr>
  </tbody>
</table>
<p>上表數字是基於 NVMe SSD 環境的量級估算（源自 UUID v4 的 random page split 成本約為 sequential 的 1/3-1/4 這個 B-tree 特性推導），實際效能依硬體和 workload 而定。核心結論：UUID v7 的插入效能約為 v4 的 3 倍，接近 bigint sequential。</p>
<h3 id="隱私考量v4-vs-v7">隱私考量：v4 vs v7</h3>
<p>UUID v7 的前 48 bit 是 Unix 時間戳（毫秒精度）。攻擊者拿到 UUID v7 可以推算「這個 ID 在幾點幾分產生」。這在不同場景有不同風險：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>v7 洩漏的資訊</th>
          <th>風險等級</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內部事件追蹤 ID</td>
          <td>事件產生時間</td>
          <td>無風險（log 本身有 timestamp）</td>
          <td>v7</td>
      </tr>
      <tr>
          <td>DB 主鍵（內部）</td>
          <td>資料建立時間</td>
          <td>低風險</td>
          <td>v7</td>
      </tr>
      <tr>
          <td>Session ID（自用工具）</td>
          <td>Session 開始時間</td>
          <td>低風險</td>
          <td>v7</td>
      </tr>
      <tr>
          <td>Session ID（商業產品、有外部使用者）</td>
          <td>使用者活動時間</td>
          <td>中風險（可交叉比對身份）</td>
          <td>v4</td>
      </tr>
      <tr>
          <td>API key / token</td>
          <td>簽發時間</td>
          <td>高風險（可推斷 key 輪換週期）</td>
          <td>v4 或加密</td>
      </tr>
      <tr>
          <td>訂單 ID（外部可見）</td>
          <td>下單時間 + 量級趨勢</td>
          <td>中風險</td>
          <td>v4 或 NanoID</td>
      </tr>
  </tbody>
</table>
<p>經驗法則：<strong>對外暴露給不可信第三方的 ID 用 v4（不可預測），內部 ID 用 v7（有序、效能好）。</strong></p>
<h3 id="各語言的標準庫支援">各語言的標準庫支援</h3>
<table>
  <thead>
      <tr>
          <th>語言</th>
          <th>UUID v4</th>
          <th>UUID v7</th>
          <th>套件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Python 3.14+</td>
          <td><code>uuid.uuid4()</code></td>
          <td><code>uuid.uuid7()</code></td>
          <td>標準庫</td>
      </tr>
      <tr>
          <td>Python &lt; 3.14</td>
          <td><code>uuid.uuid4()</code></td>
          <td><code>uuid_utils.uuid7()</code></td>
          <td>第三方</td>
      </tr>
      <tr>
          <td>Go</td>
          <td><code>google/uuid</code> v4</td>
          <td><code>google/uuid</code> v7（1.6+）</td>
          <td>事實標準</td>
      </tr>
      <tr>
          <td>TypeScript</td>
          <td><code>crypto.randomUUID()</code></td>
          <td>標準庫無（<code>uuidv7</code> npm）</td>
          <td>第三方</td>
      </tr>
      <tr>
          <td>Dart</td>
          <td><code>uuid</code> package</td>
          <td><code>uuid</code> package v4+（支援 v7）</td>
          <td>pub.dev</td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td><code>gen_random_uuid()</code></td>
          <td><code>uuidv7()</code>（pg_uuidv7 extension）</td>
          <td>擴展</td>
      </tr>
  </tbody>
</table>
<p>Go 的 <code>google/uuid</code> v1.6+ 內建 <code>uuid.NewV7()</code>，效能約 350ns/op（含 crypto/rand），和 JSON 解析（5-10μs）、DB 寫入（200μs）相比不是瓶頸。</p>
<p>對應 KV 案例：<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads partition key</a>、<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft composite key</a> 都是主鍵策略的延伸。</p>
<h2 id="index-設計">Index 設計</h2>
<p>index 設計要從查詢路徑反推、不是從欄位列表前推。每個高頻查詢至少要回答三件事：過濾條件是什麼、排序規則是什麼、回傳範圍有多大。這三件事能否由索引覆蓋、決定了 latency 與成本。</p>
<p><strong>Index 類型對照</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Index 類型</th>
          <th>適用 query</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>B-tree（預設）</td>
          <td><code>WHERE col = ?</code> / <code>WHERE col &gt; ?</code> / <code>ORDER BY col</code></td>
          <td>多數查詢</td>
      </tr>
      <tr>
          <td>Hash</td>
          <td><code>WHERE col = ?</code>（不支援 range）</td>
          <td>PostgreSQL 限定、少用</td>
      </tr>
      <tr>
          <td>GIN</td>
          <td>JSONB / array / full-text search</td>
          <td><code>WHERE jsonb_data @&gt; ?</code></td>
      </tr>
      <tr>
          <td>GiST</td>
          <td>範圍 / 地理 / 自訂型別</td>
          <td>PostGIS、range type</td>
      </tr>
      <tr>
          <td>BRIN</td>
          <td>大表時序資料、欄位跟物理順序相關</td>
          <td>log table by timestamp</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td><code>WHERE</code> 條件下才建 index</td>
          <td><code>WHERE status = 'pending'</code></td>
      </tr>
      <tr>
          <td>Covering index</td>
          <td>包含所有查詢欄位、避免 heap lookup</td>
          <td><code>INDEX (a) INCLUDE (b, c)</code></td>
      </tr>
      <tr>
          <td>Compound index</td>
          <td>多欄位、順序敏感</td>
          <td><code>INDEX (a, b)</code> 對 <code>WHERE a=? AND b=?</code></td>
      </tr>
  </tbody>
</table>
<p><strong>常見設計原則</strong>：</p>
<ol>
<li>先保護交易關鍵查詢、再處理報表與後台查詢</li>
<li>複合索引依查詢過濾與排序順序排列、避免僅憑欄位熱門度排列</li>
<li>大表變更前先評估索引建立成本與回退方案、避免在高峰時段同步放大風險</li>
<li>定期 review 未用 index（PostgreSQL <code>pg_stat_user_indexes</code>、MySQL <code>sys.schema_unused_indexes</code>）— 寫入吞吐被舊 index 拖垮</li>
<li>partial index 對 <code>boolean</code> / <code>status</code> column 特別有用 — 只 index 「pending」「failed」等小集合</li>
</ol>
<p><strong>Index 反模式</strong>：</p>
<ul>
<li>每個欄位都建 index：寫入吞吐被拖垮</li>
<li>不看 EXPLAIN 就建 index：可能跟 query planner 不對齊</li>
<li>用 OR 條件依賴單一 index：query planner 不一定能用</li>
<li>大表 ALTER INDEX 不分批：lock 整個表</li>
</ul>
<h2 id="denormalization-模式">Denormalization 模式</h2>
<p>normalize 是 SQL 的預設、但 denormalize 有時是更好的工程選擇。</p>
<p><strong>Precomputed aggregate</strong>：</p>
<ul>
<li>把 COUNT / SUM 結果存在 parent row 而非每次 query 算</li>
<li>例：<code>posts.comment_count</code> 存實際值、不每次 SELECT COUNT</li>
<li>風險：consistency（comment 寫入後 count 沒更新）</li>
<li>對策：用 trigger 或應用層 transaction 確保同步、或定期 reconcile</li>
</ul>
<p><strong>Embedded one-to-many</strong>：</p>
<ul>
<li>小量 1-many 關係可以 embed 成 JSONB / nested column</li>
<li>例：<code>order.line_items</code> JSON column、不另建 line_items table</li>
<li>風險：個別 line item 查詢不便</li>
<li>適合：line items 通常一起讀寫（同 transaction boundary）</li>
</ul>
<p><strong>Materialized view</strong>：</p>
<ul>
<li>預計算 query 結果、定期 refresh</li>
<li>適合：複雜 JOIN / aggregation 重複跑</li>
<li>風險：refresh window 內看到舊資料</li>
</ul>
<p><strong>Read model</strong>（CQRS）：</p>
<ul>
<li>寫入路徑跟讀取路徑用不同 schema</li>
<li>寫入 normalize、讀取 denormalize 成不同 read model</li>
<li>詳見 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership</a></li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+ watch list</a> — denormalize 用戶 metadata、跨裝置查詢方便</li>
<li><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — DynamoDB single-table design 是極端 denormalization</li>
</ul>
<h2 id="partition-策略">Partition 策略</h2>
<p>單表 &gt; 1 TB 時、partition 是必要的維運手段。partition 不是「擴 storage」、是「讓 vacuum / index / DROP 可分批跑」。</p>
<p><strong>Partition 類型</strong>：</p>
<ul>
<li><strong>Range partition</strong>：按 timestamp / id 範圍切。<code>orders_2024_q1</code>, <code>orders_2024_q2</code>&hellip;</li>
<li><strong>List partition</strong>：按枚舉值切。<code>orders_us</code>, <code>orders_eu</code>&hellip;</li>
<li><strong>Hash partition</strong>：按 hash 均勻切。適合無自然切分維度的大表</li>
</ul>
<p><strong>Partition 設計要點</strong>：</p>
<ol>
<li>partition key 必須出現在 <em>多數 query 的 WHERE clause</em>（partition pruning 才能生效）</li>
<li>partition 數量 <em>適中</em>（10-100）— 太少 partition 太大、太多 partition metadata 開銷大</li>
<li>老 partition 可以 DROP 或 archive、儲存成本可控</li>
<li><code>cross-partition unique constraint</code> 限制 — 唯一鍵必須含 partition key</li>
</ol>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 200 個獨立 Aurora cluster 是極端 partition by business</li>
<li><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — DynamoDB 透明 partition、應用層不必管</li>
</ul>
<h2 id="schema-evolution-友好設計">Schema Evolution 友好設計</h2>
<p>schema 從 day 1 就要為演進設計、不能假設「以後不會改」。</p>
<p><strong>避免 breaking changes</strong>：</p>
<ul>
<li><strong>加欄位</strong>：safe（nullable 或 default）</li>
<li><strong>刪欄位</strong>：unsafe（先讓所有 code 不再讀 → 部署 → 再刪）</li>
<li><strong>改欄位類型</strong>：unsafe（先加新欄位、雙寫、backfill、移除舊欄位）</li>
<li><strong>改欄位名</strong>：unsafe（同上）</li>
<li><strong>加 NOT NULL constraint</strong>：unsafe（先 backfill default、再加 constraint）</li>
</ul>
<p><strong>Evolution-friendly schema 原則</strong>：</p>
<ol>
<li><strong>欄位 nullable by default</strong>：除非業務不允許 null、否則先 nullable、之後再 tighten</li>
<li><strong>避免大表 ALTER TABLE</strong>：用 <a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a> 模式</li>
<li><strong>predict breaking changes</strong>：訂版本、跟 application code 同步演進</li>
<li><strong>schema version column</strong>：每 row 帶 version、應用層按版本處理</li>
<li><strong>migration 工具版本控</strong>：Flyway / Liquibase / Atlas / golang-migrate 必須有</li>
</ol>
<p>詳見 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>。</p>
<h2 id="naming-與一致性">Naming 與一致性</h2>
<p>命名規則的責任是維持跨版本可讀性。table、column、index 的命名若沒有一致語意、migration 與故障排查會持續變慢。穩定做法是把命名和業務語意對齊、並保留可辨識版本與作用域。</p>
<p><strong>Naming 慣例</strong>：</p>
<ul>
<li><strong>Table</strong>：複數名詞、<code>snake_case</code>（<code>orders</code>, <code>payment_methods</code>）</li>
<li><strong>Column</strong>：<code>snake_case</code>、明確語意（<code>created_at</code> 不是 <code>ts</code>）</li>
<li><strong>Foreign key</strong>：<code>{referenced_table}_id</code>（<code>user_id</code> 指 <code>users.id</code>）</li>
<li><strong>Boolean</strong>：<code>is_*</code> / <code>has_*</code> / <code>can_*</code>（<code>is_active</code>, <code>has_subscription</code>）</li>
<li><strong>Timestamp</strong>：<code>*_at</code> for events（<code>created_at</code>, <code>paid_at</code>）、<code>*_on</code> for dates（<code>born_on</code>）</li>
<li><strong>Index</strong>：<code>idx_{table}_{cols}</code>（<code>idx_orders_user_id_created_at</code>）</li>
<li><strong>Unique constraint</strong>：<code>uq_{table}_{cols}</code></li>
<li><strong>Foreign key constraint</strong>：<code>fk_{table}_{ref}</code></li>
</ul>
<p><strong>避免的反模式</strong>：</p>
<ul>
<li>縮寫不一致（<code>u_id</code> vs <code>user_id</code>）</li>
<li>隱性意義（<code>status</code> 是 enum、值在哪裡？）</li>
<li>跨表同義不同名（<code>user.name</code> vs <code>customer.full_name</code>）</li>
<li>反向命名（<code>name_first</code> vs 業界 <code>first_name</code>）</li>
</ul>
<p>schema 演進時、命名與結構要一起考慮。欄位重命名、拆欄位、合併欄位都應配合 <a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a> 與 <a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a> 策略、讓新舊版本在過渡期可共存。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一查詢在資料量成長後延遲快速上升</td>
          <td>索引與查詢模型不對齊</td>
          <td>補複合索引、重寫查詢條件</td>
      </tr>
      <tr>
          <td>migration 後查詢計畫顯著變化</td>
          <td>統計資訊或索引選擇偏移</td>
          <td>重建統計、校正索引與查詢</td>
      </tr>
      <tr>
          <td>交易流程需跨多表同步更新</td>
          <td>table 邊界與業務聚合邊界不一致</td>
          <td>重切聚合邊界、減少跨聚合同步更新</td>
      </tr>
      <tr>
          <td>同義欄位在多表重複存在且語意漂移</td>
          <td>命名與責任邊界失控</td>
          <td>收斂欄位責任、補資料字典與遷移計畫</td>
      </tr>
      <tr>
          <td>修復事故時需要多次手動比對資料</td>
          <td>可追蹤欄位與關聯鍵不足</td>
          <td>補追蹤欄位、設計對帳查詢與修復流程</td>
      </tr>
      <tr>
          <td>單表 &gt; 1 TB 且 vacuum 變慢</td>
          <td>沒 partition、後續維運成本爆</td>
          <td>規劃 partition by range / hash</td>
      </tr>
      <tr>
          <td>大量 unused index</td>
          <td>寫入吞吐被舊 index 拖垮</td>
          <td>review pg_stat_user_indexes、定期 drop</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 schema 設計等同於「先能寫入就好」、會把結構債延後到流量成長與事故時一次爆發。資料模型的工程價值在於可演進性、不在於初版欄位數量最少。</p>
<p>把索引當成效能補丁、忽略查詢模型與資料責任、也會讓後續維護成本持續疊加。索引與查詢要一起設計、才能在演進中保持穩定。</p>
<p>把 normalize 當成 <em>絕對守則</em>、忽略 denormalize 的工程效益。1NF / 2NF / 3NF 是理論起點、不是 <em>production 必須</em>。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>Schema 設計重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>DynamoDB single-table design、極端 denormalize</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>Composite partition key、event_id × user_id_hash</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a></td>
          <td>200 個獨立 cluster、按業務切 partition</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a></td>
          <td>watch list embedded design、跨裝置同步</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a></td>
          <td>Cosmos DB synthetic partition key 強制分散</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>資料建模議題可以用 <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 做回寫練習。讀這個事件時、先看跨區拓樸切換如何影響資料一致性、再回到本章檢查三件事：聚合邊界是否清晰、交易查詢與對帳查詢是否分層、修復時是否有可追蹤欄位與對帳鍵。</p>
<p>這個案例主要支撐的是「查詢與資料模型邊界」判讀、不直接支撐 transaction retry 或 queue replay 調校；若問題是重試放大、應轉到 1.3 或 3.x 章節處理。</p>
<p>當事件呈現長時間人工比對或查詢語意漂移時、先修正本章的 query boundary 與 naming 一致性、再補 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> 的驗證與回退路徑。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>schema 設計會直接影響後續可靠性與事故處理。</p>
<ol>
<li>與 1.3 的交接：交易一致性邊界落在 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a>。</li>
<li>與 1.6 的交接：演進策略落在 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">資料庫轉換實作</a>。</li>
<li>與 1.7 的交接：欄位責任進入 production rollout 時、讀 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout 證據實作示範</a>。</li>
<li>與 1.8 的交接：state ownership 跟 query boundary 設計落在 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">State Ownership</a>。</li>
<li>與 1.10 的交接：KV / Document 的 partition key 設計落在 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">KV / Document 容量規劃</a>。</li>
<li>與 4.20 的交接：查詢與資料驗證證據進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.11 的交接：高風險 schema 變更進入 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a>。</li>
<li>與 8.19 的交接：資料修復與回退決策記錄進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行：<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a>、<a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership</a></li>
<li>下游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> / <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a> / <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a></li>
<li>Vendor：<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 index 設計</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL InnoDB clustered index</a>、<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 single-table design</a></li>
<li>DynamoDB schema 深入：<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</a> / <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> / <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 設計</a></li>
<li>MongoDB schema 深入：<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">schema design pattern</a> / <a href="/blog/backend/01-database/vendors/mongodb/shard-key-selection/" data-link-title="MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius" data-link-desc="MongoDB sharded cluster shard key 選型（hashed / ranged / compound）、單 cluster 分 shard vs 多 cluster 分 blast radius 對照、跟 DynamoDB / Cosmos DB partition key 可逆性的跨 vendor 紀律">shard key 選型</a></li>
<li>Cosmos DB schema 深入：<a href="/blog/backend/01-database/vendors/cosmosdb/partition-key-design/" data-link-title="Cosmos DB Partition Key Design：synthetic / composite / hierarchical &#43; 不可逆性硬約束" data-link-desc="Cosmos DB logical partition 10000 RU/s 上限、partition key 不可改、三種設計模式（synthetic / composite / hierarchical）、跟 DynamoDB / MongoDB 可逆性對比、latency budget 拆解 — 從 Minecraft Earth &#43; ASOS 切入">partition key 設計</a></li>
</ul>
]]></content:encoded></item><item><title>1.3 Transaction 與一致性邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/</guid><description>&lt;p>交易邊界（transaction boundary）的核心責任是定義哪些資料變更必須一起成立。資料庫交易的價值在於讓同一個業務動作可以被明確提交、明確回退、明確重試。&lt;/p>
&lt;p>本章從業務邊界切分開始、進入 isolation level 工程細節、再到 retry 策略、最後處理跨服務 / 跨 region 的 distributed transaction。讀完後讀者能回答：transaction 範圍該多大、isolation 該訂多嚴、deadlock 怎麼處理、跨服務一致性怎麼設計、什麼時候該換 Saga 模式。&lt;/p>
&lt;h2 id="邊界先於語法">邊界先於語法&lt;/h2>
&lt;p>交易邊界先從業務動作切分、再回到 SQL。建立訂單、扣庫存、寫付款狀態是一個動作；更新推薦分數、寫審計摘要、送通知事件屬於不同節奏、適合拆成後續流程。&lt;/p>
&lt;p>當同一個動作內同時包含高延遲外部呼叫、交易範圍會直接放大鎖持有時間。穩定做法是把交易內責任收斂在「需要同時成功」的資料集合、讓外部呼叫或延伸副作用透過 queue / outbox 交給後續流程。&lt;/p>
&lt;h2 id="isolation-level-五級深度">Isolation Level 五級深度&lt;/h2>
&lt;p>SQL 標準定義四個 isolation level、實務上 PostgreSQL / MySQL / Spanner 等實作有微妙差異。理解各級的具體行為、才能在 &lt;em>正確性 vs 性能&lt;/em> 之間做取捨。&lt;/p>
&lt;p>&lt;strong>0. Read Uncommitted（dirty read 可能）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>可讀到別的 transaction 還沒 commit 的資料&lt;/li>
&lt;li>多數 DB 不真的支援這級（會 fallback 到 Read Committed）&lt;/li>
&lt;li>實務不要用&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>1. Read Committed（PostgreSQL / Oracle 預設）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>只讀到 commit 的資料&lt;/li>
&lt;li>同一個 transaction 內、多次 SELECT 同一筆資料可能讀到不同值（non-repeatable read）&lt;/li>
&lt;li>適合：read-heavy workload、不要求同 transaction 內 read consistency&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. Repeatable Read（MySQL InnoDB 預設）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>同 transaction 內 read 一致（snapshot at transaction start）&lt;/li>
&lt;li>不防 phantom read（標準定義）、但 InnoDB 的 RR 加 gap lock 實際上防住了&lt;/li>
&lt;li>適合：報表類 transaction、需要 snapshot 一致性&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. Serializable（最強）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>看起來像所有 transaction 序列執行&lt;/li>
&lt;li>兩種實作：strict 2PL（lock-based、MySQL）vs SSI（snapshot isolation + 衝突檢測、PostgreSQL）&lt;/li>
&lt;li>衝突時會 serialization failure、應用層必須 retry&lt;/li>
&lt;li>適合：金融交易、ticketing inventory、需要絕對正確&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>4. External Consistency / Linearizable（Spanner、Aurora DSQL）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>比 Serializable 更強：跨 transaction 的順序跟 wall clock 一致&lt;/li>
&lt;li>全球分散式系統的特殊取捨&lt;/li>
&lt;li>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP&lt;/a> 的 Spanner TrueTime 段&lt;/li>
&lt;li>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner case&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>選擇原則&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>90% 業務用 Read Committed 夠&lt;/li>
&lt;li>報表 / 對帳用 Repeatable Read&lt;/li>
&lt;li>金融交易 / inventory 用 Serializable&lt;/li>
&lt;li>全球強一致用 Spanner / Aurora DSQL 等 linearizable 系統&lt;/li>
&lt;/ul>
&lt;h2 id="isolation-跟-retry-的關係">Isolation 跟 Retry 的關係&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a> 的責任是定義交易彼此可見性。&lt;code>Read Committed&lt;/code> 在高併發寫入下可維持一般業務一致性；&lt;code>Repeatable Read&lt;/code> 與 &lt;code>Serializable&lt;/code> 提供更強約束、同時提高鎖競爭與重試頻率。&lt;/p></description><content:encoded><![CDATA[<p>交易邊界（transaction boundary）的核心責任是定義哪些資料變更必須一起成立。資料庫交易的價值在於讓同一個業務動作可以被明確提交、明確回退、明確重試。</p>
<p>本章從業務邊界切分開始、進入 isolation level 工程細節、再到 retry 策略、最後處理跨服務 / 跨 region 的 distributed transaction。讀完後讀者能回答：transaction 範圍該多大、isolation 該訂多嚴、deadlock 怎麼處理、跨服務一致性怎麼設計、什麼時候該換 Saga 模式。</p>
<h2 id="邊界先於語法">邊界先於語法</h2>
<p>交易邊界先從業務動作切分、再回到 SQL。建立訂單、扣庫存、寫付款狀態是一個動作；更新推薦分數、寫審計摘要、送通知事件屬於不同節奏、適合拆成後續流程。</p>
<p>當同一個動作內同時包含高延遲外部呼叫、交易範圍會直接放大鎖持有時間。穩定做法是把交易內責任收斂在「需要同時成功」的資料集合、讓外部呼叫或延伸副作用透過 queue / outbox 交給後續流程。</p>
<h2 id="isolation-level-五級深度">Isolation Level 五級深度</h2>
<p>SQL 標準定義四個 isolation level、實務上 PostgreSQL / MySQL / Spanner 等實作有微妙差異。理解各級的具體行為、才能在 <em>正確性 vs 性能</em> 之間做取捨。</p>
<p><strong>0. Read Uncommitted（dirty read 可能）</strong>：</p>
<ul>
<li>可讀到別的 transaction 還沒 commit 的資料</li>
<li>多數 DB 不真的支援這級（會 fallback 到 Read Committed）</li>
<li>實務不要用</li>
</ul>
<p><strong>1. Read Committed（PostgreSQL / Oracle 預設）</strong>：</p>
<ul>
<li>只讀到 commit 的資料</li>
<li>同一個 transaction 內、多次 SELECT 同一筆資料可能讀到不同值（non-repeatable read）</li>
<li>適合：read-heavy workload、不要求同 transaction 內 read consistency</li>
</ul>
<p><strong>2. Repeatable Read（MySQL InnoDB 預設）</strong>：</p>
<ul>
<li>同 transaction 內 read 一致（snapshot at transaction start）</li>
<li>不防 phantom read（標準定義）、但 InnoDB 的 RR 加 gap lock 實際上防住了</li>
<li>適合：報表類 transaction、需要 snapshot 一致性</li>
</ul>
<p><strong>3. Serializable（最強）</strong>：</p>
<ul>
<li>看起來像所有 transaction 序列執行</li>
<li>兩種實作：strict 2PL（lock-based、MySQL）vs SSI（snapshot isolation + 衝突檢測、PostgreSQL）</li>
<li>衝突時會 serialization failure、應用層必須 retry</li>
<li>適合：金融交易、ticketing inventory、需要絕對正確</li>
</ul>
<p><strong>4. External Consistency / Linearizable（Spanner、Aurora DSQL）</strong>：</p>
<ul>
<li>比 Serializable 更強：跨 transaction 的順序跟 wall clock 一致</li>
<li>全球分散式系統的特殊取捨</li>
<li>詳見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的 Spanner TrueTime 段</li>
<li>詳見 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner case</a></li>
</ul>
<p><strong>選擇原則</strong>：</p>
<ul>
<li>90% 業務用 Read Committed 夠</li>
<li>報表 / 對帳用 Repeatable Read</li>
<li>金融交易 / inventory 用 Serializable</li>
<li>全球強一致用 Spanner / Aurora DSQL 等 linearizable 系統</li>
</ul>
<h2 id="isolation-跟-retry-的關係">Isolation 跟 Retry 的關係</h2>
<p><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a> 的責任是定義交易彼此可見性。<code>Read Committed</code> 在高併發寫入下可維持一般業務一致性；<code>Repeatable Read</code> 與 <code>Serializable</code> 提供更強約束、同時提高鎖競爭與重試頻率。</p>
<p>併發交易的常見結果是 deadlock 或 serialization failure。這些結果代表資料庫在保護一致性、應用層需要把它視為可重試路徑：</p>
<ul>
<li><strong>重試次數有上限</strong>（通常 3-5 次）— 避免 retry storm</li>
<li><strong>重試間隔有抖動</strong>（exponential backoff + jitter）— 避免同步衝突</li>
<li><strong>重試前提是動作可重入</strong>（idempotent）— 不會放大副作用</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">Exponential Backoff</a> 跟 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a> 卡片。</p>
<h2 id="optimistic-vs-pessimistic-locking">Optimistic vs Pessimistic Locking</h2>
<p>當多個 transaction 同時操作同一筆資料、有兩種防衝突策略：</p>
<p><strong>Pessimistic locking（悲觀鎖）</strong>：</p>
<ul>
<li><code>SELECT ... FOR UPDATE</code>、提前 lock 行</li>
<li>適合：衝突機率高、retry 成本高</li>
<li>缺點：lock 期間其他 transaction 等待、容易 deadlock</li>
</ul>
<p><strong>Optimistic locking（樂觀鎖）</strong>：</p>
<ul>
<li>不 lock、用 version column 或 <code>WHERE old_value = ?</code></li>
<li>commit 時若 version 不對、整個 transaction 失敗、應用層 retry</li>
<li>適合：衝突機率低、性能優先</li>
<li>缺點：高衝突場景 retry 多、整體吞吐反而低</li>
</ul>
<p><strong>選擇邏輯</strong>：</p>
<ul>
<li>衝突 &lt; 5% → optimistic（更高吞吐）</li>
<li>衝突 &gt; 30% → pessimistic（避免 retry waste）</li>
<li>中間區 → 量測再決定</li>
</ul>
<p>對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">hot row contention 處理</a>（<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a>）— 高衝突 hot row 通常該換 KV / cache、不該硬擴 SQL。</p>
<h2 id="服務情境checkout-多層邊界">服務情境：Checkout 多層邊界</h2>
<p>電商 checkout 是典型的 transaction boundary 設計題、可拆成兩層邊界。</p>
<p><strong>第一層：交易層（即時一致）</strong>：</p>
<ul>
<li>建立訂單主表</li>
<li>寫入訂單項目</li>
<li>扣減可售庫存</li>
<li>寫入付款待確認狀態</li>
</ul>
<p><strong>第二層：延伸層（最終可達）</strong>：</p>
<ul>
<li>寄訂單確認 email</li>
<li>同步 CRM 系統</li>
<li>觸發 analytics event</li>
<li>更新推薦模型</li>
</ul>
<p>這種切法讓交易控制面跟非同步控制面各自穩定：</p>
<ul>
<li>交易層關注 <em>鎖、隔離與回退</em></li>
<li>非同步層關注 <em>投遞、重試與補償</em></li>
</ul>
<p>對應案例：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a> — 體育博彩 ledger、200 個獨立 cluster 處理 transaction、後續 settlement 跑非同步</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 跨市場銀行 transaction、各市場獨立、跨市場結算非同步</li>
</ul>
<h2 id="distributed-transaction2pc-vs-saga">Distributed Transaction：2PC vs Saga</h2>
<p>當業務動作跨越 <em>多個服務 / 資料庫</em>、傳統 ACID transaction 不夠用、需要 distributed transaction 模式。</p>
<p><strong>Two-Phase Commit (2PC)</strong>：</p>
<ul>
<li>階段 1：coordinator 詢問所有 participant「你能 commit 嗎？」</li>
<li>階段 2：所有都說 yes → coordinator 廣播 commit；任一說 no → 廣播 abort</li>
<li><strong>優點</strong>：強一致、ACID 保證</li>
<li><strong>缺點</strong>：coordinator failure 會 block 所有 participant、性能差、跨服務複雜</li>
<li>適合：少數高一致性需求的場景（金融交易、跨多 DB 一致性）</li>
</ul>
<p><strong>Saga Pattern</strong>：</p>
<ul>
<li>把長 transaction 拆成多個 local transaction + compensating transaction</li>
<li>每個 step 成功 → 進下個；任一失敗 → 倒回去跑 compensation</li>
<li>例：訂單 step1 扣庫存、step2 收款、step3 送貨。step2 失敗 → 跑 step1 的 compensation（補庫存）</li>
<li><strong>優點</strong>：高可用、性能好、容易擴展</li>
<li><strong>缺點</strong>：不是強一致、中間狀態可見、compensation 必須設計</li>
<li>適合：multi-service 業務流程、可接受 <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a></li>
</ul>
<p><strong>Choreography vs Orchestration</strong>：</p>
<ul>
<li>Choreography：每個 service 自己決定下一步（event-driven）</li>
<li>Orchestration：中央 orchestrator 控制流程（state machine）</li>
<li>大規模傾向 orchestration（容易追蹤、debug）、小規模 choreography 足夠</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票 + 付款分開：DynamoDB 接搶單（local transaction）、legacy server 跑付款（compensation 處理庫存回退）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> — 投注 → 結算的 saga 流程</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern 卡片</a> 跟 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 Outbox Pattern</a>。</p>
<h2 id="跨-region-transactioncap-取捨">跨 Region Transaction：<a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP</a> 取捨</h2>
<p>當 transaction 必須跨 region 同時成立、CAP 定理開始作用。</p>
<p><strong>Single-region transaction</strong>（PostgreSQL / MySQL / Aurora）：</p>
<ul>
<li>ACID within region</li>
<li>跨 region 用 async replication、不是 transaction</li>
</ul>
<p><strong>Multi-region eventual consistency</strong>（DynamoDB Global Tables、Cosmos DB session/eventual）：</p>
<ul>
<li>各 region 都能寫</li>
<li>LWW 或 application-level conflict resolution</li>
<li>不是 ACID、是 BASE</li>
</ul>
<p><strong>Multi-region strong consistency</strong>（Spanner、Aurora DSQL、CockroachDB）：</p>
<ul>
<li>跨 region linearizable transaction</li>
<li>代價是 latency（跨洲 100-200ms <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>）</li>
<li>對應 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
</ul>
<p><strong>決策邏輯</strong>：</p>
<ul>
<li>業務不需要跨 region 強一致 → single-region OLTP + eventual replication</li>
<li>需要跨 region 強一致 + 接受 latency → Spanner / Aurora DSQL</li>
<li>需要跨 region 寫但接受最終一致 → Cosmos DB session / DynamoDB Global Tables</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>deadlock rate 升高</td>
          <td>交易範圍過大或鎖順序不一致</td>
          <td>統一更新順序、縮小 transaction 範圍</td>
      </tr>
      <tr>
          <td>transaction duration 在尖峰時段上升</td>
          <td>交易內含慢查詢或外部依賴</td>
          <td>將外部呼叫移出交易、補索引與查詢計畫</td>
      </tr>
      <tr>
          <td>retry 成功率下降</td>
          <td>重試條件與業務冪等假設不一致</td>
          <td>補 idempotency key、調整 retry 邏輯</td>
      </tr>
      <tr>
          <td>rollback 後仍出現業務狀態殘留</td>
          <td>邊界切分和副作用落點未對齊</td>
          <td>將副作用統一移到 outbox / consumer 路徑</td>
      </tr>
      <tr>
          <td>交易內讀寫跨多資料域導致 contention 爆發</td>
          <td>業務聚合邊界與資料模型邊界衝突</td>
          <td>重新切 aggregate 與拆分熱點資料結構</td>
      </tr>
      <tr>
          <td>Serializable retry 率 &gt; 10%</td>
          <td>isolation 太嚴或業務衝突高</td>
          <td>降到 Repeatable Read 或拆 hot row</td>
      </tr>
      <tr>
          <td>跨服務 transaction 用 2PC 卡住</td>
          <td>coordinator failure 阻塞</td>
          <td>改 Saga + compensation</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>交易保護的是一致性、不是吞吐量最大化。把過多步驟包進單一交易、會同時放大鎖競爭與回退成本。把交易切成可驗證的業務單位、能讓高併發下的可預期性更高。</p>
<p>重試保護的是暫時性失敗、不是所有失敗。沒有冪等保護的重試會放大副作用、特別是金流、庫存、配額這類正式狀態。</p>
<p>isolation level 不是「越強越好」。Serializable 比 Read Committed 慢數倍、且 retry rate 上升。只在 <em>必要</em> 場景用最強 isolation、其他場景用最低可接受 isolation。</p>
<p>distributed transaction 不是「跨服務就要 2PC」。多數 multi-service 業務用 Saga 更可靠、2PC 是少數場景的特殊工具。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>Transaction 相關重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></td>
          <td>Aurora MySQL ACID transaction、200 個獨立 cluster 隔離 transaction scope</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></td>
          <td>External consistency（linearizable）跨 region transaction、TrueTime</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>跨市場 transaction 各市場獨立 cluster、合規限制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>搶票 + 付款 saga 模式、DynamoDB queue + legacy SQL</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>交易邊界可用 <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 做回寫。先看事件中的主從切換與恢復順序、再回到本章判讀三件事：哪些變更必須同交易成功、哪些副作用應拆到 outbox、哪些錯誤屬於可重試而非立即回退。</p>
<p>這個案例主要支撐的是「提交與副作用切分」判讀、不直接支撐 schema naming 或 cache freshness；若問題落在資料命名或快取新鮮度、應回到 1.2 或 2.x。</p>
<p>若事件出現資料已寫入但外部流程落後、或重試後副作用重複、先收斂本章的邊界切分與重試前提、再同步更新 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a> 與 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>交易邊界設計會直接影響後續模組的可操作性。</p>
<ol>
<li>與 03 的交接：交易外副作用透過 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> 與 consumer 落地。</li>
<li>與 1.7 的交接：付款狀態拆欄位、雙寫與回呼更新要進入 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout 證據</a> 的驗證流程。</li>
<li>與 1.10 / 1.11 的交接：KV 跟全球分散式 OLTP 的 transaction model 不同、選型時要回到本章邊界判讀。</li>
<li>與 04 的交接：交易失敗需要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 的查詢與證據欄位。</li>
<li>與 06 的交接：高風險交易變更納入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a> 與 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a>。</li>
<li>與 08 的交接：交易層回退或 fail-forward 判斷記錄到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a>（connection pool / hot row）</li>
<li>下游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> / <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a> / <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li>跨模組：<a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a> / <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</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 瓶頸定位流程</a></li>
<li>卡片：<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">Isolation Level</a> / <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">Transaction Boundary</a> / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a> / <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern</a> / <a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">Exponential Backoff</a></li>
<li>Spanner 一致性深入：<a href="/blog/backend/01-database/vendors/spanner/truetime-api-depth/" data-link-title="Spanner TrueTime API 深度：GPS &#43; 原子鐘、commit wait、為什麼 line-rate scaling 才是設計目的" data-link-desc="TrueTime 是手段、line-rate scaling 才是 Spanner 的設計目的。本文先扣商業邏輯：傳統 OLTP coordinator 為什麼是 bottleneck、Spanner 怎麼用 TrueTime &#43; Paxos 換成拓樸感知多 leader；再展開 TrueTime ε / commit wait 數學、ε 暴衝失敗模式、cross-region voting 對 latency 的影響、跟 9.C10 Google internal dogfood 揭露的線性擴展模式對照">TrueTime API 深入</a> / <a href="/blog/backend/01-database/vendors/spanner/consistency-models-comparison/" data-link-title="Spanner Consistency Models 對照：external consistency vs serializability vs linearizability" data-link-desc="external consistency、serializability、linearizability 是三個常被混用的概念。本文先精確定義三者差異、再用 line-rate scaling 對照表（PG SSI / CockroachDB / Spanner / Aurora DSQL）回答為什麼 Spanner 不只是『更強的 serializable』、最後用 9.C10 揭露的 cross-region quorum 100-200ms 物理硬限解釋『強一致 &#43; 全球部署』的真實 cost">Spanner 一致性模型對照</a></li>
<li>CockroachDB retry / 隔離深入：<a href="/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/" data-link-title="CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑" data-link-desc="CockroachDB default SERIALIZABLE、application 必須包 retry loop 處理 40001 serialization_failure。本文走 PG → CockroachDB application contract 重塑視角、SAVEPOINT cockroach_restart 語法、5 種失敗模式（retry storm / 非冪等 / cross-statement state / hot row / long-running transaction）。**整篇是跨 case 合成 frame**：DoorDash case 沒揭露 retry pattern、只揭露 PG wire protocol 相容 &#43; SQL 行為仍要 audit、本章 retry contract 重塑屬通用工程議題從 Cockroach Labs 官方 docs 合成">CockroachDB transaction retry pattern</a> / <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a></li>
<li>Aurora 寫入語意深入：<a href="/blog/backend/01-database/vendors/aurora/storage-architecture/" data-link-title="Aurora Storage Architecture：quorum-based 分散式 log 與韌性即性能設計" data-link-desc="Aurora storage / compute 分離、6-way 跨 AZ replication、4-of-6 write / 3-of-6 read quorum、韌性投資自動 amortize 成 read 性能、DraftKings 6ms 寫 / &lt;1ms 讀 production reference">Aurora 儲存層架構</a>（6 寫 / 4 讀 quorum 對 transaction 的影響）</li>
</ul>
]]></content:encoded></item><item><title>1.4 Repository Adapter 實作</title><link>https://tarrragon.github.io/blog/backend/01-database/repository-adapter/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/repository-adapter/</guid><description>&lt;p>資料庫倉儲轉接層（repository adapter）的核心責任是把應用層語意轉成資料庫可執行操作、並把資料庫錯誤回譯成業務可判讀結果。它是 &lt;code>domain model&lt;/code> 和 &lt;code>SQL model&lt;/code> 之間的邊界層、不承擔業務流程編排。&lt;/p>
&lt;p>本章從 hexagonal architecture 的 port / adapter 模式出發、處理 mapping、error translation、testing 跟跨服務 transaction 等實作議題。讀完後讀者能設計一個可演進、可測試、可換 DB 的 repository 層。&lt;/p>
&lt;h2 id="port--adapter-邊界">Port / Adapter 邊界&lt;/h2>
&lt;p>Repository 在 hexagonal architecture（也叫 ports &amp;amp; adapters）中是 &lt;em>outbound port&lt;/em> 的實作。&lt;/p>
&lt;p>&lt;strong>Port（domain layer 定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>抽象 interface / protocol、描述 &lt;em>領域語意&lt;/em>&lt;/li>
&lt;li>不暴露 SQL、不暴露 DB 細節&lt;/li>
&lt;li>例：&lt;code>type OrderRepository interface { Find(id) Order; Save(order); ... }&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Adapter（infrastructure layer 實作）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>實作 port、負責跟具體 DB 對話&lt;/li>
&lt;li>翻譯 domain entity ↔ DB row&lt;/li>
&lt;li>翻譯 DB error → domain error&lt;/li>
&lt;li>例：&lt;code>type SQLOrderRepository struct { db *sql.DB }&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>為什麼這層抽象有價值&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>可替換性&lt;/strong>：DB 換 vendor 時、domain layer 不必改&lt;/li>
&lt;li>&lt;strong>可測試性&lt;/strong>：在 domain layer test 時可注入 memory fake、不必起 DB&lt;/li>
&lt;li>&lt;strong>語意清楚&lt;/strong>：domain 不被 SQL 細節污染、business rule 集中&lt;/li>
&lt;li>&lt;strong>演進可控&lt;/strong>：schema 改動時、只在 adapter 改 mapping、不擴散到全程式&lt;/li>
&lt;/ol>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter 卡片&lt;/a>。&lt;/p>
&lt;h2 id="adapter-三個核心責任">Adapter 三個核心責任&lt;/h2>
&lt;p>adapter 接收應用層輸入、負責三件事：查詢與命令組裝、row mapping、錯誤翻譯。業務規則判斷留在 service / usecase 層、adapter 聚焦在資料持久化語意與資料庫行為。&lt;/p>
&lt;p>邊界清楚的好處是演進可控。schema 調整時、只需要在 adapter 收斂欄位映射與查詢變更、不用把 SQL 細節滲透回 domain 層。&lt;/p>
&lt;h3 id="1-查詢與命令組裝">1. 查詢與命令組裝&lt;/h3>
&lt;p>把 domain 操作翻成具體 SQL / NoSQL query。實作層級有取捨：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Raw SQL&lt;/strong>：完全控制、易追 query plan、但容易拼錯字、易 SQL injection&lt;/li>
&lt;li>&lt;strong>Query builder&lt;/strong>（GORM Build、Knex、SQLAlchemy Core）：型別安全、不寫字串、但學 DSL&lt;/li>
&lt;li>&lt;strong>ORM&lt;/strong>（GORM、SQLAlchemy ORM、Active Record）：高抽象、自動 mapping、但隱藏細節、容易產生 N+1&lt;/li>
&lt;/ul>
&lt;p>詳見下方「ORM vs Query Builder vs Raw SQL」段。&lt;/p>
&lt;h3 id="2-row-mapping-與-nullable-handling">2. Row Mapping 與 Nullable Handling&lt;/h3>
&lt;p>row mapping 的責任是把資料庫欄位轉成穩定模型。欄位型別、時間格式、枚舉值、可空欄位都要有明確轉換規則。可空欄位需要顯式處理、避免把「缺值」誤當有效預設值。&lt;/p>
&lt;p>&lt;strong>Nullable handling 模式&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Optional type&lt;/strong>：Go &lt;code>sql.NullString&lt;/code>、Java &lt;code>Optional&amp;lt;T&amp;gt;&lt;/code>、Rust &lt;code>Option&amp;lt;T&amp;gt;&lt;/code>、Python &lt;code>Optional[T]&lt;/code>&lt;/li>
&lt;li>&lt;strong>Sentinel value&lt;/strong>：用特殊值代表 null（不推薦、易混淆）&lt;/li>
&lt;li>&lt;strong>Default fallback&lt;/strong>：null → 預設值（要明確、不要悄悄轉換）&lt;/li>
&lt;/ul>
&lt;p>資料模型演進時、新舊欄位可能共存。adapter 要支援過渡期讀寫相容、讓版本切換能分批進行。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>資料庫倉儲轉接層（repository adapter）的核心責任是把應用層語意轉成資料庫可執行操作、並把資料庫錯誤回譯成業務可判讀結果。它是 <code>domain model</code> 和 <code>SQL model</code> 之間的邊界層、不承擔業務流程編排。</p>
<p>本章從 hexagonal architecture 的 port / adapter 模式出發、處理 mapping、error translation、testing 跟跨服務 transaction 等實作議題。讀完後讀者能設計一個可演進、可測試、可換 DB 的 repository 層。</p>
<h2 id="port--adapter-邊界">Port / Adapter 邊界</h2>
<p>Repository 在 hexagonal architecture（也叫 ports &amp; adapters）中是 <em>outbound port</em> 的實作。</p>
<p><strong>Port（domain layer 定義）</strong>：</p>
<ul>
<li>抽象 interface / protocol、描述 <em>領域語意</em></li>
<li>不暴露 SQL、不暴露 DB 細節</li>
<li>例：<code>type OrderRepository interface { Find(id) Order; Save(order); ... }</code></li>
</ul>
<p><strong>Adapter（infrastructure layer 實作）</strong>：</p>
<ul>
<li>實作 port、負責跟具體 DB 對話</li>
<li>翻譯 domain entity ↔ DB row</li>
<li>翻譯 DB error → domain error</li>
<li>例：<code>type SQLOrderRepository struct { db *sql.DB }</code></li>
</ul>
<p><strong>為什麼這層抽象有價值</strong>：</p>
<ol>
<li><strong>可替換性</strong>：DB 換 vendor 時、domain layer 不必改</li>
<li><strong>可測試性</strong>：在 domain layer test 時可注入 memory fake、不必起 DB</li>
<li><strong>語意清楚</strong>：domain 不被 SQL 細節污染、business rule 集中</li>
<li><strong>演進可控</strong>：schema 改動時、只在 adapter 改 mapping、不擴散到全程式</li>
</ol>
<p>詳見 <a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter 卡片</a>。</p>
<h2 id="adapter-三個核心責任">Adapter 三個核心責任</h2>
<p>adapter 接收應用層輸入、負責三件事：查詢與命令組裝、row mapping、錯誤翻譯。業務規則判斷留在 service / usecase 層、adapter 聚焦在資料持久化語意與資料庫行為。</p>
<p>邊界清楚的好處是演進可控。schema 調整時、只需要在 adapter 收斂欄位映射與查詢變更、不用把 SQL 細節滲透回 domain 層。</p>
<h3 id="1-查詢與命令組裝">1. 查詢與命令組裝</h3>
<p>把 domain 操作翻成具體 SQL / NoSQL query。實作層級有取捨：</p>
<ul>
<li><strong>Raw SQL</strong>：完全控制、易追 query plan、但容易拼錯字、易 SQL injection</li>
<li><strong>Query builder</strong>（GORM Build、Knex、SQLAlchemy Core）：型別安全、不寫字串、但學 DSL</li>
<li><strong>ORM</strong>（GORM、SQLAlchemy ORM、Active Record）：高抽象、自動 mapping、但隱藏細節、容易產生 N+1</li>
</ul>
<p>詳見下方「ORM vs Query Builder vs Raw SQL」段。</p>
<h3 id="2-row-mapping-與-nullable-handling">2. Row Mapping 與 Nullable Handling</h3>
<p>row mapping 的責任是把資料庫欄位轉成穩定模型。欄位型別、時間格式、枚舉值、可空欄位都要有明確轉換規則。可空欄位需要顯式處理、避免把「缺值」誤當有效預設值。</p>
<p><strong>Nullable handling 模式</strong>：</p>
<ul>
<li><strong>Optional type</strong>：Go <code>sql.NullString</code>、Java <code>Optional&lt;T&gt;</code>、Rust <code>Option&lt;T&gt;</code>、Python <code>Optional[T]</code></li>
<li><strong>Sentinel value</strong>：用特殊值代表 null（不推薦、易混淆）</li>
<li><strong>Default fallback</strong>：null → 預設值（要明確、不要悄悄轉換）</li>
</ul>
<p>資料模型演進時、新舊欄位可能共存。adapter 要支援過渡期讀寫相容、讓版本切換能分批進行。詳見 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>。</p>
<h3 id="3-error-translation">3. Error Translation</h3>
<p>error translation 的責任是把底層錯誤分類成應用層可決策訊號。唯一鍵衝突、外鍵限制、交易衝突、連線逾時、都需要翻譯成可行動錯誤類型、而不是將原生錯誤字串直接外漏。</p>
<p><strong>常見錯誤分類</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Domain error</th>
          <th>SQL error 對應</th>
          <th>應用層動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ErrAlreadyExists</code></td>
          <td><code>unique_violation</code>（PostgreSQL 23505）</td>
          <td>409 Conflict / 業務 retry</td>
      </tr>
      <tr>
          <td><code>ErrNotFound</code></td>
          <td>empty result set</td>
          <td>404</td>
      </tr>
      <tr>
          <td><code>ErrConstraintFailed</code></td>
          <td><code>foreign_key_violation</code>（23503）</td>
          <td>400 Bad Request</td>
      </tr>
      <tr>
          <td><code>ErrConflict</code></td>
          <td><code>serialization_failure</code>（40001）</td>
          <td>retry with backoff</td>
      </tr>
      <tr>
          <td><code>ErrTimeout</code></td>
          <td><code>query_canceled</code>（57014）/ context deadline</td>
          <td>retry / circuit break</td>
      </tr>
      <tr>
          <td><code>ErrUnavailable</code></td>
          <td>connection refused / pool exhausted</td>
          <td>circuit break / fallback</td>
      </tr>
  </tbody>
</table>
<p>這層翻譯會直接影響重試、回退與事故判讀。分類越穩定、越能在 06/08 模組形成一致決策語言。</p>
<h2 id="orm-vs-query-builder-vs-raw-sql">ORM vs Query Builder vs Raw SQL</h2>
<p>選 mapping 工具是 repository adapter 的核心取捨。</p>
<h3 id="raw-sql">Raw SQL</h3>
<ul>
<li>優勢：完全控制 query plan、易 tune</li>
<li>優勢：大規模 query 性能最好</li>
<li>限制：易拼錯字、IDE 支援差</li>
<li>風險：一不小心就 SQL injection（用 prepared statement / parameterized query）</li>
<li>適合：性能極限關鍵 / 複雜 query / 已有 SQL 專家團隊</li>
</ul>
<h3 id="query-builder">Query Builder</h3>
<p>主流工具：Knex（Node）、SQLAlchemy Core（Python）、jOOQ（Java）、sqlc（Go）、Diesel（Rust）。</p>
<ul>
<li>優勢：型別安全、IDE 自動完成</li>
<li>優勢：不需要 ORM 的複雜度</li>
<li>優勢：仍可看到生成的 SQL</li>
<li>限制：學 DSL 成本</li>
<li>適合：中等複雜度 + 想要安全性 + 想看 SQL</li>
</ul>
<h3 id="orm">ORM</h3>
<p>主流工具：GORM（Go）、SQLAlchemy ORM（Python）、Active Record（Rails）、JPA / Hibernate（Java）、Entity Framework（.NET）、Prisma（TypeScript）。</p>
<ul>
<li>優勢：CRUD 操作快速、boilerplate 少</li>
<li>優勢：自動 mapping、自動 transaction</li>
<li>優勢：migration 工具通常整合</li>
<li>限制：隱藏 SQL 細節、易產生 N+1 query</li>
<li>限制：複雜 query 反而比 raw SQL 難寫</li>
<li>風險：lazy loading 容易意外性能問題</li>
<li>適合：CRUD 為主的應用、團隊偏業務開發</li>
</ul>
<h3 id="選型決策">選型決策</h3>
<ol>
<li><strong>小團隊 + CRUD-heavy</strong>：ORM（快速 prototype、boilerplate 少）</li>
<li><strong>中型 + 混合需求</strong>：Query Builder（安全 + 仍能寫複雜 query）</li>
<li><strong>大型 + 性能極限</strong>：Raw SQL + Query Builder（複雜 query 用 raw、簡單用 builder）</li>
<li><strong>microservice 私有 store</strong>：通常 Query Builder 為主（見 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> 模式）</li>
</ol>
<h3 id="orm-反模式">ORM 反模式</h3>
<ul>
<li><code>find()</code> 隨手呼叫導致 N+1 query</li>
<li>lazy loading 在 view 層觸發 query</li>
<li>用 ORM 寫複雜 aggregation（應該 raw SQL）</li>
<li>不 eager load 關聯資料</li>
</ul>
<h2 id="testing-策略">Testing 策略</h2>
<p>repository 是 <em>infrastructure</em> 層、test 策略不同於 domain layer。</p>
<h3 id="memory-fakeunit-test-友善">Memory Fake（unit test 友善）</h3>
<ul>
<li>用 in-memory implementation 滿足 port interface</li>
<li>不必起 DB、快、可隔離</li>
<li>適合：domain layer test、test repository 的 <em>呼叫者</em></li>
<li>反模式：用 memory fake test repository 本身（測不到實際 SQL 行為）</li>
</ul>
<h3 id="integration-test驗證真實-db-行為">Integration Test（驗證真實 DB 行為）</h3>
<ul>
<li>用 testcontainers / Docker 起真實 DB（PostgreSQL / MySQL）</li>
<li>跑真實 SQL、抓真實 error</li>
<li>用 transaction rollback 隔離各 test</li>
<li>適合：test repository adapter 本身</li>
</ul>
<h3 id="contract-test">Contract Test</h3>
<ul>
<li>驗證 adapter 對外語意穩定：同一輸入是否得到一致輸出、同一錯誤是否被穩定分類、同一查詢語意在 schema 演進後是否保持相容</li>
<li>測試重點是邊界語意覆蓋、資料庫產品特性覆蓋是另一件事</li>
<li>例：「unique 衝突必須回 <code>ErrAlreadyExists</code>」這條 contract、不管底層是 PostgreSQL / MySQL / SQLite 都成立</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">Contract 卡片</a> 跟 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing</a>。</p>
<h3 id="sqlite-作為-test-db">SQLite 作為 test DB</h3>
<ul>
<li>起 quick、無 external dependency</li>
<li>但 SQL dialect 跟 PostgreSQL / MySQL 有差異</li>
<li>適合：簡單 query 的 test、不適合 production-fidelity test</li>
<li>對應 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite vendor page</a></li>
</ul>
<h2 id="transaction-傳遞">Transaction 傳遞</h2>
<p>repository 操作通常要支援「我自己起 transaction」跟「在已有 transaction 內操作」兩種模式。</p>
<p><strong>Pattern 1：repository 自己起 transaction</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">OrderRepo</span><span class="p">)</span> <span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">order</span> <span class="nx">Order</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">tx</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">BeginTx</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">defer</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1">// ... 操作 ...</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>問題：跨多個 repository 時無法共用 transaction。</p>
<p><strong>Pattern 2：unit of work pattern</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Service</span><span class="p">)</span> <span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">order</span> <span class="nx">Order</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">s</span><span class="p">.</span><span class="nx">uow</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="nx">Transaction</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">orderRepo</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">inventoryRepo</span><span class="p">.</span><span class="nf">Decrease</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">.</span><span class="nx">Items</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">paymentRepo</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">.</span><span class="nx">Payment</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>把 transaction 從 repository 抽到 unit-of-work、跨 repository 共用。</p>
<p><strong>Pattern 3：context-based transaction</strong>：</p>
<ul>
<li>把 transaction 塞進 context</li>
<li>repository 從 context 拿 transaction（有 → 用、沒有 → 自己起）</li>
<li>Go 常用 pattern、但有「context 不該裝這種東西」的爭議</li>
</ul>
<p><strong>選擇邏輯</strong>：</p>
<ul>
<li>簡單應用：pattern 1 夠用</li>
<li>跨 repository transaction：pattern 2 或 3</li>
<li>大型 application：pattern 2（最清楚）</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a>。</p>
<h2 id="microservice-私有-store-對應">Microservice 私有 Store 對應</h2>
<p>現代 microservice 設計強調「每個 service 私有 DB」、不跟其他 service 共用。</p>
<p><strong>對 repository adapter 的影響</strong>：</p>
<ul>
<li>每個 service 自己的 schema、自己的 adapter</li>
<li>跨 service 不直接 DB query、要透過 API</li>
<li>transaction 不跨 service（用 Saga 或 outbox）</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a>、<a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 100+ microservice</a></li>
</ul>
<p><strong>反模式</strong>：</p>
<ul>
<li>共用 DB schema、不同 service 都 query 同一張表 → 強耦合、schema 改一個影響全部</li>
<li>跨 service 用 DB foreign key → 不能 enforce、會壞掉</li>
</ul>
<h2 id="repository-adapter-五個常見變體">Repository Adapter 五個常見變體</h2>
<p>實務上 repository 不止「CRUD」這個樣態：</p>
<ol>
<li><strong>Pure CRUD repository</strong>：Find / Save / Delete、最簡單</li>
<li><strong>Aggregate repository</strong>：操作 aggregate root、含 nested entities</li>
<li><strong>Read model repository</strong>（CQRS）：專門 read、不 write</li>
<li><strong>Event-sourced repository</strong>：存 events、不存 state</li>
<li><strong>Cached repository</strong>：包一層 cache（pass-through、refresh-ahead）</li>
</ol>
<p>實作時要明確選哪種、不要讓一個 repository 跨多種 pattern。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一業務錯誤在不同路徑返回不同型別</td>
          <td>error translation 分類漂移</td>
          <td>收斂錯誤分類介面與 mapping</td>
      </tr>
      <tr>
          <td>schema 變更後應用層出現大量 null 問題</td>
          <td>nullable handling 規則不足</td>
          <td>補顯式轉換與 fallback 規則</td>
      </tr>
      <tr>
          <td>SQL 細節在 service 層大量出現</td>
          <td>adapter 邊界被繞過</td>
          <td>收斂資料操作入口到 repository</td>
      </tr>
      <tr>
          <td>同一查詢在不同環境結果不一致</td>
          <td>contract test 覆蓋不足</td>
          <td>補跨環境合約測試與 fixture</td>
      </tr>
      <tr>
          <td>事故排查時難以判斷重試與回退條件</td>
          <td>錯誤分類無法對應決策</td>
          <td>建立錯誤分類到 gate/incident 的映射表</td>
      </tr>
      <tr>
          <td>N+1 query 在 ORM 環境下出現</td>
          <td>lazy loading 反模式</td>
          <td>改 eager loading 或換 query builder</td>
      </tr>
      <tr>
          <td>跨 repository 的 transaction 不一致</td>
          <td>transaction 沒共用機制</td>
          <td>引入 unit-of-work pattern</td>
      </tr>
      <tr>
          <td>Test 跑很慢、需要起 DB</td>
          <td>test 沒分層</td>
          <td>unit test 用 memory fake、integration 才用 DB</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 repository adapter 寫成「直接包 SQL 的工具函式」、容易讓業務規則與資料邏輯混雜。邊界失焦後、schema 演進與事故修復都會擴大影響面。</p>
<p>把資料庫錯誤原樣往上拋、也會讓上層決策不穩定。錯誤翻譯是可靠性控制面的必要前置。</p>
<p>把 ORM 當銀彈、忘了 SQL 還在背後。N+1 query、lazy loading 災難、複雜 aggregation 反而難寫 — 這些都是「過度信任 ORM 抽象」的後果。</p>
<p>把 memory fake 拿來 test repository 本身、不會抓到實際 DB bug。memory fake 是給 <em>呼叫者</em> test 用的、不是給 repository test 用的。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>repository / adapter 設計重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a></td>
          <td>microservice 私有 store、每個 service 自己 repository</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 100+ microservice</a></td>
          <td>微服務私有 DB、跨 service 不直接 DB query</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>TiDB → DynamoDB、repository adapter 是換 DB 的關鍵抽象</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>adapter 邊界可用 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a> 的資料一致性段落回寫。若事件中出現同一錯誤在不同路徑被不同方式處理、通常代表 adapter 的錯誤翻譯與契約分層不足。</p>
<p>這個案例主要支撐的是「錯誤分類與契約映射」判讀、不直接支撐 broker delivery 參數調整；若根因在 ack/retry 節奏、應回到 3.1/3.2。</p>
<p>回寫步驟是先盤點錯誤分類、再對齊重試與回退決策、最後把分類結果映射到 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing 與 Schema 演進</a> 的驗證欄位、讓發版前可先發現漂移。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位與索引語意回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design 與資料建模</a>。</li>
<li>與 1.3 的交接：交易錯誤與重試語意回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction 與一致性邊界</a>。</li>
<li>與 1.12 的交接：cross-DB migration 時、repository 是 <em>關鍵抽象</em> — 詳見 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">大規模 DB 遷移實戰</a>。</li>
<li>與 6.10 的交接：跨服務契約一致性回到 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">Contract Testing 與 Schema 演進</a>。</li>
<li>與 8.19 的交接：資料層錯誤判斷與回退決策回到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 Schema Design</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing 與 Schema 演進</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 瓶頸定位流程</a></li>
<li>跨 vendor adapter 深入：<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 正向用例">DynamoDB single-table design</a>（document KV adapter 邊界）、<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">MongoDB schema design pattern</a>（document adapter 的 ODM 取捨）、<a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a>（multi-API adapter 取捨）</li>
</ul>
]]></content:encoded></item><item><title>1.5 攻擊者視角（紅隊）：資料層弱點判讀</title><link>https://tarrragon.github.io/blog/backend/01-database/red-team-data-layer/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/red-team-data-layer/</guid><description>&lt;p>資料層紅隊判讀的核心目標是確認「誰能讀到什麼資料、資料會從哪裡流出、錯誤狀態如何回復」。這裡的紅隊指攻擊者視角的風險檢查：從可被濫用的路徑反向檢查資料邊界。database 一旦承擔 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、弱點就同時影響正確性、隱私與可恢復性。&lt;/p>
&lt;p>本章聚焦在 &lt;em>資料層&lt;/em>（DB 自身）的攻擊面、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 資安與資料保護模組&lt;/a> 的網路 / 身份 / 加密層形成互補。讀完後讀者能盤點：DB 上有哪些 &lt;em>攻擊路徑&lt;/em>、哪些 &lt;em>外洩管道&lt;/em>、哪些 &lt;em>偵測訊號&lt;/em>。&lt;/p>
&lt;h2 id="資料層弱點的主要軸線">資料層弱點的主要軸線&lt;/h2>
&lt;p>資料層弱點可分成三條軸線：存取邊界、狀態邊界、資料流邊界。&lt;/p>
&lt;p>&lt;strong>存取邊界&lt;/strong>：看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary&lt;/a>。哪些 user / role / tenant 可以 read / write 哪些資料。
&lt;strong>狀態邊界&lt;/strong>：看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a>。同時讀寫時的 race condition、TOCTOU。
&lt;strong>資料流邊界&lt;/strong>：看查詢結果、匯出、備份、觀測與支援工具的資料暴露路徑。&lt;/p>
&lt;p>三條軸線各有典型攻擊模式、要分別檢查。&lt;/p>
&lt;h2 id="db-攻擊面的外圍層次">DB 攻擊面的外圍層次&lt;/h2>
&lt;p>DB 攻擊面分三層、每層有典型攻擊向量跟防禦邊界、紅隊盤點要逐層檢查。傳統做法常把 90% 精力放在最內層 DB、外圍兩層的失守會讓內層防禦變成無效投資。&lt;/p>
&lt;p>&lt;strong>Layer 1：DB 本身&lt;/strong>（最直接、防禦最成熟）— SQL injection、authentication、authorization、RLS 都在這層。&lt;/p>
&lt;p>&lt;strong>Layer 2：DB 周邊產品&lt;/strong>（最常被忽略）— file transfer service（MFT）、API gateway、search proxy、admin console 都「接 DB」、且通常 perimeter 設定比 DB 鬆。對應 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023&lt;/a> — MOVEit Transfer 是 file transfer 產品、漏洞讓攻擊者直接存取後端資料、屬於 edge-exposure 類別的批量利用事件。判讀重點：任何「接 DB」的產品都屬於 DB 攻擊面、要盤 &lt;em>所有上游 caller 產品&lt;/em>。類似結構還有 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere MFT 2023&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023&lt;/a>。&lt;/p>
&lt;p>&lt;strong>Layer 3：認證信任根&lt;/strong>（最致命、最少人想到）— signing key、token issuer、IAM &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation&lt;/a> 都決定「誰能宣稱是哪個 user」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558&lt;/a> — 簽章金鑰外洩後、攻擊者偽造可被驗證的身分權杖、application 層的 BOLA / BOPLA / RLS 都會在底層 trust 失守時被繞過。判讀重點：DB authorization 接受上游認證結果、上游 trust 失守時、DB 層的精緻設計就被旁路掉。&lt;/p>
&lt;p>&lt;strong>設計含義&lt;/strong>：紅隊盤點順序是由外向內。先盤「誰能通過認證」（trust root）、再盤「通過認證後能打到哪些產品」（caller surface）、最後盤「打到 DB 後能做什麼」（DB authorization）。三層任一失守、後續層的防禦投資都會被旁路。&lt;/p>
&lt;h2 id="攻擊模式-1注入類">攻擊模式 1：注入類&lt;/h2>
&lt;p>&lt;strong>SQL Injection&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>經典攻擊、把 user input 拼進 SQL 字串&lt;/li>
&lt;li>防禦：parameterized query / prepared statement、絕不字串拼接&lt;/li>
&lt;li>二階注入：input 已存進 DB、後續 query 時才觸發 — 比一階更難偵測&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>NoSQL Injection&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>MongoDB / DynamoDB 也可能被注入（不同形式）&lt;/li>
&lt;li>MongoDB：&lt;code>{$where: ...}&lt;/code> operator injection、&lt;code>{$ne: null}&lt;/code> 跳過 auth&lt;/li>
&lt;li>DynamoDB：FilterExpression 注入（少見、需要特定 application 結構）&lt;/li>
&lt;li>防禦：白名單 user input、不直接組 query operator&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>ORM Injection&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>資料層紅隊判讀的核心目標是確認「誰能讀到什麼資料、資料會從哪裡流出、錯誤狀態如何回復」。這裡的紅隊指攻擊者視角的風險檢查：從可被濫用的路徑反向檢查資料邊界。database 一旦承擔 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、弱點就同時影響正確性、隱私與可恢復性。</p>
<p>本章聚焦在 <em>資料層</em>（DB 自身）的攻擊面、跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 資安與資料保護模組</a> 的網路 / 身份 / 加密層形成互補。讀完後讀者能盤點：DB 上有哪些 <em>攻擊路徑</em>、哪些 <em>外洩管道</em>、哪些 <em>偵測訊號</em>。</p>
<h2 id="資料層弱點的主要軸線">資料層弱點的主要軸線</h2>
<p>資料層弱點可分成三條軸線：存取邊界、狀態邊界、資料流邊界。</p>
<p><strong>存取邊界</strong>：看 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a> 與 <a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary</a>。哪些 user / role / tenant 可以 read / write 哪些資料。
<strong>狀態邊界</strong>：看 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 與 <a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a>。同時讀寫時的 race condition、TOCTOU。
<strong>資料流邊界</strong>：看查詢結果、匯出、備份、觀測與支援工具的資料暴露路徑。</p>
<p>三條軸線各有典型攻擊模式、要分別檢查。</p>
<h2 id="db-攻擊面的外圍層次">DB 攻擊面的外圍層次</h2>
<p>DB 攻擊面分三層、每層有典型攻擊向量跟防禦邊界、紅隊盤點要逐層檢查。傳統做法常把 90% 精力放在最內層 DB、外圍兩層的失守會讓內層防禦變成無效投資。</p>
<p><strong>Layer 1：DB 本身</strong>（最直接、防禦最成熟）— SQL injection、authentication、authorization、RLS 都在這層。</p>
<p><strong>Layer 2：DB 周邊產品</strong>（最常被忽略）— file transfer service（MFT）、API gateway、search proxy、admin console 都「接 DB」、且通常 perimeter 設定比 DB 鬆。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a> — MOVEit Transfer 是 file transfer 產品、漏洞讓攻擊者直接存取後端資料、屬於 edge-exposure 類別的批量利用事件。判讀重點：任何「接 DB」的產品都屬於 DB 攻擊面、要盤 <em>所有上游 caller 產品</em>。類似結構還有 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/goanywhere-mft-2023-exfiltration-chain/" data-link-title="7.R7.4.7 GoAnywhere MFT 2023：傳輸中樞被利用的外送鏈" data-link-desc="MFT 中樞服務漏洞會把檔案交換流程直接轉成資料外送風險">GoAnywhere MFT 2023</a>、<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/progress-wsftp-2023-file-service-breach/" data-link-title="7.R7.4.6 Progress WS_FTP 2023：檔案服務入口與資料外送" data-link-desc="對外檔案服務漏洞在企業環境常直接轉為資料外送風險">Progress WS_FTP 2023</a>。</p>
<p><strong>Layer 3：認證信任根</strong>（最致命、最少人想到）— signing key、token issuer、IAM <a href="/blog/backend/knowledge-cards/federation/" data-link-title="Federation" data-link-desc="跨系統信任與授權交換的聯邦機制">federation</a> 都決定「誰能宣稱是哪個 user」。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558</a> — 簽章金鑰外洩後、攻擊者偽造可被驗證的身分權杖、application 層的 BOLA / BOPLA / RLS 都會在底層 trust 失守時被繞過。判讀重點：DB authorization 接受上游認證結果、上游 trust 失守時、DB 層的精緻設計就被旁路掉。</p>
<p><strong>設計含義</strong>：紅隊盤點順序是由外向內。先盤「誰能通過認證」（trust root）、再盤「通過認證後能打到哪些產品」（caller surface）、最後盤「打到 DB 後能做什麼」（DB authorization）。三層任一失守、後續層的防禦投資都會被旁路。</p>
<h2 id="攻擊模式-1注入類">攻擊模式 1：注入類</h2>
<p><strong>SQL Injection</strong>：</p>
<ul>
<li>經典攻擊、把 user input 拼進 SQL 字串</li>
<li>防禦：parameterized query / prepared statement、絕不字串拼接</li>
<li>二階注入：input 已存進 DB、後續 query 時才觸發 — 比一階更難偵測</li>
</ul>
<p><strong>NoSQL Injection</strong>：</p>
<ul>
<li>MongoDB / DynamoDB 也可能被注入（不同形式）</li>
<li>MongoDB：<code>{$where: ...}</code> operator injection、<code>{$ne: null}</code> 跳過 auth</li>
<li>DynamoDB：FilterExpression 注入（少見、需要特定 application 結構）</li>
<li>防禦：白名單 user input、不直接組 query operator</li>
</ul>
<p><strong>ORM Injection</strong>：</p>
<ul>
<li>即使用 ORM、<code>Raw()</code> / <code>Exec()</code> 等 escape hatch 仍能注入</li>
<li>用 <code>where</code> clause 接 user input 不過濾、ORM 不會自動防</li>
<li>防禦：永遠 parameterized、<code>Raw()</code> 必須 review</li>
</ul>
<p><strong>Second-order Injection</strong>：</p>
<ul>
<li>第一次寫入時看起來安全、第二次讀出來時觸發</li>
<li>例：username 帶 SQL fragment、寫入時 escape、後續 admin 查詢時不 escape</li>
<li>防禦：<em>所有</em> DB output 都當 untrusted、不能依賴「寫入時的 escape」</li>
</ul>
<p><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023 mass exfiltration</a> 是 SQL injection 升級成 mass data exfil 的代表性事件。Progress Software 的 MOVEit Transfer 是 file transfer 產品、漏洞讓未認證攻擊者直接打到後端 DB、跨上百家客戶持續外洩。判讀重點：file transfer 這類「次要產品」也接 DB、且因為通常 perimeter 設定鬆、變成最先被打的點。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">Attack Surface 卡片</a> 跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 entrypoint security</a>。</p>
<h2 id="攻擊模式-2授權繞過類">攻擊模式 2：授權繞過類</h2>
<p><strong>BOLA</strong>（Broken Object Level Authorization）：</p>
<ul>
<li>用戶 A 改 user_id 為 B 的請求、後端不檢查就回 B 的資料</li>
<li>最常見的 web app 漏洞（OWASP API Top 10 第 1 名）</li>
<li>防禦：每個 DB query 都帶 <code>WHERE owner_id = current_user_id</code>、不只信 URL parameter</li>
<li>對應 <a href="/blog/backend/knowledge-cards/bola-idor/" data-link-title="BOLA / IDOR" data-link-desc="說明物件層授權缺失如何讓使用者存取不屬於自己的資料">BOLA / IDOR 卡片</a></li>
</ul>
<p><strong>BOPLA</strong>（Broken Object Property Level Authorization）：</p>
<ul>
<li>物件級檢查過了、但物件內 <em>某些屬性</em> 不該被存取 / 修改</li>
<li>例：用戶能更新自己 profile、但不該改 <code>is_admin</code> flag</li>
<li>防禦：應用層 <em>allowlist</em> 屬性、不是 deny-list</li>
<li>對應 <a href="/blog/backend/knowledge-cards/bopla/" data-link-title="BOPLA" data-link-desc="說明屬性層授權缺失如何讓使用者讀寫不該暴露的欄位">BOPLA 卡片</a></li>
</ul>
<p><strong>Mass Assignment</strong>：</p>
<ul>
<li>應用層直接把 request body bind 到 DB row、含未檢查欄位</li>
<li>例：<code>Order.fromJSON(request.body)</code> 自動 set <code>is_admin_override</code> 為 true</li>
<li>防禦：明確 allowlist 哪些 field 可從 request 來</li>
<li>對應 <a href="/blog/backend/knowledge-cards/mass-assignment/" data-link-title="Mass Assignment" data-link-desc="說明自動綁定 request 欄位如何造成未授權欄位被修改">Mass Assignment 卡片</a></li>
</ul>
<p><strong>Multi-tenant Boundary Leak</strong>：</p>
<ul>
<li>multi-tenant SaaS：tenant A 的 query 不該看到 tenant B 的資料</li>
<li>常見錯誤：忘了 <code>WHERE tenant_id = ?</code>、用 application 層而非 DB 層強制</li>
<li>進階防禦：Row-Level Security（PostgreSQL RLS）、由 DB 強制 tenant boundary</li>
</ul>
<p><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 credential abuse</a> 揭露 <em>資料平台帳號沒強制 MFA</em> 的代價、攻擊者拿到外洩 credential 後直接 query 多家客戶的 Snowflake account、大量外送資料。判讀重點：DB 認證 = 資料邊界、但雲端資料平台預設未必開 MFA、要主動 enforce。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 紅隊版</a> — signing key 洩漏後攻擊者直接以任意 user 身份查任意 mailbox、application 層 BOLA / BOPLA 全部失效、因為攻擊者通過了底層 trust boundary。</p>
<h2 id="攻擊模式-3資料外洩類">攻擊模式 3：資料外洩類</h2>
<p><strong>Excessive Data Exposure</strong>：</p>
<ul>
<li>API 回應比需要的多（內部欄位、PII、信用卡末四碼）</li>
<li>「前端會 filter」是反模式 — 攻擊者直接看 raw response</li>
<li>防禦：DTO / response schema 明確列哪些欄位可回、不要 <code>SELECT *</code></li>
<li>對應 <a href="/blog/backend/knowledge-cards/excessive-data-exposure/" data-link-title="Excessive Data Exposure" data-link-desc="說明 API 回傳過多資料如何增加敏感資訊外洩風險">Excessive Data Exposure 卡片</a></li>
</ul>
<p><strong>Log / Trace 洩漏</strong>：</p>
<ul>
<li>把 query 含 PII 直接寫進 log、log 進 SIEM、SIEM 給多人看</li>
<li>distributed tracing 把 query 跟 user_id 都記下來</li>
<li>防禦：log 前 redact、敏感欄位 mask、distributed tracing 的 attribute allowlist</li>
</ul>
<p><strong>Backup / Export 洩漏</strong>：</p>
<ul>
<li>DB backup 沒加密、放公開 S3 bucket</li>
<li>客服 / BI 工具導出 CSV、檔案被搬到不該的地方</li>
<li>防禦：backup encryption、export audit、emit-once endpoint</li>
<li><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 backup chain</a> — 開發環境被入侵後、攻擊者沿著 <em>備份路徑</em> 拿到 production vault backup、雖然 vault 內容是加密的、但 master password 弱的客戶可被離線爆破。判讀重點：備份檔案的 <em>存放位置</em> 跟 <em>加密狀態</em> 是攻擊面、不只 production DB。</li>
</ul>
<p><strong>Support Tool Path</strong>：</p>
<ul>
<li>客服 admin 工具可以 query 任何用戶資料</li>
<li>內部工具沒有 audit log、不知道誰看了什麼</li>
<li>防禦：客服 tool 必須 audit log、敏感欄位 mask、access 按 ticket 限制</li>
<li><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta Support System 事件</a> — 攻擊者拿到 Okta support 系統存取後、能看到客戶上傳的 HAR 檔（含 session token）、再用 token 進客戶 tenant。Support tool 的 <em>查詢能力</em> 跟 <em>資料分級</em> 不對等就會放大事故面。</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 data protection and masking</a> 跟 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 audit trail</a>。</p>
<h2 id="攻擊模式-4競態--toctou-類">攻擊模式 4：競態 / TOCTOU 類</h2>
<p><strong>TOCTOU</strong>（Time of Check Time of Use）：</p>
<ul>
<li>檢查時是 A 狀態、用的時候是 B 狀態</li>
<li>例：先 SELECT 確認 user 有 100 credit、再 UPDATE 扣 100、中間有別的 transaction 改了 credit</li>
<li>防禦：用 <code>SELECT ... FOR UPDATE</code> 鎖、或用 atomic operation（<code>UPDATE ... WHERE credit &gt;= 100</code>）</li>
</ul>
<p><strong>Double-spend 攻擊</strong>：</p>
<ul>
<li>多個 request 同時花同一筆錢</li>
<li>防禦：optimistic locking with version、unique constraint、或交易層 serializable</li>
<li>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 的 isolation level 段</li>
</ul>
<p><strong>Race condition in business logic</strong>：</p>
<ul>
<li>註冊：兩個 request 同時用同一個 email、可能都成功</li>
<li>防禦：unique constraint 在 DB 層、不只 application 層 check</li>
</ul>
<h2 id="攻擊模式-5dos--資源耗盡類">攻擊模式 5：DoS / 資源耗盡類</h2>
<p><strong>Unrestricted Resource Consumption</strong>：</p>
<ul>
<li>沒分頁的 <code>SELECT *</code>、用戶傳 <code>?limit=999999</code></li>
<li>沒 timeout 的長 query</li>
<li>防禦：query timeout、pagination 強制上限、rate limit</li>
</ul>
<p><strong>Connection 耗盡</strong>：</p>
<ul>
<li>攻擊者開大量 connection、佔光 DB connection pool</li>
<li>防禦：connection pool 限制、application 層 connection limit、PgBouncer 共享</li>
</ul>
<p><strong>Storage 灌爆</strong>：</p>
<ul>
<li>API 允許大量 insert、storage 被填滿</li>
<li>防禦：rate limit、quota per tenant、auto-archive</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/unrestricted-resource-consumption/" data-link-title="Unrestricted Resource Consumption" data-link-desc="說明缺少資源限制如何讓 API 被濫用或拖垮">Unrestricted Resource Consumption 卡片</a>。</p>
<h2 id="何時要提高紅隊檢查優先級">何時要提高紅隊檢查優先級</h2>
<p>下列訊號出現時、資料層弱點通常會放大成系統風險：</p>
<ul>
<li>角色與租戶模型快速增加、且查詢條件跨多個權限層</li>
<li>migration 頻率提高、且 schema 與讀寫流程同時變更</li>
<li>匯出、對帳、客服查詢與搜尋索引共用同一批敏感欄位</li>
<li>事故修復高度依賴人工 SQL 與臨時腳本</li>
<li>新引入的 ORM / query builder / cache layer 改變了 query 路徑</li>
</ul>
<h2 id="失敗代價">失敗代價</h2>
<p>資料層弱點會把單點錯誤轉成長尾影響。</p>
<ul>
<li><strong>越權查詢</strong>：直接資料洩漏 → 通知監管 + 客戶 + 媒體</li>
<li><strong>交易邊界混亂</strong>：部分寫入與狀態偏移 → 對帳成本 + 退款處理</li>
<li><strong>資料外洩進 log / backup</strong>：拉長處理週期 → 跨 team 清理</li>
<li><strong>support tool 濫用</strong>：無 audit log → 無法追究、信任成本上升</li>
<li><strong>業務全面中斷</strong>：資料事件升級成 availability 事件、整條業務鏈停擺</li>
</ul>
<p>這些問題的共同代價是：修復路徑長、稽核負擔高、信任成本上升。</p>
<p><strong>真實事件對照</strong>：<a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024 ops impact</a> 是「資料事件變成業務連續性事件」的代表。攻擊者進入 DB 後、不只外洩資料、還破壞處理能力、讓整個美國醫療支付網路停擺數週。判讀重點：DB 失守不只代表 <em>資料外洩</em> 一種損失、還可能直接停掉 <em>上游業務流程</em>、評估代價時要把這層算進去。<a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 identity lateral impact</a> 是另一個對照：vishing 拿到 identity 後橫向到核心系統、酒店訂房 / 自助 check-in / 老虎機全停。資料層的攻擊代價要跨業務流量去評估、不只看 DB 本身。</p>
<h2 id="incident-三角db-事故的同步處置">Incident 三角：DB 事故的同步處置</h2>
<p>DB 事故的處置三角是 <em>同步</em> 執行三件事、共同消除攻擊者在處置間隙繼續入侵的時間窗：</p>
<ol>
<li><strong>漏洞修補</strong>：補上被利用的具體漏洞或 misconfiguration</li>
<li><strong>Session / 憑證失效</strong>：撤銷所有可能被攻擊者拿到的 session、token、credential</li>
<li><strong>異常痕跡清查</strong>：盤點攻擊者已經做了什麼、哪些資料動過、哪些 backdoor 留下</li>
</ol>
<p>同步執行的理由是 <em>攻擊者擁有平行能力</em>：用已拿到的 credential 在 patch 完成前重新進入、或用清查前還沒被發現的 backdoor 繞過修補。線性執行「先修漏洞、再失效憑證、再清查」會留下兩個時間窗、攻擊代價被放大。</p>
<p><strong>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a></strong> — 公告漏洞到攻擊者大規模利用之間只有數小時、單純等 vendor 修補來不及。實務做法是：</p>
<ul>
<li><strong>發布前</strong>：對外服務建立 <em>即時隔離開關</em>、不等 vendor patch</li>
<li><strong>事故中</strong>：先把入口下線（DNS 切走 / WAF rule 全擋）、同步進行 patch + token revoke + audit log review</li>
<li><strong>前提</strong>：事先有 inventory（知道哪些產品接 DB）+ 自動化失效能力（不是手動逐個 revoke）</li>
</ul>
<p>這個三角是 <em>能力前提</em>、不是 <em>當下決策</em>。事故當下發現缺哪一角、就只能線性執行、攻擊代價會被放大。</p>
<h2 id="偵測與審計">偵測與審計</h2>
<p>紅隊檢查不只「找漏洞」、也要設計 <em>持續偵測</em>：</p>
<h3 id="1-query-audit">1. Query audit</h3>
<ul>
<li>DB query 寫進 audit log（誰、什麼時候、查了什麼）</li>
<li>不只 admin tool、application 也要 audit</li>
<li>對應 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log 卡片</a></li>
</ul>
<h3 id="2-anomaly-detection">2. Anomaly detection</h3>
<ul>
<li>異常 query pattern（突然 SELECT 全表、跨 tenant 範圍）</li>
<li>異常 export volume</li>
<li>Cross-tenant token 異常（同一 issuer 出現本不應跨域的軌跡）</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 detection coverage</a></li>
</ul>
<p>Cross-tenant token 偵測是觀測單一 issuer 發出的 token 在不應跨域的 tenant 出現的能力。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558</a> — 偽造 token <em>形式上完全合法</em>、單看 token validation 找不到異常、要看 <em>軌跡</em>（哪個 issuer 的 token 跨了哪些 tenant、跟歷史 baseline 比對）。這層偵測需要 application 跟 DB layer 都記下「token 來源 → tenant 目的」的對應、才能事後比對。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a> 揭露的異常查詢偵測維度：</p>
<ul>
<li>query 體積異常（單一 user 短時間內查詢量遠超日常）</li>
<li>來源 IP 異常（從合法網段突然變成未知 endpoint）</li>
<li>跨 schema scan 模式（單一 user 突然查多個 tenant 的表）</li>
<li>匯出頻率異常（單位時間匯出次數遠超基線）</li>
</ul>
<p>這些維度都需要足夠歷史 telemetry 建立基線、新部署的 DB 在累積基線前處於偵測盲區、要靠 <em>絕對閾值</em> 補（例如「任何 user 單次查詢 &gt; 1GB 都告警」、不等基線）。</p>
<h3 id="3-db-level-monitoring">3. DB-level monitoring</h3>
<ul>
<li>slow query log（可能是 attacker 在 enumerate）</li>
<li>failed login（DB 層 connection attempt）</li>
<li>privilege escalation event</li>
</ul>
<h3 id="4-periodic-review">4. Periodic review</h3>
<ul>
<li>每季 review role / permission</li>
<li>每年 audit support tool access pattern</li>
<li>migration 後重新檢查 access boundary</li>
</ul>
<h2 id="認證--網路雙重防護">認證 + 網路雙重防護</h2>
<p>DB 認證 = 資料邊界、但雲端資料平台（Snowflake、BigQuery、Cosmos DB）預設未必開 MFA、且 <em>網路層通常 open</em>（任何 IP 都能嘗試連線）。任一層失守、攻擊者就進來。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a> — 外洩 credential + 未強制 MFA + 沒設 network policy → 攻擊者直接從任意 IP 用 leaked credential 登入、查多家 tenant 的資料。</p>
<p><strong>雙重防護設計</strong>：</p>
<ul>
<li><strong>網路層</strong>：network rule allowlist（只允許公司 IP / VPN / 雲端 NAT 連線）— leaked credential 即使有效、也碰不到 DB</li>
<li><strong>認證層</strong>：強制 MFA + 條件式存取（context-aware：時間 / 地點 / 裝置）— 即使網路層失守、credential 還要過 MFA</li>
<li><strong>應用層</strong>：API key / service account 跟 user credential 分開、各有 lifecycle</li>
</ul>
<p>兩層獨立、單層失守仍能阻擋資料外送。資料平台預設應強制 MFA + network policy、把「credential 外洩 = 資料外送」這條捷徑切斷。</p>
<h2 id="批量憑證撤銷的工程能力">批量憑證撤銷的工程能力</h2>
<p>批量憑證撤銷能力是事故當下「攔停攻擊者」的核心動作、要 <em>快速、大量、選擇性</em> 執行可疑憑證撤銷。這個能力屬於 <em>事先準備</em>、事故當下臨時建來不及。</p>
<p><strong>最小能力清單</strong>：</p>
<ul>
<li><strong>Credential inventory</strong>：列出所有 active credential（user password、API key、service account token、session）。事故當下若靠工程師記憶查、會漏掉長期沒人動的 service account 或 OAuth integration、變成攻擊者 persist 的後門。Inventory 要 <em>自動產生</em>、不是人工維護的 spreadsheet。</li>
<li><strong>分批撤銷 API</strong>：能按 user group / service / scope 批次撤銷、不是逐個 revoke。批次需要 idempotency key、避免重複撤銷產生競爭。受影響範圍大時、逐個撤銷可能需要數小時、攻擊者持續外送資料。</li>
<li><strong>撤銷後 audit</strong>：撤銷紀錄要存（誰被撤、什麼時間、什麼原因、誰執行）、避免事後爭議。</li>
<li><strong>重新發放流程</strong>：撤銷後使用者要重新登入、SSO + MFA 流程在事故當下要能撐住瞬間湧入的重新驗證請求。若流程卡住、會在「沒攻擊但用戶進不來」狀態下被迫降回安全等級較低的應急 fallback、形成新攻擊面。</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a> 的事故處置 — 平台級事故影響數百家客戶、撤銷必須跨 tenant 同步進行、單一客戶手動撤銷來不及。</p>
<h2 id="長期可重複匯出工件">長期可重複匯出工件</h2>
<p>Long-lived repeatable export artifact 是事故後仍能持續產出資料的工件、屬於跨事故時間軸的 attack surface。攻擊者拿到一次、就能長期外送、不需要每次重新進入系統。常見類型：</p>
<ul>
<li><strong>預先生成的報表 URL</strong>（內部 BI tool 給 download link、URL 通常長期有效）</li>
<li><strong>API key 綁定的 export endpoint</strong>（key 沒過期、endpoint 一直能匯出最新資料）</li>
<li><strong>資料平台的 scheduled / saved query</strong>（以合法 user 身份定期執行匯出）</li>
<li><strong>Database backup 的 share link</strong>（雲端儲存的 signed URL、有效期可達數年）</li>
</ul>
<p><strong>防禦設計</strong>：</p>
<ul>
<li><strong>預設短 TTL</strong>：所有匯出 URL / signed link 預設 1-24 小時失效</li>
<li><strong>單次性匯出</strong>：sensitive export 限定 emit-once、用過就失效</li>
<li><strong>匯出記錄審計</strong>：每次匯出寫進 audit log、定期審查哪些 endpoint 異常高頻使用</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024</a> 連結的紅隊 problem-card「Long-lived repeatable export artifact」— 這類工件的核心風險是 <em>憑證撤銷後仍可運作</em>、修復不只要撤 credential、還要盤所有由該 credential 建立的長效工件。</p>
<h2 id="備份-vs-正式環境的權限獨立性">備份 vs 正式環境的權限獨立性</h2>
<p>備份系統是 <em>獨立</em> 的攻擊面、跟正式環境要 <em>不同權限域</em>。常見錯誤是「備份用同一組 IAM principal 跟同一把 KMS key」、結果正式環境被打、攻擊者沿著 <em>備份路徑</em> 拿到所有歷史資料。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 backup chain</a> — 開發環境被入侵後、攻擊者沿著備份路徑拿到雲端備份的加密保管庫資料、形成長尾資料保護壓力。判讀重點：備份的 <em>存放位置</em>、<em>金鑰管理</em>、<em>存取權限</em> 都是攻擊面、不只 production DB；備份檔加密本身不足以擋下取走後的離線分析。</p>
<p><strong>權限獨立性設計</strong>：</p>
<ul>
<li><strong>不同 IAM principal</strong>：production 跟 backup 用不同 service account、production 帳號沒有 backup 讀權限</li>
<li><strong>不同 KMS key audience</strong>：production 用 production key、backup 用 backup key、兩者 lifecycle 分離</li>
<li><strong>不同 audit log</strong>：production read / write 跟 backup read 在 <em>不同</em> audit stream、後續調查能區分「正常運作」vs「備份被讀」</li>
<li><strong>不同 access pattern review</strong>：定期審查哪些 principal 在哪些時段讀 backup（正常情況很少有人讀 backup、頻繁讀取是異常訊號）</li>
</ul>
<p>「正式環境的接管不直接通到備份」是設計準則、不是 best practice 加分項。對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 reconciliation</a> 的備份 / PITR 段討論。</p>
<h2 id="最低控制面">最低控制面</h2>
<p>資料層在討論具體服務前、先定義四個控制面最穩定：</p>
<ol>
<li><strong>權限模型</strong>：資料存取與角色、租戶、操作情境的對應關係</li>
<li><strong>交易與一致性模型</strong>：哪些操作必須同成敗、哪些可以延遲一致</li>
<li><strong>資料分級與遮罩模型</strong>：哪些欄位可回傳、可觀測、可匯出</li>
<li><strong>恢復模型</strong>：錯誤資料如何比對、回復、追蹤與稽核</li>
</ol>
<h2 id="案例對照">案例對照</h2>
<h3 id="07-主案例產品--平台事故">07 主案例（產品 / 平台事故）</h3>
<table>
  <thead>
      <tr>
          <th>07 案例</th>
          <th>跟資料層的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-route-leak-2026/" data-link-title="7.C1 Cloudflare：2026 Route Leak 事件" data-link-desc="BGP 路由政策自動化失誤如何回寫控制面治理。">7.C1 Cloudflare Route Leak</a></td>
          <td>控制面變更可能影響資料層存取</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">7.C2 Cloudflare Token 事件</a></td>
          <td>Token 洩漏 → DB 存取被濫用</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">7.C3 Azure AD 2021</a></td>
          <td>identity failure → 應用 fallback、可能讓 DB 存取錯誤路徑</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">7.C4 Microsoft Storm-0558</a></td>
          <td>signing key 洩漏 → 任意 user 身份、可 query 任何資料</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">7.C5 Okta Support System</a></td>
          <td>support tool 洩漏 → 客戶資料被存取</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/okta-cross-tenant-impersonation-2023/" data-link-title="7.C6 Okta：Cross-tenant Impersonation 防禦回寫" data-link-desc="跨租戶 impersonation 風險如何轉成身份治理與偵測策略。">7.C6 Okta Cross-Tenant</a></td>
          <td>tenant boundary 失守 → DB-level RLS 也擋不住</td>
      </tr>
  </tbody>
</table>
<h3 id="07-紅隊案例攻擊鏈--入侵路徑">07 紅隊案例（攻擊鏈 / 入侵路徑）</h3>
<table>
  <thead>
      <tr>
          <th>紅隊案例</th>
          <th>攻擊鏈到資料層的路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024 憑證濫用</a></td>
          <td>外洩 credential + 未強制 MFA → 直接 query 多家 tenant 資料</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022 備份鏈</a></td>
          <td>開發環境 → production backup 路徑 → 客戶加密 vault 外送</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023 mass exfiltration</a></td>
          <td>file transfer 產品零時差 → 後端資料批量外送</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024 ops impact</a></td>
          <td>DB 入侵 → 醫療支付網路全面停擺、資料事件升級成業務中斷</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 signing key chain</a></td>
          <td>signing key 洩漏 → 任意身份 token forge → application BOLA / BOPLA 全部失效</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/mgm-2023-identity-lateral-impact/" data-link-title="7.R7.1.4 MGM 2023：身分流程被打穿後的營運中斷" data-link-desc="社交工程造成身分邊界失守後，如何演變成可用性與營運衝擊">MGM 2023 identity lateral impact</a></td>
          <td>社交工程 → identity lateral → 業務系統全停、資料層攻擊代價跨業務流量</td>
      </tr>
  </tbody>
</table>
<p>紅隊案例庫的完整入口看 <a href="/blog/backend/07-security-data-protection/red-team/cases/case-reference-map/" data-link-title="7.R7.M 案例引用地圖（服務主題 -&gt; 案例 -&gt; workflow）" data-link-desc="把服務主題連到完整案例體系，再連回 incident workflow 檢查點">紅隊案例參考地圖</a> — 那邊有按攻擊階段（exposure / exfiltration / identity / supply-chain）的完整索引。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.3 的交接：race condition / TOCTOU 用 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a> 的 isolation level 處理</li>
<li>與 1.4 的交接：repository adapter 應用 allowlist / parameterized query — <a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">repository adapter</a></li>
<li>與 1.8 的交接：state ownership 決定哪些資料需要嚴格存取控制 — <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">State Ownership</a></li>
<li>與 7.2 的交接：identity / authorization 邊界 — <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity &amp; Access Boundary</a></li>
<li>與 7.4 的交接：資料保護與遮罩 — <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection and Masking</a></li>
<li>與 7.7 的交接：audit trail — <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">Audit Trail and Accountability Boundary</a></li>
<li>與 7.13 的交接：detection coverage — <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">Detection Coverage and Signal Governance</a></li>
<li>與 8.19 的交接：事故時的資料層判讀 — <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></li>
<li>合規驅動的多 region 部署選型：<a href="/blog/backend/01-database/vendors/aurora/global-database-multi-region/" data-link-title="Aurora Global Database：跨 region async replication、&lt; 1 秒 lag 與合規 anti-recommendation" data-link-desc="Aurora Global Database 跨 region storage-level async replication、&lt; 1 秒 typical lag、planned vs unplanned failover RTO 數量級對比、Standard Chartered 合規禁止跨境複製為什麼讓 Global Database 變反指標">Aurora global database 多 region</a>、<a href="/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/" data-link-title="Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約" data-link-desc="Aurora cross-AZ failover lifecycle（detection / promotion / DNS update）、&lt; 30 秒 RTO、application DNS cache 跟 connection pool 對齊、Standard Chartered 受監管場景為什麼用獨立 cluster 而非 Global Database failover">Aurora 跨 AZ failover RTO</a>、<a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency 知識卡</a></li>
</ol>
<h2 id="關聯卡片">關聯卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">Attack Surface</a></li>
<li><a href="/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">Trust Boundary</a></li>
<li><a href="/blog/backend/knowledge-cards/excessive-data-exposure/" data-link-title="Excessive Data Exposure" data-link-desc="說明 API 回傳過多資料如何增加敏感資訊外洩風險">Excessive Data Exposure</a></li>
<li><a href="/blog/backend/knowledge-cards/bola-idor/" data-link-title="BOLA / IDOR" data-link-desc="說明物件層授權缺失如何讓使用者存取不屬於自己的資料">BOLA / IDOR</a></li>
<li><a href="/blog/backend/knowledge-cards/bopla/" data-link-title="BOPLA" data-link-desc="說明屬性層授權缺失如何讓使用者讀寫不該暴露的欄位">BOPLA</a></li>
<li><a href="/blog/backend/knowledge-cards/mass-assignment/" data-link-title="Mass Assignment" data-link-desc="說明自動綁定 request 欄位如何造成未授權欄位被修改">Mass Assignment</a></li>
<li><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a></li>
<li><a href="/blog/backend/knowledge-cards/data-reconciliation/" data-link-title="Data Reconciliation" data-link-desc="說明多個資料來源不一致時如何比對、修復與留下證據">Data Reconciliation</a></li>
<li><a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">Tenant Boundary</a></li>
<li><a href="/blog/backend/knowledge-cards/unrestricted-resource-consumption/" data-link-title="Unrestricted Resource Consumption" data-link-desc="說明缺少資源限制如何讓 API 被濫用或拖垮">Unrestricted Resource Consumption</a></li>
</ul>
]]></content:encoded></item><item><title>1.6 資料庫轉換實作：雙寫、回填、切流與回滾</title><link>https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/</guid><description>&lt;p>資料庫轉換實作的核心責任是讓 schema、資料與流量切換都可分段驗證、並在任一階段可安全回退。這一頁不討論要不要轉換、專注回答「決定要換之後怎麼做」。&lt;/p>
&lt;p>本章跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰&lt;/a> 分工：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>1.6 同 DB 內&lt;/strong>：schema 演進、資料變更、新舊欄位共存、雙寫驗證、切流。例：加欄位、改欄位、拆表、合表、加 partition。&lt;/li>
&lt;li>&lt;strong>1.12 跨 DB 引擎&lt;/strong>：換 vendor（PostgreSQL → Aurora、MongoDB → Cosmos DB、TiDB → DynamoDB）。例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a>。&lt;/li>
&lt;/ul>
&lt;p>兩者用同樣的工程方法論（dual-write、shadow、cutover、rollback）、但 &lt;em>stakes&lt;/em> 跟 &lt;em>跨越的邊界&lt;/em> 不同。本章先處理 1.6 的同 DB schema 轉換、1.12 處理更大規模的 cross-engine。若來源是託管平台（Shopify / Firebase / WordPress）的匯出而非自建資料庫、整場遷出的資產線盤點與並行期設計見 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出&lt;/a>；資料落地自建後的 schema 演進回到本章、跨引擎搬遷走 1.12。&lt;/p>
&lt;h2 id="實作流程">實作流程&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>核心動作&lt;/th>
 &lt;th>交付成果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1. 邊界定義&lt;/td>
 &lt;td>定義 source of truth、切換範圍、不可中斷路徑&lt;/td>
 &lt;td>migration scope 與 rollback 邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2. Expand&lt;/td>
 &lt;td>新欄位 / 新表先上線、應用可同時讀舊寫新或雙寫&lt;/td>
 &lt;td>新舊版本相容窗口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3. Backfill&lt;/td>
 &lt;td>批次回填歷史資料、保留節流與 checkpoint&lt;/td>
 &lt;td>可追蹤的回填進度與失敗重試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4. 驗證&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read&lt;/a>、checksum、業務指標對帳&lt;/td>
 &lt;td>一致性證據包&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5. Cutover&lt;/td>
 &lt;td>逐步切讀、再切寫、保留快速回切策略&lt;/td>
 &lt;td>切流完成且可回退&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>6. Contract&lt;/td>
 &lt;td>移除舊欄位與舊路徑、收斂技術債&lt;/td>
 &lt;td>單一資料語意落地&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="expand-contract-模式">Expand-Contract 模式&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract&lt;/a>（也叫 parallel change）是同 DB schema 演進的核心模式。&lt;/p>
&lt;p>&lt;strong>為什麼需要這個模式&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>應用 deploy 跟 DB migration 不能 &lt;em>原子&lt;/em> 完成&lt;/li>
&lt;li>在 deploy window 內、有些 instance 跑舊 code、有些跑新 code&lt;/li>
&lt;li>DB 必須同時容納舊 code 跟新 code 的 schema&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Expand 階段&lt;/strong>（加新欄位、不刪舊）：&lt;/p>
&lt;ul>
&lt;li>加 &lt;code>new_column&lt;/code>、允許 nullable&lt;/li>
&lt;li>應用層 dual-write：同時寫 &lt;code>old_column&lt;/code> 跟 &lt;code>new_column&lt;/code>&lt;/li>
&lt;li>應用層 read 仍走 &lt;code>old_column&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Backfill 階段&lt;/strong>（資料同步）：&lt;/p>
&lt;ul>
&lt;li>把歷史 row 的 &lt;code>new_column&lt;/code> 補上值（從 &lt;code>old_column&lt;/code> 算出來）&lt;/li>
&lt;li>分批跑、用 checkpoint 追進度、避開 peak&lt;/li>
&lt;li>監控：rate、error、progress、unaffected rows count&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Migrate Reads 階段&lt;/strong>（切讀）：&lt;/p></description><content:encoded><![CDATA[<p>資料庫轉換實作的核心責任是讓 schema、資料與流量切換都可分段驗證、並在任一階段可安全回退。這一頁不討論要不要轉換、專注回答「決定要換之後怎麼做」。</p>
<p>本章跟 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> 分工：</p>
<ul>
<li><strong>1.6 同 DB 內</strong>：schema 演進、資料變更、新舊欄位共存、雙寫驗證、切流。例：加欄位、改欄位、拆表、合表、加 partition。</li>
<li><strong>1.12 跨 DB 引擎</strong>：換 vendor（PostgreSQL → Aurora、MongoDB → Cosmos DB、TiDB → DynamoDB）。例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a>、<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>。</li>
</ul>
<p>兩者用同樣的工程方法論（dual-write、shadow、cutover、rollback）、但 <em>stakes</em> 跟 <em>跨越的邊界</em> 不同。本章先處理 1.6 的同 DB schema 轉換、1.12 處理更大規模的 cross-engine。若來源是託管平台（Shopify / Firebase / WordPress）的匯出而非自建資料庫、整場遷出的資產線盤點與並行期設計見 <a href="/blog/backend/10-system-evolution/managed-platform-exit/" data-link-title="10.3 託管形態遷出：資產線盤點與並行期執行" data-link-desc="0.21 升級自建 tripwire 觸發後的執行劇本 — 把遷出拆成資料、身分、流量、整合各自的可攜性與斷點、設計舊平台與新系統的並行期與回切窗口、用部分遷出作為中繼形態">10.3 託管形態遷出</a>；資料落地自建後的 schema 演進回到本章、跨引擎搬遷走 1.12。</p>
<h2 id="實作流程">實作流程</h2>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>核心動作</th>
          <th>交付成果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 邊界定義</td>
          <td>定義 source of truth、切換範圍、不可中斷路徑</td>
          <td>migration scope 與 rollback 邊界</td>
      </tr>
      <tr>
          <td>2. Expand</td>
          <td>新欄位 / 新表先上線、應用可同時讀舊寫新或雙寫</td>
          <td>新舊版本相容窗口</td>
      </tr>
      <tr>
          <td>3. Backfill</td>
          <td>批次回填歷史資料、保留節流與 checkpoint</td>
          <td>可追蹤的回填進度與失敗重試</td>
      </tr>
      <tr>
          <td>4. 驗證</td>
          <td><a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a>、checksum、業務指標對帳</td>
          <td>一致性證據包</td>
      </tr>
      <tr>
          <td>5. Cutover</td>
          <td>逐步切讀、再切寫、保留快速回切策略</td>
          <td>切流完成且可回退</td>
      </tr>
      <tr>
          <td>6. Contract</td>
          <td>移除舊欄位與舊路徑、收斂技術債</td>
          <td>單一資料語意落地</td>
      </tr>
  </tbody>
</table>
<h2 id="expand-contract-模式">Expand-Contract 模式</h2>
<p><a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a>（也叫 parallel change）是同 DB schema 演進的核心模式。</p>
<p><strong>為什麼需要這個模式</strong>：</p>
<ul>
<li>應用 deploy 跟 DB migration 不能 <em>原子</em> 完成</li>
<li>在 deploy window 內、有些 instance 跑舊 code、有些跑新 code</li>
<li>DB 必須同時容納舊 code 跟新 code 的 schema</li>
</ul>
<p><strong>Expand 階段</strong>（加新欄位、不刪舊）：</p>
<ul>
<li>加 <code>new_column</code>、允許 nullable</li>
<li>應用層 dual-write：同時寫 <code>old_column</code> 跟 <code>new_column</code></li>
<li>應用層 read 仍走 <code>old_column</code></li>
</ul>
<p><strong>Backfill 階段</strong>（資料同步）：</p>
<ul>
<li>把歷史 row 的 <code>new_column</code> 補上值（從 <code>old_column</code> 算出來）</li>
<li>分批跑、用 checkpoint 追進度、避開 peak</li>
<li>監控：rate、error、progress、unaffected rows count</li>
</ul>
<p><strong>Migrate Reads 階段</strong>（切讀）：</p>
<ul>
<li>應用層 read 改走 <code>new_column</code></li>
<li>仍 dual-write、可以快速 fallback 回 <code>old_column</code></li>
<li>持續 shadow read 驗證一致性</li>
</ul>
<p><strong>Contract 階段</strong>（刪舊）：</p>
<ul>
<li>確認所有 application instance 都跑新 code 後</li>
<li>刪 <code>old_column</code>、停止 dual-write</li>
<li>移除應用層的 fallback 邏輯</li>
</ul>
<p>每個階段都是 <em>可獨立 rollback</em> 的、不像 big-bang 一次切完。</p>
<h2 id="同-db-內常見-migration-類型">同 DB 內常見 migration 類型</h2>
<h3 id="type-a加欄位最簡單">Type A：加欄位（最簡單）</h3>
<ul>
<li>直接 <code>ALTER TABLE ADD COLUMN</code>（nullable 或 default）</li>
<li>應用層後續加寫入、讀取</li>
<li>風險：低</li>
<li>注意：大表 ADD COLUMN with DEFAULT 在 PostgreSQL 11+ 是 instant、之前要 rewrite</li>
</ul>
<h3 id="type-b刪欄位">Type B：刪欄位</h3>
<ul>
<li>先讓所有 application 不再讀寫該欄位</li>
<li>部署完成、確認後再 DROP COLUMN</li>
<li>風險：中</li>
<li>注意：DROP COLUMN 是 instant、但無法 rollback、必須 backup</li>
</ul>
<h3 id="type-c改欄位型別">Type C：改欄位型別</h3>
<ul>
<li>用 expand-contract：加新欄位、dual-write、backfill、切讀、刪舊</li>
<li>風險：高（特別是大表）</li>
<li>注意：直接 <code>ALTER COLUMN TYPE</code> 可能 rewrite 整表、lock 時間長</li>
</ul>
<h3 id="type-d改欄位名--表名">Type D：改欄位名 / 表名</h3>
<ul>
<li>同型別改名：用 expand-contract、加新名 + dual-write、切讀、刪舊</li>
<li>DB 端 native rename 是 instant 但 application 需要同步 update — 不適合大規模 deploy</li>
</ul>
<h3 id="type-e拆表--合表">Type E：拆表 / 合表</h3>
<ul>
<li>拆：先 dual-write 到新舊表、backfill、切讀、刪舊</li>
<li>合：先 dual-write 到新表、backfill、切讀、刪舊</li>
<li>風險：高 — 影響面廣</li>
</ul>
<h3 id="type-f加-index">Type F：加 index</h3>
<ul>
<li>PostgreSQL：<code>CREATE INDEX CONCURRENTLY</code>（不 lock 表、可能 slow）</li>
<li>MySQL：<code>gh-ost</code> / <code>pt-online-schema-change</code>（ghost table）</li>
<li>風險：低-中（看 index 大小）</li>
</ul>
<h3 id="type-g加-not-null-constraint">Type G：加 NOT NULL constraint</h3>
<ul>
<li>先確保 application 所有 instance 都不寫 null</li>
<li>backfill null 為 default</li>
<li>加 NOT NULL constraint</li>
<li>風險：中</li>
</ul>
<h3 id="type-h加-partition">Type H：加 partition</h3>
<ul>
<li>先把現有表變成 partition 0</li>
<li>加新 partition 接新資料</li>
<li>漸進把舊資料 move 到對應 partition</li>
<li>風險：高（schema 大變）</li>
</ul>
<h2 id="online-schema-change-工具">Online Schema Change 工具</h2>
<p>大表 ALTER TABLE 直接跑會 lock。生產級 migration 用 online schema change 工具：</p>
<p><strong>PostgreSQL</strong>：</p>
<ul>
<li><code>CREATE INDEX CONCURRENTLY</code>（內建）</li>
<li><code>pg_repack</code>（vacuum + reindex without lock）</li>
<li><code>pgroll</code>（zero-downtime migration）</li>
<li>Atlas（schema-as-code）</li>
</ul>
<p><strong>MySQL</strong>：</p>
<ul>
<li><code>gh-ost</code>（GitHub 開源、無觸發器、推薦）</li>
<li><code>pt-online-schema-change</code>（Percona、用觸發器）</li>
<li>Vitess online DDL（managed via Vitess）</li>
</ul>
<p><strong>機制概要</strong>：</p>
<ul>
<li>建 ghost table（新 schema）</li>
<li>copy 資料到 ghost table（漸進、avoid peak）</li>
<li>用 trigger 或 binlog 同步 ongoing changes</li>
<li>切換：原 table → ghost table（atomic rename）</li>
</ul>
<p>對應 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor page</a> 跟 <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 page</a> 的相關段落。</p>
<h2 id="validation-query-設計">Validation Query 設計</h2>
<p>migration 過程中必須有 <em>validation query</em> 確認資料一致性。</p>
<p><strong>Checksum 對比</strong>：</p>
<ul>
<li>跑 <code>MD5(new_column) = MD5(derived_from_old)</code></li>
<li>抽樣 10% 跑、不打全表</li>
<li>不一致 → 修轉換函式、不直接修資料</li>
</ul>
<p><strong>Row count 對比</strong>：</p>
<ul>
<li>新欄位 NULL count 跟預期 backfill 進度比對</li>
<li>過慢 → 增加 backfill worker</li>
<li>不一致 → 找出 backfill 漏跑的 batch</li>
</ul>
<p><strong>業務指標對比</strong>：</p>
<ul>
<li>跟業務 metric 對齊（訂單金額總和、用戶數）</li>
<li>比 row-level checksum 更貼近 business correctness</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">Validation Query 卡片</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>。</p>
<h2 id="backfill-設計">Backfill 設計</h2>
<p>backfill 是 migration 中最 <em>容易出錯</em> 的環節 — 大量寫、影響 production。</p>
<p><strong>設計要點</strong>：</p>
<ol>
<li><strong>節流（throttle）</strong>：每秒寫入限制、跟 production peak 錯開</li>
<li><strong>Checkpoint</strong>：紀錄進度、可 resume</li>
<li><strong>錯誤分類</strong>：可 retry 的錯誤 vs 必須人工處理</li>
<li><strong>dry-run mode</strong>：先看會修改多少、不實際寫</li>
<li><strong>monitoring</strong>：rate、error、progress、replica lag</li>
</ol>
<p><strong>backfill 反模式</strong>：</p>
<ul>
<li>一個大 transaction 跑全表 → lock 太久、可能 OOM</li>
<li>沒 checkpoint → 中途失敗從頭開始</li>
<li>沒 throttle → 影響 production read</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill 卡片</a>。</p>
<h2 id="各階段監控訊號">各階段監控訊號</h2>
<p>每階段都要監控、不只是「最後驗證」：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>主要訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Expand</td>
          <td>DDL 執行時間、replication lag</td>
      </tr>
      <tr>
          <td>Backfill</td>
          <td>rate、error rate、checkpoint progress、production load 影響</td>
      </tr>
      <tr>
          <td>驗證</td>
          <td>shadow read 不一致率、checksum 結果、業務 metric 差異</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>error rate、p99 latency、rollback trigger 是否就緒</td>
      </tr>
      <tr>
          <td>Contract</td>
          <td>DDL 執行時間、無 application 還在用舊 column 的證據</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>回填速度不穩、延遲飆高</td>
          <td>可能與線上流量競爭 IOPS</td>
          <td>降低批次大小、加節流、避開 peak</td>
      </tr>
      <tr>
          <td>雙寫成功率高但 shadow read 漂移</td>
          <td>業務語意映射不一致</td>
          <td>先修轉換函式、再重跑對帳</td>
      </tr>
      <tr>
          <td>切流後 error rate 升高</td>
          <td>新庫讀寫路徑與索引未對齊</td>
          <td>回切舊讀路徑、補索引後再灰度</td>
      </tr>
      <tr>
          <td>rollback 時間超出 RTO</td>
          <td>回退流程過度人工</td>
          <td>把回退腳本化並演練</td>
      </tr>
      <tr>
          <td>大表 ALTER TABLE 卡住</td>
          <td>online 工具沒用對 / lock</td>
          <td>用 gh-ost / pgroll、或分批執行</td>
      </tr>
      <tr>
          <td>Backfill 後 NULL count 不歸零</td>
          <td>有漏跑的 batch、或新寫入沒走 dual-write</td>
          <td>補檢查 dual-write 邏輯、re-run backfill</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把資料庫轉換當成單次 DDL 任務、會讓風險集中在 cutover 當下。穩定做法是把每一階段都做成可驗證、可回退的獨立里程碑。</p>
<p>把 dual-write 當成最終保障也常出錯。雙寫只能保證「兩邊都有寫」、不保證「語意一致」、仍要配 shadow read 與業務對帳。</p>
<p>把 online schema change 工具當「萬能」也是錯。gh-ost / pgroll 仍有 <em>限制</em>（例如 trigger 限制、IO 影響）、要按工具規格操作。</p>
<h2 id="案例回寫">案例回寫</h2>
<ul>
<li>選型層案例： <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 營運後技術轉換</a></li>
<li>可靠性治理： <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a></li>
<li>事故反饋： <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a></li>
<li>大規模跨 DB 遷移： <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato</a>、<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix</a>、<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">Microsoft 365</a> 等 case）</li>
</ul>
<p>這組案例主要支撐的是「分段切換與可回退驗證」判讀、不直接支撐快取 TTL 或 broker delivery 參數；若問題核心在快取新鮮度或投遞語意、應轉到 2.x 或 3.x。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位演進與命名語意回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design</a>。</li>
<li>與 1.3 的交接：交易邊界與副作用切分回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a>。</li>
<li>與 1.7 的交接：production rollout 證據實作 — <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout Evidence</a>。</li>
<li>與 1.12 的交接：跨 DB 引擎遷移 — <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">大規模 DB 遷移實戰</a>。</li>
<li>與 4.20 的交接：validation query 與一致性證據進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.11 / 6.8 的交接：放行與停損條件進入 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 8.19 的交接：pause、rollback、fail-forward 決策記錄到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>若你還在判斷是否該轉換、先回 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4</a> 看決策訊號。若你要把這套流程寫成 production rollout evidence、接著讀 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據實作示範</a>。若你在設計放行與演練、接著看 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a>。若你在事故回溯、接著看 <a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.23 Post-incident Review</a>。若你要做 <em>跨 DB 引擎遷移</em>、看 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12</a>。</p>
]]></content:encoded></item><item><title>1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範</title><link>https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/</guid><description>&lt;p>Schema migration rollout 證據（Schema Migration Rollout Evidence）的核心責任是把正式狀態的演進拆成可觀測、可放行、可停止與可回寫的服務路徑。這篇以訂單資料表的付款狀態欄位演進為例，示範資料庫變更如何從 schema design、backfill、cutover 交接到 evidence package、release gate 與 incident decision log。&lt;/p>
&lt;h2 id="服務路徑與狀態責任">服務路徑與狀態責任&lt;/h2>
&lt;p>這條服務路徑是 &lt;code>checkout-api -&amp;gt; order-db -&amp;gt; payment-callback -&amp;gt; reconciliation-job&lt;/code>。Checkout 建立訂單時先寫入訂單主檔與付款待確認狀態；payment callback 會更新付款結果；客服後台與對帳 job 會讀取同一筆訂單狀態來判斷是否需要補償、退款或人工處理。&lt;/p>
&lt;p>本篇示範的變更是把原本單一 &lt;code>status&lt;/code> 欄位中的付款語意拆到 &lt;code>payment_state&lt;/code>。這個欄位屬於正式狀態，會影響使用者看到的訂單結果、付款回呼的冪等更新、客服查詢與對帳流程，因此 rollout 的核心是讓新舊狀態語意在過渡期同時成立；DDL 只是其中一個執行動作。&lt;/p>
&lt;p>這條路徑的前置概念來自 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作&lt;/a>。1.2 定義欄位責任，1.3 定義哪些更新要在同一個交易邊界內成立，1.6 定義 expand、backfill、cutover 與 contract 的執行節奏。&lt;/p>
&lt;h2 id="rollout-階段">Rollout 階段&lt;/h2>
&lt;p>Migration rollout 的責任是把一次高風險資料變更切成多個可驗證階段。每個階段都要有輸入條件、完成訊號與停止條件，讓團隊能在資料漂移擴大前停下來。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>服務責任&lt;/th>
 &lt;th>完成訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Expand&lt;/td>
 &lt;td>新欄位與新程式碼能和舊版本共存&lt;/td>
 &lt;td>新舊程式可同時讀寫，舊欄位仍可支撐服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backfill&lt;/td>
 &lt;td>歷史訂單補齊 &lt;code>payment_state&lt;/code>&lt;/td>
 &lt;td>checkpoint 穩定前進，mismatch 維持在門檻內&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cutover&lt;/td>
 &lt;td>讀取路徑改以新欄位為主&lt;/td>
 &lt;td>新欄位讀取成功率與對帳結果達到放行條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Contract&lt;/td>
 &lt;td>移除舊語意與舊寫入路徑&lt;/td>
 &lt;td>舊欄位已無服務依賴，回寫與監控已更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是責任轉移。Expand 保護相容性，backfill 保護歷史資料，cutover 保護線上讀取，contract 保護長期維護成本；四者對應不同 evidence，也需要不同 release gate 判讀。&lt;/p>
&lt;h2 id="實作基準先寫出狀態契約">實作基準：先寫出狀態契約&lt;/h2>
&lt;p>狀態契約的責任是讓 migration 先有可驗證的語意邊界。這篇的範例把 &lt;code>orders.status&lt;/code> 裡混合的訂單生命週期與付款語意拆開：訂單仍用 &lt;code>status&lt;/code> 表示 &lt;code>created&lt;/code>、&lt;code>fulfilled&lt;/code>、&lt;code>cancelled&lt;/code> 這類流程狀態，付款結果則交給 &lt;code>payment_state&lt;/code> 表示 &lt;code>pending&lt;/code>、&lt;code>authorized&lt;/code>、&lt;code>captured&lt;/code>、&lt;code>failed&lt;/code> 與 &lt;code>refunded&lt;/code>。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>舊狀態&lt;/th>
 &lt;th>新欄位 &lt;code>payment_state&lt;/code>&lt;/th>
 &lt;th>判讀理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>pending_payment&lt;/code>&lt;/td>
 &lt;td>&lt;code>pending&lt;/code>&lt;/td>
 &lt;td>訂單已建立，付款結果仍未確認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>paid&lt;/code>&lt;/td>
 &lt;td>&lt;code>captured&lt;/code>&lt;/td>
 &lt;td>付款已完成，可進入出貨或履約流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>payment_failed&lt;/code>&lt;/td>
 &lt;td>&lt;code>failed&lt;/code>&lt;/td>
 &lt;td>付款失敗，需要重試或取消路由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>refunded&lt;/code>&lt;/td>
 &lt;td>&lt;code>refunded&lt;/code>&lt;/td>
 &lt;td>付款已逆向處理，客服與對帳要可查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>cancelled_before_pay&lt;/code>&lt;/td>
 &lt;td>&lt;code>pending&lt;/code>&lt;/td>
 &lt;td>沒有付款成功事實，只保留流程取消&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>manual_review_required&lt;/code>&lt;/td>
 &lt;td>&lt;code>pending&lt;/code>&lt;/td>
 &lt;td>付款狀態未完成，等待人工判讀&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/mapping-table/" data-link-title="Mapping Table" data-link-desc="說明遷移或轉換期間如何把舊語意明確對應到新語意">mapping table&lt;/a> 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query&lt;/a>、backfill job 與 incident decision log 的共同語意來源。Mapping table 留在工程師腦中時，後續 mismatch 會變成「資料看起來怪」；mapping table 進入 artifact 後，gate 就能判斷錯誤集中在哪個付款語意，而不是停在總筆數。&lt;/p>
&lt;h2 id="expand先建立相容窗口">Expand：先建立相容窗口&lt;/h2>
&lt;p>Expand phase 的核心責任是讓新資料結構先進入 production，同時保留舊程式的可運作性。以 &lt;code>payment_state&lt;/code> 為例，常見起點是新增 nullable 欄位、補上必要索引，並讓寫入路徑可以在新欄位缺值時仍使用舊 &lt;code>status&lt;/code> 判讀付款狀態。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COLUMN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">payment_state&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CONCURRENTLY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">idx_orders_payment_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payment_state&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">payment_state&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 SQL 的用途是示範 artifact 形狀。Nullable 欄位保留舊資料的相容窗口；partial index 讓新讀取路徑能先被驗證，同時避免把尚未 backfill 的歷史資料全部推進新查詢模型。不同資料庫會有不同線上 DDL 能力，release gate 要把 lock 行為、index build 進度與 replication lag 納入 checks。&lt;/p></description><content:encoded><![CDATA[<p>Schema migration rollout 證據（Schema Migration Rollout Evidence）的核心責任是把正式狀態的演進拆成可觀測、可放行、可停止與可回寫的服務路徑。這篇以訂單資料表的付款狀態欄位演進為例，示範資料庫變更如何從 schema design、backfill、cutover 交接到 evidence package、release gate 與 incident decision log。</p>
<h2 id="服務路徑與狀態責任">服務路徑與狀態責任</h2>
<p>這條服務路徑是 <code>checkout-api -&gt; order-db -&gt; payment-callback -&gt; reconciliation-job</code>。Checkout 建立訂單時先寫入訂單主檔與付款待確認狀態；payment callback 會更新付款結果；客服後台與對帳 job 會讀取同一筆訂單狀態來判斷是否需要補償、退款或人工處理。</p>
<p>本篇示範的變更是把原本單一 <code>status</code> 欄位中的付款語意拆到 <code>payment_state</code>。這個欄位屬於正式狀態，會影響使用者看到的訂單結果、付款回呼的冪等更新、客服查詢與對帳流程，因此 rollout 的核心是讓新舊狀態語意在過渡期同時成立；DDL 只是其中一個執行動作。</p>
<p>這條路徑的前置概念來自 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a> 與 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a>。1.2 定義欄位責任，1.3 定義哪些更新要在同一個交易邊界內成立，1.6 定義 expand、backfill、cutover 與 contract 的執行節奏。</p>
<h2 id="rollout-階段">Rollout 階段</h2>
<p>Migration rollout 的責任是把一次高風險資料變更切成多個可驗證階段。每個階段都要有輸入條件、完成訊號與停止條件，讓團隊能在資料漂移擴大前停下來。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>服務責任</th>
          <th>完成訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Expand</td>
          <td>新欄位與新程式碼能和舊版本共存</td>
          <td>新舊程式可同時讀寫，舊欄位仍可支撐服務</td>
      </tr>
      <tr>
          <td>Backfill</td>
          <td>歷史訂單補齊 <code>payment_state</code></td>
          <td>checkpoint 穩定前進，mismatch 維持在門檻內</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>讀取路徑改以新欄位為主</td>
          <td>新欄位讀取成功率與對帳結果達到放行條件</td>
      </tr>
      <tr>
          <td>Contract</td>
          <td>移除舊語意與舊寫入路徑</td>
          <td>舊欄位已無服務依賴，回寫與監控已更新</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是責任轉移。Expand 保護相容性，backfill 保護歷史資料，cutover 保護線上讀取，contract 保護長期維護成本；四者對應不同 evidence，也需要不同 release gate 判讀。</p>
<h2 id="實作基準先寫出狀態契約">實作基準：先寫出狀態契約</h2>
<p>狀態契約的責任是讓 migration 先有可驗證的語意邊界。這篇的範例把 <code>orders.status</code> 裡混合的訂單生命週期與付款語意拆開：訂單仍用 <code>status</code> 表示 <code>created</code>、<code>fulfilled</code>、<code>cancelled</code> 這類流程狀態，付款結果則交給 <code>payment_state</code> 表示 <code>pending</code>、<code>authorized</code>、<code>captured</code>、<code>failed</code> 與 <code>refunded</code>。</p>
<table>
  <thead>
      <tr>
          <th>舊狀態</th>
          <th>新欄位 <code>payment_state</code></th>
          <th>判讀理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>pending_payment</code></td>
          <td><code>pending</code></td>
          <td>訂單已建立，付款結果仍未確認</td>
      </tr>
      <tr>
          <td><code>paid</code></td>
          <td><code>captured</code></td>
          <td>付款已完成，可進入出貨或履約流程</td>
      </tr>
      <tr>
          <td><code>payment_failed</code></td>
          <td><code>failed</code></td>
          <td>付款失敗，需要重試或取消路由</td>
      </tr>
      <tr>
          <td><code>refunded</code></td>
          <td><code>refunded</code></td>
          <td>付款已逆向處理，客服與對帳要可查</td>
      </tr>
      <tr>
          <td><code>cancelled_before_pay</code></td>
          <td><code>pending</code></td>
          <td>沒有付款成功事實，只保留流程取消</td>
      </tr>
      <tr>
          <td><code>manual_review_required</code></td>
          <td><code>pending</code></td>
          <td>付款狀態未完成，等待人工判讀</td>
      </tr>
  </tbody>
</table>
<p>這張 <a href="/blog/backend/knowledge-cards/mapping-table/" data-link-title="Mapping Table" data-link-desc="說明遷移或轉換期間如何把舊語意明確對應到新語意">mapping table</a> 是 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、backfill job 與 incident decision log 的共同語意來源。Mapping table 留在工程師腦中時，後續 mismatch 會變成「資料看起來怪」；mapping table 進入 artifact 後，gate 就能判斷錯誤集中在哪個付款語意，而不是停在總筆數。</p>
<h2 id="expand先建立相容窗口">Expand：先建立相容窗口</h2>
<p>Expand phase 的核心責任是讓新資料結構先進入 production，同時保留舊程式的可運作性。以 <code>payment_state</code> 為例，常見起點是新增 nullable 欄位、補上必要索引，並讓寫入路徑可以在新欄位缺值時仍使用舊 <code>status</code> 判讀付款狀態。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="k">NULL</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></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">CONCURRENTLY</span><span class="w"> </span><span class="n">idx_orders_payment_state</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">payment_state</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span></span></span></code></pre></div><p>這段 SQL 的用途是示範 artifact 形狀。Nullable 欄位保留舊資料的相容窗口；partial index 讓新讀取路徑能先被驗證，同時避免把尚未 backfill 的歷史資料全部推進新查詢模型。不同資料庫會有不同線上 DDL 能力，release gate 要把 lock 行為、index build 進度與 replication lag 納入 checks。</p>
<p>應用程式在 expand 階段要支援 <a href="/blog/backend/knowledge-cards/read-compatibility/" data-link-title="Read Compatibility" data-link-desc="說明資料或服務演進期間讀取路徑如何同時支援新舊語意">read compatibility</a>。相容性較高的寫法是讀取時優先使用 <code>payment_state</code>，缺值時 fallback 到舊 <code>status</code> 的付款語意；寫入時則依交易邊界同步更新舊欄位與新欄位，直到 cutover 前都保留一致性檢查。</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">readPaymentState(order):
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  if order.payment_state is not null:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    return order.payment_state
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  return mapLegacyStatusToPaymentState(order.status)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">applyPaymentCallback(order, callback):
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  nextPaymentState = mapCallbackToPaymentState(callback)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  update orders
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    set status = mapPaymentStateToLegacyStatus(nextPaymentState),
</span></span><span class="line"><span class="ln">10</span><span class="cl">        payment_state = nextPaymentState
</span></span><span class="line"><span class="ln">11</span><span class="cl">    where id = order.id</span></span></code></pre></div><p>這段相容讀寫的重點是「同一個 callback 只產生一個付款判讀」。舊欄位與新欄位可以同時存在，但它們要由同一份 mapping function 產生，否則 payment callback、客服修復與 reconciliation job 會各自形成一套隱性規則。</p>
<p>這裡要特別看 <a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">dual write</a> 的風險。雙寫只表示兩個欄位都有被寫入，仍要用 validation query 驗證兩者語意是否一致。若付款回呼、手動退款與對帳修復走不同程式路徑，雙寫函式也要被這些路徑共同使用。</p>
<h3 id="dual-write-divergence-schema">Dual-write divergence schema</h3>
<p>Dual-write 的責任不只是「兩邊都寫」、是「兩邊寫的結果一致」。要證明這件事、需要明確的 divergence schema、否則事故當下無法區分 mapping bug 跟 race condition。</p>
<p>最小 divergence 紀錄欄位：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>order_id</code></td>
          <td>哪一筆訂單</td>
      </tr>
      <tr>
          <td><code>legacy_value</code></td>
          <td>舊欄位寫入後的值</td>
      </tr>
      <tr>
          <td><code>new_value</code></td>
          <td>新欄位寫入後的值</td>
      </tr>
      <tr>
          <td><code>expected_new</code></td>
          <td>用 mapping function 從 <code>legacy_value</code> 推算的預期新值</td>
      </tr>
      <tr>
          <td><code>divergence_type</code></td>
          <td><code>mapping-mismatch</code> / <code>race-condition</code> / <code>manual-override</code></td>
      </tr>
      <tr>
          <td><code>write_path</code></td>
          <td>哪個程式路徑寫的（callback / refund / manual / reconciliation）</td>
      </tr>
      <tr>
          <td><code>detected_at</code></td>
          <td>偵測時間</td>
      </tr>
  </tbody>
</table>
<p><code>expected_new</code> 跟 <code>new_value</code> 對不上、表示 mapping function 在某些 path 沒被使用、是 mapping bug。<code>legacy_value</code> 跟 <code>new_value</code> 對不上、且 <code>expected_new == legacy_value</code> 對得上、是 dual-write 本身少寫一筆、可能是 race condition 或部分失敗。兩種情況的修法完全不同、不分類會在事故當下亂修。</p>
<p>Dual-write 失敗回退策略：寫舊欄位成功、寫新欄位失敗時、不能直接 retry 新欄位（會跟主寫入競爭）。實務做法是把 divergence 寫進 outbox / repair queue、由 backfill 同類流程補。對應 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a> 的 outbox-style 設計。</p>
<h3 id="線上-ddl-的-vendor-差異">線上 DDL 的 vendor 差異</h3>
<p>Expand 階段加欄位 / 加索引、不同資料庫的 <em>阻塞行為</em> 差異極大、選錯時機會直接讓 production 鎖表。</p>
<ul>
<li><strong>PostgreSQL</strong>：<code>ALTER TABLE ADD COLUMN ... NULL</code> 是 metadata-only、不重寫 table。<code>ADD COLUMN ... NOT NULL DEFAULT ...</code> 在 PG 11+ 才是 metadata-only。<code>CREATE INDEX CONCURRENTLY</code> 不阻塞寫入、但更慢、且 transaction 中不能用。<code>ALTER TABLE ALTER COLUMN TYPE</code> 通常會重寫整張表、要先評估規模。</li>
<li><strong>MySQL / Aurora MySQL</strong>：<code>ALTER TABLE ... ALGORITHM=INSTANT</code> 是 8.0+ 的 metadata-only、5.7 則靠 <code>ALGORITHM=INPLACE</code> / <code>LOCK=NONE</code>。Aurora MySQL 還有 fast DDL（部分變更秒級完成、不重寫）。判讀重點是 <em>explicitly 指定 ALGORITHM</em>、不要讓 MySQL 自己選（可能掉回 COPY 算法、整張表複製）。</li>
<li><strong>Spanner</strong>：schema change 預設非阻塞、後端 async 補欄位。新欄位 read 在 schema change 完成前可能讀不到、應用層要容忍。</li>
<li><strong>DynamoDB</strong>：表本身沒 schema、但 <em>GSI（Global Secondary Index）創建是 async</em>、可能跑數小時、且新 GSI 在 backfill 完成前查不到完整資料。判讀重點：cutover 不能假設新 GSI 立即可用、要等 <code>IndexStatus = ACTIVE</code>。</li>
<li><strong>Cosmos DB</strong>：document 級別無 schema、新 indexed path 加進 indexing policy 後、後端 <em>re-index</em> 整個 partition、期間 RU consumption 飆升。</li>
</ul>
<p>各 vendor 的線上 DDL evidence 都要包含：操作開始時間、預估完成時間、是否阻塞讀寫、實際 lock duration。expand gate 通過條件不能只看 DDL 跑完、要看 <em>所有副效應收斂</em>（index status active、re-indexing 完成、replica 同步）。</p>
<p>對應 vendor pages：<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</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a>、<a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora</a>、<a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner</a>、<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>、<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> 的線上 DDL 段。</p>
<h2 id="backfill把歷史資料變成可驗證進度">Backfill：把歷史資料變成可驗證進度</h2>
<p>Backfill phase 的核心責任是把歷史資料補齊成可追蹤、可暫停、可重試的進度。訂單表通常會同時承擔交易查詢、客服查詢與對帳查詢；backfill 若只追求速度，容易和線上流量競爭 I/O、放大 replication lag 或改變查詢計畫。</p>
<p>Backfill job 應以 checkpoint 管理進度。每批選取固定範圍的訂單，轉換 <code>status</code> 到 <code>payment_state</code>，寫入後立刻產生該批 validation query 結果。批次大小要能依延遲、鎖等待、replication lag 與線上錯誤率調整。</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">checkpoint:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  migration_id: orders-payment-state-2026-05
</span></span><span class="line"><span class="ln">3</span><span class="cl">  last_order_id: 18420000
</span></span><span class="line"><span class="ln">4</span><span class="cl">  batch_size: 5000
</span></span><span class="line"><span class="ln">5</span><span class="cl">  started_at: 2026-05-11T02:10:00Z
</span></span><span class="line"><span class="ln">6</span><span class="cl">  completed_at: 2026-05-11T02:12:40Z
</span></span><span class="line"><span class="ln">7</span><span class="cl">  rows_scanned: 5000
</span></span><span class="line"><span class="ln">8</span><span class="cl">  rows_updated: 4921
</span></span><span class="line"><span class="ln">9</span><span class="cl">  mismatch_count: 3</span></span></code></pre></div><p>Checkpoint 的角色是把 backfill 變成可恢復流程。<code>last_order_id</code> 告訴下一批從哪裡繼續，<code>rows_updated</code> 與 <code>mismatch_count</code> 告訴 gate 這批是否可以被納入放行證據，時間欄位則讓 replication lag、slow query 與錯誤率能回到同一個觀察窗口。</p>
<p><a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">Validation query</a> 的責任是證明語意一致。最小集合包含總筆數、已補筆數、缺值筆數、新舊語意不一致樣本、每批耗時、慢查詢與 replication lag。這些查詢要保留 <a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 與 <a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a>，後續才能進入 <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>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">total_rows</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="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="n">FILTER</span><span class="w"> </span><span class="p">(</span><span class="k">WHERE</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">missing_payment_state</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="n">FILTER</span><span class="w"> </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="k">WHERE</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span><span class="k">AND</span><span class="w"> </span><span class="n">payment_state</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="n">map_legacy_status_to_payment_state</span><span class="p">(</span><span class="n">status</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="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">mismatch_rows</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="mi">18415001</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="mi">18420000</span><span class="p">;</span></span></span></code></pre></div><p>Validation query 要和 mapping table 共用同一個語意。資料庫端缺少同一份 mapping function 時，查詢至少要把 mapping 規則展開成明確 CASE expression，並把 query version 保存在 evidence package；這樣事後才能知道 mismatch 是資料錯誤、mapping 規則改變，還是查詢本身落後。</p>
<h2 id="cutover先切讀取再收斂寫入">Cutover：先切讀取，再收斂寫入</h2>
<p>Cutover phase 的核心責任是把服務判讀權交給新欄位，同時保留可回退窗口。對訂單付款狀態來說，切換順序通常先從低風險讀取路徑開始，例如客服後台與內部對帳，再進入 checkout 查詢與使用者可見狀態；每一批切換都要有自己的 <a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover window</a>。</p>
<p>讀取 cutover 的 <a href="/blog/backend/knowledge-cards/stop-condition/" data-link-title="Stop Condition" data-link-desc="說明變更、實驗或事故處理何時必須暫停、回退或改路線">stop condition</a> 要比寫入 cutover 更早觸發。新欄位讀取後出現 mismatch、客服查詢結果漂移、對帳 job 補償量異常時，先回到 <a href="/blog/backend/knowledge-cards/fallback-read/" data-link-title="Fallback Read" data-link-desc="說明讀取路徑切換失敗時如何暫時回到舊資料語意或舊讀取來源">fallback read</a>，讓錯誤限制在判讀層，再重新驗證寫入收斂條件。</p>
<p>寫入 cutover 要確認所有更新來源都已對齊。付款回呼、手動修復、退款、訂單取消與 reconciliation job 都可能更新付款狀態；只切主 checkout 寫入路徑會留下長尾漂移。完成 cutover 前，要用 audit query 確認仍在寫舊欄位的程式路徑已經歸零或被納入例外清單。</p>
<h3 id="shadow-read-patterncutover-前的讀取驗證">Shadow read pattern：cutover 前的讀取驗證</h3>
<p>Shadow read 的責任是讓新讀取路徑在 <em>真實流量</em> 下被驗證、但 <em>不影響使用者結果</em>。這跟 dual-write 是對偶機制：dual-write 證寫入收斂、<a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a> 證讀取分歧。</p>
<p>實作模式：</p>
<ol>
<li>每一筆讀取請求、同時用 <em>舊邏輯</em> 跟 <em>新邏輯</em> 查一次。</li>
<li>回給用戶的仍是舊邏輯結果（用戶體驗不變）。</li>
<li>在背景把兩個結果差異寫進 divergence log。</li>
<li>收集足夠樣本後、再決定切換 cutover。</li>
</ol>





<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">readPaymentStateWithShadow(order):
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  legacy = mapLegacyStatusToPaymentState(order.status)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  new_result = order.payment_state ?? legacy
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  if legacy != new_result:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    asyncLogDivergence({
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      order_id: order.id,
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      legacy: legacy,
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      new: new_result,
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      sample_at: now(),
</span></span><span class="line"><span class="ln">10</span><span class="cl">      caller: requestContext.caller,
</span></span><span class="line"><span class="ln">11</span><span class="cl">    })
</span></span><span class="line"><span class="ln">12</span><span class="cl">  return legacy  // 用戶仍拿舊邏輯結果</span></span></code></pre></div><p>Shadow read 的判讀重點：</p>
<ul>
<li><strong>抽樣率</strong>：1% / 10% / 100% — 高流量場景全量 shadow 會雙倍 DB 讀取、要先評估容量。Cosmos DB / DynamoDB 的 RU 成本要乘 2。</li>
<li><strong>分歧分類</strong>：跟 dual-write 一樣、divergence 要分類（mapping bug / race condition / <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a>）、不分類無法定位修法。</li>
<li><strong>覆蓋條件</strong>：要驗證所有 caller path（checkout / support / reconciliation / external API）都跑過 shadow、否則 cutover 後可能踩到沒測試過的 path。</li>
<li><strong>退場條件</strong>：shadow read 不該長期跑、會增加負載。設明確 sunset deadline、cutover 完成後一週內移除。</li>
</ul>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato TiDB → DynamoDB migration</a> — migration 期間用 shadow read 持續驗證 mapping 規則、抓到 mapping drift。</p>
<p>Dual-write 跟 shadow read 的選擇不是互斥、是依風險組合：</p>
<table>
  <thead>
      <tr>
          <th>風險場景</th>
          <th>建議組合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新邏輯只影響讀取（cache、index）</td>
          <td>shadow read 即可、不需要 dual-write</td>
      </tr>
      <tr>
          <td>新欄位是 source of truth</td>
          <td>dual-write 必要、cutover 前加 shadow read 驗證</td>
      </tr>
      <tr>
          <td>跨 service 共用欄位</td>
          <td>dual-write + shadow read + cross-service contract test</td>
      </tr>
      <tr>
          <td>跨 region migration</td>
          <td>dual-write + shadow read + 跨 region replication evidence</td>
      </tr>
  </tbody>
</table>
<h2 id="multi-region-與跨服務協調">Multi-region 與跨服務協調</h2>
<p>Migration 跨越 region 或多個 service 時、rollout 順序錯誤是最常見的失敗模式。Service A 切到新欄位、service B 還在讀舊欄位、結果整條業務流量看到不一致。</p>
<h3 id="multi-region-rollout-順序">Multi-region rollout 順序</h3>
<p>跨 region 的 schema migration 要從 <em>最後寫入點</em> 開始 expand、從 <em>最後讀取點</em> 開始 cutover。先 expand 寫端、再 expand 讀端；先 cutover 讀端、再 cutover 寫端。順序反了會在過渡期讀到沒被寫的新欄位、或寫了沒被讀的新欄位。</p>
<p>實務步驟：</p>
<ol>
<li><strong>Schema expand</strong>：所有 region 同步加新欄位（先寫端再讀端、不能跳）。確認跨 region replication lag 在新欄位上收斂、再進下一步。</li>
<li><strong>Backfill</strong>：可以平行跑、但每 region 各自 checkpoint、不共用。某 region backfill stuck 不應該卡住其他 region。</li>
<li><strong>Cutover read</strong>：region by region 切讀、用 canary region 先試 24-48 小時、再擴散。</li>
<li><strong>Cutover write</strong>：所有 region 都切完讀、再統一切寫。寫端切換比讀端更敏感、跨 region 寫差異會放大成跨 region inconsistency。</li>
</ol>
<p>對應 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 的跨 region consistency 段。</p>
<h3 id="cross-service-migration-協調">Cross-service migration 協調</h3>
<p>當 schema 變更影響多個 service 時、API contract 是 <em>鬆耦合</em> 介面、不該讓所有 service 同步切換。</p>
<p>協調機制：</p>
<ul>
<li><strong>新欄位先在 API 是 optional</strong>：API contract 加新欄位、預設 nullable / optional。下游 service 可選擇何時讀。</li>
<li><strong>舊欄位保留至少一個版本週期</strong>：API 不能跟 DB schema 同步 contract、否則下游沒時間切。實務上保留 1-2 季、給下游充足 cutover 窗口。</li>
<li><strong>owner-by-owner cutover roster</strong>：明確列出每個下游 service 的 owner、預計 cutover 時間、目前狀態。常用工具是共享 dashboard、不是散落的 ticket。</li>
<li><strong>Contract test</strong>：每個下游 service 對新欄位都要有 contract test、在 CI gate 跑過。避免上游 cutover 後下游才發現沒讀對。</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato TiDB → DynamoDB</a> — 跨多個 service 的 access pattern 變更、必須每個 service 各自驗證、不能假設「DB 切了就好」。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>資料庫 migration 的 evidence package 負責證明資料演進是否可判讀。這份 package 要把 validation query、時間窗、資料限制與 owner 包成後續放行與事故判斷可引用的證據，dashboard 只作為摘要入口。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>訂單欄位演進中的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>validation query、DB metric、migration job log、audit log</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>expand、backfill、cutover 各階段的查詢窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>row count、mismatch sample、replication lag、slow query</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>database owner、checkout owner、reconciliation owner</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>query 延遲、replica freshness、sample completeness</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>未覆蓋的手動修復路徑、低流量 tenant、延遲回呼</td>
      </tr>
  </tbody>
</table>
<p>Source 欄位要保留資料來源的能力邊界。Validation query 能證明欄位語意一致，DB metric 能看出 latency 與 lag，job log 能追進度，audit log 能判斷是否有高權限修復行為。把這些來源混在一起會讓下游誤判證據的用途。</p>
<p><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a> 欄位要直接寫出限制。若查詢只跑 primary、replica lag 還在回復、某些 tenant 因資料遮罩未被抽樣，這些限制要跟 evidence 一起交給 release gate，讓 gate 能以證據完整度決定是否放行。</p>





<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">evidence_package</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">orders-payment-state-cutover-batch-37</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">source</span><span class="p">:</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">validation_query</span><span class="p">:</span><span class="w"> </span><span class="l">q_orders_payment_state_batch_37</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">db_metric</span><span class="p">:</span><span class="w"> </span><span class="l">replication_lag_orders_primary</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">job_log</span><span class="p">:</span><span class="w"> </span><span class="l">backfill_orders_payment_state_2026_05</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">time_range</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T02:10:00Z</span><span class="l">/2026-05-11T02:20:00Z</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">owner</span><span class="p">:</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">database</span><span class="p">:</span><span class="w"> </span><span class="l">data-platform-oncall</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">service</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-oncall</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">reconciliation</span><span class="p">:</span><span class="w"> </span><span class="l">finance-ops-owner</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">data_quality</span><span class="p">:</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">replica_freshness</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;primary only; replica lag still recovering&#34;</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">sample_completeness</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;tenant tier enterprise covered; sandbox tenants excluded&#34;</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">confidence</span><span class="p">:</span><span class="w"> </span><span class="l">suspected</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">known_gap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;manual refund repair path not yet sampled&#34;</span></span></span></code></pre></div><p>這份 package 故意把 <code>confidence</code> 標成 <code>suspected</code>。原因是 evidence 已能支持 backfill 繼續前進，但還不足以支持使用者可見讀取 cutover；這種中間狀態要被明確寫出，gate 才能做分階段決策。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Schema migration 的 release gate 負責判斷下一階段是否可以放行。它接收 evidence package，但決策語言要回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>：<code>Gate decision</code>、<code>Checks</code>、<code>Stop condition</code>、<code>Rollback window</code>、<code>Owner</code>。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>這條路徑的最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批 backfill、暫停 cutover、回到 fallback read 或 fail-forward</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td>compatibility result、mismatch rate、replication lag、slow query</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>mismatch 超門檻、交易錯誤率上升、lag 超窗口、客服查詢漂移</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>讀取 fallback 可用時間、舊欄位可支撐多久、contract 前最後回退點</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>migration owner、service owner、on-call owner</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a> 要用服務語言書寫。<code>migration pass</code> 這種結論對下游不夠具體；<code>放行 10% 訂單 backfill</code>、<code>暫停使用者可見讀取 cutover</code>、<code>維持 fallback read 24 小時</code> 才能讓執行團隊知道下一步。</p>
<p><a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">Rollback window</a> 是資料庫 migration 的關鍵欄位。Expand 與 backfill 階段通常能回到舊讀取；cutover 後仍可 fallback；contract 後舊語意被移除，回退會變成資料修復或 <a href="/blog/backend/knowledge-cards/fail-forward/" data-link-title="Fail-forward" data-link-desc="說明無法回到舊狀態時如何用受控前進完成修復">fail-forward</a>。gate 要在每階段說清楚目前還剩哪種退路。</p>





<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">release_gate</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">gate_decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;allow next 10% backfill; block customer-visible read cutover&#34;</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">checks</span><span class="p">:</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">mismatch_rate</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0.04%, below 0.1% batch threshold&#34;</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">replication_lag</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;p95 12s, below 30s stop condition&#34;</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">slow_query</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;no new support-admin slow query above 500ms&#34;</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">stop_condition</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;mismatch_rate &gt;= 0.1% for two consecutive batches&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;replication_lag &gt;= 30s for 10 minutes&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span>- <span class="s2">&#34;support-admin query drift confirmed by reconciliation owner&#34;</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">rollback_window</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;fallback read available until contract phase starts&#34;</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">owner</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-oncall</span></span></span></code></pre></div><p>這份 gate record 把「繼續 backfill」和「暫緩讀取 cutover」拆成兩個決策。資料庫 migration 常見的判讀問題是 evidence 只支撐下一批資料修補，還支撐不了使用者可見行為切換。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>Migration 進入 production 後，pause、rollback 與 fail-forward 都是事故決策。這些決策要同步寫入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>，讓事中交班與事後復盤能回放當時的證據與限制。</p>
<p>常見決策包括暫停 backfill、降低 batch size、回到舊讀取、停止 contract、手動修補 mismatch、選擇 fail-forward。每筆都要保留 <code>Timestamp</code>、<code>Decision</code>、<code>Context</code>、<code>Evidence</code>、<code>Owner</code>、<code>Expected effect</code> 與 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a>。</p>
<p>例如 cutover 後發現客服查詢 mismatch 升高，decision log 可以寫成：</p>





<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">incident_decision</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">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T03:05:00Z</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">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;rollback support-admin read path to legacy status fallback&#34;</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">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;support-admin mismatch increased after internal read cutover&#34;</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">evidence</span><span class="p">:</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">query</span><span class="p">:</span><span class="w"> </span><span class="l">q_orders_payment_state_support_mismatch</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">window</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T02:35:00Z</span><span class="l">/2026-05-11T03:05:00Z</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">interpretation</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;suspected callback mapping drift&#34;</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">owner</span><span class="p">:</span><span class="w"> </span><span class="l">checkout-incident-commander</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">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;support ticket misclassification returns to baseline&#34;</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">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;mismatch remains above threshold after 15 minutes&#34;</span></span></span></code></pre></div><p>這種記錄能避免事後只剩「當時有回退」的模糊敘事。後續 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log and Write-back 實作示範</a> 可承接同一組決策紀錄，把缺少 validation、owner 或 runbook 的地方回寫成改善項。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>判讀訊號的責任是讓讀者知道何時該繼續、何時該停、何時該改路線。Migration 訊號要同時看資料正確性、線上健康度與回退窗口。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>mismatch rate 持續低於門檻</td>
          <td>新舊欄位語意大致一致</td>
          <td>放行下一批 backfill 或低風險讀取 cutover</td>
      </tr>
      <tr>
          <td>mismatch 樣本集中在特定 callback</td>
          <td>轉換函式或特定付款路徑語意不一致</td>
          <td>暫停 cutover，修 mapping 後重跑該批</td>
      </tr>
      <tr>
          <td>dual-write divergence 分布偏向 mapping</td>
          <td>mapping function 在某 path 沒被使用</td>
          <td>找出該 path、強制走共用 mapping function</td>
      </tr>
      <tr>
          <td>dual-write divergence 偏向 race</td>
          <td>部分寫入失敗、寫順序問題</td>
          <td>切到 outbox-based dual-write、別直連</td>
      </tr>
      <tr>
          <td>shadow read 抽樣 RU 飆升</td>
          <td>shadow 讀取沒設抽樣率、雙倍負載</td>
          <td>降低抽樣率、或改成 off-peak shadow</td>
      </tr>
      <tr>
          <td>replication lag 在 backfill 升高</td>
          <td>migration 與線上查詢競爭資源</td>
          <td>降低 batch size，避開 peak，延長觀察窗口</td>
      </tr>
      <tr>
          <td>slow query 出現在客服查詢</td>
          <td>新欄位索引或查詢模型未對齊</td>
          <td>回到 fallback read，補 index 或改查詢條件</td>
      </tr>
      <tr>
          <td>DynamoDB GSI 仍在 building</td>
          <td>cutover 前依賴未 ACTIVE 的 GSI</td>
          <td>等 GSI ACTIVE 再切讀、別假設立即可用</td>
      </tr>
      <tr>
          <td>跨 region replica lag 在新欄位上漂移</td>
          <td>expand 階段沒等所有 region 收斂</td>
          <td>暫停 backfill、等 region 同步</td>
      </tr>
      <tr>
          <td>某下游 service 沒 cutover</td>
          <td>cross-service 協調沒做 contract test</td>
          <td>補 contract test、推遲 contract 階段</td>
      </tr>
      <tr>
          <td>contract 前仍有舊欄位寫入</td>
          <td>更新來源尚未完全收斂</td>
          <td>延後 contract，盤點寫入來源與 owner</td>
      </tr>
  </tbody>
</table>
<p>這些訊號要放回服務路徑判讀。Mismatch 要看集中在哪個業務入口；若 mismatch 只出現在延遲付款 callback，它代表外部 provider 回呼語意未對齊。Replication lag 要看是否和 backfill 批次對位；若它只在 backfill 批次出現，gate 應調整 migration 節奏，再判斷 schema 設計是否需要修正。</p>
<p>Dual-write 跟 shadow read 的 divergence 要分開看 — 兩者偵測不同層的問題。Dual-write divergence 偏向 mapping bug 或 race condition；shadow read divergence 偏向讀取邏輯漂移或 stale read。混在同一個 dashboard 會讓 reviewer 看不出問題真正在哪一層。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 schema migration 寫成 DDL 任務，會讓風險集中在切換當下。穩定做法是先建立相容窗口，再用 evidence 證明資料語意已經跟上，最後才收斂舊路徑。</p>
<p>把 validation query 當成事後對帳，也會削弱 rollout 控制。Validation query 適合在 expand、backfill、cutover 每一階段都產生證據，讓 release gate 能在風險擴大前停下來。</p>
<p>把 rollback 寫成單一動作容易誤導團隊。資料庫 migration 的 rollback 會隨階段改變：expand 可回退 schema 使用，backfill 可暫停與重跑，cutover 可回到 fallback read，contract 後多半只能做資料修復或 fail-forward。</p>
<p>把 dual-write 跟 shadow read 當成同一個工具。兩者偵測不同層、結合使用可以互補、互相替代會留下盲點。Dual-write 不跑 shadow read、cutover 後可能踩到沒驗過的讀取 path；shadow read 不跑 dual-write、新欄位可能在某些寫路徑根本沒被寫進去。</p>
<p>把線上 DDL 當「一個 SQL 跑完就好」。各 vendor 的 DDL 語意差異大、PostgreSQL 的 <code>ADD COLUMN NOT NULL DEFAULT</code> 在 PG 10 重寫整張表、PG 11+ 是 metadata-only；MySQL 不指定 <code>ALGORITHM=INSTANT</code> 可能掉回 COPY。Expand evidence 要包含 <em>實際 lock duration</em>、不是只看 DDL 是否回傳成功。</p>
<p>只在主寫入路徑切 cutover、忘記補償流程跟 reconciliation job 也會寫舊欄位。這些長尾寫入會在 contract 階段才暴露、那時候已經沒有 fallback 可走。Cutover 前要 audit 所有寫舊欄位的程式路徑、不只看主流程。</p>
<h2 id="案例回寫">案例回寫</h2>
<p><a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 營運後技術轉換</a> 可以回寫這篇的決策層。當服務營運後需要拆欄位、拆庫、分片或升級儲存引擎，先用 0.C4 判斷「為什麼要換」，再用本篇判斷「進入 production 後如何證明每一步成立」。</p>
<p><a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 可以回寫這篇的事故層。該事件顯示資料一致性優先時，團隊需要可回放的 fail-forward / fail-back 判準；本篇則把這個需求落到 migration rollout 的 evidence、gate 與 decision log。</p>
<p>這兩個案例共同支撐的是「資料狀態演進需要證據閉環」。0.C4 提供轉換動機與選型壓力，GitHub 事故提供資料一致性與恢復決策的代價；兩者都不直接替代 validation query、release gate 與 decision log 的實作細節。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位責任、命名與查詢模型回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design</a>。</li>
<li>與 1.3 的交接：付款回呼、手動修復與對帳更新的交易邊界回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a>。</li>
<li>與 1.6 的交接：expand、backfill、cutover 與 contract 的執行流程回到 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">資料庫轉換實作</a>。</li>
<li>與 4.20 / 4.22 的交接：validation query、row count、lag 與 slow query 進入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">Checkout API Evidence Package</a>。</li>
<li>與 6.11 / 6.8 / 6.25 的交接：migration 可逆性與放行條件進入 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a> 與 <a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">Provider Dependency Release Gate</a>。</li>
<li>與 8.19 / 8.23 的交接：pause、rollback、fail-forward 與 write-back 進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a> 與 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">Control Plane Decision Log and Write-back</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把資料庫 migration 的 evidence 交給 release gate，接著讀 <a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate 實作示範</a>，並把 provider 依賴示範中的 gate 欄位改寫成 migration gate 欄位。要看下一條分類服務路徑，接著進 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 Cache / Redis 模組</a> 的 <code>Cache migration and stampede rollback</code> 服務路徑。</p>
<p>跨 vendor schema migration 深入：</p>
<ul>
<li><a href="/blog/backend/01-database/vendors/spanner/schema-migration-interleaved-tables/" data-link-title="Spanner Schema Migration Without Downtime &#43; Interleaved Tables" data-link-desc="Spanner DDL 是 long-running operation、用 TrueTime 給每次 schema change 分配 version timestamp、所有 read / write 對應自己 transaction timestamp 看到對應 schema。Interleaved table 是 storage-level parent-child 物理交錯、不是 logical FK。本文走 schema change lifecycle、interleaved layout 機制、backfill capacity 影響、5 production 踩雷、跟 PostgreSQL online schema change 對照">Spanner interleaved table 的 schema migration</a> — 全球分散式表結構變更的 evidence shape</li>
<li><a href="/blog/backend/01-database/vendors/aurora/migrate-from-self-managed-pg-mysql/" data-link-title="從自管 PostgreSQL / MySQL 遷到 Aurora：operational redesign migration playbook" data-link-desc="PostgreSQL / MySQL → Aurora 的 Type C operational redesign hybrid playbook、6 規格面（Driver / Diff audit / Phase plan / Evidence / Cutover / Cleanup）、Standard Chartered 合規 lead time 模型、Netflix 非 all-purpose store 邊界">Aurora 從自管 PostgreSQL / MySQL 遷入</a> — schema 比對與 dual-write 證據鏈</li>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a> — multi-API document 在 rollout 階段的相容性 evidence</li>
</ul>
]]></content:encoded></item><item><title>1.8 State Ownership 與 Query Boundary</title><link>https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/</guid><description>&lt;p>State ownership 與 query boundary 的核心責任是先定義資料由誰承擔正式判斷、再定義不同查詢路徑能回答什麼問題。進入 MySQL、PostgreSQL、MSSQL 或其他資料庫前、讀者需要先知道資料庫同時是儲存工具與服務狀態的責任邊界。&lt;/p>
&lt;p>本章從 source of truth 的責任分層開始、引入 CQRS / event sourcing / materialized view 等模式、最後處理四種 query 邊界的設計。讀完後讀者能回答：哪些資料是正式狀態、什麼時候該分讀寫 model、materialized view 怎麼用、replica lag 怎麼影響 query。&lt;/p>
&lt;h2 id="state-ownership">State Ownership&lt;/h2>
&lt;p>State ownership 的責任是判斷哪些資料是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、哪些資料屬於 cache、search index、event log 或報表副本。正式狀態會影響交易結果、權限判斷、對帳與客服修復、因此需要清楚的 owner、schema、驗證方式與變更流程。&lt;/p>
&lt;p>訂單狀態、付款狀態、會員方案、權限授權與發票紀錄通常屬於正式狀態。商品搜尋索引、快取值、統計摘要與推薦結果通常是派生狀態；派生狀態可以錯過短暫更新、但正式狀態需要能被追溯、修復與稽核。&lt;/p>
&lt;h2 id="canonical-state-vs-derived-state">Canonical State vs Derived State&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Canonical state&lt;/th>
 &lt;th>Derived state&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>角色&lt;/td>
 &lt;td>source of truth&lt;/td>
 &lt;td>從 canonical 計算 / 同步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入&lt;/td>
 &lt;td>用戶 / 業務操作&lt;/td>
 &lt;td>從 canonical 推&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性&lt;/td>
 &lt;td>strong / serializable&lt;/td>
 &lt;td>eventual 通常夠用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復&lt;/td>
 &lt;td>必須能精確修復&lt;/td>
 &lt;td>可以「砍掉重建」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>範例&lt;/td>
 &lt;td>訂單、付款、餘額&lt;/td>
 &lt;td>搜尋 index、recommendation、daily summary&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Canonical state 的特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>業務決策依據（付款、權限）&lt;/li>
&lt;li>不能從其他地方重建（一旦丟、無法找回）&lt;/li>
&lt;li>需要 audit log、point-in-time recovery、backup&lt;/li>
&lt;li>通常在 OLTP DB（PostgreSQL / Aurora / Spanner）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Derived state 的特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>從 canonical 推算出來&lt;/li>
&lt;li>可以「rebuild」（lazy 或 eager）&lt;/li>
&lt;li>失效可接受（用戶可能看到舊的）&lt;/li>
&lt;li>通常在 cache / search / analytics store&lt;/li>
&lt;li>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache&lt;/a> 配對快取、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store&lt;/a> feature&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>設計原則&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>同一資料 &lt;em>不能&lt;/em> 同時是兩個地方的 canonical → 衝突時不知道信誰&lt;/li>
&lt;li>寫入永遠先寫 canonical、再 propagate 到 derived&lt;/li>
&lt;li>derived 出錯只能 rebuild、不能拿來「修正 canonical」&lt;/li>
&lt;/ul>
&lt;h2 id="cqrs-在資料庫情境的應用">CQRS 在資料庫情境的應用&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS&lt;/a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：state ownership 的決策如何影響你要不要分離讀寫模型。&lt;/p>
&lt;p>State ownership 跟 CQRS 的交叉點是：當 canonical state 的 schema 為寫入正確性最佳化（normalize、強一致、transaction boundary 清楚），但讀取面的多種消費者各自需要不同的反正規化形狀（列表頁要扁平 summary、報表要聚合、搜尋要全文索引），canonical schema 無法同時服務這些讀取需求。這時候分離 write model 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 是解決形狀不對稱的方式。&lt;/p></description><content:encoded><![CDATA[<p>State ownership 與 query boundary 的核心責任是先定義資料由誰承擔正式判斷、再定義不同查詢路徑能回答什麼問題。進入 MySQL、PostgreSQL、MSSQL 或其他資料庫前、讀者需要先知道資料庫同時是儲存工具與服務狀態的責任邊界。</p>
<p>本章從 source of truth 的責任分層開始、引入 CQRS / event sourcing / materialized view 等模式、最後處理四種 query 邊界的設計。讀完後讀者能回答：哪些資料是正式狀態、什麼時候該分讀寫 model、materialized view 怎麼用、replica lag 怎麼影響 query。</p>
<h2 id="state-ownership">State Ownership</h2>
<p>State ownership 的責任是判斷哪些資料是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、哪些資料屬於 cache、search index、event log 或報表副本。正式狀態會影響交易結果、權限判斷、對帳與客服修復、因此需要清楚的 owner、schema、驗證方式與變更流程。</p>
<p>訂單狀態、付款狀態、會員方案、權限授權與發票紀錄通常屬於正式狀態。商品搜尋索引、快取值、統計摘要與推薦結果通常是派生狀態；派生狀態可以錯過短暫更新、但正式狀態需要能被追溯、修復與稽核。</p>
<h2 id="canonical-state-vs-derived-state">Canonical State vs Derived State</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Canonical state</th>
          <th>Derived state</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>角色</td>
          <td>source of truth</td>
          <td>從 canonical 計算 / 同步</td>
      </tr>
      <tr>
          <td>寫入</td>
          <td>用戶 / 業務操作</td>
          <td>從 canonical 推</td>
      </tr>
      <tr>
          <td>一致性</td>
          <td>strong / serializable</td>
          <td>eventual 通常夠用</td>
      </tr>
      <tr>
          <td>修復</td>
          <td>必須能精確修復</td>
          <td>可以「砍掉重建」</td>
      </tr>
      <tr>
          <td>範例</td>
          <td>訂單、付款、餘額</td>
          <td>搜尋 index、recommendation、daily summary</td>
      </tr>
  </tbody>
</table>
<p><strong>Canonical state 的特徵</strong>：</p>
<ul>
<li>業務決策依據（付款、權限）</li>
<li>不能從其他地方重建（一旦丟、無法找回）</li>
<li>需要 audit log、point-in-time recovery、backup</li>
<li>通常在 OLTP DB（PostgreSQL / Aurora / Spanner）</li>
</ul>
<p><strong>Derived state 的特徵</strong>：</p>
<ul>
<li>從 canonical 推算出來</li>
<li>可以「rebuild」（lazy 或 eager）</li>
<li>失效可接受（用戶可能看到舊的）</li>
<li>通常在 cache / search / analytics store</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> 配對快取、<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store</a> feature</li>
</ul>
<p><strong>設計原則</strong>：</p>
<ul>
<li>同一資料 <em>不能</em> 同時是兩個地方的 canonical → 衝突時不知道信誰</li>
<li>寫入永遠先寫 canonical、再 propagate 到 derived</li>
<li>derived 出錯只能 rebuild、不能拿來「修正 canonical」</li>
</ul>
<h2 id="cqrs-在資料庫情境的應用">CQRS 在資料庫情境的應用</h2>
<p><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：state ownership 的決策如何影響你要不要分離讀寫模型。</p>
<p>State ownership 跟 CQRS 的交叉點是：當 canonical state 的 schema 為寫入正確性最佳化（normalize、強一致、transaction boundary 清楚），但讀取面的多種消費者各自需要不同的反正規化形狀（列表頁要扁平 summary、報表要聚合、搜尋要全文索引），canonical schema 無法同時服務這些讀取需求。這時候分離 write model 跟 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 是解決形狀不對稱的方式。</p>
<p>資料庫情境的 CQRS 有不同的實作強度：</p>
<p><strong>最輕量 — 同 DB 不同 query path</strong>：寫入走 canonical table，讀取走 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a> 或反正規化 view。同一個 PostgreSQL 裡用 materialized view 就能實現最基本的讀寫分離，不需要兩個 DB、不需要事件同步。適合讀寫形狀不同但流量規模還不需要獨立擴展的階段。</p>
<p><strong>中度 — 同 DB 加 read replica</strong>：寫入走 primary，列表跟報表走 read replica。Replica lag 決定哪些 query 能走 replica（見下方 Replica Lag 段）。適合讀取流量開始壓迫寫入的階段。</p>
<p><strong>完整 — 獨立 read store</strong>：寫入走 OLTP DB，讀取走獨立的 analytics store（BigQuery、Athena）或搜尋引擎（Elasticsearch）。透過 CDC 或事件同步維護 read store。適合讀取形狀、流量、SLA 都跟寫入完全不同的階段。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> — 交易層（OLTP）跟資料層（BigQuery / Athena）分開。<a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a> — on-prem OLTP + GCP BigQuery analytics。</p>
<h2 id="event-sourcing-與-state-ownership">Event Sourcing 與 State Ownership</h2>
<p><a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">Event sourcing</a> 的概念定義、設計判準與代價見知識卡。本段聚焦在資料庫層面：event sourcing 怎麼改變 state ownership 跟 query boundary。</p>
<p>Event sourcing 把 state ownership 的正式紀錄從 mutable row 改成 append-only <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a>。這個改變影響本章的每一個面向：</p>
<p><strong>對 canonical / derived 分類的影響</strong>：採用 event sourcing 後，event log 是 canonical state，current state 變成 derived state。這跟傳統 CRUD 架構相反 — 傳統架構中 current state（mutable row）是 canonical，歷史紀錄（audit log）是 derived。</p>
<p><strong>對 query boundary 的影響</strong>：event log 不適合直接服務交易查詢跟列表查詢（每次 replay 整條事件流太慢）。Event sourcing 幾乎必然搭配 <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 維護 read model — projection 持續消費事件流、更新反正規化的查詢 view。交易查詢讀 projection 的輸出而非直接讀 event log。</p>
<p><strong>對修復流程的影響</strong>：傳統架構的資料修復是「直接改 row」；event sourcing 的修復是「發一筆補償事件（compensating event）」。修復本身也是事件、會被記錄在 event log 裡、提供完整的修復 audit trail。</p>
<p>Event sourcing 的設計門檻在於 projection 的維護跟 event schema evolution。Projection 數量增長後，每次 event schema 改版都需要同步更新所有 projection；projection 的 replay 跟 reconciliation 是長期運維的主要成本。這些代價決定了 event sourcing 適合「需要完整變更歷史」的業務場景（金融帳務、訂單流程、法規合規），而非所有資料存取場景。</p>
<h2 id="materialized-view-在資料庫的應用">Materialized View 在資料庫的應用</h2>
<p><a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">Materialized view</a> 的概念定義見知識卡。本段聚焦在 OLTP 資料庫裡 materialized view 作為最輕量 read model 的具體實作。</p>
<p>Materialized view 是「同 DB 內最簡單的讀寫分離」。不需要事件同步、不需要獨立 read store、不需要 projection consumer — 資料庫自己定期執行查詢、存放結果。</p>
<p><strong>跟 regular view 的差別</strong>：regular view 是 SQL 別名，每次 query 重跑底層查詢；materialized view 有實體儲存，query 時直接讀預計算結果。差別在 query-time cost — 複雜 JOIN / aggregation 重複跑時，materialized view 把計算推到 refresh 時、query 時接近零成本。</p>
<p><strong>Refresh 策略</strong>：</p>
<ul>
<li><strong>全量 refresh</strong>：PostgreSQL 的 <code>REFRESH MATERIALIZED VIEW</code>，refresh 期間 view 預設 unavailable。</li>
<li><strong>Concurrent refresh</strong>：PostgreSQL 的 <code>CONCURRENTLY</code> 模式，refresh 期間 view 仍可讀但資料可能 stale。</li>
<li><strong>增量 refresh</strong>：PostgreSQL 的 <code>pg_ivm</code>、Oracle 的 fast refresh — 只更新變更的部分，成本低但配置複雜。</li>
<li><strong>Trigger-based</strong>：特定 event 觸發 refresh，適合低頻變更的資料。</li>
</ul>
<p><strong>在 state ownership 的定位</strong>：materialized view 是 derived state，修復方式是 refresh（重建）而非直接修改。大量 materialized view 會拖累寫入吞吐 — 每次 base table 變更都可能觸發 refresh 計算。設計時要平衡 refresh 頻率跟 query freshness 需求。</p>
<p><strong>跟觀測領域的對照</strong>：觀測領域的 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 在概念上等同於 TSDB 層的 materialized view — 定期執行 query expression、把結果寫成新 series。兩者面對同樣的設計問題：refresh 頻率、freshness lag、維護成本與儲存增長。觀測領域的 CQRS 特化應用見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
<h2 id="query-boundary-四種">Query Boundary 四種</h2>
<p>Query boundary 的責任是讓不同查詢路徑承擔不同服務問題。交易查詢、列表查詢、報表查詢與對帳查詢都可能讀同一張表、但它們的正確性、延遲與資料新鮮度要求不同。</p>
<table>
  <thead>
      <tr>
          <th>查詢類型</th>
          <th>服務責任</th>
          <th>典型 latency</th>
          <th>容忍 stale</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>交易查詢</td>
          <td>支援使用者當下動作、例如付款、下單、授權</td>
          <td>&lt; 100ms</td>
          <td>不容忍</td>
          <td>延遲或錯誤會直接影響交易結果</td>
      </tr>
      <tr>
          <td>列表查詢</td>
          <td>支援使用者瀏覽與管理、例如訂單列表、會員清單</td>
          <td>&lt; 500ms</td>
          <td>可容忍秒級</td>
          <td>可能放大 index、pagination 與排序成本</td>
      </tr>
      <tr>
          <td>報表查詢</td>
          <td>支援營運分析、財務統計與趨勢判讀</td>
          <td>秒到分鐘級</td>
          <td>可容忍 hour 級</td>
          <td>容易壓迫線上資料庫與混淆資料時效</td>
      </tr>
      <tr>
          <td>對帳查詢</td>
          <td>驗證正式狀態與外部事實是否一致</td>
          <td>分鐘到小時級</td>
          <td>視業務</td>
          <td>查詢定義錯誤會造成錯修或漏修</td>
      </tr>
  </tbody>
</table>
<p>這四種查詢混在一起時、資料庫會同時承擔低延遲交易與高成本分析、最後讓任何一種資料庫選型都變得模糊。</p>
<h3 id="交易路徑的邊界">交易路徑的邊界</h3>
<p>交易路徑的責任是維持使用者動作的即時正確性。它需要短查詢、明確 index、可控 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction boundary</a> 與清楚 timeout。</p>
<p>交易路徑的設計要把報表聚合或長時間掃描移到其他查詢路徑。若下單 API 同時查歷史報表、計算大範圍統計或同步重建派生狀態、交易延遲會被非交易責任拖慢。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> — 200 個獨立 Aurora cluster 把不同業務 transaction 分開、避免互相影響。</p>
<h3 id="列表與報表的邊界">列表與報表的邊界</h3>
<p>列表查詢的責任是支援產品體驗中的瀏覽與定位。列表查詢需要穩定排序、分頁策略、篩選條件與查詢成本界線；它應建立自己的讀取模型或索引策略、避免直接借用交易查詢的資料模型造成 slow query、排序漂移與 pagination 重複。</p>
<p>報表查詢的責任是支援分析與決策。報表通常可以接受資料延遲、因此更適合使用 read replica、materialized view、ETL 或 analytics store。把報表直接壓在線上 primary 上、會讓交易服務承擔不必要的容量風險。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair hybrid burst</a>、<a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a> — 交易層跟資料層分開部署。</p>
<h3 id="對帳查詢的邊界">對帳查詢的邊界</h3>
<p>對帳查詢的責任是驗證正式狀態是否與外部事實一致。付款、發票、庫存與訂閱方案都需要對帳查詢、但對帳查詢要保留時間窗、資料來源、差異定義與人工修復入口。</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 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
<p>詳見 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a>。</p>
<h2 id="replica-lag-對-query-boundary-的影響">Replica Lag 對 Query Boundary 的影響</h2>
<p>當應用使用 read replica 擴 read traffic 時、replica lag 會直接影響 query boundary 設計。</p>
<p><strong>典型 lag</strong>：</p>
<ul>
<li>PostgreSQL streaming：&lt; 100ms（同 AZ）</li>
<li>Aurora：10-30ms（同 region）</li>
<li>跨 region replica：秒級到分鐘級</li>
</ul>
<p><strong>不同 query 對 lag 的容忍</strong>：</p>
<ul>
<li>交易查詢：不可容忍 lag、必須走 primary</li>
<li>read-after-write（剛寫完查自己）：必須 primary、或 session sticky</li>
<li>列表查詢：通常容忍 lag &lt; 1 秒</li>
<li>報表查詢：lag 分鐘級可接受</li>
<li>對帳查詢：通常用 batch、lag 不關鍵</li>
</ul>
<p><strong>Stale read 容忍策略</strong>：</p>
<ul>
<li>「能容忍秒級 stale」的 read → replica（用戶 profile、報表）</li>
<li>「不能 stale」的 read → primary（剛寫入後的查詢、餘額確認）</li>
<li>read-after-write：用 session token 標記「剛寫過」、N 秒內讀走 primary</li>
</ul>
<p>對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 的「Read Replica Scaling」段。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>資料庫選型前要先回答四個問題：</p>
<ol>
<li>哪些資料是正式狀態、哪些是派生狀態</li>
<li>哪些查詢屬於交易路徑、哪些可以延遲或離線化</li>
<li>哪些查詢結果會觸發修復、退款、補償或人工決策</li>
<li>哪些資料需要 audit、masking、retention 或刪除責任</li>
</ol>
<p>這些問題決定後續該比較 relational database、document database、search index、analytics store 還是 cache。工具差異要放在責任邊界之後討論。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體資料庫文章要承接本篇的 state ownership 與 query boundary。PostgreSQL、MySQL、MSSQL 或其他 relational database 的比較、應先問它們如何支援正式狀態、交易查詢、列表查詢、報表查詢與對帳查詢、再進入索引、隔離層級、replica 或工具語法。</p>
<p>若主問題是正式狀態與交易一致性、後續文章要優先比較 transaction、isolation、index 與 migration 能力。若主問題是報表與搜尋、後續文章要評估 read replica、materialized view、search index 或 analytics store。若主問題是對帳與修復、後續文章要比較 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、audit log、backup/restore 與資料修復流程。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>state / query 設計重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></td>
          <td>200 個獨立 cluster 隔離 transaction scope</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/bookmyshow-indian-ticketing-platform/" data-link-title="9.C17 BookMyShow：印度年售 2 億張票的資料架構現代化" data-link-desc="BookMyShow 從 15 年自建 analytics 遷移到 AWS modern data architecture、4 個月完成、分析成本下降 80%">9.C17 BookMyShow</a></td>
          <td>OLTP 交易層 + BigQuery / Athena 分析層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/wayfair-gcp-burst-capacity/" data-link-title="9.C22 Wayfair：用 GCP 提供 Way Day / Black Friday 的 burst capacity" data-link-desc="Wayfair 22M&#43; 商品 &#43; 16,000&#43; 供應商、用 GCP 補充 on-prem data center 在峰值事件的 burst capacity">9.C22 Wayfair</a></td>
          <td>on-prem OLTP + GCP BigQuery 分析、典型 CQRS 配置</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>feature store（derived state）、跟 source 分離</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a></td>
          <td>watch list（user state）跟 content metadata 分層</td>
      </tr>
  </tbody>
</table>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位與索引語意回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design</a></li>
<li>與 1.3 的交接：transaction boundary 設計影響哪些 query 走 primary、哪些可走 replica</li>
<li>與 1.7 的交接：正式狀態變更要進入 production rollout — <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout Evidence</a></li>
<li>與 1.9 的交接：對帳查詢的下游修復 — <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">Reconciliation and Data Repair</a></li>
<li>與 2 的交接：cache layer 是 derived state 最常見的形式 — <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a></li>
<li>與 4.20 的交接：query evidence 跟 reconciliation evidence — <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a></li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要進一步處理 schema 與資料模型、接著讀 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a>。要處理 schema 演進與正式狀態變更、接著讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。要處理對帳跟資料修復、接著讀 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation</a>。要設計 KV / Document 的 state ownership、接著讀 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document 容量規劃</a>。</p>
]]></content:encoded></item><item><title>1.9 Reconciliation 與 Data Repair</title><link>https://tarrragon.github.io/blog/backend/01-database/reconciliation-data-repair/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/reconciliation-data-repair/</guid><description>&lt;p>Reconciliation 與 data repair 的核心責任是把資料錯誤從模糊異常轉成可驗證、可修復、可稽核的流程。進入特定資料庫或 ORM 前、讀者需要先理解資料修復屬於正式狀態責任的一部分。&lt;/p>
&lt;p>本章從不一致分類開始、進入偵測模式（連續 vs scheduled）、處理修復策略（auto vs manual）、最後對接 audit trail 跟 backup recovery。讀完後讀者能設計：對帳機制、修復 runbook、evidence handoff、audit chain。&lt;/p>
&lt;h2 id="reconciliation">Reconciliation&lt;/h2>
&lt;p>Reconciliation 的責任是比較兩個或多個資料來源、確認正式狀態是否與外部事實一致。付款狀態要和金流 provider 對齊、發票狀態要和開票系統對齊、庫存狀態要和出貨或倉儲系統對齊。&lt;/p>
&lt;p>對帳需要明確定義資料來源、時間窗、比對鍵、差異分類與 owner。這些欄位能把「資料看起來不一致」轉成可分派、可修復、可驗證的決策材料。&lt;/p>
&lt;h3 id="對帳系統的設計欄位">對帳系統的設計欄位&lt;/h3>
&lt;p>設計對帳作業時、要先把這幾件事談清楚、再寫 query。少談任何一項、對帳結果都會在事故當下被質疑可信度。&lt;/p>
&lt;p>&lt;strong>來源 A 與來源 B&lt;/strong>：明確指出哪個是內部 source of truth、哪個是外部事實。金流對帳的 A 是訂單表、B 是 provider 結算檔；庫存對帳的 A 是訂單庫存表、B 是倉儲 WMS 報表。兩邊都要有明確 owner、否則差異發生時沒人能解釋為何資料長那樣。&lt;/p>
&lt;p>&lt;strong>比對鍵（comparison key）&lt;/strong>：A 跟 B 要用什麼欄位對齊。最理想是雙方共用的業務 ID（例如金流交易序號）；次優是 timestamp + 業務外鍵組合；最差是用 fuzzy matching（金額 + 時間範圍）、這時對帳結果天然帶有噪音、要在 output schema 標示信心度。&lt;/p>
&lt;p>&lt;strong>時間窗（time window）&lt;/strong>：對帳要對哪段時間的資料、什麼時候做。每日對帳通常設定 T-1 整天、跳過今天（避免 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> 資料）；分鐘級對帳要明確處理 in-flight：是排除最近 N 分鐘、還是允許重複跑直到收斂。在跨時區業務裡、時間窗要對齊雙方 timezone、不然每天差異會穩定出現在 0:00 前後。&lt;/p>
&lt;p>&lt;strong>差異分類規則&lt;/strong>：mismatch 不是只有「不一致」一種。常見要再切：「A 有 B 沒有」（missing in B）、「B 有 A 沒有」（missing in A）、「兩邊都有但欄位不同」（value mismatch）、「同一個 key 在 A 有多筆」（duplicate）。每類差異的處理路徑跟 owner 都不同、不分類會讓修復決策無法分派。&lt;/p>
&lt;p>&lt;strong>Output schema&lt;/strong>：對帳產出的不是「對 / 不對」、而是一份結構化報告。最少要有：mismatch 樣本（不是全部）、總筆數與金額影響、覆蓋率（總共比對了多少筆）、未覆蓋資料（哪些 A 或 B 沒涵蓋）、結果時間戳。這份報告會被 &lt;a href="https://tarrragon.github.io/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&lt;/a> 收進釋出證據鏈、結構不穩定會讓上游 release gate 拒絕採信。&lt;/p>
&lt;h3 id="對帳跟-anomaly-detection-的差異">對帳跟 anomaly detection 的差異&lt;/h3>
&lt;p>兩件事都是「找資料異常」、但本質不同、不能互相替代。&lt;/p>
&lt;p>對帳是 deterministic：給定兩個來源、結果是確定的差異集合、可以被任何工程師重跑驗證。anomaly detection 是 statistical：用模型或閾值判斷一筆資料是否「看起來不對」、結果帶機率、不同模型跑出來不一樣。&lt;/p>
&lt;p>在金流、庫存、付款這類正式狀態場景、對帳是必須、anomaly detection 是補充。anomaly detection 適合抓「對帳沒設計到的維度」（突然某 tenant 訂單量爆增）、但不能用它當 source of truth、因為事故時無法回答「為何這筆被判定為異常」。&lt;/p>
&lt;p>兩者輸出格式也不同：對帳輸出 mismatch list、anomaly detection 輸出 confidence score。把兩者混在同一份報告會讓 incident reviewer 無法判斷哪些是必修、哪些是可疑。&lt;/p>
&lt;h2 id="不一致的三種分類">不一致的三種分類&lt;/h2>
&lt;p>不是所有「資料不一致」都一樣。按 &lt;em>成因&lt;/em> 分三類、各有不同處理策略。&lt;/p>
&lt;h3 id="temporal-inconsistency時間性不一致">Temporal Inconsistency（時間性不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：replication lag、async event delivery、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency&lt;/a>&lt;/li>
&lt;li>特徵：兩邊都是「對的」、只是 &lt;em>時間點&lt;/em> 不同&lt;/li>
&lt;li>例：cache 跟 DB 看到不同 value（cache 還沒 invalidate）、replica 跟 primary 不同步&lt;/li>
&lt;li>處理：等待收斂或主動觸發 sync、不必修資料&lt;/li>
&lt;li>持續時間：通常 &amp;lt; 1 秒到分鐘級&lt;/li>
&lt;/ul>
&lt;h3 id="structural-inconsistency結構性不一致">Structural Inconsistency（結構性不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：schema migration 期間、dual-write 失敗、partial write&lt;/li>
&lt;li>特徵：兩邊應該一致但實際不一致、其中一邊是 &lt;em>錯的&lt;/em>&lt;/li>
&lt;li>例：訂單寫進主表但 line items 沒寫、外鍵 reference 一個不存在的 row&lt;/li>
&lt;li>處理：必須修復、不能等&lt;/li>
&lt;li>持續時間：永久（直到修復）&lt;/li>
&lt;/ul>
&lt;h3 id="semantic-inconsistency語意不一致">Semantic Inconsistency（語意不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：業務邏輯 bug、應用層 race condition、人工誤操作&lt;/li>
&lt;li>特徵：資料結構 OK、但 &lt;em>業務語意&lt;/em> 錯&lt;/li>
&lt;li>例：訂單付款狀態是 &lt;code>paid&lt;/code> 但金流端是 &lt;code>refunded&lt;/code>、帳戶餘額跟交易紀錄 sum 不符&lt;/li>
&lt;li>處理：複雜、需要業務判斷哪邊是 source of truth&lt;/li>
&lt;li>持續時間：永久（且容易擴大）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>處理優先序&lt;/strong>：Semantic &amp;gt; Structural &amp;gt; Temporal。Semantic 影響業務最深、Temporal 通常自動收斂。&lt;/p></description><content:encoded><![CDATA[<p>Reconciliation 與 data repair 的核心責任是把資料錯誤從模糊異常轉成可驗證、可修復、可稽核的流程。進入特定資料庫或 ORM 前、讀者需要先理解資料修復屬於正式狀態責任的一部分。</p>
<p>本章從不一致分類開始、進入偵測模式（連續 vs scheduled）、處理修復策略（auto vs manual）、最後對接 audit trail 跟 backup recovery。讀完後讀者能設計：對帳機制、修復 runbook、evidence handoff、audit chain。</p>
<h2 id="reconciliation">Reconciliation</h2>
<p>Reconciliation 的責任是比較兩個或多個資料來源、確認正式狀態是否與外部事實一致。付款狀態要和金流 provider 對齊、發票狀態要和開票系統對齊、庫存狀態要和出貨或倉儲系統對齊。</p>
<p>對帳需要明確定義資料來源、時間窗、比對鍵、差異分類與 owner。這些欄位能把「資料看起來不一致」轉成可分派、可修復、可驗證的決策材料。</p>
<h3 id="對帳系統的設計欄位">對帳系統的設計欄位</h3>
<p>設計對帳作業時、要先把這幾件事談清楚、再寫 query。少談任何一項、對帳結果都會在事故當下被質疑可信度。</p>
<p><strong>來源 A 與來源 B</strong>：明確指出哪個是內部 source of truth、哪個是外部事實。金流對帳的 A 是訂單表、B 是 provider 結算檔；庫存對帳的 A 是訂單庫存表、B 是倉儲 WMS 報表。兩邊都要有明確 owner、否則差異發生時沒人能解釋為何資料長那樣。</p>
<p><strong>比對鍵（comparison key）</strong>：A 跟 B 要用什麼欄位對齊。最理想是雙方共用的業務 ID（例如金流交易序號）；次優是 timestamp + 業務外鍵組合；最差是用 fuzzy matching（金額 + 時間範圍）、這時對帳結果天然帶有噪音、要在 output schema 標示信心度。</p>
<p><strong>時間窗（time window）</strong>：對帳要對哪段時間的資料、什麼時候做。每日對帳通常設定 T-1 整天、跳過今天（避免 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> 資料）；分鐘級對帳要明確處理 in-flight：是排除最近 N 分鐘、還是允許重複跑直到收斂。在跨時區業務裡、時間窗要對齊雙方 timezone、不然每天差異會穩定出現在 0:00 前後。</p>
<p><strong>差異分類規則</strong>：mismatch 不是只有「不一致」一種。常見要再切：「A 有 B 沒有」（missing in B）、「B 有 A 沒有」（missing in A）、「兩邊都有但欄位不同」（value mismatch）、「同一個 key 在 A 有多筆」（duplicate）。每類差異的處理路徑跟 owner 都不同、不分類會讓修復決策無法分派。</p>
<p><strong>Output schema</strong>：對帳產出的不是「對 / 不對」、而是一份結構化報告。最少要有：mismatch 樣本（不是全部）、總筆數與金額影響、覆蓋率（總共比對了多少筆）、未覆蓋資料（哪些 A 或 B 沒涵蓋）、結果時間戳。這份報告會被 <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> 收進釋出證據鏈、結構不穩定會讓上游 release gate 拒絕採信。</p>
<h3 id="對帳跟-anomaly-detection-的差異">對帳跟 anomaly detection 的差異</h3>
<p>兩件事都是「找資料異常」、但本質不同、不能互相替代。</p>
<p>對帳是 deterministic：給定兩個來源、結果是確定的差異集合、可以被任何工程師重跑驗證。anomaly detection 是 statistical：用模型或閾值判斷一筆資料是否「看起來不對」、結果帶機率、不同模型跑出來不一樣。</p>
<p>在金流、庫存、付款這類正式狀態場景、對帳是必須、anomaly detection 是補充。anomaly detection 適合抓「對帳沒設計到的維度」（突然某 tenant 訂單量爆增）、但不能用它當 source of truth、因為事故時無法回答「為何這筆被判定為異常」。</p>
<p>兩者輸出格式也不同：對帳輸出 mismatch list、anomaly detection 輸出 confidence score。把兩者混在同一份報告會讓 incident reviewer 無法判斷哪些是必修、哪些是可疑。</p>
<h2 id="不一致的三種分類">不一致的三種分類</h2>
<p>不是所有「資料不一致」都一樣。按 <em>成因</em> 分三類、各有不同處理策略。</p>
<h3 id="temporal-inconsistency時間性不一致">Temporal Inconsistency（時間性不一致）</h3>
<ul>
<li>來源：replication lag、async event delivery、<a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a></li>
<li>特徵：兩邊都是「對的」、只是 <em>時間點</em> 不同</li>
<li>例：cache 跟 DB 看到不同 value（cache 還沒 invalidate）、replica 跟 primary 不同步</li>
<li>處理：等待收斂或主動觸發 sync、不必修資料</li>
<li>持續時間：通常 &lt; 1 秒到分鐘級</li>
</ul>
<h3 id="structural-inconsistency結構性不一致">Structural Inconsistency（結構性不一致）</h3>
<ul>
<li>來源：schema migration 期間、dual-write 失敗、partial write</li>
<li>特徵：兩邊應該一致但實際不一致、其中一邊是 <em>錯的</em></li>
<li>例：訂單寫進主表但 line items 沒寫、外鍵 reference 一個不存在的 row</li>
<li>處理：必須修復、不能等</li>
<li>持續時間：永久（直到修復）</li>
</ul>
<h3 id="semantic-inconsistency語意不一致">Semantic Inconsistency（語意不一致）</h3>
<ul>
<li>來源：業務邏輯 bug、應用層 race condition、人工誤操作</li>
<li>特徵：資料結構 OK、但 <em>業務語意</em> 錯</li>
<li>例：訂單付款狀態是 <code>paid</code> 但金流端是 <code>refunded</code>、帳戶餘額跟交易紀錄 sum 不符</li>
<li>處理：複雜、需要業務判斷哪邊是 source of truth</li>
<li>持續時間：永久（且容易擴大）</li>
</ul>
<p><strong>處理優先序</strong>：Semantic &gt; Structural &gt; Temporal。Semantic 影響業務最深、Temporal 通常自動收斂。</p>
<h2 id="偵測模式">偵測模式</h2>
<p>不同類型的不一致需要不同偵測模式。</p>
<h3 id="continuous-detection持續偵測">Continuous Detection（持續偵測）</h3>
<ul>
<li>每筆寫入跑 sanity check（trigger、constraint）</li>
<li>應用層 invariant check</li>
<li>適合：structural inconsistency（讓 DB 自己擋）</li>
<li>成本：每筆寫入有 overhead</li>
</ul>
<h3 id="scheduled-detection定期對帳">Scheduled Detection（定期對帳）</h3>
<ul>
<li>每 N 分鐘 / 每天跑對帳 query</li>
<li>跟外部 provider 比對</li>
<li>適合：semantic inconsistency（業務級對齊）</li>
<li>成本：對帳 query 本身耗資源</li>
</ul>
<h3 id="sampling-detection抽樣偵測">Sampling Detection（抽樣偵測）</h3>
<ul>
<li>不跑全表、抽樣 10% / 1% 跑 checksum</li>
<li>適合：大表（全表對帳成本高）</li>
<li>成本：可能漏掉低頻 inconsistency</li>
</ul>
<h3 id="reactive-detection反應式偵測">Reactive Detection（反應式偵測）</h3>
<ul>
<li>用戶 / 客服回報後才查</li>
<li>適合：尾長 inconsistency（找不到通用 pattern）</li>
<li>成本：用戶體驗已受影響</li>
</ul>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> — migration 期間 <a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a> 持續對帳、抓 mapping 規則漂移。</p>
<h2 id="data-repair">Data Repair</h2>
<p>Data repair 的責任是把已確認的資料差異修回正式狀態、並保留修復原因、範圍、證據與回退條件。修復可以是 SQL update、補事件、補發 webhook、重建 projection 或人工客服流程、但每種修復都要有範圍控制。</p>
<p>資料修復要先分成三種：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>說明</th>
          <th>常見風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>欄位修復</td>
          <td>修正單筆或小批正式欄位</td>
          <td>mapping 規則錯誤會造成二次污染</td>
      </tr>
      <tr>
          <td>派生狀態重建</td>
          <td>重建 index、cache、read model</td>
          <td>可能掩蓋正式狀態尚未修復</td>
      </tr>
      <tr>
          <td>補償動作</td>
          <td>補退款、補發票、補通知</td>
          <td>可能產生重複副作用</td>
      </tr>
  </tbody>
</table>
<p>修復前要先確認問題落在哪一層。正式欄位錯誤要修 source of truth；派生狀態錯誤要重建副本；外部副作用漏做要走補償流程。</p>
<p>欄位修復的判讀重點是 mapping 規則是否正確、因為錯誤規則會把單點差異擴成批次污染。派生狀態重建的判讀重點是 source of truth 是否已經正確、否則重建會複製錯誤。補償動作的判讀重點是副作用是否可逆、因為退款、通知或外部 webhook 可能已經被使用者或第三方看見。</p>
<h2 id="repair-原則">Repair 原則</h2>
<p>不管哪種修復、都遵守三個原則：</p>
<h3 id="1-idempotency冪等">1. Idempotency（冪等）</h3>
<ul>
<li>同樣的修復跑兩次、結果跟跑一次一樣</li>
<li>用 <code>WHERE current_value != target_value</code> 而不是無條件 update</li>
<li>補通知 / webhook 帶 idempotency key、第三方可去重</li>
<li>對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency 卡片</a></li>
</ul>
<h3 id="2-auditable可稽核">2. Auditable（可稽核）</h3>
<ul>
<li>每次修復都有 record：誰、什麼時候、改了什麼、為什麼</li>
<li>修復前 + 修復後的 snapshot 都要存</li>
<li>對應 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log 卡片</a>、<a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 Red Team</a> 的 audit 段</li>
</ul>
<h3 id="3-reversible可逆">3. Reversible（可逆）</h3>
<ul>
<li>萬一修復是錯的、能回退到 before state</li>
<li>不可逆操作（DELETE）必須有 dry-run、必須備份</li>
<li>對應 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">Rollback Window 卡片</a></li>
</ul>
<h2 id="修復前的-dry-run-與-impact-assessment">修復前的 dry-run 與 impact assessment</h2>
<p>修復前要先回答「這次修復會碰多少筆、影響多少業務、最壞情況是什麼」、才能進入執行。直接跑 update 是 production-grade 流程的反例、即使在 incident 壓力下也不能跳過這步。</p>
<p><strong>Dry-run 的責任</strong>：把 update 改成 select、用同樣的 WHERE 條件、產出將被修改的資料樣本。Dry-run 結果要包含：影響筆數總計、影響金額或業務值（如果有）、affected tenant / user list 的抽樣、未涵蓋的邊界 case。Dry-run 跟正式修復必須共用 mapping 規則、否則 dry-run 結果無法當審核依據。</p>
<p><strong>規模分級的執行策略</strong>：影響筆數會決定執行方式。</p>
<ul>
<li><strong>單筆到十筆</strong>：客服等級的修復、一名工程師執行 + 一名同儕審核 + audit log 即可。</li>
<li><strong>百筆到千筆</strong>：要在低流量時段執行、分批跑、每批跑完比對 invariant、發現意外停下。</li>
<li><strong>萬筆以上</strong>：當成 production deploy 處理、要有 deploy review、staged rollout（先 1% tenant、再 10%、再全量）、跟 oncall 同步。</li>
<li><strong>跨表 / 跨 service</strong>：必須先做跨團隊 review、確認下游依賴（cache、search index、外部 webhook）的處理計畫、不能單一團隊獨自決定。</li>
</ul>
<p><strong>Impact assessment 的必看欄位</strong>：除了筆數、還要看 <em>連帶影響</em>。修復 orders 表會不會觸發 audit trigger 把每筆寫進 audit log 表？會不會觸發 outbox event 把每筆當成新事件對外發布？會不會讓某 tenant 的 metric 一次性異常、誤觸 alert？這些 second-order effect 在 dry-run 階段就要識別、否則修復本身會變成新事故。</p>
<p><strong>Sandbox / staging 驗證</strong>：不可逆或大規模修復、先在 staging 跑一次、確認 query plan、執行時間、lock 行為。Production 規模沒辦法在 staging 重現的話、至少要在 production 的某個低風險 tenant / region 先試跑、再擴大。</p>
<p><strong>Approval gate（4-eyes process）</strong>：超出單筆規模或修復金錢、權限、個資的場合、必須 <em>兩位以上人員</em> 各自看過 dry-run 結果再簽核。常見實作是：執行者提 PR / ticket 帶 dry-run output、reviewer 簽核後才能執行、執行後產出 audit log 帶兩人簽核紀錄。Reviewer 的責任不是橡皮圖章、是獨立驗證 dry-run 結果跟 incident 描述一致。</p>
<h2 id="repair-patterns">Repair Patterns</h2>
<p>實務上常見的 repair pattern：</p>
<h3 id="pattern-1條件式-update">Pattern 1：條件式 UPDATE</h3>
<p>最簡單也最安全的修復。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;paid&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">12345</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">payment_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;abc&#39;</span><span class="p">;</span></span></span></code></pre></div><p><code>AND</code> 條件確保只在 <em>當前狀態符合預期</em> 時才改、避免 race condition。</p>
<h3 id="pattern-2批次修復--節流">Pattern 2：批次修復 + 節流</h3>
<p>大量資料修復、必須節流避免影響 production。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 每批 100 筆、間隔 1 秒
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;fixed&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;broken&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;broken&#39;</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">100</span><span class="p">);</span></span></span></code></pre></div><p>對應 <a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill 卡片</a> — backfill 跟 batch repair 是同類技術。</p>
<h3 id="pattern-3補事件--補-webhook">Pattern 3：補事件 / 補 webhook</h3>
<p>外部副作用漏做時、補發事件。</p>
<ul>
<li>必須帶 idempotency key（third-party 才能去重）</li>
<li>紀錄補發原因（incident report 連結）</li>
<li>注意：補發前確認 third-party 是否真的沒收到</li>
</ul>
<h3 id="pattern-4重建-derived-state">Pattern 4：重建 derived state</h3>
<p>cache 跟 search index 是 derived state、出錯通常 <em>砍掉重建</em>。</p>
<ul>
<li>不是直接修 cache value、是 invalidate 讓下次 read 重算</li>
<li>大規模重建用 batch job 跑、避免 thundering herd</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a> feature store 重建模式</li>
</ul>
<h3 id="pattern-5point-in-time-recovery">Pattern 5：Point-in-time Recovery</h3>
<p>當資料 <em>損毀且無法重建</em> 時、靠 backup recovery。</p>
<ul>
<li>PostgreSQL：WAL + base backup → PITR</li>
<li>MySQL：binlog + snapshot → PITR</li>
<li>Aurora：cluster snapshot + continuous backup</li>
<li>注意：recovery 期間可能要 <em>整個 DB restore</em>、影響範圍大</li>
</ul>
<h2 id="repair-runbook">Repair Runbook</h2>
<p>Repair runbook 的責任是讓資料修復可重複執行、並降低對當下工程師記憶的依賴。最小 runbook 需要包含：</p>
<ol>
<li>差異查詢與 query link</li>
<li>影響範圍與 tenant / region / time range</li>
<li>修復方式與 dry-run 結果</li>
<li>審核 owner 與執行 owner</li>
<li>rollback condition 與後續 validation query</li>
</ol>
<p>runbook 要和 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a> 共用語意。若查詢與修復程式用不同 mapping 規則、修復結果就難以被同一份 evidence 驗證。</p>
<h2 id="audit-與權限邊界">Audit 與權限邊界</h2>
<p>Data repair 常常需要高權限、因此必須接到 audit 與資料保護邊界。修復個資、付款、權限或方案資料時、要保留操作者、審核者、查詢範圍、寫入範圍與修復前後樣本。</p>
<p><strong>Audit log 必要欄位</strong>：</p>
<ul>
<li>timestamp（操作時間）</li>
<li>actor（誰執行）</li>
<li>reviewer（誰審核、如果是 4-eyes process）</li>
<li>query（執行了什麼 SQL / API call）</li>
<li>before / after snapshot（值的變化）</li>
<li>reason（為什麼做這次修復、incident ID）</li>
<li>rollback path（如何回退）</li>
</ul>
<p>這裡要接到 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 Audit Trail 與 Accountability Boundary</a>。資料修復同時是可靠性、資安與合規問題。</p>
<h3 id="權限分離與憑證時效">權限分離與憑證時效</h3>
<p>修復權限不該是常駐權限。日常開發 / SRE 帳號只該有 read-only、修復需要時才透過 break-glass 流程申請臨時 write 權限。</p>
<p>常見實作：</p>
<ul>
<li><strong>角色分離</strong>：reviewer 跟 executor 是不同帳號、reviewer 不能執行、executor 不能 self-approve。系統強制檢查兩個帳號不同、避免一人偽造另一身分。</li>
<li><strong>時效性憑證</strong>：申請 write 權限時帶 expiry（30 分鐘 / 2 小時）、過期自動回收。不是「給了就一直有」、避免遺留高權限帳號變成攻擊面。</li>
<li><strong>範圍限定</strong>：申請時要指定哪張表、哪個 tenant / region。粒度不細的話、一次申請就拿到全 production write、超出實際需求。</li>
<li><strong>同步 alert</strong>：高權限被啟用要同步發 alert 到 security channel、給 security team reviewer 看見。事後若 audit log 跟 alert 對不上、表示權限被繞過。</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity Access Boundary</a> 跟 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">Secrets and Machine Credential Governance</a>。修復權限管理跟 incident-time 緊急存取是同一套機制、不該各做各的。</p>
<h2 id="跨服務--跨組織的對帳責任">跨服務 / 跨組織的對帳責任</h2>
<p>當對帳跨團隊、跨子系統、跨外部 provider 時、責任不清是首要失敗模式。對帳結果在組織邊界 <em>穿越</em> 時、要明確標記每段的 owner、否則 mismatch 出現後、所有相關方都會說「不是我們的問題」。</p>
<p><strong>跨服務對帳的責任切分</strong>：</p>
<ul>
<li><strong>資料 owner</strong>：誰擁有那張表 / 那組欄位、誰負責解釋為何資料長那樣。資料 owner 通常是寫入該表的服務團隊。</li>
<li><strong>對帳作業 owner</strong>：誰負責定義 reconciliation query、跑、看結果。可能跟資料 owner 是不同人（例如平台團隊跑對帳、業務團隊擁有資料）。</li>
<li><strong>差異處理 owner</strong>：mismatch 出現後、誰負責決定修復策略。通常跟資料 owner 一致、但跨團隊 mismatch 要先約定誰主導。</li>
<li><strong>修復執行 owner</strong>：實際下 SQL / call API 的人。可能跟差異處理 owner 不同（後者決策、前者執行）。</li>
</ul>
<p>四個 owner 在簡單場景可以是同一人、在複雜跨團隊場景必須清楚分派。AGENTS.md 規範優先序段的「明確 owner」原則在這裡指的是 <em>對每一段流程</em> 都有人能簽收、不是只指對帳這件事整體有 owner。</p>
<p><strong>跨組織對帳的特殊問題</strong>：跟外部 provider（金流、物流、SaaS supplier）對帳時、對方不見得會接受你的對帳結果、也不見得會給差異列表。常見處理：</p>
<ul>
<li>自己跑兩份對帳：A vs provider report（每天）、A vs provider API（即時抽樣）、兩份結果不同代表 provider report 本身有問題。</li>
<li>約定差異仲裁流程：簽 SLA 時就寫清楚、mismatch 出現後雙方各保留多久的資料、誰先給對方檢視。</li>
<li>不能依賴 provider 修：金流 provider 通常只負責對帳、不負責修你的 DB。修復永遠是你方責任。</li>
</ul>
<h2 id="跟-backup--pitr-整合">跟 Backup / PITR 整合</h2>
<p>備份的 <em>權限獨立性</em> 跟 <em>attack surface</em> 屬於 <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 Red Team 備份段</a> — 本段聚焦 <em>recovery</em> 角度的資料修復責任。兩者互補：1.5 解決「備份本身怎麼防被攻擊」、本段解決「事故後怎麼用備份回復」。</p>
<p>當修復必須跨越「point in time」時、需要 backup 配合。</p>
<h3 id="snapshot-based-recovery">Snapshot-based recovery</h3>
<ul>
<li>整個 cluster 從 N 小時前的 snapshot 還原</li>
<li>影響：所有 <em>其他</em> 資料也回到那個時間點</li>
<li>適合：catastrophic data corruption</li>
</ul>
<h3 id="pitrpoint-in-time-recovery">PITR（Point-in-Time Recovery）</h3>
<ul>
<li>snapshot + WAL / binlog replay 到指定時間</li>
<li>影響：只在指定時間點 stop replay</li>
<li>適合：「3 小時前 admin 誤刪一張表」這類精準回放</li>
</ul>
<h3 id="logical-backupmysqldump--pg_dump">Logical backup（mysqldump / pg_dump）</h3>
<ul>
<li>整個 schema + data 的 SQL script</li>
<li>適合：跨環境遷移、特定表回復、小規模修復</li>
</ul>
<h3 id="continuous-archive">Continuous archive</h3>
<ul>
<li>WAL / binlog 持續備份到 S3 / GCS</li>
<li>一直可以回放到 <em>任何時間點</em></li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 99.999%</a> — 高可用需要快速 PITR</li>
</ul>
<h3 id="recovery-時的對抗壓力">Recovery 時的對抗壓力</h3>
<p>PITR / snapshot recovery 不是純技術問題、會在事故當下面對「為了快、要不要跳檢查」的取捨。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023 ransomware recovery pressure</a> — 虛擬化平台勒索後、團隊在 <em>營運壓力</em> 跟 <em>資料可信度</em> 之間擺盪：snapshot 是否乾淨、回復後資料是否被污染、跳過 integrity check 換 RTO 是否可接受。判讀重點：recovery 流程要事前 <em>演練</em> 過、否則事故當下不知道要 verify 什麼、容易在壓力下接受被污染的 backup。對應 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.5 Incident Decision Log</a>、事故當下的取捨要寫進 decision log。</p>
<h3 id="rtorpo-跟業務可接受中斷的對照表">RTO/RPO 跟業務可接受中斷的對照表</h3>
<p>業務可接受中斷時間是 RTO/RPO 的判讀對照基準。RTO（Recovery Time Objective、多久能恢復）跟 RPO（Recovery Point Objective、最多丟多少資料）是技術指標、要對照業務側的可接受上限才能判斷夠不夠。常見錯誤是把 RTO/RPO 訂在「技術上能做到的最佳值」、忽略業務實際的容忍範圍。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a> — 「定義核心流程的 RTO / RPO、讓資料修復時間跟業務可接受中斷時間明示對照、不藏在直覺」。事故當下發現「DB 能 2 小時恢復、但業務只能容忍 30 分鐘中斷」、來不及補救。</p>
<p><strong>對照表設計</strong>：</p>
<table>
  <thead>
      <tr>
          <th>業務流程</th>
          <th>RTO（技術）</th>
          <th>業務可接受中斷</th>
          <th>落差處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶登入</td>
          <td>30 分鐘</td>
          <td>5 分鐘</td>
          <td>加 standby region failover</td>
      </tr>
      <tr>
          <td>訂單寫入</td>
          <td>1 小時</td>
          <td>30 分鐘</td>
          <td>加 outbox + replay</td>
      </tr>
      <tr>
          <td>報表查詢</td>
          <td>4 小時</td>
          <td>1 天</td>
          <td>RTO 充裕、不需投資</td>
      </tr>
      <tr>
          <td>對帳 batch</td>
          <td>8 小時</td>
          <td>3 天</td>
          <td>RTO 充裕</td>
      </tr>
      <tr>
          <td>付款</td>
          <td>1 小時</td>
          <td>0（不能停）</td>
          <td>必須 active-active</td>
      </tr>
  </tbody>
</table>
<p><strong>關鍵情境延伸</strong>：</p>
<ul>
<li><strong>付款（必須 active-active）</strong>：業務可接受中斷為 0、單一 region failover 都不能用（failover 期間用戶看到失敗）、必須多 region 同時寫入、靠 Aurora DSQL / Spanner / Cosmos DB multi-region write 撐。設計權衡是 <em>跨 region 寫入延遲</em> 跟 <em>對帳一致性的特殊處理</em>（同一筆款項可能在兩個 region 各被處理一次、要靠 idempotency key 去重）。詳見 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>。</li>
<li><strong>訂單寫入（outbox + replay）</strong>：30 分鐘容忍區間夠用 outbox pattern — 訂單寫進 DB 同步寫進 outbox table、async worker 把 outbox event 推下游。即使下游中斷、訂單本身已落地、event 可在恢復後 replay。設計權衡是 outbox table 的儲存成本跟 replay 邏輯的冪等性、跟 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 的 outbox pattern 整合。</li>
<li><strong>用戶登入（standby region failover）</strong>：5 分鐘容忍意味 <em>自動 failover</em> 必須在這時間內完成、人類介入做不到、要靠 DNS health check + Route 53 / Cloudflare 自動切流。權衡是 standby region 平時付閒置成本、跟 active-active 比、便宜但 failover 時有 1-3 分鐘延遲跟 cache miss。</li>
</ul>
<p>落差是 <em>投資訊號</em>、不是「忽略它」。RTO &gt; 業務容忍時、要嘛降 RTO（加 HA / DR 投資）、要嘛跟業務協商提高容忍（通常不接受）。</p>
<p>判讀重點：對照表要每年 review。業務模式變了（例如從 B2C 變 B2B 客服 SaaS）、容忍時間會大幅縮短、RTO 必須跟著降。</p>
<h2 id="事故角色預定義">事故角色預定義</h2>
<p>DB 事故當下、<em>資安處置</em> 跟 <em>業務連續性處置</em> 要 <em>分軌並行</em>、不是線性執行。這要求事先有 dual-track IC（Incident Command）角色、不是事故當下臨時拉人。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a> — 「技術處置與業務處置分軌並行的前提是事先有 dual-track IC 角色」。沒事先定義、事故當下會出現「資安 team 在隔離系統、business team 在喊客戶等不及」、兩條軌道互相干擾。</p>
<p><strong>Dual-track IC 角色定義</strong>（以下為通用 IC 模型、非案例直接揭露；具體角色細分視組織規模調整）：</p>
<table>
  <thead>
      <tr>
          <th>軌道</th>
          <th>角色</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>技術軌道</td>
          <td>Tech IC</td>
          <td>漏洞修補、系統恢復、技術決策（rollback / restart 等）</td>
      </tr>
      <tr>
          <td>業務軌道</td>
          <td>Business IC</td>
          <td>客戶溝通、降級流程啟動、合規通報、業務 fallback</td>
      </tr>
      <tr>
          <td>協調軌道</td>
          <td>Overall IC</td>
          <td>兩條軌道協調、跨軌道決策、對外發言</td>
      </tr>
      <tr>
          <td>資料軌道</td>
          <td>Data IC</td>
          <td>資料完整性驗證、修復決策、audit chain</td>
      </tr>
      <tr>
          <td>Comms 軌道</td>
          <td>Communications Lead</td>
          <td>內部通報、外部公告、media 應對</td>
      </tr>
  </tbody>
</table>
<p><strong>Overall IC 跟一般技術 IC 的差異</strong>：一般 IC 主要在技術軌道內決策（要不要 rollback、要不要重啟）；Overall IC 額外承擔 <em>跨軌道仲裁</em> 責任 — 當 Tech IC 想停服務止血、Business IC 想保服務維持收入、兩者衝突時、由 Overall IC 拍板。這個角色需要對技術跟業務都有足夠理解、不能只懂一邊；通常由高階工程主管或 CTO/VP Eng 兼任、不是輪值的 oncall。</p>
<p><strong>Data IC 的特殊角色</strong>：跟其他軌道相比、Data IC 的決策時間軸最長 — 技術修復可能 1 小時完成、但 <em>資料是否被污染、要不要 PITR、PITR 到哪個時間點</em> 可能要 24-72 小時驗證。Data IC 不能被 Tech IC 跟 Business IC 的「快快上線」壓力推動、必須有獨立判斷權。實務上常見的失誤是讓 Tech IC 兼任 Data IC、結果為了 RTO 跳過 integrity check、事後發現資料污染擴大。</p>
<p><strong>事先準備</strong>：</p>
<ul>
<li><strong>Primary + backup 雙人配置</strong>：每個角色都要有 primary + backup、避免單人不可用（休假、生病、被另一事故占住）讓事故當下卡住。實務上要有 <em>指定流程</em> 而非「臨時找誰」、避免事故當下浪費 30 分鐘喬人。</li>
<li><strong>責任寫進 runbook</strong>：runbook 要列出每個角色該做什麼決策、不該做什麼決策（避免越權）。事故當下查職位、會在最壓力大的時候做組織決策、出錯機會高。</li>
<li><strong>定期 tabletop 演練</strong>：演練的重點不是「技術修復對不對」、是「角色交接是否流暢」。Overall IC 跟 Tech IC 之間的權限邊界、Data IC 何時介入、Comms Lead 何時對外發言、都要在演練中試出來。</li>
<li><strong>跨時區 follow-the-sun 輪值</strong>：B2B SaaS 跟全球業務、事故不分時區、要有 24/7 覆蓋。單一時區團隊在事故發生在凌晨時、人力不足或反應慢、會放大事故代價。</li>
</ul>
<p>判讀重點：DB 事故不只是技術事件、會成為 <em>跨多軌道</em> 的事件。角色預定義是組織能力、不是技術能力、但缺它會放大技術事故的代價。</p>
<p>對應 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.5 Incident Decision Log</a> 跟 <a href="/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 Security Routing</a> — 角色預定義是這些跨模組工作的前置。</p>
<h2 id="evidence-handoff">Evidence Handoff</h2>
<p>資料修復的 evidence handoff 要能支援 release gate 與 incident review。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>reconciliation query、provider report、audit log</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>差異發生窗口與修復窗口</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>mismatch sample、修復前後驗證</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>data owner、service owner、reviewer</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>抽樣覆蓋率、延遲、未覆蓋資料</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>尚未確認的 provider callback、低流量 tenant</td>
      </tr>
  </tbody>
</table>
<p>這份 handoff 要進入 <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/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a>。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對帳差異率持續上升</td>
          <td>上游邏輯有 bug、或時間窗對齊問題</td>
          <td>修上游 + 確認對帳時間窗</td>
      </tr>
      <tr>
          <td>同筆資料對帳 run-to-run 結果不同</td>
          <td>對帳 query 沒處理 in-flight 資料邊界</td>
          <td>排除最近 N 分鐘、或允許收斂多跑幾次</td>
      </tr>
      <tr>
          <td>修復後不一致再次出現</td>
          <td>沒修根因、只修了 symptom</td>
          <td>找根因、增加 invariant check</td>
      </tr>
      <tr>
          <td>修復影響超出預期範圍</td>
          <td>mapping 規則錯誤、二次污染</td>
          <td>立即停止修復、回退</td>
      </tr>
      <tr>
          <td>修復沒 dry-run 直接執行</td>
          <td>流程違規、事後無法佐證影響範圍</td>
          <td>事後 audit、把 dry-run 列入 gate</td>
      </tr>
      <tr>
          <td>Recovery 後 derived state 仍錯</td>
          <td>重建 derived 時 source 還沒修</td>
          <td>先修 source、再重建 derived</td>
      </tr>
      <tr>
          <td>Audit log 缺欄位</td>
          <td>事故時無法追究、難 rollback</td>
          <td>補 audit schema、加 reviewer 欄位</td>
      </tr>
      <tr>
          <td>高權限帳號在非 incident 時段啟用</td>
          <td>可能誤用或攻擊面、break-glass 沒回收</td>
          <td>立刻檢查 audit log、回收憑證</td>
      </tr>
      <tr>
          <td>跨服務 mismatch、各方都推卸</td>
          <td>對帳 owner 沒分派、責任空白</td>
          <td>補資料 owner / 對帳 owner / 執行 owner</td>
      </tr>
      <tr>
          <td>anomaly alert 跟對帳 mismatch 混報</td>
          <td>兩種訊號性質不同、reviewer 無法判讀</td>
          <td>拆 dashboard、deterministic 跟 statistical 分開</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把對帳當成「定期 batch job」、不關心 <em>當下不一致</em>。實時對帳跟 batch 對帳是 <em>不同工具</em>、不能互相替代。</p>
<p>把資料修復當成「一個工程師動手改」、沒 audit、沒 review、沒 rollback。資料修復本質是 production 操作、跟 deploy 同等嚴格。</p>
<p>把 PITR 當成 <em>常規修復工具</em>。PITR 影響大、適合 catastrophic event、不適合單筆資料修復。</p>
<p>把 derived state 不一致跟 canonical state 不一致 <em>混在一起</em> 處理。derived 是 <em>再生</em> 的、canonical 是 <em>永久</em> 的、處理流程完全不同。</p>
<p>把對帳結果跟 anomaly detection 結果放同一份報告。前者是 deterministic、後者是 statistical、混報會讓 incident reviewer 無法判斷必修跟可疑。對帳 mismatch 要有獨立追蹤面板、anomaly 走另一條路徑。</p>
<p>跳過 dry-run、直接 update。即使單筆修復、也要先 select 看到當前 row、確認 WHERE 條件命中預期。incident 壓力下尤其容易跳、結果反而把單點問題擴成批次污染。</p>
<p>把修復權限當常駐權限發放。長期 write 權限放在工程師帳號上、會在事故無關時段被誤用、且事後無法區分「正常工作」跟「非法修復」。修復權限要時效化、申請即用即收。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>reconciliation 重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>migration 期間用 shadow read 持續對帳</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a></td>
          <td>體育博彩 ledger、結算後對帳</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>跨市場銀行、每市場獨立對帳</td>
      </tr>
  </tbody>
</table>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體資料庫文章要承接本篇的 reconciliation 與 data repair 責任。PostgreSQL、MySQL、MSSQL 或其他資料庫的差異、應放在它們如何產生 validation query、保留 audit trail、支援 point-in-time recovery、處理 replica lag 與控制修復權限。</p>
<p>若服務需要高頻對帳、後續文章要比較查詢成本、索引策略與 replica 讀取延遲。若服務需要高風險資料修復、後續文章要比較 transaction log、backup/restore、row-level audit 與權限分離。若服務需要跨系統補償、後續文章要把資料庫能力接到 queue replay 與 incident decision log。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.3 的交接：transaction boundary 決定哪些不一致可避免 — <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">Transaction Boundary</a></li>
<li>與 1.5 的交接：audit 跟 access control — <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">Red Team Data Layer</a></li>
<li>與 1.7 的交接：migration 後驗證 — <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout Evidence</a></li>
<li>與 1.8 的交接：canonical vs derived 是修復的前置 — <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">State Ownership</a></li>
<li>與 3.8 的交接：消息重放與補事件 — <a href="/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/" data-link-title="3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）" data-link-desc="以 order_created consumer 示範 queue 路徑如何交付 idempotency evidence、DLQ handling、replay runbook 與 incident decision log。">Queue Consumer Retry / Replay</a></li>
<li>與 4.20 的交接：evidence handoff — <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a></li>
<li>與 7.7 的交接：audit trail — <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">Audit Trail and Accountability Boundary</a></li>
<li>與 8.22 的交接：incident evidence write-back — <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a></li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要處理 migration 造成的資料差異、接著讀 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。要處理事件漏發造成的副作用修復、接著讀 <a href="/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/" data-link-title="3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）" data-link-desc="以 order_created consumer 示範 queue 路徑如何交付 idempotency evidence、DLQ handling、replay runbook 與 incident decision log。">3.8 Queue Consumer Retry 與 Replay Handoff</a>。要設計跨服務 reconciliation 跟 saga compensation、接著讀 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 的 Saga 段。</p>
]]></content:encoded></item><item><title>1.10 KV / Document DB 容量規劃</title><link>https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>KV / Document DB 的容量規劃跟傳統 OLTP 完全不同。OLTP 容量靠「instance type 升級 + read replica」、KV 靠「partition 切分 + capacity unit 配置」。兩者瓶頸不同、可擴範圍不同、設計取捨也不同。&lt;/p>
&lt;p>本章針對 DynamoDB、Azure Cosmos DB、Google Cloud Bigtable、MongoDB Atlas 等主流 KV / Document DB、整理容量規劃的共通方法論。讀完後讀者能回答：partition key 怎麼設計才不會 hot partition、on-demand vs provisioned 怎麼選、什麼時候從 single-region 升到 multi-region。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取&lt;/a> 的關係：1.1 處理 OLTP 高併發、本章處理 KV 高併發。兩者讀者群有重疊但解法不同。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/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 容量規劃模型&lt;/a> 的關係：本章從 &lt;em>DB 視角&lt;/em> 看容量、9.4 / 9.6 從 &lt;em>workload 視角&lt;/em> 看容量、兩者互補。&lt;/p>
&lt;h2 id="kv--document-db-的容量模型">KV / Document DB 的容量模型&lt;/h2>
&lt;p>KV 容量模型可以簡化成一條公式：&lt;strong>總容量 = partition 數量 × 每 partition 上限&lt;/strong>。&lt;/p>
&lt;p>vendor 不同、細節不同，但都遵循這個邏輯。&lt;/p>
&lt;h3 id="http-api-db-vs-connection-based-db-的本質差異">HTTP API DB vs connection-based DB 的本質差異&lt;/h3>
&lt;p>KV DB 在 surge 場景比 OLTP 有結構性優勢的主因、不只是 partition 設計、是 &lt;em>連線模型&lt;/em> 的本質差異。&lt;/p>
&lt;p>&lt;strong>Connection-based DB&lt;/strong>（PostgreSQL、MySQL、MongoDB、Cassandra）：&lt;/p>
&lt;ul>
&lt;li>用戶端跟 DB 維持 TCP connection、connection 有 state（authenticated session）&lt;/li>
&lt;li>每個 connection 在 DB server 端佔記憶體 + 一個 process/thread&lt;/li>
&lt;li>connection 上限通常 1K-5K&lt;/li>
&lt;li>application 想開更多 connection、DB 直接拒絕&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>HTTP API DB&lt;/strong>（DynamoDB、Cosmos DB、Bigtable、Firestore）：&lt;/p>
&lt;ul>
&lt;li>用戶端每次 request 開新 HTTP connection（或用 keep-alive 池）&lt;/li>
&lt;li>DB 端沒有「per-user connection state」、是 stateless API server&lt;/li>
&lt;li>沒有 connection 上限概念、能力上限是 &lt;em>每 partition 的 RU / RCU&lt;/em>&lt;/li>
&lt;li>application 加多少 instance 都不影響 DB&lt;/li>
&lt;/ul>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/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 &amp;#43; AWS Media Services 撐 30 channels live &amp;#43; 5M MAU、工程工時下降 90%">9.C29 Lemino&lt;/a> — NTT DOCOMO 串流服務選 DynamoDB 而非 RDB 的關鍵原因是 RDB 的 connection limit 在 surge 場景變成 bottleneck、HTTP API 模型沒這個問題。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>KV / Document DB 的容量規劃跟傳統 OLTP 完全不同。OLTP 容量靠「instance type 升級 + read replica」、KV 靠「partition 切分 + capacity unit 配置」。兩者瓶頸不同、可擴範圍不同、設計取捨也不同。</p>
<p>本章針對 DynamoDB、Azure Cosmos DB、Google Cloud Bigtable、MongoDB Atlas 等主流 KV / Document DB、整理容量規劃的共通方法論。讀完後讀者能回答：partition key 怎麼設計才不會 hot partition、on-demand vs provisioned 怎麼選、什麼時候從 single-region 升到 multi-region。</p>
<p>跟 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 的關係：1.1 處理 OLTP 高併發、本章處理 KV 高併發。兩者讀者群有重疊但解法不同。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 跟 <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> 的關係：本章從 <em>DB 視角</em> 看容量、9.4 / 9.6 從 <em>workload 視角</em> 看容量、兩者互補。</p>
<h2 id="kv--document-db-的容量模型">KV / Document DB 的容量模型</h2>
<p>KV 容量模型可以簡化成一條公式：<strong>總容量 = partition 數量 × 每 partition 上限</strong>。</p>
<p>vendor 不同、細節不同，但都遵循這個邏輯。</p>
<h3 id="http-api-db-vs-connection-based-db-的本質差異">HTTP API DB vs connection-based DB 的本質差異</h3>
<p>KV DB 在 surge 場景比 OLTP 有結構性優勢的主因、不只是 partition 設計、是 <em>連線模型</em> 的本質差異。</p>
<p><strong>Connection-based DB</strong>（PostgreSQL、MySQL、MongoDB、Cassandra）：</p>
<ul>
<li>用戶端跟 DB 維持 TCP connection、connection 有 state（authenticated session）</li>
<li>每個 connection 在 DB server 端佔記憶體 + 一個 process/thread</li>
<li>connection 上限通常 1K-5K</li>
<li>application 想開更多 connection、DB 直接拒絕</li>
</ul>
<p><strong>HTTP API DB</strong>（DynamoDB、Cosmos DB、Bigtable、Firestore）：</p>
<ul>
<li>用戶端每次 request 開新 HTTP connection（或用 keep-alive 池）</li>
<li>DB 端沒有「per-user connection state」、是 stateless API server</li>
<li>沒有 connection 上限概念、能力上限是 <em>每 partition 的 RU / RCU</em></li>
<li>application 加多少 instance 都不影響 DB</li>
</ul>
<p>對應 <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%">9.C29 Lemino</a> — NTT DOCOMO 串流服務選 DynamoDB 而非 RDB 的關鍵原因是 RDB 的 connection limit 在 surge 場景變成 bottleneck、HTTP API 模型沒這個問題。</p>
<p>判讀含義：選 KV DB 不只是「擴容容易」、是 <em>連線模型</em> 適合無 state HTTP 服務的天然契合。微服務數量增加時、HTTP API DB 不需要每次都 review connection pool 設定。但若 application 仍以 SQL transaction 為主流程設計、改 KV 需要 <em>改 application 架構</em>、不是換 driver 而已。</p>
<p><strong>Amazon DynamoDB</strong>：</p>
<ul>
<li>容量單位是 RCU（Read Capacity Unit）跟 WCU（Write Capacity Unit）</li>
<li>1 RCU = 1 strongly consistent read of 4KB / sec、2 eventually consistent reads</li>
<li>1 WCU = 1 write of 1KB / sec</li>
<li>每個 partition 上限：3000 RCU / 1000 WCU、底層 partition 數量透明</li>
</ul>
<p><strong>Azure Cosmos DB</strong>：</p>
<ul>
<li>容量單位是 RU（Request Unit）— 把 read / write / query 統一抽象</li>
<li>1 RU = strongly consistent read of 1KB document</li>
<li>寫成本約 5x read、複雜 query 可達數百 RU</li>
<li>每個 logical partition 上限：10,000 RU/s</li>
</ul>
<p><strong>Google Cloud Bigtable</strong>：</p>
<ul>
<li>容量單位是 node（SSD / HDD）</li>
<li>每個 node 約 10,000 reads/sec、10,000 writes/sec（依 row size）</li>
<li>partition 透明、靠 tablet 自動分裂</li>
</ul>
<p><strong>MongoDB Atlas</strong>：</p>
<ul>
<li>容量單位是 cluster tier（M10、M30、M60 等）+ shard</li>
<li>每個 shard 是獨立 mongod replica set、容量按 instance type 跟 storage</li>
<li>主動 sharding 設計、跟 DynamoDB 透明 partition 不同</li>
</ul>
<p><strong>共通點</strong>：容量上限不是「單一 number」、是「partition / shard 數量 × 每 partition 上限」。要擴容、要嘛加 partition、要嘛升級 partition、不能像 OLTP 一樣換更大 instance。</p>
<h2 id="partition-key-設計容量的命脈">Partition key 設計：容量的命脈</h2>
<p>partition key 設計不均勻、實際容量遠低於名義。這是 KV DB 最常見的 production issue。</p>
<p><strong>Hot partition 的成因</strong>：</p>
<ul>
<li>名義容量 = partition 數量 × 每 partition 上限</li>
<li>實際容量 = 最熱 partition 上限（如果分布不均）</li>
<li>100K RPS 名義能撐、若 80% 流量集中在 1 個 partition、實際 <em>只能撐 3K RPS（DynamoDB partition 上限）</em></li>
</ul>
<p><strong>識別 hot partition 的訊號</strong>：</p>
<ul>
<li>throughput 上不去、但 average resource utilization 低</li>
<li>某些 key 的 request latency 飆、其他 key 正常</li>
<li>DynamoDB throttling event 出現（即使 capacity 還沒滿）</li>
<li>Cosmos DB 顯示「per-partition RU consumption skew」</li>
</ul>
<p><strong>設計策略</strong>：</p>
<ol>
<li><strong>天然均勻 partition key</strong>：user_id、order_id、device_id 等天然分布廣的 ID。最簡單、最常用。</li>
<li><strong>Composite partition key</strong>：把容易集中的維度（event_id）跟均勻的維度（user_id_hash）組合。例如 <code>event_id#user_id_hash_mod_100</code>、強制把同一 event 的流量分散到 100 個 sub-partition。</li>
<li><strong>Write sharding</strong>：在 partition key 後加 random suffix。<code>event_id#0</code> ~ <code>event_id#9</code> 讓同一個 event 變成 10 個 partition。讀的時候要 scatter-gather 從 10 個 partition 讀回來。</li>
<li><strong>Time-bucket</strong>：對時序資料、加 minute / hour bucket。<code>metric#2026-05-13-T12</code>、每個時段一個 partition。</li>
</ol>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 9000 萬 reads/sec 靠 partition 設計均勻、不是純擴 capacity</li>
<li><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票 event_id 天然容易 hot、必須用 composite key 或 write sharding 分散</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> — Cosmos DB synthetic partition key 強制分散</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a>。</p>
<h3 id="彈性來自-partition-key-均勻分布">彈性來自 partition key 均勻分布</h3>
<p>KV DB 的吞吐彈性等於 partition key 均勻分布的結果。partition key 均勻時、總容量 ≈ partition 數量 × 單 partition 上限；partition key 不均時、實際容量 = 最熱 partition 上限（DynamoDB 每 partition 3000 RCU / 1000 WCU）、跟 partition 總數無關。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票 IOPS 從 20 衝到 135K 的 6,750 倍彈性、前提是 partition key 把流量分散到大量 partition（合理做法是 composite key <code>event_id + user_id_hash</code> 或 write sharding <code>event_id + random_suffix</code>）。若用裸 <code>event_id</code> 當 partition key、同一場演唱會所有訂單擠進同一個 partition、實際 IOPS 上限被鎖在 1000 WCU、跟 partition 總數無關。</p>
<p>判讀重點：讀「Amazon Ads 9000 萬 reads/sec」、「DynamoDB 1.51 億 RPS」這類數字、要追問「partition 設計是什麼」、再判斷自己的服務能否複製。換 DynamoDB 是必要前提、partition key 設計是充分前提；只換 DB 而沒解決 partition key、會出「換了 DB 但 hot partition 依舊」的事故。</p>
<h2 id="capacity-modeon-demand-vs-provisioned">Capacity mode：on-demand vs provisioned</h2>
<p>DynamoDB / Cosmos DB 都提供兩種容量模式、各有適用場景。</p>
<p><strong>On-demand（pay-per-use）</strong>：</p>
<ul>
<li>不需事前配置 RCU / WCU / RU</li>
<li>自動 scale up / down、處理突發流量</li>
<li>單位成本高（約 7x provisioned）</li>
<li>適合：流量不可預測、burst 頻繁、開發 / 測試環境</li>
</ul>
<p><strong>Provisioned（預配置）</strong>：</p>
<ul>
<li>預先訂購 RCU / WCU / RU</li>
<li>超過配額會 throttle（除非開 auto-scaling）</li>
<li>單位成本低</li>
<li>適合：流量可預測、sustained workload、生產環境</li>
</ul>
<p><strong>選型決策</strong>：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議 mode</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>流量 peak/avg 比 &lt; 3x</td>
          <td>provisioned + auto-scaling</td>
      </tr>
      <tr>
          <td>流量 peak/avg 比 &gt; 5x</td>
          <td>on-demand</td>
      </tr>
      <tr>
          <td>流量極端 bursty（flash-sale）</td>
          <td>on-demand</td>
      </tr>
      <tr>
          <td>sustained growth 穩定上升</td>
          <td>provisioned + scheduled scaling</td>
      </tr>
      <tr>
          <td>短期測試 / POC</td>
          <td>on-demand</td>
      </tr>
      <tr>
          <td>已知大事件（Black Friday）</td>
          <td>provisioned baseline + scheduled scale-up</td>
      </tr>
  </tbody>
</table>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> — TiDB 必須長期 over-provision、換 DynamoDB on-demand 後 pay-per-use、50% 成本下降</li>
<li><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a> — sustained 3 億 msg/day 適合 provisioned + auto-scaling</li>
<li><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 9000 萬 RPS sustained workload 必然 provisioned + careful tuning</li>
</ul>
<p>詳見 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 的成本曲線分析。</p>
<h3 id="計費粒度-vs-工程顆粒">計費粒度 vs 工程顆粒</h3>
<p>KV / Document DB 的計費單位（DynamoDB 的 RCU/WCU、Cosmos DB 的 RU、Spanner 的 processing unit）決定容量規劃可以從多小開始。計費粒度太大、中小規模負載付過多錢；計費粒度太小、大規模負載要管理很多細項。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> — Spanner 早期最小單位是 100 processing units（pu）≈ 1 node、對中小負載門檻過高。後來推出 100 pu 起跳的 granular sizing、讓容量規劃可以從小開始、降低 onboarding 門檻。</p>
<p><strong>選型含義</strong>：</p>
<ul>
<li><strong>新服務 / 中小規模</strong>：選計費粒度小的選項（Cosmos DB serverless、Spanner granular sizing、DynamoDB on-demand）、避免一開始就為了「未來會用到」過配。中小規模付過配成本、實際就是替「不確定的未來」付保險費、保險費過高代表選錯產品。</li>
<li><strong>穩定大規模</strong>：計費粒度可大（DynamoDB provisioned with reserved capacity、Spanner full-node provisioning）、單價較低。Reserved capacity 通常綁 1-3 年合約、要看業務 <em>未來 12-24 月需求是否穩定</em>、若業務量可能下降或遷移、Reserved 反成沉沒成本；若業務量穩定上升、Reserved 是合理 hedging。</li>
<li><strong>POC / 測試</strong>：選 on-demand 或 serverless、付實際用量、別為了未實際 production 的 workload 付 reserved 成本。</li>
</ul>
<p>判讀重點：計費粒度同時是 <em>vendor 商業策略</em> 跟 <em>工程顆粒</em>、選 vendor 時要看 <em>min sizing</em> 跟 <em>增量 granularity</em>、不只看 max throughput。</p>
<h3 id="業務邏輯變化--讀寫比跳量級">業務邏輯變化 → 讀寫比跳量級</h3>
<p>讀寫比變化是容量規劃的早期警訊、但常被忽略。原始容量規劃通常基於某個讀寫比（例如 1:1 或 5:1）、業務邏輯改變可能讓比例跳一個量級、原容量規劃失效。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 廣告事件量測讀寫比 18:1（曝光發生 1 次、後續查詢 18 次）。如果業務新增即時報表功能、讀次數從 18 跳到 50、容量規劃要重做、不是「再加一點 capacity」。</p>
<p><strong>常見業務變化導致讀寫比跳量級</strong>：</p>
<ul>
<li>新增即時 dashboard：每筆資料被查詢頻率從 1 次跳到 N 次</li>
<li>新增推薦演算法：每用戶 read profile 從每次登入 1 次變成每次推薦 1 次（× 推薦頻率）</li>
<li>新增 audit / compliance 查詢：每筆敏感資料額外被查 5-10 次</li>
<li>新增 cache：讀次數從 100 降到 5（cache hit rate 95%）— 跟其他變化方向相反、是 <em>capacity 該縮容</em> 的訊號、若沒同步 review 反而會繼續按舊容量付錢</li>
<li>新增 anti-fraud 檢測：每寫入觸發 N 次 read 驗證</li>
</ul>
<p>判讀重點：容量規劃 review cadence 不只看流量、要 review <em>讀寫比</em> 是否漂移。比例跳量級是設計需要重做的訊號、不是單純 capacity 增加（或減少）的訊號。</p>
<h2 id="一致性模型strong-vs-eventual-vs-session">一致性模型：strong vs eventual vs session</h2>
<p>KV / Document DB 通常提供多個 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>、不同 level 對應不同延遲跟可用性。</p>
<p><strong>DynamoDB</strong>：</p>
<ul>
<li>Eventually consistent reads（預設、便宜）：1 sec 內收斂、cost = 0.5 RCU</li>
<li>Strongly consistent reads：跨 AZ <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>、cost = 1 RCU、不可跨 region</li>
<li>沒有中間 level</li>
</ul>
<p><strong>Cosmos DB</strong>（最豐富）：</p>
<ul>
<li><strong>Strong</strong>：linearizable、跨 region quorum、最高 latency</li>
<li><strong>Bounded staleness</strong>：訂上限（時間 / 版本差異）</li>
<li><strong>Session</strong>：同一 session 內強一致（最常用）</li>
<li><strong>Consistent prefix</strong>：保證寫入順序、不保證收斂時間</li>
<li><strong>Eventual</strong>：最便宜、最終一致</li>
</ul>
<p><strong>Bigtable</strong>：</p>
<ul>
<li>Single-region：strongly consistent</li>
<li>Replicated：eventually consistent</li>
</ul>
<p><strong>選 consistency level 的工程後果</strong>：</p>
<ul>
<li>Strong consistency → 跨 region 延遲（quorum round-trip）</li>
<li>Eventual → 用戶可能看到舊資料、需要 application 容忍</li>
<li>Session → 大多數網路服務的 sweet spot（用戶看自己寫的東西要立即、別人寫的可以稍晚）</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> — external consistency（線性化）跨地區、付出 quorum 延遲代價</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365 Cosmos DB</a> — 分析平台用 weakest consistency 換最大 throughput</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 的一致性取捨。</p>
<h2 id="multi-model-取捨">Multi-model 取捨</h2>
<p>部分 KV / Document DB 支援多個 model interface、同一服務跑不同抽象。</p>
<p><strong>Cosmos DB（最廣 multi-model）</strong>：</p>
<ul>
<li>SQL API（document）</li>
<li>MongoDB API（document、wire-protocol compatible）</li>
<li>Cassandra API（wide-column）</li>
<li>Gremlin（graph）</li>
<li>Table（key-value）</li>
</ul>
<p><strong>DynamoDB（KV + document）</strong>：</p>
<ul>
<li>原生 KV、但 attribute 可以是 nested map / list（document-like）</li>
<li>沒有 SQL interface（PartiQL 是 query language、不是 model）</li>
</ul>
<p><strong>Bigtable（wide-column）</strong>：</p>
<ul>
<li>沒有 multi-model、純 wide-column</li>
<li>替代方案：用 Spanner + Bigtable 組合</li>
</ul>
<p><strong>Multi-model 的優缺</strong>：</p>
<ul>
<li>優勢：同一團隊不必管多個 vendor、ops 簡化</li>
<li>優勢：不同 use case 用同一 datastore、減少 data sync</li>
<li>限制：vendor lock-in 加深、難換</li>
<li>限制：每個 API 都不是 <em>最好</em> 的（compromise）— MongoDB API 跟 native MongoDB 有 behavior 差異</li>
</ul>
<p><strong>選型建議</strong>：</p>
<ul>
<li>已用 single model → 不必為 multi-model 而換</li>
<li>多種 use case 同時上 → 評估 Cosmos DB（特別是 MongoDB workload + 新需求）</li>
<li>純 KV 高吞吐 → DynamoDB / Bigtable 比 Cosmos DB 通常便宜</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB → Cosmos DB MongoDB API、應用層幾乎不改、底層改用 Cosmos 分散式架構</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> — 用 SQL API、不需要 MongoDB compat</li>
</ul>
<h2 id="kv-db-作為寫入緩衝的特殊用法">KV DB 作為寫入緩衝的特殊用法</h2>
<p>本節展開 KV 在 <em>flash-sale 架構</em> 的特殊角色、屬於資料層責任、但跟 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a> 跟 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 互補（後者主寫 broker / queue 設計、本節聚焦把 KV 當 buffer 的取捨）。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 揭露一個非傳統用法：DynamoDB 不當 OLTP、當 <em>durable queue</em>。</p>
<p><strong>模式</strong>：前端把訂單塞進 DynamoDB（高吞吐、partition 均勻）、後端 legacy server 按自己能承受的速度從 DynamoDB 消費。</p>
<p><strong>為什麼用 DynamoDB 而非 SQS / Kafka</strong>：</p>
<ul>
<li>DynamoDB Stream 提供 change data capture、後端可以 stream 消費</li>
<li>寫入後立即可查（OLTP-like）、不是純 fire-and-forget</li>
<li>partition 設計讓單一事件可以分散到多個 partition</li>
<li>同樣 vendor、不必另起一個 broker 服務</li>
</ul>
<p><strong>適用場景</strong>：</p>
<ul>
<li>突發流量遠超後端處理能力</li>
<li>後端是 legacy、不容易擴</li>
<li>需要寫入後立即可查（用戶看「我下單成功了」）</li>
</ul>
<p><strong>不適用場景</strong>：</p>
<ul>
<li>純 fire-and-forget（用 SQS 更便宜）</li>
<li>高吞吐 stream processing（用 Kafka 更專業）</li>
<li>順序性嚴格要求（DynamoDB Streams 只在 partition 內保證順序）</li>
</ul>
<p>詳見 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 案例</a> 的詳細分析。</p>
<h2 id="連線管理跟-oltp-完全不同">連線管理：跟 OLTP 完全不同</h2>
<p>KV / Document DB 通常是 <em>HTTP / gRPC 介面</em>、不是 <em>connection pool</em>。這是跟 OLTP 完全不同的設計、影響應用層架構。</p>
<p><strong>OLTP（PostgreSQL / MySQL）</strong>：</p>
<ul>
<li>每個 application instance 維護 connection pool（10-100 connections）</li>
<li>connection 是有狀態的（transaction、session variable）</li>
<li>pool size × instance 數量 ≤ DB 上限（PostgreSQL 預設 100、PgBouncer 可破百）</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%">9.C29 Lemino 案例</a> 揭露 RDB connection 是隱性 bottleneck</li>
</ul>
<p><strong>KV（DynamoDB / Cosmos DB）</strong>：</p>
<ul>
<li>純 HTTP / gRPC、無 stateful connection</li>
<li>每個 request 獨立、不必預先 establish connection</li>
<li>沒有 connection limit 概念</li>
<li>應用層擴容不會打爆 DB connection</li>
</ul>
<p>這個差異是 KV DB 在 <em>surge 場景</em> 比 OLTP 有優勢的主因 — KV 不會 connection saturate。</p>
<h2 id="隱性限流-vs-明確限流">隱性限流 vs 明確限流</h2>
<p>flash-sale 或極端負載場景的限流可能分散在多層元件、不是單一「rate limiter」。同一架構可能同時包含 <em>隱性</em> 限流（用 DB / LB 上限自然攔截）跟 <em>明確</em> 限流（用排隊系統精確控速）。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票架構圖上看不到明確「rate limiter」元件、但限流發生在多層：</p>
<ul>
<li><strong>DynamoDB 寫入排隊</strong>：DynamoDB 把訂單塞進 queue、傳統 server 按自己能力消費 — DynamoDB throughput 就是隱性限流</li>
<li><strong>ELB max connection</strong>：load balancer 上限自動拒絕超量請求</li>
<li><strong>Application 層 connection pool</strong>：超過 pool size 的 request 排隊或被拒</li>
<li><strong>付款層獨立</strong>：搶票流量塞爆時、付款不受影響、低頻路徑「自然限流」</li>
</ul>
<p>對比 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek Virtual Waiting Room</a> 的 <em>明確限流</em>：用 Counters table 精確控發 token 速率、用戶看得到排隊位置。</p>
<p><strong>選擇取捨</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>隱性限流（Tixcraft）</th>
          <th>明確限流（SeatGeek）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶體驗</td>
          <td>用戶以為成功、實際排隊</td>
          <td>用戶看得到等待時間</td>
      </tr>
      <tr>
          <td>流量吸收能力</td>
          <td>極高（DB 直接吸）</td>
          <td>受限於 token 發放速度</td>
      </tr>
      <tr>
          <td>開發複雜度</td>
          <td>低（用 DB 自帶 throughput）</td>
          <td>高（需要 token 系統）</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>DB 滿了用戶才被拒</td>
          <td>排隊系統爆了用戶被拒</td>
      </tr>
      <tr>
          <td>適合業務</td>
          <td>流量瞬間到頂、要全收</td>
          <td>流量持續高、要排序公平</td>
      </tr>
  </tbody>
</table>
<p><strong>失敗模式延伸</strong>：隱性限流的失敗特徵是「provisioned capacity / connection pool 飽和、用戶看到 5xx / timeout、沒人收到排隊位置」— 監控訊號是 DynamoDB throttling event 或 ELB queue length 飆。明確限流的失敗特徵是「排隊系統本身的 DB / counter 飽和、token 發不出來、所有用戶包含 VIP 都被擋」— 監控訊號是 token issuance success rate 掉。兩種失敗對應不同 runbook、混在同一 alert dashboard 會誤判。</p>
<p><strong>適合業務延伸</strong>：隱性限流適合「流量瞬間到頂、業務願意接受用戶看不見排隊」的場景（演唱會搶票、Black Friday 開賣瞬間、限量商品）— 業務優先收住流量、用戶體驗可以事後解釋。明確限流適合「流量持續高、用戶等待時間長、需要顯示進度減少跳離」的場景（IPO 開盤、長期熱門商品上架、跨小時的搶購事件）— 用戶能看到「我還有 30 分鐘」會繼續等。</p>
<p>判讀重點：選哪種限流取決於業務願意接受什麼用戶體驗、不是工程偏好。隱性限流用透明度換流量吸收能力、明確限流用流量吸收能力換體驗可見度。兩者並存、沒有「best practice」。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day 2025</a></td>
          <td>DynamoDB 24 小時 1.51 億 RPS、毫秒級延遲、可預期峰值上限參考</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>9000 萬 RPS + 99.999% 可用 — partition 均勻設計典範</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a></td>
          <td>Cosmos DB 1M RU/s + multi-model + global distribution</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>DynamoDB 當 durable queue、IOPS 20→135K</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a></td>
          <td>DynamoDB 4 表 + Lambda 實作 virtual waiting room、跟 Tixcraft 的隱性緩衝形成姊妹案</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a></td>
          <td>30x DAU surge、DynamoDB 撐 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a></td>
      </tr>
      <tr>
          <td><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%">9.C19 Capcom</a></td>
          <td>遊戲後端 KV、billions of requests + single-digit ms</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>TiDB → DynamoDB、50% 成本下降的取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a></td>
          <td>Black Friday 1.67 億請求 / 24h、Cosmos DB 多 region</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a></td>
          <td>99.999% 跨 15 region、DynamoDB 為預設 DB</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a></td>
          <td>3 億訊息 / 天、TTL 自動清理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">9.C27 Disney+</a></td>
          <td>billions of actions daily、watchlist + 播放進度</td>
      </tr>
      <tr>
          <td><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%">9.C29 Lemino</a></td>
          <td>connection limit 才是 RDB bottleneck、改用 DynamoDB</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a> 把 DynamoDB 當 <em>排隊調度系統</em>、不只當 queue buffer：用 Counters table 控發 token 的速率、Queue table 紀錄序號、Connection table 串 WebSocket。這個架構跟 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> 的「全部塞進 DynamoDB 隱性緩衝」是兩種對立取捨 — Tixcraft 用透明度換流量吸收能力、SeatGeek 用流量吸收能力換體驗可見度。判讀重點：KV DB 不只能當 OLTP 替代品、4 張表組合就能變成業務級調度引擎、選表前要先確定業務需要哪一面。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2 State Storage Selection</a> — KV vs OLTP vs SearchIndex 選型</li>
<li>平行：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a>（OLTP 版本）/ <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a>、<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（含「預設 DB 治理 pattern」— KV 在大規模平台的選型治理）</li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（hot partition 量測）、<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>、<a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界</a></li>
<li>DynamoDB 深入：<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>、<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>、<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</a>、<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 設計</a></li>
<li>Cosmos DB 深入：<a href="/blog/backend/01-database/vendors/cosmosdb/partition-key-design/" data-link-title="Cosmos DB Partition Key Design：synthetic / composite / hierarchical &#43; 不可逆性硬約束" data-link-desc="Cosmos DB logical partition 10000 RU/s 上限、partition key 不可改、三種設計模式（synthetic / composite / hierarchical）、跟 DynamoDB / MongoDB 可逆性對比、latency budget 拆解 — 從 Minecraft Earth &#43; ASOS 切入">partition key 設計</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/ru-cost-model-sizing/" data-link-title="Cosmos DB RU/s 成本模型 &#43; 容量規劃：RU 思維、payload、index、provisioned vs autoscale vs serverless" data-link-desc="從 CPU&#43;IOPS 思維轉到 RU 思維的學習曲線、依負載形狀選容量模式、payload &#43; index policy 對 RU 的影響、autoscale reactive 限制 — 從 ASOS Black Friday &#43; Minecraft Earth 1M RU/s 壓測切入">RU 成本模型</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/" data-link-title="Cosmos DB 5 Consistency Levels：Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略" data-link-desc="Cosmos DB 5 個 consistency level 的工程選擇邏輯、Session 為何是 production 預設、per-request override 跟跨 collection 分流的進階策略、Strong &#43; multi-region 互斥的 cross-link — 從 Minecraft Earth &#43; ASOS 切入">一致性層次工程</a></li>
<li>MongoDB 深入：<a href="/blog/backend/01-database/vendors/mongodb/shard-key-selection/" data-link-title="MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius" data-link-desc="MongoDB sharded cluster shard key 選型（hashed / ranged / compound）、單 cluster 分 shard vs 多 cluster 分 blast radius 對照、跟 DynamoDB / Cosmos DB partition key 可逆性的跨 vendor 紀律">shard key 選型</a>、<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">schema design pattern</a>、<a href="/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/" data-link-title="MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling" data-link-desc="MongoDB 大規模 OLTP 撞牆不是單一 driver 議題、是 driver × 部署模型 × cache × scaling trigger 三層協作；含 Coinbase mongobetween / freshness token / ML 預測擴容三件套 &#43; 適用範圍紀律">connection 管理與 cache 層</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
<li><a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
</ul>
]]></content:encoded></item><item><title>1.11 全球分散式 OLTP</title><link>https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/global-distributed-oltp/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>全球分散式 OLTP 解決一個傳統 DB 做不到的問題：跨地理位置 &lt;em>同時&lt;/em> 維持強一致性、低延遲、高可用性。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP 定理&lt;/a>過往把這視為「三選二」，但近 15 年的工程進展（Google Spanner、AWS Aurora DSQL、CockroachDB、Microsoft Cosmos DB 等）顯示「在投入 &lt;em>專屬硬體&lt;/em> 或 &lt;em>特殊演算法&lt;/em> 的條件下、可以同時拿到 strong consistency + global distribution + 可接受 latency」。&lt;/p>
&lt;p>本章整理這類系統的工程設計、容量取捨、跟傳統 single-region OLTP 的差異。讀完後讀者能回答：什麼業務需求需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/global-oltp/" data-link-title="Global OLTP" data-link-desc="跨地理區域仍維持交易一致性的 OLTP 設計責任與代價">global OLTP&lt;/a>、跨 region &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum&lt;/a> 的延遲代價、選 Spanner vs Aurora DSQL vs Cosmos DB 的決策依據。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary&lt;/a> 的關係：1.3 處理 single-region OLTP 的 transaction 設計、本章處理 multi-region OLTP 的特殊取捨。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃&lt;/a> 的關係：1.10 KV 通常 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency&lt;/a> 全球分散容易、本章處理 &lt;em>強一致&lt;/em> 全球分散的工程挑戰。&lt;/p>
&lt;h2 id="cap-跟-pacelc理論工具">CAP 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pacelc/" data-link-title="PACELC" data-link-desc="在 CAP 之外補上正常時段的延遲與一致性取捨框架">PACELC&lt;/a>：理論工具&lt;/h2>
&lt;p>選擇全球 DB 前要先理解兩個理論框架。&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP 定理&lt;/a>&lt;/strong>：分散式系統 &lt;em>發生分區（network partition）&lt;/em> 時、必須在 Consistency 跟 Availability 二選一。&lt;/p>
&lt;ul>
&lt;li>CP 系統：強一致、partition 時拒絕服務（Spanner、Cosmos DB strong）&lt;/li>
&lt;li>AP 系統：高可用、partition 時可能回舊資料（Cassandra、DynamoDB Global Tables）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>PACELC（Daniel Abadi 提出）&lt;/strong>：擴充 CAP、加上「沒 partition 時」的取捨。&lt;/p>
&lt;ul>
&lt;li>沒 partition 時：Latency vs Consistency 二選一&lt;/li>
&lt;li>結合表示：PA/EL（partition 時選 Availability、平時選 Latency）vs PC/EC（partition 時選 Consistency、平時選 Consistency）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>工程含義&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Spanner、Aurora DSQL、Cosmos DB strong：PC/EC — 永遠選一致、付出 latency&lt;/li>
&lt;li>Cassandra、DynamoDB Global Tables：PA/EL — 永遠選快、付出可能不一致&lt;/li>
&lt;li>Cosmos DB session：PA/EL 但對同一 session 內保持 EC — 妥協方案&lt;/li>
&lt;/ul>
&lt;p>選 global DB 不是「哪個最好」、是「業務需要哪一邊」。金融交易、ticketing inventory、payment ledger 通常需要 EC；社群 feed、推薦、analytics 通常 EL 夠用。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>全球分散式 OLTP 解決一個傳統 DB 做不到的問題：跨地理位置 <em>同時</em> 維持強一致性、低延遲、高可用性。<a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP 定理</a>過往把這視為「三選二」，但近 15 年的工程進展（Google Spanner、AWS Aurora DSQL、CockroachDB、Microsoft Cosmos DB 等）顯示「在投入 <em>專屬硬體</em> 或 <em>特殊演算法</em> 的條件下、可以同時拿到 strong consistency + global distribution + 可接受 latency」。</p>
<p>本章整理這類系統的工程設計、容量取捨、跟傳統 single-region OLTP 的差異。讀完後讀者能回答：什麼業務需求需要 <a href="/blog/backend/knowledge-cards/global-oltp/" data-link-title="Global OLTP" data-link-desc="跨地理區域仍維持交易一致性的 OLTP 設計責任與代價">global OLTP</a>、跨 region <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a> 的延遲代價、選 Spanner vs Aurora DSQL vs Cosmos DB 的決策依據。</p>
<p>跟 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 的關係：1.3 處理 single-region OLTP 的 transaction 設計、本章處理 multi-region OLTP 的特殊取捨。</p>
<p>跟 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 的關係：1.10 KV 通常 <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a> 全球分散容易、本章處理 <em>強一致</em> 全球分散的工程挑戰。</p>
<h2 id="cap-跟-pacelc理論工具">CAP 跟 <a href="/blog/backend/knowledge-cards/pacelc/" data-link-title="PACELC" data-link-desc="在 CAP 之外補上正常時段的延遲與一致性取捨框架">PACELC</a>：理論工具</h2>
<p>選擇全球 DB 前要先理解兩個理論框架。</p>
<p><strong><a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP 定理</a></strong>：分散式系統 <em>發生分區（network partition）</em> 時、必須在 Consistency 跟 Availability 二選一。</p>
<ul>
<li>CP 系統：強一致、partition 時拒絕服務（Spanner、Cosmos DB strong）</li>
<li>AP 系統：高可用、partition 時可能回舊資料（Cassandra、DynamoDB Global Tables）</li>
</ul>
<p><strong>PACELC（Daniel Abadi 提出）</strong>：擴充 CAP、加上「沒 partition 時」的取捨。</p>
<ul>
<li>沒 partition 時：Latency vs Consistency 二選一</li>
<li>結合表示：PA/EL（partition 時選 Availability、平時選 Latency）vs PC/EC（partition 時選 Consistency、平時選 Consistency）</li>
</ul>
<p><strong>工程含義</strong>：</p>
<ul>
<li>Spanner、Aurora DSQL、Cosmos DB strong：PC/EC — 永遠選一致、付出 latency</li>
<li>Cassandra、DynamoDB Global Tables：PA/EL — 永遠選快、付出可能不一致</li>
<li>Cosmos DB session：PA/EL 但對同一 session 內保持 EC — 妥協方案</li>
</ul>
<p>選 global DB 不是「哪個最好」、是「業務需要哪一邊」。金融交易、ticketing inventory、payment ledger 通常需要 EC；社群 feed、推薦、analytics 通常 EL 夠用。</p>
<h2 id="spanner--truetime-模型">Spanner / <a href="/blog/backend/knowledge-cards/truetime/" data-link-title="TrueTime" data-link-desc="分散式資料庫用來界定時間不確定性的時間語意機制">TrueTime</a> 模型</h2>
<p><a href="https://cloud.google.com/spanner">Google Cloud Spanner</a> 是目前最成熟的 global strong-consistency OLTP。</p>
<p><strong>TrueTime API</strong>：用 GPS + 原子鐘提供「全球 <em>unambiguous</em> 時間戳」、解決分散式系統最難的問題之一 — 跨節點時序排序。</p>
<p><strong><a href="/blog/backend/knowledge-cards/external-consistency/" data-link-title="External Consistency" data-link-desc="交易可見順序與外部真實時間順序一致的強一致性語意">External consistency</a>（線性化）</strong>：用 TrueTime 保證「全球任何節點看到的交易順序、跟 wall clock 一致」。比 CAP 的 strong consistency 更強。</p>
<p><strong>容量特性</strong>（引自 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner 案例</a>）：</p>
<ul>
<li>內部峰值 &gt; 10 億 requests / 秒</li>
<li>線性擴展：2 nodes → 45K reads/sec、4 nodes → 90K reads/sec</li>
<li>跨地區交易延遲 100-200ms（quorum round-trip 不可壓縮）</li>
<li>multi-region instance 可設定 quorum location（影響哪幾個 region 必須同意）</li>
</ul>
<h3 id="線性擴展為什麼是-oltp-設計的最高目標">線性擴展為什麼是 OLTP 設計的最高目標</h3>
<p>「2 nodes → 45K reads/sec、4 nodes → 90K reads/sec」這個線性對應在傳統 OLTP（PostgreSQL、MySQL）做不到。原因是 <em>跨節點交易需要 coordinator 確認順序、coordinator 本身是 bottleneck</em>。加更多節點不會線性加吞吐、因為 coordinator 處理速度跟不上、其他節點得排隊等。</p>
<p>Spanner 用 Paxos + TrueTime 把 coordinator 變成「拓樸感知的多 leader」、每個 leader 只管自己 partition、不需要全域 coordinator。這層演算法 + 硬體（GPS + 原子鐘）配合、才達成線性擴展。</p>
<p><strong>為什麼這個 frame 對選型重要</strong>：讀「Spanner 撐 10 億 req/sec」不該理解成「能力差距」、而是「設計差距」— 傳統 OLTP 不是「沒它快」、是「結構上做不到線性」。如果業務未來會跨 region 擴展、必須在最初就選 <a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a>、不是先用 PostgreSQL 再「之後加 sharding」。</p>
<p><strong>對等技術跟取捨</strong>：</p>
<ul>
<li><strong>AWS Aurora DSQL</strong>：用其他協議（OCC + 分散式時鐘）達成跨 region strong consistency、不用 TrueTime 硬體。</li>
<li><strong>CockroachDB</strong>：用 HLC（Hybrid Logical Clock）+ Raft、可在通用硬體上跑、但 cross-region linearizability 需要 OCC retry。</li>
<li><strong>TiDB</strong>：用 TSO（Timestamp Oracle）服務發 global timestamp、TSO 本身是 single point、可用性要靠 TSO failover 設計。</li>
</ul>
<p>TrueTime 是 <em>專屬硬體投資</em>、其他方案是 <em>軟體 only</em>、兩者一致性保證等級類似、但運維成本跟認證難度差很大。可複製性低的 TrueTime 是 Google 的競爭優勢、不是普遍 best practice。</p>
<p><strong>容量規劃</strong>：</p>
<ul>
<li>節點數量 = 容量單位（每年 review）</li>
<li>跨 region quorum 配置決定 latency baseline</li>
<li>不能像 single-region OLTP 那樣短期擴容、需要提前 ramp</li>
</ul>
<p><strong>適用場景</strong>：</p>
<ul>
<li>金融交易、ticketing inventory</li>
<li>全球客戶但需要強一致</li>
<li>不能容忍跨地區 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 的業務</li>
</ul>
<p><strong>不適用</strong>：</p>
<ul>
<li>跨洲低延遲（沒辦法、TrueTime 也壓不下 100ms 跨洲）</li>
<li>高 throughput 但容忍 eventual consistency（Bigtable / Cassandra 更便宜）</li>
</ul>
<h3 id="分散式-sql-的-over-provision-屬結構性成本">分散式 SQL 的 over-provision 屬結構性成本</h3>
<p>分散式 SQL（TiDB、CockroachDB、Spanner）要求恆常 over-provision、是結構性成本、不是 capacity planning 失誤。三個原因都來自跨節點協調的物理需求：</p>
<ul>
<li>跨節點 transaction 需要 coordinator 角色、leader election 在尖峰當下不能發生、否則整個 cluster 卡住。</li>
<li>預留 buffer 讓 leader / follower lag 在尖峰時仍能收斂、否則 replication lag 爆增、讀走 replica 的 query 拿到太舊資料。</li>
<li>跨 region quorum 在某個 region 暫時不可用時、剩下 region 要能繼續 quorum、所以每 region 的容量都要 &gt;= quorum 所需。</li>
</ul>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> — Zomato 從 TiDB 遷出是業務需求側的判斷：該 workload 本身就能接受 eventually consistent、為 strong consistency 付的 over-provision 屬於浪費。判讀重點：strong consistency 是業務需求時、distributed SQL 的常態 over-provision 是合理代價；業務需求不到這個層級時、KV / 傳統 OLTP 是更划算的選項。</p>
<p>選型公式：先問業務需求要什麼一致性層級、再選 DB 類型、避免倒過來「先選 DB 再硬塞需求」。</p>
<h2 id="aurora-dsqlaws-的全球-strong-consistency-答案">Aurora DSQL：AWS 的全球 strong consistency 答案</h2>
<p>AWS 在 2024 re:Invent 推出 Aurora DSQL、是 AWS 對 Spanner 的回應。</p>
<p><strong>設計特點</strong>（引自 <a href="https://aws.amazon.com/blogs/database/amazon-aurora-dsql-for-global-scale-financial-transactions/">Aurora DSQL announcement</a>）：</p>
<ul>
<li>跨 region active-active write</li>
<li>強一致性（線性化）</li>
<li>PostgreSQL wire protocol compatible（應用層改動小）</li>
<li>Serverless（不必管 instance）</li>
</ul>
<p><strong>跟 Spanner 的差異</strong>：</p>
<ul>
<li>Spanner 用 TrueTime 硬體、Aurora DSQL 用其他協議</li>
<li>Aurora DSQL 跟 PostgreSQL 相容（容易遷移）、Spanner 是專屬 SQL dialect</li>
<li>Aurora DSQL 較新（2024）、生態還在成長</li>
<li>Spanner 服務時間長（內部 2007、外部 2017）、production 案例多</li>
</ul>
<p><strong>適用場景</strong>：</p>
<ul>
<li>AWS 生態用戶想要 global strong consistency</li>
<li>已用 Aurora / PostgreSQL、想擴展到 multi-region</li>
<li>應用層想保留 PostgreSQL ORM</li>
</ul>
<h2 id="cockroachdb-跟-tidb自管選項">CockroachDB 跟 TiDB：自管選項</h2>
<p>如果不想 vendor lock-in、或需要 on-prem 部署、選擇是 <em>self-managed</em> distributed SQL。</p>
<p><strong>CockroachDB</strong>：</p>
<ul>
<li>開源、可自管或用 Cockroach Cloud</li>
<li>跟 PostgreSQL wire protocol compatible</li>
<li>線性擴展、跨 region 部署、強一致</li>
<li>設計理念近 Spanner、但不用 TrueTime（用 HLC + Raft）</li>
</ul>
<p><strong>TiDB</strong>：</p>
<ul>
<li>開源（PingCAP）、可自管或用 TiDB Cloud</li>
<li>跟 MySQL wire protocol compatible</li>
<li>TiKV + TiDB 分層架構</li>
<li>中國市場大量使用、亞洲生態成熟</li>
</ul>
<p><strong>選擇取捨</strong>：</p>
<ul>
<li>vendor lock-in 風險 → 選 CockroachDB / TiDB</li>
<li>想 managed → 選 Spanner / Aurora DSQL</li>
<li>已用 PostgreSQL → 選 CockroachDB / Aurora DSQL（migration 容易）</li>
<li>已用 MySQL → 選 TiDB</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> 從 TiDB 遷出（理由不是 TiDB 不好、是 NewSQL 必須 over-provision、KV NoSQL 對該 workload 更划算）。</p>
<h2 id="cosmos-db-multi-region-write-模式">Cosmos DB multi-region write 模式</h2>
<p><a href="https://azure.microsoft.com/products/cosmos-db/">Azure Cosmos DB</a> 提供 <em>五個一致性層級</em>、是 multi-region OLTP 最有彈性的選擇之一。</p>
<p><strong>五個 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a></strong>（從強到弱）：</p>
<ol>
<li><strong>Strong</strong>：<a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizable</a>、跨 region quorum</li>
<li><strong><a href="/blog/backend/knowledge-cards/bounded-staleness/" data-link-title="Bounded Staleness" data-link-desc="允許資料延遲，但把落後上限限制在可量化範圍內的一致性語意">Bounded staleness</a></strong>：訂版本 / 時間上限</li>
<li><strong><a href="/blog/backend/knowledge-cards/session-consistency/" data-link-title="Session Consistency" data-link-desc="同一使用者工作階段內維持讀寫一致、跨工作階段允許短暫不一致">Session consistency</a></strong>：同 session 內強一致</li>
<li><strong>Consistent prefix</strong>：保證寫入順序</li>
<li><strong>Eventual</strong>：最便宜、最終一致</li>
</ol>
<p><strong>Multi-region write 特色</strong>：</p>
<ul>
<li>每個 region 都能寫、不必所有寫入回主 region</li>
<li>conflict resolution 用 LWW（Last-Writer-Wins）或自訂 stored procedure</li>
<li>跟 Spanner 的 strong consistency 不同 — 是 <em>AP 系統</em>、不保證 <a href="/blog/backend/knowledge-cards/linearizability/" data-link-title="Linearizability" data-link-desc="每次操作看起來都在單一全域順序中即時生效的一致性語意">linearizability</a></li>
</ul>
<p><strong>適用場景</strong>：</p>
<ul>
<li>全球用戶分布、想 <em>寫入本地 region</em> 減延遲</li>
<li>容忍 <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a>（電商商品評論、社群動態）</li>
<li>不能容忍跨 region failover 中斷</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a> — AR 玩家位置用 session consistency、跨 region 寫入</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a> — Black Friday 全球用戶、Cosmos DB 跨 region 複製</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — 分析 platform 用 weakest acceptable consistency、最大 throughput</li>
</ul>
<h2 id="跨地理合規法規限制下的-global-oltp">跨地理合規：法規限制下的 global OLTP</h2>
<p>部分產業（金融、醫療、政府）有 <em>資料駐留</em> 要求 — 特定國家的資料不能離境。這跟全球分散式 OLTP 的設計有 conflict。</p>
<p><strong>典型法規</strong>：</p>
<ul>
<li>歐盟 GDPR：歐洲用戶資料應留歐</li>
<li>中國《網路安全法》、《資料安全法》：中國用戶資料留中國</li>
<li>印度資料保護法：印度金融資料留印度</li>
<li>美國各州 healthcare（HIPAA）：醫療資料規範</li>
<li>金融業：各國央行通常規定本地交易資料留本地</li>
</ul>
<p><strong>設計策略</strong>：</p>
<ul>
<li><em>多個獨立 cluster</em>、每個合規區一個。不是 single global cluster。</li>
<li>meta-data 可以 global（用戶 profile 摘要）、transaction 必須 local</li>
<li>跨區查詢通過 federated query 或 ETL、不是直接 join</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 7 個受監管市場、各自獨立 Aurora cluster、不能合併</li>
<li><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> — 15 主 region + 5 衛星、按合規區分布</li>
<li><a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent</a> — 美國支付業務、Azure SQL Hyperscale + 美國 region</li>
</ul>
<h2 id="延遲代價跨-region-quorum-不可壓縮">延遲代價：跨 region quorum 不可壓縮</h2>
<p>全球 strong consistency 必須付的延遲代價來自物理。光速跑跨大西洋（紐約 ↔ 倫敦 5500 km）大約 27ms one-way、實際網路延遲 70-90ms（含路由 / 處理）。任何 strong consistency 系統都不能比這個快。</p>
<p><strong>典型跨 region quorum latency</strong>：</p>
<ul>
<li>同 region 跨 AZ：1-3ms</li>
<li>同 continent 跨 region（us-east-1 ↔ us-west-2）：50-80ms</li>
<li>跨 continent（us ↔ eu）：80-120ms</li>
<li>跨地球（us ↔ asia）：150-250ms</li>
</ul>
<p><strong>工程含義</strong>：</p>
<ul>
<li>SLO 訂 p99 &lt; 50ms 跨 continent strong consistency → 不可能達成</li>
<li>必須在 SLO 設計時就接受跨 region 的物理 floor</li>
<li>業務不需要 strong consistency 的話、用 session / eventual 換 latency</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> — sub-ms 需求、無法跨 region、用 single-AZ cluster placement</li>
<li><a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games</a> — 35ms VALORANT 延遲門檻、靠 region cluster 滿足、不靠 global DB</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget 卡片</a>。</p>
<h3 id="業務的不同延遲代價曲線">業務的不同延遲代價曲線</h3>
<p>讀「100-200ms 跨洲延遲」這種數字、不能只看絕對值、要看 <em>業務代價怎麼隨延遲變化</em>。不同業務型態的延遲代價曲線不同、決定能不能用 strong consistency 全球分散。</p>
<p><strong>B2B agent 操作介面</strong>（客服平台、CRM）：延遲代價的特性是 <em>累積</em>。agent 一通客戶電話內連續操作數十次、每次卡 1 秒、累積 30 秒讓 agent 在用戶面前沉默 — 客服效率直接掉一半、客戶等不及掛電話、agent 績效跟 NPS 同時下降。專屬訊號是「單次 latency 看似可接受、agent 體感卻變慢」。對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> 用 15 個 region 把任一 agent 的 DB 延遲壓到 &lt; 50ms — 客服 SaaS 對單次延遲的容忍區間遠窄於一般網路服務。</p>
<p><strong>B2C 終端用戶</strong>（社群、電商）：延遲代價是 <em>一次性跳離</em>。用戶等 1 秒會抱怨、等 3 秒會跳離；但完成一個操作就走、不會像 B2B 累積多次。容忍區間在 200ms-500ms、超過就掉 conversion。專屬訊號是「session bounce rate 跟 latency p99 高度相關」、不是看平均。</p>
<p><strong>金融交易</strong>（payment、trading）：延遲代價有兩面、是其他業務型態少見的結構。一面是用戶體驗（付款卡 = 結帳放棄）、另一面是 <em>系統正確性</em>（交易順序錯 = 對帳異常、稽核失敗）。後者讓金融業願意付 100-200ms 換 strong consistency、因為對帳成本遠高於延遲成本。專屬訊號是「願意接受比 B2C 更高的 latency budget、但拒絕任何 consistency 妥協」。對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 7 個受監管市場的設計。</p>
<p><strong>IoT / Telemetry</strong>：延遲幾乎無業務代價（資料晚 10 秒進來、報表還是準）、但 throughput 才是主導指標。原因是這類業務的價值來自 <em>大量裝置的聚合趨勢</em>、不是 <em>單一裝置即時回應</em>；只要事件最終到達且順序合理、晚一點不影響決策。專屬訊號是「百萬裝置同時上報、寫入吞吐才是 SLO、latency 不在 alert 條件裡」。選型上 KV 或時序 DB 比 strong-consistency OLTP 更划算。</p>
<p>判讀重點：選 global OLTP 前先畫業務的延遲代價曲線、再決定能付多少 latency budget 給 strong consistency。「100ms 跨洲太慢」這個直覺反射只在沒有對帳 / 累積 / 趨勢這些業務代價時成立。</p>
<h2 id="容量規劃跟-single-region-oltp-完全不同">容量規劃：跟 single-region OLTP 完全不同</h2>
<p>全球分散式 OLTP 的容量規劃有獨特挑戰。</p>
<p><strong>容量單位</strong>：</p>
<ul>
<li>Spanner：節點數</li>
<li>Aurora DSQL：serverless 自動（按 ACU 計費）</li>
<li>Cosmos DB：RU/s（每個 region 獨立配置）</li>
<li>CockroachDB / TiDB：節點數 + storage</li>
</ul>
<p><strong>規劃要點</strong>：</p>
<ul>
<li>每個 region 獨立規劃（跨 region 不能 amortize）</li>
<li>quorum 配置決定哪些 region 必須同意（影響 failure domain）</li>
<li>跨 region replication lag 是 SLO 一部分</li>
<li>不能像 single-region 那樣 reactive 擴容、必須 predictive</li>
</ul>
<p><strong>對應 <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></strong>：全球 OLTP 是「不可水平擴容服務」的延伸 — 不只「單機極限」、是「跨 region 協調的物理極限」。</p>
<h2 id="可用性目標的成本曲線">可用性目標的成本曲線</h2>
<p>「我們要 99.99% 還是 99.999%」這個問題不該用直覺答、要先看每多一個 9 帶來的成本是多少。可用性是非線性、不是線性。</p>
<p><strong>九的數學意義</strong>：</p>
<table>
  <thead>
      <tr>
          <th>可用性</th>
          <th>年停機時間</th>
          <th>月停機時間</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>99%</td>
          <td>87.6 小時 / 年</td>
          <td>7.3 小時 / 月</td>
          <td>開發 / 內部工具</td>
      </tr>
      <tr>
          <td>99.9%</td>
          <td>8.76 小時 / 年</td>
          <td>43.8 分鐘 / 月</td>
          <td>一般 B2C 網站</td>
      </tr>
      <tr>
          <td>99.95%</td>
          <td>4.38 小時 / 年</td>
          <td>21.9 分鐘 / 月</td>
          <td>B2C SaaS、有 SLA 但非 mission-critical</td>
      </tr>
      <tr>
          <td>99.99%</td>
          <td>52.6 分鐘 / 年</td>
          <td>4.38 分鐘 / 月</td>
          <td>受監管產業、付款</td>
      </tr>
      <tr>
          <td>99.999%</td>
          <td>5.26 分鐘 / 年</td>
          <td>26 秒 / 月</td>
          <td>客服 SaaS、telco、5x9 是合約義務</td>
      </tr>
      <tr>
          <td>99.9999%</td>
          <td>31.5 秒 / 年</td>
          <td>2.6 秒 / 月</td>
          <td>極特殊（核電、航空管制）</td>
      </tr>
  </tbody>
</table>
<p><strong>為什麼 99.99 → 99.999 是指數成本而非線性</strong>：每多一個 9、要求 <em>每一層基礎設施</em> 都要對等冗餘。</p>
<ul>
<li>99.9 → 99.99：加 multi-AZ active-active、~2-3x 成本</li>
<li>99.99 → 99.999：加 multi-region active-active、+ DR 演練、+ failover 自動化、+ 監控覆蓋率拉滿、~5-10x 成本</li>
<li>99.999 → 99.9999：加多 cloud、+ 異地災備、+ 全自動 failover、+ 全鏈路演練、~20-50x 成本</li>
</ul>
<p><strong>適用場景的業務理由</strong>：</p>
<ul>
<li><strong>99.99%（受監管產業、付款）</strong>：合約 SLA 通常落在這層。受監管金融在中央銀行 / 金融監管機關的書面要求下、年度書面合規會審查 downtime 紀錄、超過 52 分鐘 / 年要解釋；付款 gateway 對商家 SLA 通常承諾 99.99%、低於這個值會被合作夥伴扣保證金。</li>
<li><strong>99.999%（客服 SaaS / telco）</strong>：5x9 是 B2B 客服 SaaS 跟電信業的 <em>合約義務</em>、不是行銷話術。對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> — 客服平台用 15 主 region + 5 衛星 region 達 99.999%、架構成本約是 single-region 的 15 倍、但 B2B 客服合約要 5x9、這是合理投資。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> — 廣告計費 1 分鐘斷線可能損失幾百萬美金廣告收入、5x9 對應真實營收邊界。電信業 911 緊急通話必須 5x9 是更嚴格的法規層級。</li>
<li><strong>99.9999%（核電、航空管制）</strong>：6x9 不只是工程目標、是 <em>公共安全法規</em>。核電廠 SCADA 系統、空管雷達、軌道交通信號這類業務 30 秒 / 年的中斷會威脅生命、所以付得起跨多 cloud / 異地災備 / 全鏈路演練的成本。一般網路服務談 6x9 通常是過度設計。</li>
</ul>
<p><strong>SLO 木桶效應</strong>：99.999% 是 <em>系統整體</em> 數字、不是 DB 單獨。DNS、load balancer、application、DB、storage 任何一層 single-region 就破壞整體 SLO。傳統工程師常以為「DB 多 region 就好」、忽略 application 跑在 single-region 的話、application down = 整體 down。</p>
<p>要達成 5x9、要 <em>每一層</em> 都 multi-region active-active、且 <em>failover 流程能自動執行</em>（人類在事故當下做不到 5 分鐘內完成切換）。對應 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> 的跨 region 部署、跟 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 可靠性驗證模組</a> 的 DR 演練。</p>
<p><strong>Region 成本曲線</strong>：N 個 region 的成本約是 1 個 region 的 N 倍（DB + compute + storage 都要複製）、但業務收益不是線性。</p>
<ul>
<li>1 region：覆蓋本國用戶</li>
<li>3 region（同 continent）：覆蓋整 continent、延遲 &lt; 50ms</li>
<li>6 region（跨 continent）：覆蓋全球、延遲 100-200ms</li>
<li>15 region：每個用戶 &lt; 50ms 接入（如 Genesys 模式）</li>
</ul>
<p>從 6 region → 15 region 的成本是 2.5x、但用戶體驗改善（50ms 延遲）對 B2B 客服很關鍵、對 B2C 推薦系統幾乎無感。region 數量選擇要看 <em>業務模型對延遲的敏感度</em>、不是工程「越多越好」。</p>
<h2 id="sharding-粒度跟業務一致性需求">Sharding 粒度跟業務一致性需求</h2>
<p>distributed SQL 跟 single-cluster SQL 之間還有一層：<strong>多個獨立 cluster + 應用層 sharding</strong>。選哪個跟業務的一致性需求有關。</p>
<p><strong>Hyperscale / Aurora 同類設計</strong>（storage / compute 分離）：</p>
<ul>
<li>AWS Aurora、Azure SQL Hyperscale、GCP AlloyDB、Spanner 都採類似工程哲學 — log-structured 分散式 storage + 獨立 compute scale</li>
<li>storage 最高通常 100 TB（Hyperscale）、超過要 sharding</li>
<li>compute 上限是 instance type（80 vCore 等）、超過要 sharding 或換 distributed SQL</li>
</ul>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent</a> — 5 億筆/年支付交易、用 Hyperscale 撐單一 cluster、沒拆 sharding 是因為支付業需要 <em>跨 merchant 對帳一致性</em>、共用 OLTP 比拆 cluster 划算。</p>
<p><strong>選 vendor 看生態、不看技術</strong>：Hyperscale 跟 Aurora 工程哲學一致、選哪家取決於 application 已在哪個 cloud。AWS 客戶選 Aurora、Azure 客戶選 Hyperscale、GCP 客戶選 AlloyDB / Spanner。技術差異小、生態差異大（IAM 整合、observability tooling、計費綁定）。</p>
<p><strong>業務一致性需求決定 sharding 粒度</strong>：</p>
<ul>
<li><strong>微服務各自 OLTP</strong>（Netflix Aurora consolidation）：每個微服務有自己的 Aurora cluster、跨服務一致性靠 application 層 saga / outbox。適合服務間業務 <em>天然解耦</em>（用戶服務、訂單服務、商品服務各自 owned data）。Query path 上、跨服務查詢必須走 API 而非 SQL JOIN、要接受查多個服務多次往返；一致性 path 上、跨服務 transaction 用 saga + compensation、容忍中間態。</li>
<li><strong>微服務共用 OLTP</strong>（Clearent Hyperscale）：所有微服務共用一個大 cluster、跨服務一致性靠 DB transaction。適合業務 <em>天然耦合</em>（payment 跟 refund 跟 chargeback 必須在同一 transaction）。Query path 上、可以用 SQL JOIN 直接查跨服務資料、簡單；一致性 path 上、所有微服務共享一個 schema 演進邊界、schema migration 影響所有服務、要協調。</li>
<li><strong>Sharding by tenant</strong>（B2B SaaS）：每個 enterprise tenant 自己 cluster、適合 tenant 之間完全隔離、大客戶可能要求專屬 cluster。Query path 上、跨 tenant 查詢（例如平台級報表）要走 federated query 或 ETL 聚合、不能直接 join；運維 path 上、每個 tenant cluster 的容量規劃、backup、upgrade 都獨立、運維工時隨 tenant 數量線性成長。</li>
<li><strong>Sharding by region</strong>（受監管產業）：每個合規市場自己 cluster、合規驅動、不是性能驅動。對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 7 個市場各自獨立。</li>
</ul>
<p>判讀重點：sharding 不是「擴容到不夠才做」、是「業務模型決定的初始設計」。等到 single cluster 撐不住才開始 shard、會踩進「跨 shard 一致性」的工程地雷區、修改成本遠高於初期設計成本。Managed DB（Aurora、Hyperscale）的容量上限是 <em>已知</em> 的、設計時就該知道未來何時觸發 sharding。對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 的 storage 層 replication 段 — Hyperscale / Aurora / Spanner 同類設計的容量上限同樣是 sharding 觸發點。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></td>
          <td>10 億 req/sec 線性擴展、TrueTime 實作</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth Cosmos DB</a></td>
          <td>turnkey global distribution、5 consistency levels</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>受監管金融跨市場、必須各自獨立 cluster</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Cosmos DB</a></td>
          <td>全球零售 multi-region、Black Friday 持續高峰</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 99.999%</a></td>
          <td>跨 15 region active-active 達 5 個 9 可用性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/clearent-azure-sql-hyperscale-payments/" data-link-title="9.C32 Clearent：Azure SQL Hyperscale 撐每年 5 億筆支付交易" data-link-desc="Clearent 在 Azure SQL Hyperscale 上處理每年 5 億筆支付交易、autoscale &#43; 微服務架構">9.C32 Clearent Azure SQL Hyperscale</a></td>
          <td>美國支付業、storage / compute 分離擴展</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a>（single-region OLTP）</li>
<li>平行：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>（KV 全球分散）</li>
<li>下游：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（含「預設 DB 治理 pattern」— 平台規模化階段的 OLTP 選型治理）</li>
<li>跨模組：<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>、<a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a>、<a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2 State Storage Selection</a>、<a href="/blog/backend/07-security-data-protection/data-residency-deletion-and-evidence-chain/" data-link-title="7.11 資料駐留、刪除與證據鏈" data-link-desc="定義跨區資料駐留、刪除請求與可驗證證據鏈問題">7.11 Data Residency</a></li>
<li>Spanner 深入：<a href="/blog/backend/01-database/vendors/spanner/truetime-api-depth/" data-link-title="Spanner TrueTime API 深度：GPS &#43; 原子鐘、commit wait、為什麼 line-rate scaling 才是設計目的" data-link-desc="TrueTime 是手段、line-rate scaling 才是 Spanner 的設計目的。本文先扣商業邏輯：傳統 OLTP coordinator 為什麼是 bottleneck、Spanner 怎麼用 TrueTime &#43; Paxos 換成拓樸感知多 leader；再展開 TrueTime ε / commit wait 數學、ε 暴衝失敗模式、cross-region voting 對 latency 的影響、跟 9.C10 Google internal dogfood 揭露的線性擴展模式對照">TrueTime API 深入</a>、<a href="/blog/backend/01-database/vendors/spanner/consistency-models-comparison/" data-link-title="Spanner Consistency Models 對照：external consistency vs serializability vs linearizability" data-link-desc="external consistency、serializability、linearizability 是三個常被混用的概念。本文先精確定義三者差異、再用 line-rate scaling 對照表（PG SSI / CockroachDB / Spanner / Aurora DSQL）回答為什麼 Spanner 不只是『更強的 serializable』、最後用 9.C10 揭露的 cross-region quorum 100-200ms 物理硬限解釋『強一致 &#43; 全球部署』的真實 cost">一致性模型對照</a>、<a href="/blog/backend/01-database/vendors/spanner/schema-migration-interleaved-tables/" data-link-title="Spanner Schema Migration Without Downtime &#43; Interleaved Tables" data-link-desc="Spanner DDL 是 long-running operation、用 TrueTime 給每次 schema change 分配 version timestamp、所有 read / write 對應自己 transaction timestamp 看到對應 schema。Interleaved table 是 storage-level parent-child 物理交錯、不是 logical FK。本文走 schema change lifecycle、interleaved layout 機制、backfill capacity 影響、5 production 踩雷、跟 PostgreSQL online schema change 對照">interleaved table schema migration</a></li>
<li>CockroachDB / Aurora DSQL 深入：<a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a>、<a href="/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/" data-link-title="CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑" data-link-desc="CockroachDB default SERIALIZABLE、application 必須包 retry loop 處理 40001 serialization_failure。本文走 PG → CockroachDB application contract 重塑視角、SAVEPOINT cockroach_restart 語法、5 種失敗模式（retry storm / 非冪等 / cross-statement state / hot row / long-running transaction）。**整篇是跨 case 合成 frame**：DoorDash case 沒揭露 retry pattern、只揭露 PG wire protocol 相容 &#43; SQL 行為仍要 audit、本章 retry contract 重塑屬通用工程議題從 Cockroach Labs 官方 docs 合成">CockroachDB transaction retry pattern</a>、<a href="/blog/backend/01-database/vendors/cockroachdb/survival-goals/" data-link-title="CockroachDB Survival Goals：zone 級 vs region 級配置與業務 SLO 倒推流程" data-link-desc="CockroachDB 用 SURVIVE ZONE FAILURE / SURVIVE REGION FAILURE 兩種 survival goal 宣告式控制 Raft replica 分佈、決定 RTO / RPO。本文走 Hard Rock Digital bet placement RPO=0 倒推流程、Netflix Gaming 48-node 跨 4 region 「為求 survival 而非 latency」的反直覺判讀、配置語法、寫入 latency 暴漲跟 cost 暴漲兩條失敗模式、合規邊界對比">survival goals</a>、<a href="/blog/backend/01-database/vendors/cockroachdb/locality-aware-schema/" data-link-title="CockroachDB Locality-Aware Schema：跨州合規 &#43; 邏輯一個 cluster 的 region placement 策略" data-link-desc="Hard Rock Digital 跨 8 州 sportsbook、用 AWS Outposts &#43; region placement 把運算釘在州內、邏輯上仍是一個 CockroachDB cluster。本文走 REGIONAL BY ROW / REGIONAL BY TABLE / GLOBAL 三種 locality、Hard Rock 拓樸創新對比 Standard Chartered Aurora 7 cluster fleet、AWS Outposts 是合規工具不是 latency 工具的反直覺判讀">locality-aware schema</a></li>
<li>Aurora 多 region 深入：<a href="/blog/backend/01-database/vendors/aurora/global-database-multi-region/" data-link-title="Aurora Global Database：跨 region async replication、&lt; 1 秒 lag 與合規 anti-recommendation" data-link-desc="Aurora Global Database 跨 region storage-level async replication、&lt; 1 秒 typical lag、planned vs unplanned failover RTO 數量級對比、Standard Chartered 合規禁止跨境複製為什麼讓 Global Database 變反指標">global database multi-region</a>、<a href="/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/" data-link-title="Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約" data-link-desc="Aurora cross-AZ failover lifecycle（detection / promotion / DNS update）、&lt; 30 秒 RTO、application DNS cache 跟 connection pool 對齊、Standard Chartered 受監管場景為什麼用獨立 cluster 而非 Global Database failover">跨 AZ failover RTO</a></li>
<li>Cosmos DB 多 region 深入：<a href="/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/" data-link-title="Cosmos DB 5 Consistency Levels：Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略" data-link-desc="Cosmos DB 5 個 consistency level 的工程選擇邏輯、Session 為何是 production 預設、per-request override 跟跨 collection 分流的進階策略、Strong &#43; multi-region 互斥的 cross-link — 從 Minecraft Earth &#43; ASOS 切入">一致性層次工程</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">多 region write 衝突</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">Transaction Boundary</a></li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">Universal Scalability Law</a></li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
</ul>
]]></content:encoded></item><item><title>1.12 大規模 DB 遷移實戰</title><link>https://tarrragon.github.io/blog/backend/01-database/large-scale-db-migration/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/large-scale-db-migration/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>DB 遷移是後端工程中 &lt;em>風險最高的長期工作&lt;/em> 之一。一次失敗的遷移可能造成資料丟失、用戶體驗劣化、合規違約、團隊信心受挫。本章整理近 5 年公開的大規模 DB 遷移案例、提煉出可重用的工程流程。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook&lt;/a> 的關係：1.6 是 &lt;em>generic playbook&lt;/em>、本章針對「&lt;em>跨 DB 種類&lt;/em>」遷移（PostgreSQL → Aurora、TiDB → DynamoDB、MongoDB → Cosmos DB）、規模較大、風險較高。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence&lt;/a> 的關係：1.7 處理 &lt;em>同一 DB 內&lt;/em> 的 schema 演進、本章處理 &lt;em>換 DB engine&lt;/em> 的遷移。兩者都用 evidence-based gate、但 stakes 不同。&lt;/p>
&lt;p>讀完後讀者能回答：跨 DB 遷移該怎麼分階段、dual-write 怎麼設計、shadow read 怎麼驗證、cutover 怎麼安全進行、rollback window 訂多久。&lt;/p>
&lt;h2 id="遷移類型分類">遷移類型分類&lt;/h2>
&lt;p>DB 遷移不是單一概念、按 &lt;em>變動範圍&lt;/em> 分四類、每類風險跟流程不同。&lt;/p>
&lt;p>&lt;strong>Type 1：scale-up（換 instance）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>例：m5.large → m5.4xlarge&lt;/li>
&lt;li>變動：硬體規格、不變 schema、不變 DB engine&lt;/li>
&lt;li>風險：低、通常 minutes downtime 即可&lt;/li>
&lt;li>工具：vendor 提供 in-place scaling&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Type 2：schema migration&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>例：加欄位、加 index、改 data type&lt;/li>
&lt;li>變動：schema 結構、不變 DB engine&lt;/li>
&lt;li>風險：中、需要 expand-contract 模式&lt;/li>
&lt;li>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Type 3：cross-DB engine migration&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>例：PostgreSQL → Aurora、SQL Server → PostgreSQL、TiDB → DynamoDB&lt;/li>
&lt;li>變動：DB engine、可能 schema、可能 query language&lt;/li>
&lt;li>風險：高、可能需要應用層改寫、cutover 風險大&lt;/li>
&lt;li>本章重點&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Type 4：cross-model migration&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>例：RDBMS → KV、Document → Graph&lt;/li>
&lt;li>變動：資料模型、必須應用層大改寫&lt;/li>
&lt;li>風險：極高、通常分 service 漸進遷移、不會一次切完&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato TiDB → DynamoDB&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="為什麼要做大規模-db-遷移">為什麼要做大規模 DB 遷移&lt;/h2>
&lt;p>不是所有遷移都值得做。理由要強過 &lt;em>成本 + 風險&lt;/em>、不然不該開工。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>DB 遷移是後端工程中 <em>風險最高的長期工作</em> 之一。一次失敗的遷移可能造成資料丟失、用戶體驗劣化、合規違約、團隊信心受挫。本章整理近 5 年公開的大規模 DB 遷移案例、提煉出可重用的工程流程。</p>
<p>跟 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook</a> 的關係：1.6 是 <em>generic playbook</em>、本章針對「<em>跨 DB 種類</em>」遷移（PostgreSQL → Aurora、TiDB → DynamoDB、MongoDB → Cosmos DB）、規模較大、風險較高。</p>
<p>跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a> 的關係：1.7 處理 <em>同一 DB 內</em> 的 schema 演進、本章處理 <em>換 DB engine</em> 的遷移。兩者都用 evidence-based gate、但 stakes 不同。</p>
<p>讀完後讀者能回答：跨 DB 遷移該怎麼分階段、dual-write 怎麼設計、shadow read 怎麼驗證、cutover 怎麼安全進行、rollback window 訂多久。</p>
<h2 id="遷移類型分類">遷移類型分類</h2>
<p>DB 遷移不是單一概念、按 <em>變動範圍</em> 分四類、每類風險跟流程不同。</p>
<p><strong>Type 1：scale-up（換 instance）</strong>：</p>
<ul>
<li>例：m5.large → m5.4xlarge</li>
<li>變動：硬體規格、不變 schema、不變 DB engine</li>
<li>風險：低、通常 minutes downtime 即可</li>
<li>工具：vendor 提供 in-place scaling</li>
</ul>
<p><strong>Type 2：schema migration</strong>：</p>
<ul>
<li>例：加欄位、加 index、改 data type</li>
<li>變動：schema 結構、不變 DB engine</li>
<li>風險：中、需要 expand-contract 模式</li>
<li>詳見 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a></li>
</ul>
<p><strong>Type 3：cross-DB engine migration</strong>：</p>
<ul>
<li>例：PostgreSQL → Aurora、SQL Server → PostgreSQL、TiDB → DynamoDB</li>
<li>變動：DB engine、可能 schema、可能 query language</li>
<li>風險：高、可能需要應用層改寫、cutover 風險大</li>
<li>本章重點</li>
</ul>
<p><strong>Type 4：cross-model migration</strong>：</p>
<ul>
<li>例：RDBMS → KV、Document → Graph</li>
<li>變動：資料模型、必須應用層大改寫</li>
<li>風險：極高、通常分 service 漸進遷移、不會一次切完</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato TiDB → DynamoDB</a></li>
</ul>
<h2 id="為什麼要做大規模-db-遷移">為什麼要做大規模 DB 遷移</h2>
<p>不是所有遷移都值得做。理由要強過 <em>成本 + 風險</em>、不然不該開工。</p>
<p><strong>合理動機</strong>：</p>
<ul>
<li><strong>舊系統規模上限</strong>：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> TiDB 必須長期 over-provision 應付 spike、成本不划算 → 換 DynamoDB on-demand 後 50% 成本下降</li>
<li><strong>舊系統運維成本</strong>：<a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify</a> 自管 Kafka 工程成本太高 → 換 managed Pub/Sub 釋放 SRE</li>
<li><strong>舊系統失能</strong>：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> 多套 RDBMS（PostgreSQL、MySQL、Oracle）DBA 負擔重 → 統一到 Aurora、效能 +75% 成本 -28%</li>
<li><strong>vendor 終止支援</strong>：mongoDB 改授權、TiDB 改授權、Mesos 被棄、Oracle 升級費高</li>
<li><strong>合規要求</strong>：<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 新市場上線、需要本地合規 cluster</li>
<li><strong>新功能需求</strong>：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> 需要 global distribution、原 MongoDB 達不到</li>
</ul>
<p><strong>不合理動機（要警惕）</strong>：</p>
<ul>
<li>「新技術好酷」：fad-driven、通常會後悔</li>
<li>「vendor sales 推銷」：sales 利益跟你 ROI 不一致</li>
<li>「同行 X 也在遷」：人家的場景跟你不同</li>
<li>「主管要看到 transformation」：政治、不是工程</li>
</ul>
<h2 id="遷移階段流程">遷移階段流程</h2>
<p>成熟的大規模 DB 遷移分五階段、每階段有明確 exit criteria。</p>
<h3 id="階段-1可行性評估t-180--t-90">階段 1：可行性評估（T-180 ~ T-90）</h3>
<p><strong>輸出</strong>：可行性報告、決定 go / no-go。</p>
<p><strong>評估項目</strong>：</p>
<ul>
<li>workload 在新 DB 上是否真的能跑（不是 marketing、是實測 POC）</li>
<li>應用層改寫成本（哪些 query 需要改、哪些 ORM 需要換）</li>
<li>遷移時程預估（含 <em>合規審查</em> lead time、如金融業可能 3-12 個月）</li>
<li>成本對比（總成本曲線、不只當下 snapshot）</li>
<li>失敗代價（如果遷移失敗、business 影響多大）</li>
</ul>
<p><strong>跨雲遷移特有 gap 分析</strong>：當遷移橫跨雲廠商時、評估項目要加上 <a href="/blog/backend/00-service-selection/cloud-vendor-capability-mapping/" data-link-title="0.19 雲端服務對照地圖（AWS / GCP / Azure）" data-link-desc="把後端能力分類對照到 AWS / GCP / Azure 的具體服務名稱、保留跨雲遷移與選型差異的判讀重點">0.19 雲端服務對照地圖</a> 的「對應 ≠ 等價」差異維度：</p>
<ul>
<li>一致性模型差異（如 DynamoDB eventual vs Cosmos DB 五級可選）</li>
<li>failover 時間差異（vendor 文件 vs 實測長尾）</li>
<li>計價模型差異（per-request vs provisioned capacity 換算）</li>
<li>配額差異（partition 上限、batch size、throttling 行為）</li>
<li>Data gravity / egress lock-in（PB 級資料的 egress fee 常是被低估的單筆最大成本）</li>
</ul>
<p>跨雲遷移的失敗多數來自 0.19 對照表沒做完整 gap 分析、把「名稱對應」當「能力等價」。</p>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> — POC 驗證 DynamoDB 撐得住、再決定遷移</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB API 相容讓 POC 成本低、加速決策</li>
</ul>
<h3 id="階段-2應用層相容性改造t-90--t-30">階段 2：應用層相容性改造（T-90 ~ T-30）</h3>
<p><strong>輸出</strong>：應用層支援 <em>新舊 DB 雙寫</em>、可以隨時切換。</p>
<p><strong>改造項目</strong>：</p>
<ul>
<li>Repository adapter 抽象化（<a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">1.4 Repository Adapter</a>）</li>
<li>新增 <em>新 DB</em> 的 adapter 實作</li>
<li>配置「寫入 mode」：old only / dual-write / new only</li>
<li>query 端「讀取 mode」：old / new / shadow（讀兩邊比對）</li>
<li>error handling 兼容（不同 DB 的錯誤碼）</li>
</ul>
<p><strong>API-compatible 遷移的優勢</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> MongoDB → Cosmos DB MongoDB API — 應用層幾乎不用改、只換 connection string</li>
<li>Aurora PostgreSQL-compatible → 不改 SQL 跟 ORM</li>
<li>缺點：API 相容不等於行為完全相同、要 <em>特定 query pattern</em> 驗證</li>
</ul>
<h3 id="階段-3dual-write--shadow-read-驗證t-30--t-7">階段 3：Dual-write + shadow read 驗證（T-30 ~ T-7）</h3>
<p>dual-write / shadow read / backfill 的 <em>generic 機制</em> 詳見 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence</a>（含 Dual-write divergence schema 詳細分類）；本章只強調 <em>跨 DB engine</em> 遷移的特殊取捨。</p>
<p><strong>輸出</strong>：新 DB 已 <em>並行寫入</em>、跟舊 DB 結果一致。</p>
<p><strong>Dual-write 流程</strong>：</p>
<ol>
<li>應用層同時寫入 old 跟 new DB</li>
<li>用 old DB 結果回應用戶</li>
<li>log 兩邊寫入是否成功、有差異就 alert</li>
<li>backfill 之前的歷史資料到 new DB</li>
</ol>
<p><strong>Shadow read 驗證</strong>：</p>
<ol>
<li>應用層查 old DB 拿結果回用戶</li>
<li><em>也</em> 查 new DB、比對結果是否一致</li>
<li>不一致記錄到 audit log</li>
<li>跑 N 天（建議 7-14 天）確認一致性高</li>
</ol>
<p><strong>注意事項</strong>：</p>
<ul>
<li>Dual-write 期間 <em>兩邊都要可寫</em>、寫失敗的 fallback 流程明確</li>
<li>新 DB 還沒承擔流量、容量規劃要 <em>提前 ramp up</em>、不要等 cutover 才發現容量不夠</li>
<li>監控指標：write success rate、cross-DB inconsistency rate、replication lag、performance metrics</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> — 遷移前用 dual-write 驗證 4 倍吞吐改善是真的、不是 POC marketing。</p>
<h3 id="階段-4cutovert-7--t-0">階段 4：Cutover（T-7 ~ T-0）</h3>
<p><strong>輸出</strong>：用戶流量切到 new DB、old DB 變成 fallback。</p>
<p><strong>Cutover 策略</strong>：</p>
<p><strong>Big-bang cutover</strong>：一次切全部流量</p>
<ul>
<li>優點：簡單、不必維護 <em>跨 DB consistency</em></li>
<li>缺點：風險集中、rollback 困難</li>
<li>適合：小規模、low-stakes</li>
</ul>
<p><strong>Gradual cutover</strong>（推薦）：分階段切</p>
<ul>
<li>T-7：1% 流量到 new DB、觀察 1 天</li>
<li>T-6：5% → 觀察 1 天</li>
<li>T-5：25% → 觀察 1 天</li>
<li>T-3：50% → 觀察 2 天</li>
<li>T-1：100%</li>
</ul>
<p><strong>Reverse rollout</strong>：某些工作負載先切（read-only first、再 write）</p>
<ul>
<li>T-7：所有 read 切到 new DB（write 還在 old）</li>
<li>T-3：write 切到 new DB（read 已驗證）</li>
</ul>
<h3 id="階段-5rollback-window--清理t0--t30">階段 5：Rollback window + 清理（T+0 ~ T+30+）</h3>
<p><strong>Rollback window</strong>：cutover 後保持 <em>可隨時 rollback 回 old DB</em> 的狀態。</p>
<p><strong>Rollback window 設計</strong>：</p>
<ul>
<li>短期（T+7）：保持 dual-write、可以即時切回 old DB</li>
<li>中期（T+30）：保留 old DB read-only、需要 manual 切回但快</li>
<li>長期（T+90）：保留 old DB snapshot、disaster recovery 用</li>
<li>結束：徹底刪除 old DB（含 backup、ETL pipeline 改寫）</li>
</ul>
<p><strong>Cleanup 工作</strong>：</p>
<ul>
<li>移除 dual-write code</li>
<li>移除 shadow read code</li>
<li>簡化 repository adapter（只保留 new DB）</li>
<li>文件更新（runbook、onboarding doc）</li>
<li>decommission old DB（不立即砍、保留至少 90 天備援）</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub</a> — 大規模事件交付系統的 multi-month 漸進遷移、有明確 rollback path。</p>
<h2 id="api-compatible-vs-應用層改寫">API-compatible vs 應用層改寫</h2>
<p>跨 DB 遷移的關鍵決策：要不要追求 <em>應用層零改動</em>。</p>
<p><strong>API-compatible 遷移</strong>：</p>
<ul>
<li>新 DB 提供舊 DB 的 wire protocol / API</li>
<li>應用層只換 connection string、不改 query</li>
<li>例：MongoDB → Cosmos DB（MongoDB API）、Cassandra → Cosmos DB（Cassandra API）、MySQL → Aurora（MySQL）</li>
</ul>
<p><strong>優點</strong>：</p>
<ul>
<li>遷移成本低（不必改 application code）</li>
<li>風險低（不會引入 query bug）</li>
<li>時程快（不必等 application 改寫）</li>
</ul>
<p><strong>缺點</strong>：</p>
<ul>
<li>行為可能不完全一致（subtle bug）</li>
<li>性能可能不是最佳（compat 層有 overhead）</li>
<li>vendor lock-in 更深</li>
</ul>
<p><strong>應用層改寫</strong>：</p>
<ul>
<li>換 query 風格、ORM、access pattern</li>
<li>例：PostgreSQL → DynamoDB（SQL → NoSQL access pattern）</li>
</ul>
<p><strong>何時必須應用層改寫</strong>：</p>
<ul>
<li>跨 model（RDBMS → KV）</li>
<li>跨 query paradigm（SQL → MongoDB 風格）</li>
<li>想拿 native 性能 / 成本優勢</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB API compat、應用層幾乎不改</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> — 多套 RDBMS → Aurora、PostgreSQL / MySQL 相容、最小應用層改動</li>
<li><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> — TiDB（SQL）→ DynamoDB（KV）、必須改 access pattern、不能 API compat</li>
</ul>
<h2 id="容量規劃在遷移中的角色">容量規劃在遷移中的角色</h2>
<p>DB 遷移期間有特殊的容量挑戰、跟一般 capacity planning 不同。</p>
<p><strong>遷移期容量需求</strong>：</p>
<ul>
<li>old DB 持續服務 production</li>
<li>new DB 接 dual-write（額外負載）</li>
<li>backfill historical data（額外負載）</li>
<li>shadow read（讀兩倍）</li>
<li>應用層擴容（dual-write 邏輯吃 CPU）</li>
</ul>
<p><strong>典型容量增加</strong>：</p>
<ul>
<li>應用層 +20-30%（dual-write、cross-DB logic、metric）</li>
<li>new DB 必須 <em>提前 provision</em> 接 100% 流量</li>
<li>監控 / log 容量 +50%（要追蹤更多事件）</li>
</ul>
<p><strong>對應 <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></strong>：遷移期是「臨時 over-provisioning 期」、要算進 cost。遷移完才能 right-sizing。</p>
<p><strong>對應 <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></strong>：dual-write 跟 shadow read 是 production validation 的特殊形式、要按 9.10 的安全邊界設計。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>遷移類型</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify</a></td>
          <td>self-managed → managed</td>
          <td>7500 萬用戶事件交付系統遷移、人力成本驅動</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>NewSQL → KV NoSQL</td>
          <td>對照 over-provisioning 成本、50% 帳單下降</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a></td>
          <td>多套 RDBMS → 統一 Aurora</td>
          <td>DB consolidation 釋放 DBA、效能 +75%</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a></td>
          <td>MongoDB → Cosmos DB（API compat）</td>
          <td>API 相容遷移路徑、planet-scale 分析</td>
      </tr>
  </tbody>
</table>
<h2 id="遷移評估的成本曲線">遷移評估的成本曲線</h2>
<p>遷移 ROI 評估常見錯誤是 <em>只看當下流量下的成本對照</em>、忽略未來流量曲線。決策時要算 12-24 個月的累積成本、不是 snapshot。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato TiDB → DynamoDB</a> — Zomato 帳單系統「成本降 50%」是當下流量下的對照。如果未來流量繼續成長、DynamoDB on-demand 的單位成本可能比 TiDB 自管 cluster 高、達到某規模後 TiDB 反而更便宜。</p>
<p><strong>評估公式</strong>：</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">未來 N 個月累積成本 = sum(月流量 × 月單位成本)</span></span></code></pre></div><p>各 DB 的「月單位成本 vs 流量」曲線形狀不同：</p>
<ul>
<li><strong>DynamoDB on-demand</strong>：線性、按用量計費、單位成本固定</li>
<li><strong>DynamoDB provisioned + reserved</strong>：階梯、預訂量越大單價越低</li>
<li><strong>自管 TiDB / PostgreSQL</strong>：階梯 + 固定基線、低流量時單位成本高（基線分攤）、高流量時單位成本低</li>
<li><strong>Aurora Serverless</strong>：線性、但有最低 ACU 基線</li>
<li><strong>Spanner</strong>：節點數 × 單價、增量是 100 pu 一單位</li>
</ul>
<p><strong>曲線交叉點是選型決策的關鍵</strong>：DynamoDB on-demand 跟自管 PostgreSQL 在某個流量水位交叉、流量低於此值前者便宜（無基線成本）、高於此值後者便宜（基線分攤後單價低）。Aurora Serverless 跟 Aurora provisioned 也有類似交叉、波動大的 workload 在 Serverless 划算、穩定的在 provisioned 划算。Spanner 因為節點數階梯式增加、跨節點交叉點通常在 <em>每節點 70-80% 利用率</em> — 過了就要加節點、新節點利用率掉回 50% 是常態。判讀重點：選型不該只看 <em>當下流量點</em>、要看未來 12-24 月的流量曲線會跨過哪些交叉點、再決定哪種計費模式總成本最低。</p>
<p><strong>遷移 ROI 評估的維度</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>應該算進去</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infra 成本</td>
          <td>當下 + 預期成長下的累積、不是 snapshot</td>
      </tr>
      <tr>
          <td>人力成本</td>
          <td>DBA、SRE、on-call 工時、跟 vendor 整合工時</td>
      </tr>
      <tr>
          <td>機會成本</td>
          <td>遷移期間不能做新功能的時間成本</td>
      </tr>
      <tr>
          <td>Lock-in 成本</td>
          <td>換 vendor 的退場成本、合約年限</td>
      </tr>
      <tr>
          <td>合規 lead time</td>
          <td>受監管產業每市場 3-12 月審查、不算進來時程會崩</td>
      </tr>
      <tr>
          <td>Migration 本身成本</td>
          <td>dual-write infra、shadow read 雙倍負載、人力、風險</td>
      </tr>
  </tbody>
</table>
<p><strong>機會成本延伸</strong>：機會成本是遷移期間 <em>不能做新功能</em> 的時間。大型遷移通常綁住核心 team 6-12 個月、期間業務側看不到產品演進、可能流失市場機會。實務上要算「如果這 6 個月去做新產品、營收 / 競爭優勢值多少」、若超過遷移節省的 infra 成本、遷移不划算。</p>
<p><strong>Lock-in 成本延伸</strong>：vendor lock-in 不是「不能換」、是「換的時候要付多少」。包含：(1) 應用層改寫成本（DynamoDB → Spanner 要改 access pattern）、(2) 合約終止 penalty（reserved capacity 提前解約罰款）、(3) 資料導出成本（雲商出口流量費）、(4) 人才再訓練（DBA 從 Aurora 轉 Spanner 需要時間）。選 vendor 時就要評估這四項、即使沒打算換、合約年限到時也要面對。</p>
<p>判讀重點：「遷移後成本降 50%」這種敘述只看 infra 成本、且只看當下。完整評估要看所有六個維度跨 12-24 月、決策才不會出「短期省、長期更貴」或「短期看似賺、合規卡 1 年」的事故。</p>
<h2 id="合規審查-lead-time-是時程主要拉力">合規審查 lead time 是時程主要拉力</h2>
<p>受監管產業（金融、醫療、電信、政府）的 DB 遷移、<em>合規審查</em> 通常是時程主導因素、不是技術整合。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 跨 7 個受監管市場遷移到 Aurora、每個市場各自審查（中央銀行 / 金融監管機關 / 個資主管機關）、單一市場審查 3-12 個月、總時程是「市場數 × 平均審查月份」、不是「技術遷移月份」。</p>
<p><strong>合規 lead time 的常見項目</strong>：</p>
<ul>
<li>中央銀行核心系統變更審查（金融業）</li>
<li>個資主管機關的跨境傳輸審批（GDPR / 各國個資法）</li>
<li>醫療資料的隱私審查（HIPAA / 各國醫療法）</li>
<li>雲端服務商的合規認證對應（PCI-DSS、ISO 27001、SOC 2）</li>
<li>跨市場資料駐留限制（中國《數據安全法》、印度資料保護法、歐盟 GDPR）</li>
</ul>
<p><strong>規劃含義</strong>：</p>
<ul>
<li>技術側 ready ≠ 可上線、合規簽核才是 cutover gate</li>
<li>合規審查通常 serial、不能 parallel（單一審查機關沒法平行處理多 case）</li>
<li>高風險變更（DB 換 vendor、cross-border）審查週期最長</li>
<li>跨市場部署、各市場各自審、不能用某市場結果代替</li>
</ul>
<p>判讀重點：受監管產業的遷移計畫、預設技術側 50%、合規 50% 工時、不是「技術 90% / 合規 10%」。低估合規 lead time 會讓專案在最後關頭卡關、且無法用工程資源補。</p>
<h2 id="benchmark-對照基準的解讀">Benchmark 對照基準的解讀</h2>
<p>遷移案例的「X% improvement」要追問 <em>跟什麼基準比</em>、否則容易誤導。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 「10x throughput」是 <em>vs 舊系統</em>、不是 <em>vs 競爭對手</em>。受監管銀行的舊系統通常是 1990s-2000s 的 mainframe 或自建 OLTP、性能本來就低、改善幅度大不代表絕對性能領先。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a> — 「up to 75% improvement」是 <em>跨多個 workload 的最大改善幅度</em>、不是「每個 workload 都 +75%」。實際每個 workload 改善從 10% 到 75% 不等、平均可能 30-40%。</p>
<p><strong>benchmark 解讀的關鍵問題</strong>（遷移情境專屬）：</p>
<ul>
<li><em>vs 什麼基準</em>：跟舊系統比 vs 跟競爭對手比 vs 跟理論最佳比</li>
<li><em>哪個 workload</em>：是平均 vs 最快 vs 最慢</li>
<li><em>規模對照</em>：在多大流量下測的、自家業務規模類似嗎</li>
</ul>
<p>讀 vendor 案例研究時、這三個遷移專屬維度都要對照、否則「75% 改善」可能變成「在某個 cherry-picked workload、跟舊系統比、規模跟自家不同」、實際搬過去未必有對應收益。</p>
<p><strong>規模對照延伸</strong>：vendor 案例研究最容易誤判的維度。讀者要識別三個訊號才能判斷規模是否類似 — (1) <em>資料量</em>（vendor 揭露的是 GB 還是 PB？自家在哪個量級？）、(2) <em>QPS 分布</em>（vendor 是 sustained 還是 bursty？自家流量形狀是否類似？）、(3) <em>讀寫比</em>（vendor 案例是 write-heavy 還是 read-heavy？自家業務性質是否吻合？）。三個訊號至少要有兩個跟自家對齊、benchmark 數字才有參考價值。對應 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a> 案例的 18:1 讀寫比、跟一般電商的 5:1 完全不同、不能用同一份 benchmark 推論。</p>
<p><strong>Percentile 跟時間窗口維度</strong> — 是更通用的容量數字判讀問題、詳見 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取的「讀峰值數字的工程細節」</a> 段（容量三口徑、p50/p99/p999 解讀）。遷移情境只需在這個基礎上加「vs 基準 / workload / 規模對照」三個遷移專屬問題。</p>
<h2 id="預設-db治理-pattern">「預設 DB」治理 pattern</h2>
<p>大規模平台選 DB 的做法是建立「預設 DB」規則、新團隊用其他要 <em>justify</em>、逐案決定在這個規模行不通。這個治理 pattern 簡化 onboarding、降低 DB 種類太多的運維成本。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys</a> — Genesys Cloud 的 Chief Architect 明確說「Amazon DynamoDB is our primary data layer by default, and teams have to justify the use of something else」。對應 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> — 把多套 RDB 整合到 Aurora、降低 DB 種類就是降低運維 surface area。</p>
<p><strong>預設 DB 治理的工程含義</strong>：</p>
<ul>
<li>新團隊預設用 X、特殊需求才評估其他、減少 DB 評估的認知負擔</li>
<li>DBA / SRE 知識集中、不必養多個 vendor 的專業</li>
<li>監控、backup、compliance 流程統一、運維成本下降</li>
<li>多個服務的 schema migration / capacity planning 可以共用 tooling</li>
</ul>
<p><strong>選擇預設 DB 的判讀條件</strong>：</p>
<ul>
<li>平台規模夠大（10+ 微服務）、運維 surface area 是真實成本</li>
<li>業務需求大部分可以收斂到單一 DB（OLTP 90%、KV 10% 可以選 OLTP 為預設）</li>
<li>vendor 提供完整能力組合（managed + multi-region + auto-scaling）</li>
</ul>
<p><strong>預設 DB 對應</strong>：</p>
<ul>
<li>AWS 生態大規模 OLTP → Aurora（Netflix）</li>
<li>AWS 生態大規模 KV → DynamoDB（Genesys、Capcom、Disney+）</li>
<li>Azure 生態 multi-model → Cosmos DB</li>
<li>GCP 生態 OLTP → Spanner / AlloyDB</li>
</ul>
<p><strong>同一雲廠商兩個預設 DB 怎麼選邊界</strong>：AWS 生態同時有 Aurora（OLTP 預設）跟 DynamoDB（KV 預設）、不衝突、但要清楚兩者邊界。預設選 Aurora 的條件是「需要 SQL JOIN / ACID 跨表 transaction / 既有 ORM」、預設選 DynamoDB 的條件是「access pattern 已知且固定 / 預期跨 region 寫入 / surge 場景下 connection-based DB 撐不住」。這條邊界要寫進平台的 onboarding doc、否則新 team 會在「Aurora 還是 DynamoDB」之間反覆 review、抵消預設 DB 治理的價值。</p>
<p>判讀重點：小規模平台（&lt; 5 微服務）不必預設 DB 治理、case-by-case 決定即可。隨著服務數量增加、DB 種類失控成為大規模平台的隱性成本、預設 DB 治理變成規模化階段的工程紀律。</p>
<h2 id="vendor-dogfood-是-selection-signal">Vendor dogfood 是 selection signal</h2>
<p>Vendor dogfood signal 是 vendor 自家 production-critical workload 對該服務的使用程度、反映 vendor 對自家服務的真實信任度。讀 vendor 案例研究時、這個訊號比 sales material 更可信、因為 vendor 自己賭身家。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 AWS Prime Day</a> — Amazon Prime Day 用自家 DynamoDB + Aurora 撐 1.51 億 RPS + 500B txn。對應 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a> — Google 自家 Ads、Play、Search 都用 Spanner。對應 <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — Microsoft 365 usage analytics 用自家 Cosmos DB。</p>
<p><strong>Dogfood 訊號為什麼重要</strong>：</p>
<ul>
<li>vendor 自家賭身家、出問題自己第一個踩</li>
<li>內部 dogfood 通常比外部 customer earlier 用、bug 修得快</li>
<li>vendor sales team 的「能撐 X」如果跟內部 dogfood 不一致、是 marketing</li>
<li>內部用量大、vendor 對該服務的工程投入比 marginal customer 多</li>
</ul>
<p><strong>Dogfood 訊號的限制</strong>：</p>
<ul>
<li>vendor 內部享有專屬資源配額跟內部成本機制、外部用戶在公開計費下、單位成本邊界不同</li>
<li>vendor 內部享有深度 API 客製化跟特殊 SLA、外部用戶實際可取得的能力是公開版本</li>
<li>vendor 自家業務的 workload pattern 反映 vendor 自己的業務需求、跟你業務的 workload 可能不同</li>
</ul>
<p>判讀重點：dogfood 是必要訊號、不是充分訊號。看 vendor 自家用代表服務經過嚴格驗證；但「自家業務 vs 你業務」的相似度（資料量、QPS、讀寫比、一致性需求）才是 dogfood signal 是否能套用的判讀條件。</p>
<h2 id="反模式">反模式</h2>
<p>大規模 DB 遷移的常見錯誤：</p>
<ul>
<li><strong>沒做 POC 就 commit 遷移</strong>：發現新 DB 撐不住某個 query pattern、時程崩</li>
<li><strong>dual-write 沒 monitoring</strong>：兩邊不一致沒被發現、cutover 後資料錯亂。divergence 該怎麼分類追蹤、詳見 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Dual-write divergence schema</a></li>
<li><strong>shadow read 跑太短</strong>：1-2 天就 cutover、long-tail bug 沒暴露</li>
<li><strong>沒 rollback path</strong>：cutover 後發現問題、回不去</li>
<li><strong>app 跟 DB 一起遷</strong>：兩個 risk source 疊加、追根因困難</li>
<li><strong>忽略合規 lead time</strong>：技術側 ready 但合規審查還在跑、整個 stuck</li>
<li><strong>忽略 ETL pipeline</strong>：production cutover 完、下游 BI / analytics 還在打 old DB</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 database migration playbook</a>（基本流程）/ <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>（schema 演進）</li>
<li>平行：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a></li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a>（dual-write、shadow）、<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>、<a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a></li>
<li>跨 vendor 實戰深入：<a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a>（document → multi-model）、<a href="/blog/backend/01-database/vendors/aurora/migrate-from-self-managed-pg-mysql/" data-link-title="從自管 PostgreSQL / MySQL 遷到 Aurora：operational redesign migration playbook" data-link-desc="PostgreSQL / MySQL → Aurora 的 Type C operational redesign hybrid playbook、6 規格面（Driver / Diff audit / Phase plan / Evidence / Cutover / Cleanup）、Standard Chartered 合規 lead time 模型、Netflix 非 all-purpose store 邊界">Aurora 從自管 PG / MySQL 遷入</a>、<a href="/blog/backend/01-database/vendors/spanner/migrate-from-cloud-sql-pg/" data-link-title="Migration Playbook：Cloud SQL for PostgreSQL → Cloud Spanner" data-link-desc="Cloud SQL → Spanner 是 paradigm shift 級遷移、不是 drop-in。本 playbook 走 6 規格面 Driver / Diff / Phase / Evidence / Cutover / Cleanup：Driver 段明示 sizing barrier（100 pu 起跳）跟 &lt; 50ms write latency 兩條 no-go；Diff 段加 sizing / cost 第 7 規格面；Phase 0 含 sizing audit；Evidence 段補 cost crossover 報告；對照 9.C10 Google internal dogfood 邊界跟 Standard Chartered 受監管 banking case">Spanner 從 Cloud SQL PG 遷入</a>、<a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB 遷入 Atlas</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">Schema Migration</a></li>
<li><a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a></li>
<li><a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">Dual Write</a></li>
<li><a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill</a></li>
<li><a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">Cutover Window</a></li>
<li><a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">Rollback Window</a></li>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li><a href="/blog/backend/knowledge-cards/fallback-read/" data-link-title="Fallback Read" data-link-desc="說明讀取路徑切換失敗時如何暫時回到舊資料語意或舊讀取來源">Fallback Read</a></li>
</ul>
]]></content:encoded></item><item><title>1.13 應用層查詢反模式與 Query 預算</title><link>https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/</guid><description>&lt;p>應用程式變慢、第一個直覺常常是「資料庫不夠力」。多數團隊的真實瓶頸在應用程式發給資料庫的查詢方式、資料庫本身反而不是問題：N+1、select *、缺索引、ORM lazy load、長 transaction。本章把這些反模式列成可診斷、可修正的清單、並提出「每請求的 query 預算」作為發布前的判讀基準 — 讓讀者在資料層撞牆之前、先在應用層發現問題。&lt;/p>
&lt;h2 id="為什麼查詢反模式比-vendor-細節更重要">為什麼查詢反模式比 vendor 細節更重要&lt;/h2>
&lt;p>多數團隊面對「資料庫變慢」時，會先去看 vendor 的調校（buffer pool、配置升級、replica 加開）。這些調校通常把基礎效能拉高 1-2 倍；一個 N+1 query 反模式可以讓回應時間慢 10-1000 倍（具體倍數取決於 N 跟 RTT — N=100 + RTT=1ms 約慢 100 倍）。先解掉應用層的反模式、再去調 vendor 配置，整體效益遠高於反過來。&lt;/p>
&lt;p>這條優先序也對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程&lt;/a> 的精神：先定位真正的瓶頸再決定是否加資源。應用層 query 是最常被忽略的瓶頸來源。&lt;/p>
&lt;h2 id="n1-query最常見也最隱性的反模式">N+1 Query：最常見也最隱性的反模式&lt;/h2>
&lt;p>N+1 query 指「先發一個 query 取回 N 筆資料、再對每一筆各發一個 query 取相關資料」，總共 1 + N 次 round trip。N 越大、整體越慢。&lt;/p>
&lt;p>典型範例：列出 100 個訂單跟每筆訂單的客戶資料。錯誤寫法是先 &lt;code>SELECT * FROM orders LIMIT 100&lt;/code> 拿到 100 筆訂單、再對每一筆訂單做 &lt;code>SELECT * FROM customers WHERE id = ?&lt;/code>，總共 101 次 query。正確寫法是 JOIN 或 IN 一次取回：&lt;code>SELECT o.*, c.* FROM orders o JOIN customers c ON o.customer_id = c.id LIMIT 100&lt;/code>，1 次 query 完成。&lt;/p>
&lt;p>N+1 在 ORM 環境特別隱性，因為它常被框架的 lazy loading 機制隱藏。Django ORM 的 &lt;code>order.customer&lt;/code> 看起來像存取 attribute，背後對應一次 query。寫程式時看不到 SQL，發布後才從 slow log 發現問題。&lt;/p>
&lt;p>判讀方式：開啟 ORM 的 query log（debug mode）、看一個 API request 跑出幾個 query。預期是個位數；若 query 數隨著資料集大小線性成長（例如 list 100 筆觸發 100 query、list 1000 筆觸發 1000 query），這條 scaling 訊號就是 N+1 — 比固定閾值更可靠的判讀。&lt;/p>
&lt;p>修正方向：&lt;/p>
&lt;ul>
&lt;li>ORM 端用 eager loading（Django &lt;code>select_related&lt;/code> / &lt;code>prefetch_related&lt;/code>、Rails &lt;code>includes&lt;/code>、SQLAlchemy &lt;code>joinedload&lt;/code>）&lt;/li>
&lt;li>自己寫 SQL 用 JOIN 或 IN 條件批次取&lt;/li>
&lt;li>確認 ORM 預設不是 lazy（有些 ORM 的設計鼓勵 lazy，需要明確標示 eager）&lt;/li>
&lt;/ul>
&lt;h2 id="select--與超量讀取">Select * 與超量讀取&lt;/h2>
&lt;p>&lt;code>SELECT *&lt;/code> 把表的所有欄位都拉出來，包含可能很大的欄位（content、blob、JSON）跟根本用不到的欄位。代價有三：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>網路傳輸成本&lt;/strong>：query 結果在 DB 跟應用之間傳輸，欄位越多越大。&lt;/li>
&lt;li>&lt;strong>記憶體成本&lt;/strong>：應用程式要 deserialize 整個 row，物件越大記憶體佔越多。&lt;/li>
&lt;li>&lt;strong>隱性耦合&lt;/strong>：欄位有變動（新增、刪除、改型別）時，所有 &lt;code>SELECT *&lt;/code> 的 query 都會被影響。&lt;/li>
&lt;/ol>
&lt;p>修正方向是明確列出需要的欄位：&lt;code>SELECT id, name, status FROM orders&lt;/code>。如果擔心欄位列表太長，問自己是不是 query 試圖一次處理太多責任。&lt;/p></description><content:encoded><![CDATA[<p>應用程式變慢、第一個直覺常常是「資料庫不夠力」。多數團隊的真實瓶頸在應用程式發給資料庫的查詢方式、資料庫本身反而不是問題：N+1、select *、缺索引、ORM lazy load、長 transaction。本章把這些反模式列成可診斷、可修正的清單、並提出「每請求的 query 預算」作為發布前的判讀基準 — 讓讀者在資料層撞牆之前、先在應用層發現問題。</p>
<h2 id="為什麼查詢反模式比-vendor-細節更重要">為什麼查詢反模式比 vendor 細節更重要</h2>
<p>多數團隊面對「資料庫變慢」時，會先去看 vendor 的調校（buffer pool、配置升級、replica 加開）。這些調校通常把基礎效能拉高 1-2 倍；一個 N+1 query 反模式可以讓回應時間慢 10-1000 倍（具體倍數取決於 N 跟 RTT — N=100 + RTT=1ms 約慢 100 倍）。先解掉應用層的反模式、再去調 vendor 配置，整體效益遠高於反過來。</p>
<p>這條優先序也對應 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 的精神：先定位真正的瓶頸再決定是否加資源。應用層 query 是最常被忽略的瓶頸來源。</p>
<h2 id="n1-query最常見也最隱性的反模式">N+1 Query：最常見也最隱性的反模式</h2>
<p>N+1 query 指「先發一個 query 取回 N 筆資料、再對每一筆各發一個 query 取相關資料」，總共 1 + N 次 round trip。N 越大、整體越慢。</p>
<p>典型範例：列出 100 個訂單跟每筆訂單的客戶資料。錯誤寫法是先 <code>SELECT * FROM orders LIMIT 100</code> 拿到 100 筆訂單、再對每一筆訂單做 <code>SELECT * FROM customers WHERE id = ?</code>，總共 101 次 query。正確寫法是 JOIN 或 IN 一次取回：<code>SELECT o.*, c.* FROM orders o JOIN customers c ON o.customer_id = c.id LIMIT 100</code>，1 次 query 完成。</p>
<p>N+1 在 ORM 環境特別隱性，因為它常被框架的 lazy loading 機制隱藏。Django ORM 的 <code>order.customer</code> 看起來像存取 attribute，背後對應一次 query。寫程式時看不到 SQL，發布後才從 slow log 發現問題。</p>
<p>判讀方式：開啟 ORM 的 query log（debug mode）、看一個 API request 跑出幾個 query。預期是個位數；若 query 數隨著資料集大小線性成長（例如 list 100 筆觸發 100 query、list 1000 筆觸發 1000 query），這條 scaling 訊號就是 N+1 — 比固定閾值更可靠的判讀。</p>
<p>修正方向：</p>
<ul>
<li>ORM 端用 eager loading（Django <code>select_related</code> / <code>prefetch_related</code>、Rails <code>includes</code>、SQLAlchemy <code>joinedload</code>）</li>
<li>自己寫 SQL 用 JOIN 或 IN 條件批次取</li>
<li>確認 ORM 預設不是 lazy（有些 ORM 的設計鼓勵 lazy，需要明確標示 eager）</li>
</ul>
<h2 id="select--與超量讀取">Select * 與超量讀取</h2>
<p><code>SELECT *</code> 把表的所有欄位都拉出來，包含可能很大的欄位（content、blob、JSON）跟根本用不到的欄位。代價有三：</p>
<ol>
<li><strong>網路傳輸成本</strong>：query 結果在 DB 跟應用之間傳輸，欄位越多越大。</li>
<li><strong>記憶體成本</strong>：應用程式要 deserialize 整個 row，物件越大記憶體佔越多。</li>
<li><strong>隱性耦合</strong>：欄位有變動（新增、刪除、改型別）時，所有 <code>SELECT *</code> 的 query 都會被影響。</li>
</ol>
<p>修正方向是明確列出需要的欄位：<code>SELECT id, name, status FROM orders</code>。如果擔心欄位列表太長，問自己是不是 query 試圖一次處理太多責任。</p>
<p>例外是 ad-hoc query 跟 DB tool 環境，可以接受 <code>SELECT *</code>。production code 不應該有。</p>
<h2 id="缺索引查詢計畫沒走索引">缺索引：查詢計畫沒走索引</h2>
<p>缺索引的徵兆是 query 在小資料量時很快、資料一多就突然慢。原因是 query 走了 full table scan，資料量小時 scan 還快、資料量上百萬筆就慢。</p>
<p>判讀方式是用 <code>EXPLAIN</code> 看查詢計畫：</p>
<ul>
<li><code>type=ALL</code> 或 <code>Seq Scan</code> 代表沒走索引</li>
<li><code>rows</code> 估計值跟實際表大小接近，代表掃描範圍過大</li>
<li><code>Using filesort</code> / <code>Using temporary</code> 代表排序或暫存資料的成本</li>
</ul>
<p>修正方向不是「對每個 WHERE 條件都建索引」，這會讓寫入變慢、索引變大。要建索引的判讀條件：</p>
<ul>
<li>該 query 是熱路徑（頻率高、影響 user）</li>
<li>該欄位有足夠選擇性（distinct 值多）</li>
<li>該欄位沒有跟其他索引重複覆蓋</li>
<li>寫入路徑能承受多一個索引的維護成本</li>
</ul>
<p>複合索引的欄位順序也要對齊 query 的 WHERE 條件。<code>WHERE a = ? AND b = ?</code> 適合 <code>(a, b)</code> 複合索引，不適合 <code>(b, a)</code>。這部分屬於 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a> 的範圍、本章只標出徵兆跟診斷起點。</p>
<h2 id="orm-lazy-load-陷阱">ORM Lazy Load 陷阱</h2>
<p>ORM 的 lazy load 預設行為是「存取 attribute 時才發 query」，這在開發時讓 code 很乾淨，但隱藏了 query 的數量。</p>
<p>常見陷阱：</p>
<ul>
<li><strong>跨 transaction 邊界存取 lazy attribute</strong>：query 在原 transaction 已關閉後才發，連線狀態錯誤。</li>
<li><strong>在 template / serializer 裡存取 lazy attribute</strong>：一個 page render 觸發數十個額外 query。</li>
<li><strong>lazy load 跨服務邊界</strong>：DTO 傳遞時不知道哪些 attribute 是 lazy、哪些是 eager，前端拿到 DTO 後 trigger 額外 query。</li>
</ul>
<p>修正方向：</p>
<ul>
<li>明確標示 eager loading 邊界，serializer 之前完成所有需要的資料載入</li>
<li>ORM 配置改成 default eager 或 strict mode（query 太多會 warning）</li>
<li>DTO 出 service 邊界前做 fully materialized</li>
</ul>
<h2 id="long-running-transaction">Long-Running Transaction</h2>
<p>長時間佔住的 transaction 會擋住其他 query、產生 lock 等待、消耗連線池資源。</p>
<p>常見成因：</p>
<ul>
<li>在 transaction 內做 HTTP call 或外部 API 呼叫</li>
<li>在 transaction 內做檔案 I/O 或長計算</li>
<li>用 transaction 包住整個 request handler（從 request 開始到 response 結束都在 transaction）</li>
<li>ORM 設定 default transaction-per-request 但業務只需要短交易</li>
</ul>
<p>修正方向是把 transaction 範圍縮到最小：只包住「需要原子性」的那幾個 SQL 操作。外部呼叫、計算、檔案 I/O 都要在 transaction 之外。詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a>。</p>
<h2 id="其他常見反模式">其他常見反模式</h2>
<p>上面五個是讀路徑高頻反模式。實務上其他幾類在 slow log 出現頻率不低、要一併列入發布前檢查：</p>
<ul>
<li><strong><a href="/blog/backend/knowledge-cards/cardinality-explosion/" data-link-title="Query Cardinality Explosion" data-link-desc="Query 結果行數因 join / cross product / 條件缺失爆炸性放大的反模式">Cardinality explosion</a> / cross join 誤用</strong>：兩個多對多關聯 join 沒加 filter、結果集從 N 行炸成 N×M 行。判讀訊號：query 結果行數遠超業務直覺、<code>EXPLAIN</code> 估計 rows 異常大。修正方向：補 filter、改 EXISTS / IN 半連接、或拆兩段 query。</li>
<li><strong>OFFSET-based pagination on large tables</strong>：<code>LIMIT 20 OFFSET 100000</code> 在大表退化成「掃描 100020 行 + skip 100000 行」。修正方向：用 <a href="/blog/backend/knowledge-cards/keyset-pagination/" data-link-title="Keyset Pagination" data-link-desc="用上一頁最後一筆的 key 當下一頁起點、避開 OFFSET 大表時的線性退化">keyset / cursor pagination</a>（<code>WHERE id &gt; last_seen_id LIMIT 20</code>）— 一致 O(LIMIT) 而非 O(OFFSET + LIMIT)。</li>
<li><strong>隱式型別轉換讓 index 失效</strong>：<code>WHERE varchar_col = 123</code> 把 column 轉成 int 比較、index 失效退到 full scan。判讀訊號：EXPLAIN 顯示 index 沒命中但 schema 上有 index。修正方向：明示型別（<code>WHERE varchar_col = '123'</code>）。</li>
<li><strong>應用層做大結果集排序 / 聚合</strong>：把 100 萬行拉回應用、在記憶體 sort 或 group。應該 push 給 DB 做 <code>ORDER BY</code> / <code>GROUP BY</code> + <code>LIMIT</code>。判讀訊號：應用程式記憶體用量隨 endpoint 流量線性升高。</li>
<li><strong>N+1 write</strong>：在 loop 內單筆 insert / update 而非 bulk insert。每筆觸發一次 round trip + 可能的 fsync。修正方向：用 <code>INSERT ... VALUES (), (), ()</code> 或 <code>executemany</code> / <code>bulk_create</code>。</li>
</ul>
<p>NoSQL / KV DB 也有 sibling 反模式（hot partition、read amplification、scan-and-filter），不在本章 SQL 範疇但邏輯類似 — 詳見 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a>。</p>
<h2 id="每請求的-query-預算">每請求的 Query 預算</h2>
<p>把上面這些反模式收斂成一個發布前可檢查的判準：每個 API request 允許發多少個 query。</p>
<table>
  <thead>
      <tr>
          <th>API 類型</th>
          <th>建議 query 預算</th>
          <th>判讀說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>簡單 read（取單筆）</td>
          <td>1–3 個</td>
          <td>主資源 1 個 + 相關資源 join 或 1–2 個額外</td>
      </tr>
      <tr>
          <td>List read（取列表）</td>
          <td>1–5 個</td>
          <td>主列表 1 個 + filter / pagination / 關聯 batch query</td>
      </tr>
      <tr>
          <td>Write（單筆操作）</td>
          <td>2–5 個</td>
          <td>check 1 個 + write 1 個 + 觸發後續 query</td>
      </tr>
      <tr>
          <td>Complex（多步驟業務）</td>
          <td>5–15 個</td>
          <td>視業務複雜度，但每多 1 個都要能講出為什麼</td>
      </tr>
  </tbody>
</table>
<p>超過預算不一定錯，但需要解釋。CI / staging 可以加 middleware 統計每個 endpoint 的 query 數，超過閾值在 PR review 時觸發討論。這比事後從 slow log 找問題更有效。</p>
<p>這張表以 OLTP API 為主。Dashboard / report / search endpoint 常需要 10-30 query 解 join / aggregation、用「Complex」涵蓋不夠精確；batch / bulk write（一次寫入 1000 筆訂單）不該用 query count 評估、應該看 batch size 跟 transaction 範圍。預算是判讀工具、不是硬閾值。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API 在資料量增加後突然變慢</td>
          <td>缺索引或查詢計畫退化</td>
          <td>跑 EXPLAIN、檢查 query plan</td>
      </tr>
      <tr>
          <td>同一個 API 跑出 dozens 個 query</td>
          <td>N+1 反模式</td>
          <td>加 eager loading 或改寫成 JOIN</td>
      </tr>
      <tr>
          <td>應用程式記憶體用量隨流量線性升高</td>
          <td><code>SELECT *</code> 載入過多資料</td>
          <td>改成明確欄位、加 pagination</td>
      </tr>
      <tr>
          <td>DB connection 等待時間升高</td>
          <td>long transaction 或 connection pool 不足</td>
          <td>縮 transaction 範圍、評估 <a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 上限</td>
      </tr>
      <tr>
          <td>Lock wait timeout 變多</td>
          <td>long transaction 或 hot row 競爭</td>
          <td>拆 transaction、檢查 hot row 設計</td>
      </tr>
      <tr>
          <td>Slow query log 集中在某類 SQL</td>
          <td>該 query 走了 full scan 或 join 順序錯誤</td>
          <td>EXPLAIN + 加索引或改寫 query</td>
      </tr>
      <tr>
          <td>ORM debug log 顯示 hundreds query</td>
          <td>lazy load 失控</td>
          <td>換 eager loading 策略、檢視 serializer 邊界</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把「資料庫變慢」直接解讀成「該升級資料庫」。先看應用層 query。多數效能問題是反模式造成的、而不是 DB 規格不夠。</p>
<p>把索引當「想加就加」。每個索引都有寫入成本跟空間成本。索引太多會讓 INSERT/UPDATE 變慢、backup 變大。要建索引前先驗證該 query 是熱路徑。</p>
<p>把 N+1 當「在 ORM 環境無解」。多數 ORM 都有 eager loading 選項，只是預設 lazy。問題是團隊沒把這當作預設策略。設定 ORM 為 default eager 或在 CI 加 query 數量檢查就能避免。</p>
<p>把 transaction 範圍當「越大越安全」。長 transaction 是 lock 風險來源，不是一致性保證。一致性靠正確的 isolation level 跟業務邏輯，不是靠長 transaction 鎖住整個流程。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「應用層發給資料庫的 query 反模式」。當問題進入 schema 設計（要不要拆表？要不要 partition？）交給 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a>；進入 transaction 語意（什麼時候用 SERIALIZABLE？怎麼 retry？）交給 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a>；進入跨服務的查詢責任拆分（哪些查詢屬於該服務？）交給 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 state ownership 與 query boundary</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 瓶頸定位流程</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 案例庫的主軸是規模、vendor 與容量壓力，直接以「query 反模式」為主題的案例較少。下列案例可以反向讀：每一個都展示了「在沒有先用 query 反模式優化收回壓力的前提下、團隊直接走 vendor 遷移或 scale-out 路徑」的決策。讀者讀完應追問：這些 case 啟動遷移前、是否有可能用本章的反模式清單先收回一部分容量？</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB</a> — DoorDash 撞到 Aurora single-primary write 天花板（瓶頸在 primary CPU + WAL flush rate）、用 PostgreSQL wire protocol 相容的 CockroachDB 換成多主寫入、ORM 不必重寫。對照本章可問：寫入熱點是否伴隨長 transaction 或熱 row 競爭？這些是 vendor 遷移前可以先用本章「Long-Running Transaction」清單檢查的點。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato：TiDB 遷到 DynamoDB</a> — Zomato 判斷 billing 事件本身可接受 eventually consistent、用一致性語意換取 4 倍吞吐 + 50% 成本。對照本章可問：遷移前每筆業務動作平均發了多少 query、是否有 N+1 或 select * 在放大壓力？把這條問題擺進「每請求 Query 預算」段一起讀。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered：Aurora 4000 TPS 合規容量</a> — Standard Chartered 在 7 個受監管市場各跑獨立 Aurora cluster（資料不能跨境）、容量規劃單位是「per 市場」、合規邊界決定了 cluster 拓樸。對照本章可問：query 預算假設是否進入容量模型？預算寫鬆、規劃出的 per-cluster TPS 上限會偏低。</li>
</ul>
<p>DoorDash 案例是這條反向追問最直接的應用 — 寫入瓶頸的判讀不該停在 vendor 規格、而是先檢查 transaction 範圍跟熱 row 競爭。Zomato 跟 Standard Chartered 的反向追問則退一步問「query 預算假設是否進入容量模型」。三條追問共享同一條診斷邏輯：應用層 query 不是事後解釋的細節、是事前可以收回的容量。這個讀法承認案例本身不直接示範 query 反模式、是用反向追問把案例當成 query 反模式重要性的反證。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發下的 SQL 讀寫邊界</a> 的交接：1.1 處理連線池與 read replica 機制、1.13 處理 query 寫法本身。高併發場景下兩者要同步檢查。</li>
<li>與 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a> 的交接：索引設計是 schema 層的事、本章只指出徵兆。</li>
<li>與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 的交接：slow query log、APM、query trace 是判讀反模式的主要訊號來源。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 的交接：先在應用層查反模式，再考慮 DB 配置升級。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 的交接：規模成長路線上、9.13 解擴展軸選擇後、1.13 是緊接著的下一站 — 在加機器或加 replica 前、先用本章反模式清單收回單機能撐住的容量。</li>
<li>與 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分</a> 的交接：拆服務常被用來「解決 DB 慢」，但本章的反模式優化通常比拆服務 ROI 更高、應該優先嘗試。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發下的 SQL 讀寫邊界</a></strong>：query 反模式收完後、處理連線池與 read replica 的擴展。</p>
<p>其他延伸方向：</p>
<ul>
<li>Schema 與索引設計 → <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design 與資料建模</a></li>
<li>Transaction 範圍收斂 → <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a></li>
<li>瓶頸定位完整流程 → <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
</ul>
]]></content:encoded></item><item><title>1.14 Production Slow Log Closed Loop</title><link>https://tarrragon.github.io/blog/backend/01-database/production-slow-log-loop/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/production-slow-log-loop/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式&lt;/a> 列出了 query 反模式清單跟每請求預算、但沒覆蓋一件事：&lt;strong>production slow log 怎麼從「事故時才看」變成「定期審視能 catch 反模式」&lt;/strong>。本章把 slow log 包成 closed loop — 採集、分析、PR review 整合、regression 偵測四個動作串起來、讓反模式在進 production 之前就被攔下。&lt;/p>
&lt;h2 id="slow-log-的兩種讀法">Slow log 的兩種讀法&lt;/h2>
&lt;p>多數團隊把 slow log 當「事故診斷工具」— 服務變慢時去翻一下、找出當下的罪魁禍首。這條讀法在事故時有效、但有 systemic 缺陷：所有 catch 到的反模式都已經影響使用者一段時間。&lt;/p>
&lt;p>另一條讀法是把 slow log 當「定期審視訊號」— 每週 / 每 release cycle 抓 slow log top-N、看哪些 query 模式持續存在、哪些是新出現的。這條讀法的關鍵在於「對比基線」、不是「找絕對閾值」。&lt;/p>
&lt;p>兩種讀法的對比決定了 closed loop 的設計方向：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>事故診斷工具&lt;/th>
 &lt;th>定期審視訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>觸發時機&lt;/td>
 &lt;td>服務變慢時被動翻&lt;/td>
 &lt;td>排程定期掃&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>比較對象&lt;/td>
 &lt;td>跟絕對閾值比（query &amp;gt; 1 秒）&lt;/td>
 &lt;td>跟上週 / 上次 release 的 slow log 分布比&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>處理路徑&lt;/td>
 &lt;td>找出 root cause → 立即修&lt;/td>
 &lt;td>收進 PR backlog → 排序 → 規律修&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>介入點&lt;/td>
 &lt;td>事故發生後&lt;/td>
 &lt;td>反模式被引入後、影響使用者前&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對應角色&lt;/td>
 &lt;td>On-call / SRE&lt;/td>
 &lt;td>整個團隊（每週輪流 review）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>定期審視這條讀法是本章的核心、後續四個動作都環繞它建立。&lt;/p>
&lt;h2 id="loop-第一步採集">Loop 第一步：採集&lt;/h2>
&lt;p>Slow log 採集的設計關鍵是「採集標準要穩定、retention 要夠長」。常見的採集配置選擇：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Threshold 設定&lt;/strong>：MySQL &lt;code>long_query_time&lt;/code>、PostgreSQL &lt;code>log_min_duration_statement&lt;/code> 設多久才記？常見 default 1 秒太寬鬆、會漏掉「200ms-1s」這層慢但累積成大量壓力的 query。建議 100ms 或更低（依 application 需求）。&lt;/li>
&lt;li>&lt;strong>採集對象&lt;/strong>：純 SELECT 慢？還是含 INSERT/UPDATE/DELETE？寫路徑慢通常代表 lock contention 或 transaction 範圍問題、跟讀路徑反模式不同、要分開分析。&lt;/li>
&lt;li>&lt;strong>Retention&lt;/strong>：log 保留多久？至少 30 天（覆蓋一個 sprint）、有資源的話 90 天（覆蓋季度 regression 對比）。雲端 managed DB（RDS / Aurora）的 slow log 通常自動匯出到 CloudWatch / S3、設定 retention policy 而不是依賴 DB instance 本身的 log。&lt;/li>
&lt;li>&lt;strong>Sample rate&lt;/strong>：高流量服務全採會把 disk I/O 拖垮。Production 環境用 sampling（如 10% 取樣）平衡採集完整度跟系統壓力。&lt;/li>
&lt;/ul>
&lt;p>採集出來的 raw log 不適合直接讀、要先 normalize。&lt;/p>
&lt;h2 id="loop-第二步normalize-與聚合">Loop 第二步：Normalize 與聚合&lt;/h2>
&lt;p>Raw slow log 每筆都帶具體參數（&lt;code>WHERE user_id = 12345&lt;/code>、&lt;code>WHERE user_id = 67890&lt;/code>），直接看會看到上千筆「不同 query」。實際上多數是同一個 query template 的不同參數實例。&lt;/p>
&lt;p>Normalize 動作把參數抽掉、留 query shape：&lt;/p>
&lt;ul>
&lt;li>&lt;code>WHERE user_id = 12345&lt;/code> → &lt;code>WHERE user_id = ?&lt;/code>&lt;/li>
&lt;li>&lt;code>IN (1, 2, 3, 4, 5)&lt;/code> → &lt;code>IN (?)&lt;/code>&lt;/li>
&lt;li>字串常數同樣抽掉&lt;/li>
&lt;/ul>
&lt;p>工具上：MySQL 用 &lt;code>pt-query-digest&lt;/code>（Percona Toolkit）；PostgreSQL 用 &lt;code>pg_stat_statements&lt;/code> extension（已內建 normalize）；雲端用 vendor 工具（AWS Performance Insights、GCP Query Insights、Azure SQL Insights）。Normalize 後可以按 query shape 聚合、看哪些 shape 累計時間最長、出現次數最多、平均延遲最高。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a> 列出了 query 反模式清單跟每請求預算、但沒覆蓋一件事：<strong>production slow log 怎麼從「事故時才看」變成「定期審視能 catch 反模式」</strong>。本章把 slow log 包成 closed loop — 採集、分析、PR review 整合、regression 偵測四個動作串起來、讓反模式在進 production 之前就被攔下。</p>
<h2 id="slow-log-的兩種讀法">Slow log 的兩種讀法</h2>
<p>多數團隊把 slow log 當「事故診斷工具」— 服務變慢時去翻一下、找出當下的罪魁禍首。這條讀法在事故時有效、但有 systemic 缺陷：所有 catch 到的反模式都已經影響使用者一段時間。</p>
<p>另一條讀法是把 slow log 當「定期審視訊號」— 每週 / 每 release cycle 抓 slow log top-N、看哪些 query 模式持續存在、哪些是新出現的。這條讀法的關鍵在於「對比基線」、不是「找絕對閾值」。</p>
<p>兩種讀法的對比決定了 closed loop 的設計方向：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>事故診斷工具</th>
          <th>定期審視訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發時機</td>
          <td>服務變慢時被動翻</td>
          <td>排程定期掃</td>
      </tr>
      <tr>
          <td>比較對象</td>
          <td>跟絕對閾值比（query &gt; 1 秒）</td>
          <td>跟上週 / 上次 release 的 slow log 分布比</td>
      </tr>
      <tr>
          <td>處理路徑</td>
          <td>找出 root cause → 立即修</td>
          <td>收進 PR backlog → 排序 → 規律修</td>
      </tr>
      <tr>
          <td>介入點</td>
          <td>事故發生後</td>
          <td>反模式被引入後、影響使用者前</td>
      </tr>
      <tr>
          <td>對應角色</td>
          <td>On-call / SRE</td>
          <td>整個團隊（每週輪流 review）</td>
      </tr>
  </tbody>
</table>
<p>定期審視這條讀法是本章的核心、後續四個動作都環繞它建立。</p>
<h2 id="loop-第一步採集">Loop 第一步：採集</h2>
<p>Slow log 採集的設計關鍵是「採集標準要穩定、retention 要夠長」。常見的採集配置選擇：</p>
<ul>
<li><strong>Threshold 設定</strong>：MySQL <code>long_query_time</code>、PostgreSQL <code>log_min_duration_statement</code> 設多久才記？常見 default 1 秒太寬鬆、會漏掉「200ms-1s」這層慢但累積成大量壓力的 query。建議 100ms 或更低（依 application 需求）。</li>
<li><strong>採集對象</strong>：純 SELECT 慢？還是含 INSERT/UPDATE/DELETE？寫路徑慢通常代表 lock contention 或 transaction 範圍問題、跟讀路徑反模式不同、要分開分析。</li>
<li><strong>Retention</strong>：log 保留多久？至少 30 天（覆蓋一個 sprint）、有資源的話 90 天（覆蓋季度 regression 對比）。雲端 managed DB（RDS / Aurora）的 slow log 通常自動匯出到 CloudWatch / S3、設定 retention policy 而不是依賴 DB instance 本身的 log。</li>
<li><strong>Sample rate</strong>：高流量服務全採會把 disk I/O 拖垮。Production 環境用 sampling（如 10% 取樣）平衡採集完整度跟系統壓力。</li>
</ul>
<p>採集出來的 raw log 不適合直接讀、要先 normalize。</p>
<h2 id="loop-第二步normalize-與聚合">Loop 第二步：Normalize 與聚合</h2>
<p>Raw slow log 每筆都帶具體參數（<code>WHERE user_id = 12345</code>、<code>WHERE user_id = 67890</code>），直接看會看到上千筆「不同 query」。實際上多數是同一個 query template 的不同參數實例。</p>
<p>Normalize 動作把參數抽掉、留 query shape：</p>
<ul>
<li><code>WHERE user_id = 12345</code> → <code>WHERE user_id = ?</code></li>
<li><code>IN (1, 2, 3, 4, 5)</code> → <code>IN (?)</code></li>
<li>字串常數同樣抽掉</li>
</ul>
<p>工具上：MySQL 用 <code>pt-query-digest</code>（Percona Toolkit）；PostgreSQL 用 <code>pg_stat_statements</code> extension（已內建 normalize）；雲端用 vendor 工具（AWS Performance Insights、GCP Query Insights、Azure SQL Insights）。Normalize 後可以按 query shape 聚合、看哪些 shape 累計時間最長、出現次數最多、平均延遲最高。</p>
<p>聚合後產出三條訊號：</p>
<ol>
<li><strong>Top-N by total time</strong>：累計時間最長的 query — 改一條就能省最多 DB 壓力</li>
<li><strong>Top-N by count</strong>：出現次數最多的 query — 改一條就能降最多 connection 占用</li>
<li><strong>Top-N by avg latency</strong>：平均延遲最高的 query — 個別 request 體驗最差的</li>
</ol>
<p>三條訊號可能指向不同 query、各自值得 attention。</p>
<h2 id="loop-第三步pr-review-整合">Loop 第三步：PR review 整合</h2>
<p>把 slow log 的 top-N 帶回 PR review 是 closed loop 的關鍵。常見三種整合機制：</p>
<ul>
<li><strong>每週 slow log review 會議</strong>：固定時段（每週 30 分鐘）、團隊輪流 owner、把 top-10 過一輪、決定每筆是修 / 留 / 標 acceptable。產出進 backlog、不是當場修。</li>
<li><strong>PR-level query budget check</strong>：CI 加 middleware 統計每個 endpoint 的 query 數（per <a href="/blog/backend/01-database/query-anti-patterns/#%e6%af%8f%e8%ab%8b%e6%b1%82%e7%9a%84-query-%e9%a0%90%e7%ae%97" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 預算</a>）、超過閾值的 PR 在 review 時觸發討論。這層比 slow log 早、catch 的是「新引入」反模式。</li>
<li><strong>Production regression alert</strong>：當某個 query shape 的 P99 latency 比上週 baseline 偏高 50%+、自動發 alert 給該服務 owner。這層 catch 的是「漸進惡化」反模式（如資料量增加、index 失效）。</li>
</ul>
<p>三層機制按介入點分層：PR check 是「進 production 前」、weekly review 是「進 production 後的固定盤點」、regression alert 是「漸進惡化的訊號偵測」。三層覆蓋率最高、單跑任一層都會漏。</p>
<h2 id="loop-第四步regression-偵測">Loop 第四步：Regression 偵測</h2>
<p>Slow log 的對比基線需要主動維護。沒有基線、定期審視會退化成「每次都看到同樣的 top-10、習以為常」。建立基線的常見做法：</p>
<ul>
<li><strong>每 release 凍結 baseline</strong>：上線新版本前抓一份 slow log snapshot、release 後跟它比。新增的 query shape 跟惡化的 query shape 都會浮出來。</li>
<li><strong>資料量分位點 marker</strong>：在 schema 加註「這張表預期 1M / 10M / 100M 行的 query 計畫」、實際成長到對應規模時驗證 plan 是否還對。Index 失效常常是「資料量過某個門檻、optimizer 改用 full scan」造成的。</li>
<li><strong>跨 release 趨勢圖</strong>：把 slow log top-10 的累計時間做時序圖、看一年的趨勢。穩定升高代表反模式 / 資料成長壓力、突然升高代表新引入問題。</li>
</ul>
<p>Regression 偵測的 false-positive 風險是「業務本身在變、流量本身在長」、不是反模式造成的。用「query shape 佔比」而非「絕對延遲」當訊號可以降低 false positive — 某個 query shape 從佔 5% 變成佔 30%，不論絕對延遲是否升高、都值得審視。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slow log top-10 一直是同一批 query</td>
          <td>Closed loop 沒形成、review 退化成擺設</td>
          <td>啟動 PR-level query budget check 或 weekly review</td>
      </tr>
      <tr>
          <td>某個 query shape 突然從 top-100 升到 top-10</td>
          <td>新版本引入反模式 / 流量結構變化</td>
          <td>對照最近 release diff、找出引入時點</td>
      </tr>
      <tr>
          <td>Top-N 累計時間穩定升高、但 query shape 沒變</td>
          <td>資料量增加、index 退化或 query 計畫漂移</td>
          <td>EXPLAIN 對比、檢查是否該加 covering index 或 partition</td>
      </tr>
      <tr>
          <td>Slow log 異常稀少（&lt; 預期）</td>
          <td>Threshold 設太寬、或採集 sample rate 太低</td>
          <td>降 threshold、提高 sample rate</td>
      </tr>
      <tr>
          <td>同一個 endpoint 在 PR check 過、production 卻爆</td>
          <td>PR 環境資料量太小、CI 無法 catch 大資料量退化</td>
          <td>加 production-like load test、或在 CI 用 anonymized prod data</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 slow log 當「事故工具」、不做定期審視。事故時的 slow log 是 lagging indicator — 反模式已經影響使用者一段時間才被看見。定期審視是把它變成 leading indicator 的關鍵。</p>
<p>把 threshold 設太鬆（1 秒、5 秒）。多數反模式落在 100ms-1s 區間、設 1 秒會漏掉。Threshold 應該對齊「user-perceived 慢」門檻、通常 100-500ms。</p>
<p>把 top-10 當「不能動」。一些 top-10 是業務本質慢（複雜 report、bulk write）、改起來代價遠超效益。Review 時要明示標記「acceptable」、避免下週又被當未解決問題討論。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「production slow log 怎麼變成 closed loop」。當問題進入具體反模式分析（這條 query 是哪種反模式？怎麼改？）、回到 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a>；進入 EXPLAIN 解讀細節、回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a>；進入 application-side query 數量控制機制（ORM middleware、query log 觀察），跨到 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 模組。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 案例庫中、slow log closed loop 直接示範的案例稀少（多數案例談規模 / vendor、不談 ops loop 設計）。可用以下案例反向追問：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash：Aurora Postgres 寫入瓶頸</a> — 寫入飽和被識別為 vendor 層問題、但若 production slow log loop 早期就 catch 到 transaction 範圍跟熱 row 競爭、可能延後遷移時點。對照本章可問：DoorDash 在啟動遷移前、是否有定期 slow log review 機制？</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered：合規驅動容量規劃</a> — 容量規劃以合規為驅動、但 query 預算假設若無 production 驗證、規劃出的 TPS 上限會偏低。對照本章「Regression 偵測」段：合規 cluster 是否有 query shape 趨勢圖？</li>
</ul>
<p>反向追問框架（per <a href="/blog/report/case-misalignment-reverse-inquiry/" data-link-title="案例庫不對齊章節主題時用反向追問取代強掛" data-link-desc="當案例庫主軸跟章節主題不在同一維度時、引用框架要從『正向掛入』切換到『反向追問』；強掛 case 的根因是『想填滿案例段』的模板配額、而非『想讓讀者看到證據』；反向追問把案例庫的限制當 first-class 訊息傳給讀者、case 變成『沒做 X 的後果』的反證、不是 X 的示範；reviewer 第一輪 fact-check 就能抓出強掛、修正成本高；判讀徵兆是引用句寫不出 case 具體段落 / 多個 case 句型雷同 / 章節主題跟 case 庫主軸不同維度">#146</a>）：案例本身不直接示範 closed loop、但用「啟動 vendor 升級前、closed loop 能不能延後撞牆」這條追問、能看出 slow log loop 的事前價值。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a> 的交接：1.13 給反模式清單、本章給「定期 catch 它們」的機制。</li>
<li>與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 的交接：slow log 採集跟聚合是 observability 的子問題、跨服務的 query trace 需要 04 的 telemetry pipeline。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位</a> 的交接：9.5 用 USE / RED method 定位、本章用 slow log 在 DB 層做更精細的 query-level 定位。</li>
<li>與 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">06 reliability ci-pipeline</a> 的交接：PR-level query budget check 是 CI 環節、屬 06 模組的 release gate 設計。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看具體反模式怎麼修、回 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a>。要把 query 觀測接進完整 telemetry pipeline、進 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a>。要看 PR-level check 怎麼接 release gate、進 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>。</p>
]]></content:encoded></item><item><title>資料庫 Vendor 文章撰寫規格</title><link>https://tarrragon.github.io/blog/backend/01-database/vendor-article-spec/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendor-article-spec/</guid><description>&lt;p>資料庫 Vendor 文章撰寫規格的核心責任是把服務頁、深度文章與遷移 playbook 的分工固定下來。PostgreSQL 與 MySQL 已經提供 SQL baseline 的完整樣本；後續撰寫 SQLite、MongoDB、DynamoDB、Aurora、Spanner、Cosmos DB 與 CockroachDB 時，應沿用同一組教學功能檢查，但保留每個服務自己的資料形狀、操作責任與失敗語言。&lt;/p>
&lt;p>這份規格承接 &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 深度技術文章寫作方法論&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論&lt;/a>。本文只處理資料庫模組的落地規格：哪些內容留在 vendor overview，哪些議題升級成 deep article，哪些變更需要 migration playbook。&lt;/p>
&lt;h2 id="判讀錨點">判讀錨點&lt;/h2>
&lt;p>資料庫 vendor 文章的錨點是正式狀態如何被保存、查詢、複製、演進與修復。產品功能、版本差異與雲端價格都只是材料；正文要把材料轉成讀者可操作的判準，讓讀者能判斷資料模型、交易需求、查詢邊界、容量壓力、操作責任與替代路由。&lt;/p>
&lt;p>PostgreSQL 與 MySQL 的 batch 顯示三個穩定事實。第一，SQL baseline 已經足以支撐其他服務頁開寫；第二，深度文章需要「何時不用」與真實案例 anchor 防止過度工程化；第三，跨 vendor 或 topology 變更需要獨立 playbook，不適合塞回 overview。&lt;/p>
&lt;h2 id="vendor-overview-規格">Vendor Overview 規格&lt;/h2>
&lt;p>Vendor overview 的責任是教讀者完成第一輪服務判斷。這一層回答服務承擔什麼資料責任、適合什麼壓力、日常有哪些操作決策、失效時先看哪些訊號，以及何時改走相鄰服務。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>規格面&lt;/th>
 &lt;th>必答問題&lt;/th>
 &lt;th>交付形態&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>服務定位&lt;/td>
 &lt;td>這個服務承擔 SQL、embedded、document、KV 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL&lt;/a> 哪一種責任&lt;/td>
 &lt;td>開場段、教學路線、最短判讀路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料形狀&lt;/td>
 &lt;td>資料是 row、document、key-value、time-series、geo 還是 global record&lt;/td>
 &lt;td>適用場景、schema / index / partition 說明&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性與交易&lt;/td>
 &lt;td>transaction、replica、multi-region 與 stale read 如何取捨&lt;/td>
 &lt;td>適用場景、不適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作責任&lt;/td>
 &lt;td>誰負責 backup、failover、upgrade、capacity、security 與 audit&lt;/td>
 &lt;td>容量規劃要點、常見陷阱、下一步路由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代邊界&lt;/td>
 &lt;td>什麼條件下改走 SQL、document、KV、managed SQL 或 distributed SQL&lt;/td>
 &lt;td>同類對比、相鄰章節路由、下游 deep article&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>案例與限制&lt;/td>
 &lt;td>哪些案例能提供壓力訊號，哪些 claim 需要時間敏感標記&lt;/td>
 &lt;td>案例對照、已知 limitation、後續擴充候選&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務定位段要先把產品名稱放回資料庫分類語言。SQLite 的定位是 embedded formal state 與低操作成本；MongoDB 的定位是 document shape 與 schema governance；DynamoDB 的定位是 managed KV / document access pattern；Aurora 的定位是 managed SQL operation transfer；Spanner、Cosmos DB 與 CockroachDB 的定位是 global 或 distributed consistency。&lt;/p>
&lt;p>資料形狀段要讓讀者知道服務為哪種查詢與寫入模式付成本。Row model 適合交易與 ad-hoc query；document model 適合聚合資料與 schema flexibility；KV model 適合固定 access pattern；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL&lt;/a> 適合跨 region 一致性，但會把 latency、transaction retry 與成本模型帶進設計。&lt;/p></description><content:encoded><![CDATA[<p>資料庫 Vendor 文章撰寫規格的核心責任是把服務頁、深度文章與遷移 playbook 的分工固定下來。PostgreSQL 與 MySQL 已經提供 SQL baseline 的完整樣本；後續撰寫 SQLite、MongoDB、DynamoDB、Aurora、Spanner、Cosmos DB 與 CockroachDB 時，應沿用同一組教學功能檢查，但保留每個服務自己的資料形狀、操作責任與失敗語言。</p>
<p>這份規格承接 <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 深度技術文章寫作方法論</a> 與 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a>。本文只處理資料庫模組的落地規格：哪些內容留在 vendor overview，哪些議題升級成 deep article，哪些變更需要 migration playbook。</p>
<h2 id="判讀錨點">判讀錨點</h2>
<p>資料庫 vendor 文章的錨點是正式狀態如何被保存、查詢、複製、演進與修復。產品功能、版本差異與雲端價格都只是材料；正文要把材料轉成讀者可操作的判準，讓讀者能判斷資料模型、交易需求、查詢邊界、容量壓力、操作責任與替代路由。</p>
<p>PostgreSQL 與 MySQL 的 batch 顯示三個穩定事實。第一，SQL baseline 已經足以支撐其他服務頁開寫；第二，深度文章需要「何時不用」與真實案例 anchor 防止過度工程化；第三，跨 vendor 或 topology 變更需要獨立 playbook，不適合塞回 overview。</p>
<h2 id="vendor-overview-規格">Vendor Overview 規格</h2>
<p>Vendor overview 的責任是教讀者完成第一輪服務判斷。這一層回答服務承擔什麼資料責任、適合什麼壓力、日常有哪些操作決策、失效時先看哪些訊號，以及何時改走相鄰服務。</p>
<table>
  <thead>
      <tr>
          <th>規格面</th>
          <th>必答問題</th>
          <th>交付形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>這個服務承擔 SQL、embedded、document、KV 或 <a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a> 哪一種責任</td>
          <td>開場段、教學路線、最短判讀路徑</td>
      </tr>
      <tr>
          <td>資料形狀</td>
          <td>資料是 row、document、key-value、time-series、geo 還是 global record</td>
          <td>適用場景、schema / index / partition 說明</td>
      </tr>
      <tr>
          <td>一致性與交易</td>
          <td>transaction、replica、multi-region 與 stale read 如何取捨</td>
          <td>適用場景、不適用場景、跟其他 vendor 的取捨</td>
      </tr>
      <tr>
          <td>操作責任</td>
          <td>誰負責 backup、failover、upgrade、capacity、security 與 audit</td>
          <td>容量規劃要點、常見陷阱、下一步路由</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>什麼條件下改走 SQL、document、KV、managed SQL 或 distributed SQL</td>
          <td>同類對比、相鄰章節路由、下游 deep article</td>
      </tr>
      <tr>
          <td>案例與限制</td>
          <td>哪些案例能提供壓力訊號，哪些 claim 需要時間敏感標記</td>
          <td>案例對照、已知 limitation、後續擴充候選</td>
      </tr>
  </tbody>
</table>
<p>服務定位段要先把產品名稱放回資料庫分類語言。SQLite 的定位是 embedded formal state 與低操作成本；MongoDB 的定位是 document shape 與 schema governance；DynamoDB 的定位是 managed KV / document access pattern；Aurora 的定位是 managed SQL operation transfer；Spanner、Cosmos DB 與 CockroachDB 的定位是 global 或 distributed consistency。</p>
<p>資料形狀段要讓讀者知道服務為哪種查詢與寫入模式付成本。Row model 適合交易與 ad-hoc query；document model 適合聚合資料與 schema flexibility；KV model 適合固定 access pattern；<a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL</a> 適合跨 region 一致性，但會把 latency、transaction retry 與成本模型帶進設計。</p>
<p>一致性與交易段要接回 <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a>、<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication lag</a> 與 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a>。讀者需要知道的是哪種資料變更必須一起成功、哪種讀取可以接受延遲，以及跨 region 寫入是否值得支付協調成本。</p>
<p>操作責任段要把 managed 與 self-managed 的責任轉移寫清楚。自管服務保留控制權，團隊承擔 patch、backup、failover、capacity 與事故演練；managed 服務降低操作負擔，但增加平台限制、費用模型、版本節奏與 vendor-specific behavior。</p>
<p>替代邊界段要保留機會成本。PostgreSQL 或 MySQL 可以承擔多數 OLTP baseline；當 query 固定且高峰連線壓力明顯，DynamoDB 類服務可能更划算；當 document shape 主導資料模型，MongoDB 或 Cosmos DB 有更自然的操作語意；當 global write 是核心需求，Spanner、CockroachDB 或 Aurora DSQL 才進入主要比較。</p>
<p>案例與限制段要分開處理 evidence 與 backlog。案例提供流量形狀、資料形狀、失敗代價或回退路徑；limitation 承認正文還缺哪些維度，例如 PostgreSQL 目前仍需補 Security / RLS / audit logging、cross-region DR 與 managed PG 變體對比，MySQL 仍需補 deep article 的 anti-recommendation 與真實 incident anchor。</p>
<h2 id="deep-article-規格">Deep Article 規格</h2>
<p>Deep article 的責任是把 vendor overview 點到的單一機制展開成可操作教材。這一層不重寫服務選型，而是教讀者設定、觀測、除錯、容量估算與整合某個具體機制，例如 connection pool、replication topology、online schema change、<a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">CDC</a>、partitioning、lock contention 或 PITR。</p>
<table>
  <thead>
      <tr>
          <th>規格面</th>
          <th>必答問題</th>
          <th>交付形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>問題情境</td>
          <td>什麼 production 壓力會讓這個機制變成主題</td>
          <td>開場場景、痛點、失效訊號</td>
      </tr>
      <tr>
          <td>核心機制</td>
          <td>該 vendor 如何實作這個能力，跟通用概念差在哪</td>
          <td>lifecycle、模式對照、內部元件責任</td>
      </tr>
      <tr>
          <td>操作流程</td>
          <td>讀者要如何配置、驗證、調整與演練</td>
          <td>step-by-step、config、query、command、驗證條件</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>哪些踩雷最常把服務推向事故</td>
          <td>production case、徵兆、根因、修法</td>
      </tr>
      <tr>
          <td>容量與觀測</td>
          <td>什麼 metric、query、log 或 cost signal 能判斷健康狀態</td>
          <td>容量規劃、觀測 metric、alert / dashboard route</td>
      </tr>
      <tr>
          <td>邊界與整合</td>
          <td>什麼條件下要換 sub-tool、改架構或回到 overview</td>
          <td>何時用、何時不用、sibling 對比、下一步路由</td>
      </tr>
  </tbody>
</table>
<p>問題情境段要用具體壓力啟動，產品文件定義只作為補充材料。Connection pool 可以從連線風暴與 backend slot 說起；replication 可以從 lag 與 failover 說起；PITR 可以從 restore 能力與 RPO 說起；lock contention 可以從交易範圍與 deadlock 訊號說起。</p>
<p>核心機制段要保留 vendor-specific 語意。PostgreSQL 的 WAL / LSN / replication slot、MVCC / vacuum、process-per-connection model 與 extension lifecycle 都有自己的操作語意；MySQL 的 binlog / GTID、InnoDB clustered index、gap / next-key lock、ProxySQL query rule 與 Vitess VSchema 也要用自己的語言展開。</p>
<p>操作流程段要把設定與判準綁在一起。Config、SQL、CLI 或 dashboard query 只在能支撐判讀時出現；每個操作要回答「如何知道它生效」「失敗時看到什麼」「可以停在哪個 rollback boundary」。</p>
<p>失敗模式段是 deep article 的主要價值。PostgreSQL / MySQL 既有文章多數已具備「5 個 Production 踩雷」；後續服務要維持這個密度，並優先補真實案例 anchor，避免所有案例都停在合成數字或典型設定。</p>
<p>容量與觀測段要讓 deep article 接回 04 / 09。資料庫機制常見的訊號包括 connection usage、replication lag、lock wait、dead tuple、buffer hit ratio、slow query、binlog retention、WAL growth、partition pruning 與 restore duration；這些訊號要能回到 <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 瓶頸定位流程</a>。</p>
<p>邊界與整合段要補「何時不用」。MySQL audit 已經指出 deep article 容易缺 anti-recommendation；後續每篇 deep article 至少要有一段說明什麼規模、團隊能力或 workload 下暫時維持簡單設計更划算。</p>
<h2 id="hands-on--artifact-規格">Hands-on / Artifact 規格</h2>
<p>Hands-on / artifact 章節的責任是把 deep article 的機制判讀轉成可演練操作。這一層對齊 LLM <code>hands-on/</code> 的教學功能：讀者能跑出一個 local / staging lab，取得 config、query output、metric snapshot、validation result 或 rollback note，而不只停在概念理解。</p>
<table>
  <thead>
      <tr>
          <th>規格面</th>
          <th>必答問題</th>
          <th>交付形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lab scope</td>
          <td>這個操作在 local、staging、managed sandbox 哪裡跑</td>
          <td>Docker Compose、CLI、SQL script、preview environment</td>
      </tr>
      <tr>
          <td>Input</td>
          <td>需要哪些 schema、seed data、config、credential</td>
          <td>setup checklist、sample data、env var</td>
      </tr>
      <tr>
          <td>操作步驟</td>
          <td>讀者照順序做什麼</td>
          <td>command / SQL / dashboard step</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>怎麼知道操作成功、退化或失敗</td>
          <td>query output、metric snapshot、log、screenshot note</td>
      </tr>
      <tr>
          <td>Cleanup</td>
          <td>操作後哪些資料、帳號、route、backup 要清理</td>
          <td>teardown、rollback、retention note</td>
      </tr>
      <tr>
          <td>下一步路由</td>
          <td>操作結果要回到哪篇 deep article 或 migration</td>
          <td>overview、deep article、release gate、incident log</td>
      </tr>
  </tbody>
</table>
<p>PostgreSQL、MySQL 與 SQLite 已建立 hands-on 入口：<a href="/blog/backend/01-database/vendors/postgresql/hands-on/" data-link-title="PostgreSQL Hands-on 操作路線" data-link-desc="PostgreSQL local lab、connection pool、PITR restore drill、schema migration evidence 與 HA failover 的操作型章節設計">PostgreSQL hands-on</a>、<a href="/blog/backend/01-database/vendors/mysql/hands-on/" data-link-title="MySQL Hands-on 操作路線" data-link-desc="MySQL local lab、ProxySQL routing、online schema change、replication failover、backup restore 與 Vitess sandbox 的操作型章節設計">MySQL hands-on</a> 與 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite hands-on</a>。後續其他 database vendor 也要先建立 hands-on 入口，再依服務責任決定是否補完整操作正文。</p>
<h2 id="migration-playbook-規格">Migration Playbook 規格</h2>
<p>Migration playbook 的責任是處理跨 vendor、跨 topology 或跨 operational model 的變更流程。這一層的主體是差異盤點、階段切換、雙軌驗證、cutover、rollback / fail-forward 與 cleanup；它應作為獨立流程教材，而非 deep article 的長版或 vendor overview 的補充段。</p>
<table>
  <thead>
      <tr>
          <th>規格面</th>
          <th>必答問題</th>
          <th>交付形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Driver</td>
          <td>為什麼要遷，壓力來自成本、容量、合規、operation 還是 paradigm</td>
          <td>開場 driver、no-go condition、替代方案</td>
      </tr>
      <tr>
          <td>Diff audit</td>
          <td>source / target 在 schema、operation、paradigm、component、application、topology 哪裡不同</td>
          <td>6 維 audit、主導差異、type 判定</td>
      </tr>
      <tr>
          <td>Phase plan</td>
          <td>哪些工作能分段，哪些工作必須 parallel run 或長期混合</td>
          <td>phase、stream、owner、驗證門檻</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>每個階段用什麼資料證明可前進</td>
          <td>validation query、row count、lag、error budget、cost</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>什麼條件下切流，切流期間誰決策</td>
          <td>cutover window、rollback condition、decision log route</td>
      </tr>
      <tr>
          <td>Cleanup</td>
          <td>哪些舊路徑能退役，哪些證據要保留</td>
          <td>contract removal、backup retention、incident write-back</td>
      </tr>
  </tbody>
</table>
<p>Driver 段要先排除「因為新服務比較好」這類空泛動機。有效 driver 通常是單機 primary 上限、connection limit、replication lag、backup / restore 責任、multi-region residency、vendor operation transfer、schema feature gap 或成本曲線。</p>
<p>Diff audit 段要先決定 playbook type。MySQL → PostgreSQL 主要是 schema / dialect 差；PostgreSQL → Aurora 主要是 operational redesign；PostgreSQL → CockroachDB 或 Aurora DSQL 主要是 paradigm shift；partition redesign 是 topology re-layout。type 決定結構，不用把所有 playbook 壓成同一套 phase。</p>
<p>Phase plan 段要把不可逆動作放晚。Schema audit、application compatibility、shadow read、dual-write、backfill、CDC catch-up、read-only cutover 與 cleanup 要分出驗證門檻；長期混合架構要明確標示哪些 workload 保留在 source。</p>
<p>Evidence 段要把資料庫遷移接回 observability 與 reliability。Playbook 應要求 row count、checksum、replication lag、error rate、query latency、data quality 與 owner；這些 evidence 是 release gate、incident decision log 與 rollback 判斷的共同材料。</p>
<p>Cutover 段要把決策權責寫清楚。資料庫切流失敗通常代價高，正文要標示切流窗口、暫停條件、回退條件、資料凍結策略與 decision owner，並連到 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 或 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a>。</p>
<p>Cleanup 段要防止雙軌永久殘留。舊 schema、舊 writer、舊 CDC connector、舊 backup、舊 dashboard 與舊 runbook 都需要退役判準；資料保留、稽核與 incident write-back 要在 cleanup 前確認。</p>
<h2 id="從-postgresql--mysql-回收的調整項">從 PostgreSQL / MySQL 回收的調整項</h2>
<p>PostgreSQL 與 MySQL 的正文已經足以讓其他服務頁開寫。下一輪調整應集中在橫向品質；SQL baseline 可維持現有正文作為後續服務頁的比較基準。</p>
<h3 id="postgresql">PostgreSQL</h3>
<p>PostgreSQL 的下一輪擴充重點是補安全、災難復原與 managed variant。<a href="/blog/backend/01-database/vendors/postgresql/security-rls-audit-logging/" data-link-title="PostgreSQL Security / RLS / Audit Logging" data-link-desc="PostgreSQL role、grant、Row Level Security、pgAudit、log policy、PII access evidence 與合規路由">Security / RLS / audit logging</a> 可以連到資料保護與稽核章節；<a href="/blog/backend/01-database/vendors/postgresql/cross-region-dr/" data-link-title="PostgreSQL Cross-region DR" data-link-desc="PostgreSQL 跨區災難復原、physical replica、logical replication、backup restore、RPO / RTO 與 failover runbook">cross-region DR</a> 可以連到 reliability 與 incident decision；<a href="/blog/backend/01-database/vendors/postgresql/managed-pg-comparison/" data-link-title="Managed PostgreSQL Comparison" data-link-desc="RDS PostgreSQL、Aurora PostgreSQL、Cloud SQL、Azure Database for PostgreSQL、Neon、Supabase、Crunchy Bridge 的責任邊界比較">Managed PG Comparison</a> 與 <a href="/blog/backend/01-database/vendors/postgresql/specialized-pg-variants/" data-link-title="Specialized PostgreSQL Variants" data-link-desc="pgvectorscale、Citus、TimescaleDB、PostGIS、AlloyDB、Cosmos DB for PostgreSQL、serverless PG 等 PostgreSQL 變體的選型邊界">Specialized PostgreSQL Variants</a> 承接 AlloyDB、Cloud SQL、Cosmos DB for PostgreSQL 與 pgvectorscale。</p>
<p>PostgreSQL 的既有 limitation 已經標示 PG-favoring narrative 與時間敏感 claim。後續補文時要保留對手 vendor 的強項，例如專業 vector DB 的 scale、專業 time-series DB 的 ingestion、distributed SQL 的 global consistency 與 managed 平台的 operation transfer。</p>
<h3 id="mysql">MySQL</h3>
<p>MySQL 的下一輪擴充重點是補 anti-recommendation 與真實 case anchor。多數 deep article 已經有 production 踩雷，但還要加上「何時暫時不用這個機制」的段落，讓讀者知道維持單 primary、簡單 replication、原生 partition 或標準 backup 何時更划算；security、audit、Document Store、multi-source replication、HeatWave、memory contention 與 metadata lock 已先建立 outline 路由。</p>
<p>MySQL 的案例段要把 GitHub、Shopify、Slack、YouTube / Vitess 這些業界來源升級成具體 anchor。案例不只列公司名稱，還要回收它提供的流量形狀、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a> 策略、schema change 壓力、failover 責任或工具演化原因。</p>
<h2 id="後續服務撰寫順序">後續服務撰寫順序</h2>
<p>後續服務撰寫順序要從 SQL baseline 推進到資料模型與操作責任差異。每一篇先完成 vendor overview，再依 overview 暴露出的機制缺口決定 deep article 或 migration playbook。</p>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務</th>
          <th>開寫重點</th>
          <th>升級條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB2</td>
          <td>SQLite</td>
          <td>embedded formal state、local data、testing DB、backup 邊界</td>
          <td>local-first sync、edge deployment 或 file corruption</td>
      </tr>
      <tr>
          <td>DB3</td>
          <td>MongoDB / DynamoDB</td>
          <td>document shape、access pattern、partition key、capacity mode</td>
          <td>shard expansion、Atlas migration、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a></td>
      </tr>
      <tr>
          <td>DB4</td>
          <td>Aurora</td>
          <td>managed SQL、storage / compute 分離、failover、cost model</td>
          <td>PostgreSQL / MySQL 遷移、I/O-Optimized cost</td>
      </tr>
      <tr>
          <td>DB5</td>
          <td>Spanner / Cosmos DB</td>
          <td>global consistency、multi-region latency、consistency level</td>
          <td>regional rollout、API model migration</td>
      </tr>
      <tr>
          <td>DB6</td>
          <td>CockroachDB</td>
          <td>distributed SQL、transaction retry、range lease、compatibility</td>
          <td>PostgreSQL migration、multi-region topology</td>
      </tr>
  </tbody>
</table>
<p>SQLite 的重點是讓讀者知道單機正式狀態何時成立。它不應被寫成小型 PostgreSQL，而要處理 file lifecycle、embedded process boundary、backup、concurrency、migration 與測試資料責任。</p>
<p>MongoDB / DynamoDB 的重點是把資料形狀放在 SQL baseline 之後。MongoDB 應教 document shape、index、schema governance 與 transaction boundary；DynamoDB 應教 access pattern、partition key、capacity mode、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a> 與 connection-free scaling。</p>
<p>Aurora 的重點是 operation transfer。它把 PostgreSQL / MySQL 相容介面放進 AWS-managed operational model；storage / compute 分離、cluster endpoint、replica、backup、failover、cost model 與 AWS 限制都會改變團隊責任。</p>
<p>Spanner / Cosmos DB 的重點是 global data responsibility。Spanner 應教 TrueTime、strong consistency、multi-region latency 與 cost；Cosmos DB 應教 consistency level、API model、partition、RU 與 Azure 約束。</p>
<p>CockroachDB 的重點是 distributed SQL 對 application contract 的影響。SQL 相容降低導入門檻，但 transaction retry、range lease、hot range、schema feature gap 與 multi-region topology 會改變 application 與 SRE 的責任。</p>
<h2 id="llm-depth-下一輪擴章-backlog">LLM-depth 下一輪擴章 Backlog</h2>
<p>LLM-depth 下一輪的責任是把每個資料庫服務從 T1 overview 推進到可教學的章節群。Overview 只回答第一輪服務判斷；deep article 回答穩定運作與排錯；migration playbook 回答跨 vendor、跨 topology 或跨 operational model 變更。</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>目前狀態</th>
          <th>下一篇 deep article</th>
          <th>升級 playbook 候選</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQLite</td>
          <td>T1 overview 已完成</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/teaching-structure/" data-link-title="SQLite Teaching Structure" data-link-desc="SQLite 服務章節群的大綱：從 embedded formal state、WAL、backup、test fixture、local-first、edge SQLite 到遷移路由">teaching structure</a> + <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a></td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite → PostgreSQL</a>、<a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite → D1 / Turso</a></td>
      </tr>
      <tr>
          <td>MongoDB</td>
          <td>T1 overview 已完成</td>
          <td>document shape governance、index / shard key</td>
          <td>self-managed → Atlas、document model → relational split</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>T1 overview 已完成</td>
          <td>partition key / hot partition、capacity mode</td>
          <td>DynamoDB → SQL / search / analytics split</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>T1 overview 已完成</td>
          <td>failover / endpoint routing、I/O cost model</td>
          <td>PostgreSQL / MySQL → Aurora、Aurora → distributed SQL</td>
      </tr>
      <tr>
          <td>Spanner</td>
          <td>T1 overview 已完成</td>
          <td>TrueTime / transaction latency、multi-region topology</td>
          <td>regional SQL → Spanner</td>
      </tr>
      <tr>
          <td>Cosmos DB</td>
          <td>T1 overview 已完成</td>
          <td>consistency level / RU budgeting、partitioning</td>
          <td>API model migration、Cosmos DB → specialized store</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>T1 overview 已完成</td>
          <td>transaction retry、range split / leaseholder</td>
          <td>PostgreSQL → CockroachDB、single-region → multi-region</td>
      </tr>
  </tbody>
</table>
<p>Backlog 的排序以學習梯度為準。SQLite 先處理單檔案正式狀態，補足「低操作成本如何 production 化」；MongoDB / DynamoDB 再處理資料形狀與 access pattern；Aurora 接 SQL operation transfer；Spanner、Cosmos DB 與 CockroachDB 最後處理 distributed consistency 與 multi-region topology。</p>
<h2 id="規格檢查清單">規格檢查清單</h2>
<p>資料庫 vendor 文章完成前要跑一次規格檢查。檢查通過代表本次內容可作為後續服務的基準；未通過時，先修正文再開下一篇。</p>
<ul>
<li>Vendor overview 已說清楚服務責任、資料形狀、一致性、操作責任、替代邊界、案例與 limitation。</li>
<li>Deep article 已包含問題情境、核心機制、操作流程、失敗模式、容量與觀測、邊界與整合。</li>
<li>Migration playbook 已完成 driver、diff audit、phase plan、evidence、cutover 與 cleanup。</li>
<li>表格後有情境化說明，沒有讓表格取代判讀。</li>
<li>案例提供壓力、失敗代價或回退條件，不只列公司名稱。</li>
<li>「何時不用」或 no-go condition 已出現在 deep article / migration playbook。</li>
<li>Time-sensitive vendor claim 有日期語境或指向官方文件。</li>
<li>下一步路由能接回主章、knowledge card、04 / 06 / 08 / 09 或 sibling vendor。</li>
</ul>
]]></content:encoded></item></channel></rss>