<?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>Anti-Patterns on Tarragon</title><link>https://tarrragon.github.io/blog/tags/anti-patterns/</link><description>Recent content in Anti-Patterns on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 27 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/anti-patterns/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>