<?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>Table-Locality on Tarragon</title><link>https://tarrragon.github.io/blog/tags/table-locality/</link><description>Recent content in Table-Locality on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 02 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/table-locality/index.xml" rel="self" type="application/rss+xml"/><item><title>CockroachDB Multi-region Table 配置：三種 table locality 的選擇與 latency / 一致性取捨</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/multi-region-table-config/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/multi-region-table-config/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。本文聚焦 &lt;em>三種 table locality 怎麼選、選錯的 latency / 一致性後果與重配代價&lt;/em>。Schema 怎麼配合 locality 設計（合規 boundary、跨州業務邏輯、Outposts 拓樸）主寫於 &lt;a href="../locality-aware-schema/">locality-aware schema&lt;/a>、survival goal 的存活機制主寫於 &lt;a href="../survival-goals/">survival goals&lt;/a>、本文兩者都 cross-link、不重複展開。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境multi-region-cluster-起來了每張-table-該設哪種-locality">問題情境：multi-region cluster 起來了、每張 table 該設哪種 locality&lt;/h2>
&lt;p>團隊把 CockroachDB 跨 region 拉起來、&lt;code>ALTER DATABASE ... ADD REGION&lt;/code> 也跑完了，接下來面對的是逐張 table 的 locality 決策。這個決策的成本結構很不對稱：設對了，read / write 走本地 leaseholder、latency 貼著單區水準；設錯了，每次寫入或讀取都吃一趟跨 region round trip，p99 從個位數毫秒跳到上百毫秒。&lt;/p>
&lt;p>multi-region table locality 是 &lt;em>把「資料的地理歸屬」跟「讀寫路徑」綁在一起&lt;/em> 的宣告。CockroachDB 提供三種 locality，對應三種「資料屬於誰、誰要快」的業務形狀：&lt;/p>
&lt;ul>
&lt;li>&lt;code>REGIONAL BY TABLE&lt;/code>：整張 table 歸屬單一 region，該 region 讀寫快、其他 region 慢。&lt;/li>
&lt;li>&lt;code>REGIONAL BY ROW&lt;/code>：每一 row 各自歸屬一個 region，row 所在 region 讀寫快。&lt;/li>
&lt;li>&lt;code>GLOBAL&lt;/code>：資料屬於所有 region，每個 region 本地讀都快，但寫入要跨 region 達成共識。&lt;/li>
&lt;/ul>
&lt;p>讀者進來最常卡的三題：&lt;/p>
&lt;ul>
&lt;li>三種 locality 對應什麼業務形狀、判讀軸是什麼？&lt;/li>
&lt;li>&lt;code>GLOBAL&lt;/code> 既然每區讀都快，為什麼不全部設 &lt;code>GLOBAL&lt;/code>？&lt;/li>
&lt;li>上線後發現 locality 設錯，重配的代價有多高、能不能無痛改？&lt;/li>
&lt;/ul>
&lt;p>這三題都是 &lt;em>把業務的資料歸屬與讀寫熱點，翻譯成副本拓樸&lt;/em> 的設計決策，語法層面反而簡單。&lt;/p>
&lt;p>問題情境最常見的 trigger：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a> 的 60+ multi-region cluster、最大 Gaming cluster 48-node 跨 4 region。case 揭露一個反直覺判讀 — multi-region 的主要動機是 &lt;em>region failure 0 downtime&lt;/em>、不是降 latency；跨 region quorum 物理上會 &lt;em>增&lt;/em> 寫入 latency。這條判讀直接決定 table locality 怎麼設：當 multi-region 的目的是 survival 而非 latency，把高寫入 table 設成 &lt;code>GLOBAL&lt;/code>（跨區同步寫）就是把成本花在錯的地方。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a> 則提供 row-level 歸屬的 concrete framing：跨 8 州 sportsbook、bet 資料按下注州歸屬、邏輯上仍是一個 cluster。case 觀察段揭露「跨所有 region 一個 logical database」這個拓樸 fact — 也就是 row-level locality 撐起了「合規分州 placement + 單一邏輯 DB」的組合。Hard Rock 的合規驅動與 schema 設計細節在 &lt;a href="../locality-aware-schema/">locality-aware schema&lt;/a> 展開，本文只取「row-level 歸屬」這個 locality 選擇本身。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。本文聚焦 <em>三種 table locality 怎麼選、選錯的 latency / 一致性後果與重配代價</em>。Schema 怎麼配合 locality 設計（合規 boundary、跨州業務邏輯、Outposts 拓樸）主寫於 <a href="../locality-aware-schema/">locality-aware schema</a>、survival goal 的存活機制主寫於 <a href="../survival-goals/">survival goals</a>、本文兩者都 cross-link、不重複展開。</p></blockquote>
<hr>
<h2 id="問題情境multi-region-cluster-起來了每張-table-該設哪種-locality">問題情境：multi-region cluster 起來了、每張 table 該設哪種 locality</h2>
<p>團隊把 CockroachDB 跨 region 拉起來、<code>ALTER DATABASE ... ADD REGION</code> 也跑完了，接下來面對的是逐張 table 的 locality 決策。這個決策的成本結構很不對稱：設對了，read / write 走本地 leaseholder、latency 貼著單區水準；設錯了，每次寫入或讀取都吃一趟跨 region round trip，p99 從個位數毫秒跳到上百毫秒。</p>
<p>multi-region table locality 是 <em>把「資料的地理歸屬」跟「讀寫路徑」綁在一起</em> 的宣告。CockroachDB 提供三種 locality，對應三種「資料屬於誰、誰要快」的業務形狀：</p>
<ul>
<li><code>REGIONAL BY TABLE</code>：整張 table 歸屬單一 region，該 region 讀寫快、其他 region 慢。</li>
<li><code>REGIONAL BY ROW</code>：每一 row 各自歸屬一個 region，row 所在 region 讀寫快。</li>
<li><code>GLOBAL</code>：資料屬於所有 region，每個 region 本地讀都快，但寫入要跨 region 達成共識。</li>
</ul>
<p>讀者進來最常卡的三題：</p>
<ul>
<li>三種 locality 對應什麼業務形狀、判讀軸是什麼？</li>
<li><code>GLOBAL</code> 既然每區讀都快，為什麼不全部設 <code>GLOBAL</code>？</li>
<li>上線後發現 locality 設錯，重配的代價有多高、能不能無痛改？</li>
</ul>
<p>這三題都是 <em>把業務的資料歸屬與讀寫熱點，翻譯成副本拓樸</em> 的設計決策，語法層面反而簡單。</p>
<p>問題情境最常見的 trigger：<a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> 的 60+ multi-region cluster、最大 Gaming cluster 48-node 跨 4 region。case 揭露一個反直覺判讀 — multi-region 的主要動機是 <em>region failure 0 downtime</em>、不是降 latency；跨 region quorum 物理上會 <em>增</em> 寫入 latency。這條判讀直接決定 table locality 怎麼設：當 multi-region 的目的是 survival 而非 latency，把高寫入 table 設成 <code>GLOBAL</code>（跨區同步寫）就是把成本花在錯的地方。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a> 則提供 row-level 歸屬的 concrete framing：跨 8 州 sportsbook、bet 資料按下注州歸屬、邏輯上仍是一個 cluster。case 觀察段揭露「跨所有 region 一個 logical database」這個拓樸 fact — 也就是 row-level locality 撐起了「合規分州 placement + 單一邏輯 DB」的組合。Hard Rock 的合規驅動與 schema 設計細節在 <a href="../locality-aware-schema/">locality-aware schema</a> 展開，本文只取「row-level 歸屬」這個 locality 選擇本身。</p>
<h2 id="核心機制三種-locality-的判讀軸--survival-goal-互動">核心機制：三種 locality 的判讀軸 + survival goal 互動</h2>
<p>三種 table locality 的差異，本質是 <em>leaseholder（讀寫入口）跟資料歸屬 region 之間的關係</em>。leaseholder 機制屬前置、見 <a href="../hlc-raft-consensus/">HLC + Raft consensus</a>；本文聚焦三種 locality 把 leaseholder 放在哪、因此誰快誰慢。</p>
<h3 id="判讀軸資料歸屬的顆粒--讀寫熱點分佈">判讀軸：資料歸屬的顆粒 × 讀寫熱點分佈</h3>
<p>選 locality 的第一個判讀軸是 <em>資料歸屬的顆粒</em>：整張 table 屬於一個 region（table 級），還是每 row 各屬一個 region（row 級），還是屬於所有 region（global）。第二個判讀軸是 <em>讀寫熱點落在哪</em>：本地讀為主、本地寫為主、還是全球讀為主。</p>
<table>
  <thead>
      <tr>
          <th>Locality</th>
          <th>資料歸屬顆粒</th>
          <th>Read 快的條件</th>
          <th>Write 快的條件</th>
          <th>對應業務形狀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>REGIONAL BY TABLE</code></td>
          <td>整張 table 一個 region</td>
          <td>從歸屬 region 讀</td>
          <td>從歸屬 region 寫</td>
          <td>整張表服務單一市場（例：日本訂單表）</td>
      </tr>
      <tr>
          <td><code>REGIONAL BY ROW</code></td>
          <td>每 row 一個 region</td>
          <td>從 row 歸屬 region 讀</td>
          <td>從 row 歸屬 region 寫</td>
          <td>資料跟用戶地理綁定（玩家、帳戶、訂單）</td>
      </tr>
      <tr>
          <td><code>GLOBAL</code></td>
          <td>所有 region 共有</td>
          <td>任何 region 本地讀都快</td>
          <td>沒有「快」的寫（跨區共識）</td>
          <td>reference data（國碼、貨幣、規則表）</td>
      </tr>
  </tbody>
</table>
<p>每一格的判讀都要回到該情境，不能只看表。</p>
<p><code>REGIONAL BY TABLE</code> 適合 <em>整張表的讀寫熱點集中在單一 region</em> 的情況。例如一張只服務日本市場的訂單表，把整張表的 leaseholder 釘在 <code>asia-northeast1</code>，日本端的應用讀寫都走本地 leaseholder，跨區應用偶爾讀則走 follower read 接受 stale。判讀訊號：這張表的寫入請求是否 95% 以上來自同一 region。如果不是，table 級歸屬會讓多數寫入吃跨區延遲。</p>
<p><code>REGIONAL BY ROW</code> 適合 <em>每一 row 跟某個地理位置強綁定、但整張表跨多 region</em> 的情況。玩家帳戶、訂單、下注紀錄都屬於這類 — 每筆資料屬於某個用戶所在 region，但整張表服務所有 region 的用戶。row 透過隱含的 <code>crdb_region</code> 欄位決定歸屬，leaseholder 跟著 row 走。判讀訊號：同一張表的不同 row，讀寫熱點是否分散在不同 region。是的話，row 級歸屬讓每個 row 都貼著自己的用戶。</p>
<p><code>GLOBAL</code> 適合 <em>讀遠多於寫、且每個 region 都要本地快讀</em> 的 reference data。國家代碼、貨幣表、運動賽事 metadata 這類資料變更稀少、但每個 region 的每次查詢都要用到。<code>GLOBAL</code> 讓每個 region 都能本地讀（讀到 closed timestamp 前的一致快照），代價是寫入要跨 region 達成共識。判讀訊號：寫入頻率是否低到「跨區寫的慢可以忽略」。</p>
<h3 id="為什麼不全部設-global">為什麼不全部設 GLOBAL</h3>
<p><code>GLOBAL</code> 的「每區讀都快」看似適合全表套用，但它對 <em>寫入</em> 收取跨 region quorum 的全額成本。<code>GLOBAL</code> table 的讀之所以能本地完成，是因為 CockroachDB 維護一個全球同步的 closed timestamp，讓每個 region 都能安全地本地讀稍早的快照；維護這個 timestamp 的代價是每次寫入都要跟所有 region 協調。</p>
<blockquote>
<p><strong>Scope warning</strong>：<code>GLOBAL</code> table 的跨 region 寫入 p99、<code>REGIONAL BY ROW</code> 的本地寫入 p99、closed timestamp 的傳播間隔等具體數字，屬 vendor 規格與部署拓樸（region 距離、replica 數）的函數，三個 anchor case（DoorDash / Netflix / Hard Rock）都未揭露單一 table 的 latency 數字。本文只給量級判讀（本地 quorum vs 跨洲 quorum 差一到兩個數量級），具體值需 benchmark 自身拓樸並 cross-verify <a href="https://www.cockroachlabs.com/docs/stable/table-localities.html">CockroachDB Table Localities 文件</a>。</p></blockquote>
<p>因此「全部設 <code>GLOBAL</code>」會把所有寫入推上跨 region 路徑，等於放棄了 distributed SQL 把寫入分散到各 region 的核心優勢。<code>GLOBAL</code> 的正確用法是限定在 <em>變更頻率低、全球都要快讀</em> 的 reference data。</p>
<h3 id="survival-goal-怎麼跟-locality-一起決定副本拓樸">Survival goal 怎麼跟 locality 一起決定副本拓樸</h3>
<p>table locality 決定 <em>leaseholder 放哪、讀寫走哪條路徑</em>；survival goal 決定 <em>副本要分佈到幾個 failure domain 才能在故障後存活</em>。兩者一起決定每張 table 的副本拓樸。</p>
<p>survival goal 的存活機制本身（<code>SURVIVE ZONE FAILURE</code> vs <code>SURVIVE REGION FAILURE</code>、怎麼從業務 SLO 倒推、RTO / RPO 怎麼算）是 <a href="../survival-goals/">survival goals</a> 的 SSoT，本文不重複展開。本文只取兩者 <em>互動</em> 的一個關鍵後果：把 <code>SURVIVE REGION FAILURE</code> 套到 <code>REGIONAL BY ROW</code> table 時，每個 region 的 row 不只需要本地 voting replica，還需要在 <em>其他 region</em> 放足夠的 voting replica 才能在整個 region 失效後仍達成 quorum。這會把跨 region 的 voting replica 數量推高，間接增加寫入要協調的範圍。</p>
<p>判讀路線：先依業務的資料歸屬與讀寫熱點選 locality（本文），再依業務的 region failure 容忍度選 survival goal（<a href="../survival-goals/">survival goals</a>），兩者疊加後才得到最終副本拓樸與 latency 結構。</p>
<h2 id="操作流程配置驗證每步檢查生效">操作流程：配置、驗證、每步檢查生效</h2>
<h3 id="第一步確認-database-已加入所有-region">第一步：確認 database 已加入所有 region</h3>
<p>table locality 的前提是 database 已宣告 region。先確認 region 列表正確，再設 table locality。</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">-- 看 database 已有哪些 region、哪個是 primary
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">REGIONS</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="p">;</span></span></span></code></pre></div><p>驗證點：輸出的 region 數量與名稱要對齊實際部署的 region。少一個 region，後面把 table 設成該 region 的 <code>REGIONAL BY TABLE</code> 會直接報錯。</p>
<h3 id="第二步依判讀軸設定每張-table-的-locality">第二步：依判讀軸設定每張 table 的 locality</h3>





<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">-- 整張表服務單一市場
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders_jp</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="s2">&#34;asia-northeast1&#34;</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="c1">-- 資料跟用戶地理綁定
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</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></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 低寫入、全球本地讀的 reference data
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">currency_codes</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="k">GLOBAL</span><span class="p">;</span></span></span></code></pre></div><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="c1">-- 確認每張 table 的 locality 設定符合預期
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="p">;</span><span class="w">   </span><span class="c1">-- locality 子句會出現在輸出尾段</span></span></span></code></pre></div><h3 id="第三步驗證讀寫路徑真的走本地">第三步：驗證讀寫路徑真的走本地</h3>
<p>設了 locality 不代表查詢真的走本地路徑 — 寫入時 row 的 <code>crdb_region</code> 沒設對、或 query 沒帶上對應條件，仍會跨區。用 <code>EXPLAIN ANALYZE</code> 看實際 plan。</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">-- 看 query 是否在 row 歸屬 region 本地完成、有沒有跨 region 拉資料
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">ANALYZE</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><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="err">$</span><span class="mi">1</span><span class="p">;</span></span></span></code></pre></div><p>驗證點：plan 中不應出現大量跨 region 的 distributed scan；<code>REGIONAL BY ROW</code> 的點查應落在 row 歸屬 region 的單一 leaseholder。</p>
<h3 id="第四步驗證副本分佈符合-locality--survival-goal">第四步：驗證副本分佈符合 locality + survival goal</h3>





<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">-- 看每張 table 的 range 副本實際分佈在哪些 region
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">RANGES</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="p">;</span></span></span></code></pre></div><p>驗證點：副本分佈要同時滿足 locality（leaseholder 在歸屬 region）跟 survival goal（跨足夠 failure domain）。兩者衝突時，CockroachDB 以 survival goal 為硬約束調整副本數，這會反過來影響 latency — 對應 <a href="../survival-goals/">survival goals</a> 的 latency 暴漲失敗模式。</p>
<h2 id="失敗模式locality-選錯的高代價回退">失敗模式：locality 選錯的高代價回退</h2>
<h3 id="global-套到高寫入-table"><code>GLOBAL</code> 套到高寫入 table</h3>
<p>把高寫入 table（訂單、下注、status 變更）設成 <code>GLOBAL</code>，每筆寫入都跨 region 共識，寫入 p99 結構性暴漲、寫入吞吐被跨區協調卡死。徵兆：CockroachDB Console 的跨 region network traffic 隨寫入量線性成長、寫入 p99 跟 region 距離正相關。</p>
<p>修法：把 table 改成 <code>REGIONAL BY ROW</code>（按用戶歸屬）或 <code>REGIONAL BY TABLE</code>（按市場歸屬）。</p>
<p>Anti-recommendation：reference data 之外的任何 table，預設都不要設 <code>GLOBAL</code>。<code>GLOBAL</code> 的判準是「寫入頻率低到跨區寫的慢可以忽略」，高寫入 workload 直接排除。</p>
<h3 id="regional-by-row-但-row-沒帶正確-crdb_region"><code>REGIONAL BY ROW</code> 但 row 沒帶正確 <code>crdb_region</code></h3>
<p><code>REGIONAL BY ROW</code> 靠 <code>crdb_region</code> 決定 row 歸屬。寫入時沒顯式指定，default 走 <code>gateway_region()</code> — application server 所在 region 變成 row 歸屬。後果是 row 被釘在 application server 那一區，而非用戶所在區，locality 形同失效（甚至在合規場景違反 data residency，見 <a href="../locality-aware-schema/">locality-aware schema</a>）。</p>
<p>修法：寫入時顯式指定 <code>crdb_region</code> 為用戶所在 region，並用 NOT NULL + CHECK constraint 把可選值鎖死。</p>
<h3 id="選錯-locality-的重配代價高代價不可逆情境的回退敘事">選錯 locality 的重配代價（高代價不可逆情境的回退敘事）</h3>
<p>table locality 選錯，重配本身語法上一行就能改（<code>ALTER TABLE ... SET LOCALITY ...</code>），但 <em>資料層面的重配代價高且有持續影響</em>，需要專屬回退計畫，不能比照「改個 config 重啟」對待。</p>
<p>重配 locality 會觸發 CockroachDB 把受影響 range 的副本搬到新拓樸對應的位置。把一張大 table 從 <code>GLOBAL</code> 改成 <code>REGIONAL BY ROW</code>，或從 single region 改成 row-level 跨多 region，意味著大量 range 要 rebalance — 期間跨 region network 流量暴增、leaseholder 反覆換手、p99 持續波動，table 越大、region 越多，rebalance 窗口越長。這是隨資料量延長的背景過程，遠非秒級操作。</p>
<p>更關鍵的是 <code>REGIONAL BY ROW</code> 的 <code>crdb_region</code> 是 <em>資料內容</em>，不只是 metadata。如果原本 row 的歸屬區設錯（例如全部落到 application server 那一區），重配 locality 不會自動把 row 搬到正確的用戶 region — 還要 <em>回填 <code>crdb_region</code> 欄位</em>，這是一次 data migration，不是 schema 變更。合規場景下，錯誤歸屬期間寫入的資料可能已經違反 data residency，回退時要連同合規證據一起盤點。</p>
<p>回退計畫的要素：</p>
<ul>
<li>重配前估算受影響 range 數量與資料量，換算 rebalance 窗口，選低流量時段執行。</li>
<li>重配 <code>REGIONAL BY ROW</code> 時，分開處理「locality 宣告變更」與「<code>crdb_region</code> 回填」兩個動作，回填走分批 update 並監控 contention。</li>
<li>重配期間監控 rebalance queue 與跨 region traffic，設好「波動超過閾值就暫停 rebalance」的 tripwire。</li>
<li>合規場景下，先盤點錯誤歸屬期間的資料是否已違規，再決定回填策略與是否需要合規通報。</li>
</ul>
<p>Anti-recommendation：不要在 production 高峰時段直接對大 table 改 locality 試效果。locality 是「上線前依業務形狀想清楚再設」的決策，不是「線上 A/B 試」的旋鈕。</p>
<h3 id="cross-region-join-跑爆-latency">Cross-region join 跑爆 latency</h3>
<p>兩張 <code>REGIONAL BY ROW</code> table join，若 join key 不保證兩邊 row 在同 region，planner 要跨 region 拉資料，p99 暴漲。</p>
<p>修法：兩張 table 用同一個歸屬 key（如 user_id），讓 join 對應的 row co-locate 在同 region；無法 co-locate 時，對容忍 stale 的查詢改走 follower read。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Cross-region query count</code>：locality 是否生效的直接訊號，數值高代表查詢在跨區拉資料。</li>
<li><code>Leaseholder distribution by region</code>：leaseholder 是否落在資料歸屬 region，不均代表 locality 配置或 <code>crdb_region</code> 有偏。</li>
<li><code>Rebalance queue size</code>：locality 重配 / 副本搬遷期間的進度訊號，持續非零代表 rebalance 未收斂。</li>
<li><code>Cross-region network bytes</code>：<code>GLOBAL</code> table 寫入與 cross-region join 的成本訊號。</li>
</ul>
<h3 id="容量判讀">容量判讀</h3>
<ul>
<li><code>GLOBAL</code> table 的跨區寫入成本 ≈ 寫入 QPS × region 數，region 越多成本越高，所以 <code>GLOBAL</code> 只放低寫入 reference data。</li>
<li><code>REGIONAL BY ROW</code> 的跨區讀成本 ≈ 落到非歸屬 region 的讀 QPS，這部分若高，代表 <code>crdb_region</code> 歸屬與實際讀熱點不一致。</li>
<li>region 數量建議維持精簡 — 每多一個 region，跨區協調與重配窗口都變長。</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：region 數量上限建議、單 range 寫入吞吐量級、closed timestamp 傳播間隔等為 vendor 通用估算，非 case 揭露數字，容量規劃前以 <a href="https://www.cockroachlabs.com/docs/stable/multiregion-overview.html">CockroachDB Multi-Region 文件</a> cross-verify 並 benchmark 自身拓樸。</p></blockquote>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 判斷 cross-region-bound vs CPU-bound。</li>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> region count × replica × latency budget。</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> 跨 region quorum 預算。</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../locality-aware-schema/">locality-aware schema</a>：schema 怎麼配合 locality 設計 — 合規 boundary、跨州業務邏輯、Outposts 拓樸、<code>crdb_region</code> 作為合規欄位的管理。本文是「三種 locality 怎麼選」、該文是「選好後 schema 怎麼配合」，兩者互補不重複。</li>
<li><a href="../survival-goals/">survival goals</a>：survival goal 的存活機制與 SLO 倒推 — 本文只取「survival goal 與 locality 互動如何影響副本拓樸」這一個交點，存活機制本身以該文為 SSoT。</li>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>：leaseholder 與 range 機制 — locality 決定 leaseholder 放哪，前置機制在該文。</li>
</ul>
<h3 id="跟-spanner--aurora-對照">跟 Spanner / Aurora 對照</h3>
<p>Spanner 在 GCP region 內做 placement，無 AWS Outposts 等效；Aurora 不支援 row-level locality，跨 region 只能 cluster-per-region + async replication。完整三家 distributed SQL 在 multi-region placement 的選型對比，是 <a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a> 的 SSoT，本文不重展三方對比。</p>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<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> 上游 latency / 一致性取捨。</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡</a>、<a href="/blog/backend/knowledge-cards/follower-read/" data-link-title="Follower Read" data-link-desc="分散式 SQL 從 non-voting replica 讀 closed timestamp 之前的資料、不參與 Raft commit、低 latency 但 read-after-write 場景仍可能 stale">follower read 卡</a> — <code>GLOBAL</code> 與跨區讀的一致性語意。</li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>single-region 部署：用 default locality 即可，三種 locality 在單區無差異。</li>
<li>從 PostgreSQL 遷到 CockroachDB 的整體流程：見 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB migration</a>，本文只處理遷移後的 table locality 配置。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>（multi-region 動機是 survival 非 latency）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>（row-level 歸屬 + 單一邏輯 cluster）</li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed SQL 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/table-localities.html">CockroachDB Table Localities</a> / <a href="https://www.cockroachlabs.com/docs/stable/multiregion-overview.html">Multi-Region Overview</a> / <a href="https://www.cockroachlabs.com/docs/stable/follower-reads.html">Follower Reads</a></li>
</ul>
]]></content:encoded></item></channel></rss>