<?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>Jsonb on Tarragon</title><link>https://tarrragon.github.io/blog/tags/jsonb/</link><description>Recent content in Jsonb on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/jsonb/index.xml" rel="self" type="application/rss+xml"/><item><title>PostgreSQL JSONB Deep Dive：Binary Storage + GIN Index 為什麼是結構性優勢</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>JSONB deep dive&lt;/em> — binary storage + GIN index 的結構性優勢。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="json-vs-jsonb選-jsonb">JSON vs JSONB：選 JSONB&lt;/h2>
&lt;p>PG 9.2 加 &lt;code>JSON&lt;/code> type、9.4 加 &lt;code>JSONB&lt;/code>。99% 場景用 JSONB：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>JSON&lt;/th>
 &lt;th>JSONB&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>儲存&lt;/td>
 &lt;td>純文字（原樣保存）&lt;/td>
 &lt;td>Binary decomposed format&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Parse cost&lt;/td>
 &lt;td>每次 query parse&lt;/td>
 &lt;td>Insert 時 parse 一次&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index 支援&lt;/td>
 &lt;td>Limited（functional index）&lt;/td>
 &lt;td>GIN / functional / partial 都行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operator 支援&lt;/td>
 &lt;td>有限（→ / →&amp;gt;）&lt;/td>
 &lt;td>完整（@&amp;gt; / ? / @? / ? 等）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Duplicate key&lt;/td>
 &lt;td>保留（原樣）&lt;/td>
 &lt;td>只保留最後一個（normalize）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Key order&lt;/td>
 &lt;td>保留&lt;/td>
 &lt;td>不保留&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Whitespace&lt;/td>
 &lt;td>保留&lt;/td>
 &lt;td>不保留&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>JSONB 唯一缺點是 &lt;em>binary 儲存（不保留 key order / whitespace / duplicate）&lt;/em>。99% application 不在意這些。&lt;/p>
&lt;p>從 &lt;em>application semantics&lt;/em> 視角、JSONB 是 PG JSON 的 &lt;em>the right type&lt;/em>、JSON 是 &lt;em>legacy / niche&lt;/em>。&lt;/p>
&lt;h2 id="jsonb-gin-index核心結構性優勢">JSONB GIN Index：核心結構性優勢&lt;/h2>
&lt;p>PG GIN（Generalized Inverted Index）可以對 JSONB 內所有 key/value pair 建 inverted index：&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">CREATE&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">products&lt;/span>&lt;span class="w"> &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">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">SERIAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&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 class="n">metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&lt;/span>&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="p">);&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>&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="c1">-- GIN index
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&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">idx_products_metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">GIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加完後、JSONB query 用 GIN index 加速：&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="c1">-- @&amp;gt; (contains) 用 GIN
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">@&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;{&amp;#34;category&amp;#34;: &amp;#34;shoes&amp;#34;}&amp;#39;&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="c1">-- ? (has key) 用 GIN
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;discount&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- ?| (has any of these keys) 用 GIN
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">metadata&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">?|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">array&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;discount&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;promotion&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跟 MongoDB index 對比、PG 不必 &lt;em>預先 define&lt;/em> JSON path index、&lt;code>USING GIN (metadata)&lt;/code> 對 &lt;em>整個 JSONB document 任意 path&lt;/em> 都有效。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>JSONB deep dive</em> — binary storage + GIN index 的結構性優勢。</p></blockquote>
<hr>
<h2 id="json-vs-jsonb選-jsonb">JSON vs JSONB：選 JSONB</h2>
<p>PG 9.2 加 <code>JSON</code> type、9.4 加 <code>JSONB</code>。99% 場景用 JSONB：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>JSON</th>
          <th>JSONB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>儲存</td>
          <td>純文字（原樣保存）</td>
          <td>Binary decomposed format</td>
      </tr>
      <tr>
          <td>Parse cost</td>
          <td>每次 query parse</td>
          <td>Insert 時 parse 一次</td>
      </tr>
      <tr>
          <td>Index 支援</td>
          <td>Limited（functional index）</td>
          <td>GIN / functional / partial 都行</td>
      </tr>
      <tr>
          <td>Operator 支援</td>
          <td>有限（→ / →&gt;）</td>
          <td>完整（@&gt; / ? / @? / ? 等）</td>
      </tr>
      <tr>
          <td>Duplicate key</td>
          <td>保留（原樣）</td>
          <td>只保留最後一個（normalize）</td>
      </tr>
      <tr>
          <td>Key order</td>
          <td>保留</td>
          <td>不保留</td>
      </tr>
      <tr>
          <td>Whitespace</td>
          <td>保留</td>
          <td>不保留</td>
      </tr>
  </tbody>
</table>
<p>JSONB 唯一缺點是 <em>binary 儲存（不保留 key order / whitespace / duplicate）</em>。99% application 不在意這些。</p>
<p>從 <em>application semantics</em> 視角、JSONB 是 PG JSON 的 <em>the right type</em>、JSON 是 <em>legacy / niche</em>。</p>
<h2 id="jsonb-gin-index核心結構性優勢">JSONB GIN Index：核心結構性優勢</h2>
<p>PG GIN（Generalized Inverted Index）可以對 JSONB 內所有 key/value pair 建 inverted index：</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">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">products</span><span class="w"> </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="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</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="n">metadata</span><span class="w"> </span><span class="n">JSONB</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><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></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- GIN index
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_products_metadata</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="p">);</span></span></span></code></pre></div><p>加完後、JSONB query 用 GIN index 加速：</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">-- @&gt; (contains) 用 GIN
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;}&#39;</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">-- ? (has key) 用 GIN
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="s1">&#39;discount&#39;</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">-- ?| (has any of these keys) 用 GIN
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?|</span><span class="w"> </span><span class="nb">array</span><span class="p">[</span><span class="s1">&#39;discount&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;promotion&#39;</span><span class="p">];</span></span></span></code></pre></div><p>跟 MongoDB index 對比、PG 不必 <em>預先 define</em> JSON path index、<code>USING GIN (metadata)</code> 對 <em>整個 JSONB document 任意 path</em> 都有效。</p>
<h3 id="jsonb_ops-vs-jsonb_path_ops"><code>jsonb_ops</code> vs <code>jsonb_path_ops</code></h3>
<p>PG GIN 對 JSONB 有兩種 <em>operator class</em>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th><code>jsonb_ops</code>（預設）</th>
          <th><code>jsonb_path_ops</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引內容</td>
          <td>Key + value 都索引</td>
          <td>只索引 path → value pair</td>
      </tr>
      <tr>
          <td>Index size</td>
          <td>大</td>
          <td>小（約一半）</td>
      </tr>
      <tr>
          <td>支援 operator</td>
          <td><code>@&gt; / ? / ?| / ?&amp;</code></td>
          <td>只 <code>@&gt;</code> (containment)</td>
      </tr>
      <tr>
          <td>適用</td>
          <td>多種 query pattern</td>
          <td>只用 <code>@&gt;</code> 的場景</td>
      </tr>
  </tbody>
</table>





<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">-- jsonb_ops（預設）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_meta_default</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</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">-- jsonb_path_ops（小、快、但只支援 @&gt;）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_meta_path</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="w"> </span><span class="n">jsonb_path_ops</span><span class="p">);</span></span></span></code></pre></div><p><strong>選擇</strong>：</p>
<ul>
<li>只跑 <code>@&gt;</code> containment query → <code>jsonb_path_ops</code>（index 小、快）</li>
<li>跑 <code>?</code> / <code>?|</code> / <code>?&amp;</code> key existence query → <code>jsonb_ops</code>（預設）</li>
</ul>
<h2 id="operator--path-query">Operator + Path Query</h2>
<p>JSONB 提供豐富 operator + jsonpath：</p>
<h3 id="operator">Operator</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">-- Extract value（returns jsonb）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="s1">&#39;name&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</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">-- Extract text（returns text）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">-&gt;&gt;</span><span class="w"> </span><span class="s1">&#39;name&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</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">-- Path extract
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">#&gt;</span><span class="w"> </span><span class="s1">&#39;{variants, 0, price}&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</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="k">SELECT</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">#&gt;&gt;</span><span class="w"> </span><span class="s1">&#39;{variants, 0, price}&#39;</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 返回 text
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- Containment（用 GIN index）
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;, &#34;active&#34;: true}&#39;</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></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c1">-- Reverse containment
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="s1">&#39;{&#34;sub&#34;: &#34;value&#34;}&#39;</span><span class="w"> </span><span class="o">&lt;@</span><span class="w"> </span><span class="n">metadata</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="c1">-- Key existence
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="s1">&#39;discount&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?|</span><span class="w"> </span><span class="nb">array</span><span class="p">[</span><span class="s1">&#39;a&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;b&#39;</span><span class="p">];</span><span class="w">  </span><span class="c1">-- 任一 key
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">?&amp;</span><span class="w"> </span><span class="nb">array</span><span class="p">[</span><span class="s1">&#39;a&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;b&#39;</span><span class="p">];</span><span class="w">  </span><span class="c1">-- 全部 key</span></span></span></code></pre></div><h3 id="jsonpathpg-12">jsonpath（PG 12+）</h3>
<p>SQL/JSON jsonpath 是 SQL standard、PG 12+ 支援：</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">-- jsonb_path_query：展開 path 結果
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">jsonb_path_query</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*].price&#39;</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">FROM</span><span class="w"> </span><span class="n">products</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="mi">1</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></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- jsonb_path_exists：返 boolean
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></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">products</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">jsonb_path_exists</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*] ? (@.price &gt; 100)&#39;</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></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- jsonb_path_query_array：返 array of result
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">jsonb_path_query_array</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.tags[*]&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="p">;</span></span></span></code></pre></div><p>jsonpath 比 PG-specific operator 標準化、跨 vendor portable。</p>
<h2 id="partial-jsonb-index">Partial JSONB Index</h2>
<p>對 <em>只 query subset row</em> 的場景、建 partial index：</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">-- 只對 active product 建 metadata index
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_active_products_metadata</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">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</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">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;active&#39;</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></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- Query active products + JSONB filter
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></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">products</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">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;active&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;}&#39;</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="c1">-- → planner 用 partial GIN index</span></span></span></code></pre></div><p>Partial index 比 full GIN 小很多、write cost 低、index hit rate 高。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-大-jsonb--toast--性能崩潰">1. 大 JSONB + TOAST — 性能崩潰</h3>
<p>JSONB &gt; 2 KB 自動進 TOAST（PG 內外部 storage）、每次 query read 該 row 都要 <em>de-TOAST</em>（拉外部 storage 再合併）。大 JSONB（&gt; 50 KB）每次 query 慢 10-100x。</p>
<p>修法：</p>
<ul>
<li>把 <em>大 attribute 拆獨立 column</em>（如 <code>description TEXT</code> 不放 metadata）</li>
<li>用 <em>JSON path index</em> 對 hot path 加速、不必每次讀整個 JSONB</li>
<li>用 <code>pg_column_size(metadata)</code> 監控 JSONB size 分布、找 outlier</li>
<li>對 truly 大 document（&gt; 1 MB）考慮 separate table 或 object storage</li>
</ul>
<h3 id="2-nested-update--整個-jsonb-重寫">2. Nested update — 整個 JSONB 重寫</h3>
<p>PG 沒 <em>atomic partial update</em>。修改 nested key 必須讀整個 JSONB → 修改 → 寫回：</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">products</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">metadata</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">jsonb_set</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;{discount}&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;0.2&#39;</span><span class="p">::</span><span class="n">jsonb</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">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">100</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="c1">-- 等同於：讀 metadata、改 discount、寫回整個 metadata</span></span></span></code></pre></div><p>對 <em>大 JSONB + 高頻 update</em> 場景、寫吞吐受限。跟 MongoDB <code>$set</code> operator 對應 <em>partial document update</em> 不同。</p>
<p>修法：</p>
<ul>
<li>對 <em>high-update nested key</em> 拆獨立 column</li>
<li>Application 層 batch update（攢一批一次 update）</li>
<li>接受 PG JSONB <em>是 immutable-replace</em> 心智模型、不是 <em>mutable in-place</em></li>
</ul>
<h3 id="3-index-選錯-op-class---query-走-full-scan">3. Index 選錯 op class — <code>?</code> query 走 full scan</h3>
<p>對 <code>jsonb_path_ops</code> index、<code>?</code> key existence query 走 <em>full scan</em>（不用 index）。Application 看 query 慢、查 EXPLAIN 才發現 index 沒用。</p>
<p>修法：</p>
<ul>
<li>設計階段確認 <em>application query pattern</em>：只用 <code>@&gt;</code> 還是會用 <code>?</code></li>
<li>多 query pattern → <code>jsonb_ops</code>（預設）</li>
<li>純 containment → <code>jsonb_path_ops</code>（省 index size）</li>
<li>不確定先用預設、production 觀察後再優化</li>
</ul>
<h3 id="4-jsonb_path_query-跟-jsonb_path_exists-行為差">4. <code>jsonb_path_query</code> 跟 <code>jsonb_path_exists</code> 行為差</h3>
<ul>
<li><code>jsonb_path_query(metadata, '$.variants[*].price')</code> — 展開、每個 match return 一 row</li>
<li><code>jsonb_path_exists(metadata, '$.variants[*]')</code> — return boolean（true if any match）</li>
</ul>
<p>Application 想要「過濾 row」用前者寫成：</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">-- 錯：返多 row 給每個 product、結果 row count 暴增
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">jsonb_path_query</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*].price&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</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">-- 對：只過濾 product
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">jsonb_path_exists</span><span class="p">(</span><span class="n">metadata</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;$.variants[*] ? (@.price &gt; 100)&#39;</span><span class="p">);</span></span></span></code></pre></div><p>修法：</p>
<ul>
<li>區分 <em>exists 過濾 row</em> vs <em>query 展開 row</em></li>
<li>過濾用 <code>jsonb_path_exists</code> 或 <code>@&gt;</code> operator</li>
<li>展開用 <code>jsonb_path_query</code> + 配合 <code>LATERAL</code> 或 subquery</li>
</ul>
<h3 id="5-partial-index-條件不對齊-query">5. Partial index 條件不對齊 query</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="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_active_metadata</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIN</span><span class="w"> </span><span class="p">(</span><span class="n">metadata</span><span class="p">)</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;active&#39;</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></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- Application query 但 status 沒 explicit
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></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">products</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;{&#34;category&#34;: &#34;shoes&#34;}&#39;</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="c1">-- → 不用 partial index（planner 不知道 status=&#39;active&#39; 條件）</span></span></span></code></pre></div><p>修法：</p>
<ul>
<li>
<p>Application query <em>必須包含 partial index 的 WHERE 條件</em>：</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 class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">products</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;active&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">metadata</span><span class="w"> </span><span class="o">@&gt;</span><span class="w"> </span><span class="s1">&#39;...&#39;</span><span class="p">;</span></span></span></code></pre></div></li>
<li>
<p>確認 planner 用 partial index：<code>EXPLAIN</code> 看 <code>Index Scan using idx_active_metadata</code></p>
</li>
<li>
<p>不對齊 query pattern 的 partial index = waste</p>
</li>
</ul>
<h2 id="何時用-jsonb-vs-拆-column">何時用 JSONB vs 拆 column</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>選擇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不規則 schema（user-generated metadata / customization）</td>
          <td>JSONB</td>
      </tr>
      <tr>
          <td>半結構化 + 5-10 個常 query key</td>
          <td>JSONB + GIN partial index</td>
      </tr>
      <tr>
          <td>規則 schema、column 數量穩定</td>
          <td>拆 column（更快 / index 易）</td>
      </tr>
      <tr>
          <td>Nested 結構 + 經常需要展開 query</td>
          <td>JSONB + jsonb_path_query</td>
      </tr>
      <tr>
          <td>大 document（&gt; 1 KB）+ 高頻 update</td>
          <td>拆 column 或 separate table</td>
      </tr>
      <tr>
          <td>完全 schemaless workload</td>
          <td>考慮 MongoDB 而非 PG</td>
      </tr>
  </tbody>
</table>
<p>JSONB 是 <em>PG 適合 semi-structured data</em> 的工具、不是 <em>MongoDB 替代品</em>。對 <em>主要結構化 + 少量 JSON</em> 場景 JSONB 完美；對 <em>主要 JSON / 複雜 nested aggregation</em> 場景 MongoDB 仍是專業選擇。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-query-optimization">跟 Query Optimization</h3>
<p>JSONB query 的 planner 行為：</p>
<ul>
<li><code>@&gt;</code> containment 對 jsonb_ops / jsonb_path_ops 都用 GIN</li>
<li><code>?</code> 只對 jsonb_ops 用 GIN</li>
<li>jsonb_path_exists 用 <em>functional index</em>（不是 GIN）</li>
<li>看 EXPLAIN 確認用對 index、詳見 <a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">Query Optimization</a></li>
</ul>
<h3 id="跟-sql-features-baseline">跟 SQL Features Baseline</h3>
<p>JSONB 是 PG 結構性領先特性之一、詳見 <a href="/blog/backend/01-database/vendors/postgresql/sql-features-baseline/" data-link-title="PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的" data-link-desc="PG 在 SQL features 上長期領先 MySQL — CTE / window function / lateral / partial index / FTS / JSONB / GIN index / materialized view 在 PG 早 5-15 年。MySQL 8.0（2018）補多數但 *index / storage / extension* 層仍是 PG 結構優勢。本文整理 PG 早期就有的特性、MySQL 8.0 補的差異、PG 仍領先的、跟 MySQL modern-sql-features sibling 反向視角">SQL Features Baseline</a>。</p>
<h3 id="跟-mvcc--lock-model">跟 MVCC + Lock Model</h3>
<p>JSONB UPDATE 整個 column 重寫、每次 update 創新 tuple、跟 row update 相同 MVCC behavior。詳見 <a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">MVCC + Lock Model</a>。</p>
<h3 id="跟-mysql-json_table">跟 MySQL JSON_TABLE</h3>
<p>MySQL 8.0 JSON_TABLE 跟 PG jsonpath 類似（都 SQL standard）、但 <em>index 機制</em> 完全不同：</p>
<ul>
<li>PG：JSONB + GIN index over 整個 column</li>
<li>MySQL：JSON column + generated column + index over generated</li>
</ul>
<p>PG JSONB GIN 是 <em>結構性領先</em>、MySQL 短期內難對應。詳見 <a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a>。</p>
<h2 id="觀測-metric">觀測 metric</h2>
<ul>
<li><code>pg_column_size(metadata)</code> — 每 row JSONB size 分布</li>
<li><code>pg_relation_size('idx_name')</code> — JSONB GIN index 大小</li>
<li><code>pg_stat_user_indexes.idx_scan</code> — JSONB index 使用次數</li>
<li>TOAST table size：<code>SELECT pg_relation_size(reltoastrelid) FROM pg_class WHERE relname='products'</code></li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><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 overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/sql-features-baseline/" data-link-title="PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的" data-link-desc="PG 在 SQL features 上長期領先 MySQL — CTE / window function / lateral / partial index / FTS / JSONB / GIN index / materialized view 在 PG 早 5-15 年。MySQL 8.0（2018）補多數但 *index / storage / extension* 層仍是 PG 結構優勢。本文整理 PG 早期就有的特性、MySQL 8.0 補的差異、PG 仍領先的、跟 MySQL modern-sql-features sibling 反向視角">PG SQL Features Baseline</a>（JSONB 是 PG 結構領先之一）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PG Query Optimization</a>（JSONB index 用對）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PG MVCC + Lock Model</a>（JSONB update 跟 MVCC）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/modern-sql-features/" data-link-title="MySQL 8.0 Modern SQL：CTE / window function / JSON_TABLE 不是「終於跟上 PG」、是進入 SQL 工程深度的入場券" data-link-desc="MySQL 8.0 在 SQL 特性上 *終於補齊* CTE、window function、lateral derived table、JSON_TABLE、hash join 等現代 SQL 特性。本文走 5 個關鍵特性、各自實際 production 場景、跟 PostgreSQL 對應特性的行為差異（特別是 JSON_TABLE vs PG JSONB / jsonb_path_query）、配置 / migration 注意事項、5 production 踩雷（CTE 不 materialize / window function 大量 sort spill / JSON_TABLE 跟 generated column 取捨 / hash join 預設沒開 / recursive CTE 深度上限）">MySQL Modern SQL Features</a>（JSON_TABLE vs JSONB 對比）</li>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a>（純 document workload 替代）</li>
<li>官方：<a href="https://www.postgresql.org/docs/current/functions-json.html">PG JSON Functions</a> / <a href="https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING">JSONB Indexing</a></li>
</ul>
]]></content:encoded></item></channel></rss>