<?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>Baseline on Tarragon</title><link>https://tarrragon.github.io/blog/tags/baseline/</link><description>Recent content in Baseline on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/baseline/index.xml" rel="self" type="application/rss+xml"/><item><title>SQLite Backend 效能基準</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/sqlite-performance-baseline/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/sqlite-performance-baseline/</guid><description>&lt;p>SQLite Backend 的效能受三個因素影響：儲存裝置（SSD vs HDD vs SD card）、Go driver 選擇（modernc.org/sqlite pure Go vs mattn/go-sqlite3 CGO）、並發模型（WAL mode + single-writer）。本章根據 SQLite 的技術特性和業界基準推導預期效能範圍，並提供實測方法讓使用者在自己的環境驗證。所有數字是預期範圍而非實測值 — 實際效能依硬體和 workload 而定。&lt;/p>
&lt;h2 id="寫入吞吐">寫入吞吐&lt;/h2>
&lt;p>寫入吞吐決定 collector 每秒能消化多少事件。SQLite 的寫入效能主要受 fsync 頻率和 WAL checkpoint 影響。&lt;/p>
&lt;h3 id="單筆-insert">單筆 INSERT&lt;/h3>
&lt;p>每筆 INSERT 獨立一個 transaction 時，每次 commit 都會 fsync。WAL mode 的 fsync 成本比 journal mode 低（append-only），但仍是寫入的主要瓶頸。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>儲存裝置&lt;/th>
 &lt;th>單筆 INSERT 延遲&lt;/th>
 &lt;th>理論上限&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>NVMe SSD&lt;/td>
 &lt;td>10-30 μs&lt;/td>
 &lt;td>30,000-100,000 inserts/sec&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SATA SSD&lt;/td>
 &lt;td>30-50 μs&lt;/td>
 &lt;td>20,000-30,000 inserts/sec&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HDD&lt;/td>
 &lt;td>50-200 μs&lt;/td>
 &lt;td>5,000-20,000 inserts/sec&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SD card&lt;/td>
 &lt;td>500-2000 μs&lt;/td>
 &lt;td>500-2,000 inserts/sec&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>modernc.org/sqlite（pure Go）的效能約為 CGO driver（mattn/go-sqlite3）的 60-80%。上表數字基於 CGO driver，pure Go 需打八折。Go HTTP handler 的開銷（JSON 解碼、schema 驗證、goroutine 調度）再扣 10-20%。&lt;/p>
&lt;h3 id="批次-insert">批次 INSERT&lt;/h3>
&lt;p>一個 transaction 包裹多筆 INSERT，只做一次 fsync。Collector 接收 SDK 的 flush batch（一個 HTTP request 帶一批事件）天然適合批次寫入。&lt;/p>
&lt;p>吞吐提升幅度和批次大小的關係：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>批次大小&lt;/th>
 &lt;th>相對單筆的吞吐提升&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>10 筆/tx&lt;/td>
 &lt;td>3-5x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>100 筆/tx&lt;/td>
 &lt;td>5-10x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1000 筆/tx&lt;/td>
 &lt;td>8-15x&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>提升來自 fsync 次數從「每筆一次」降到「每批一次」。超過 100 筆/tx 後邊際收益遞減。&lt;/p>
&lt;h3 id="實際預期">實際預期&lt;/h3>
&lt;p>結合 pure Go driver、HTTP handler 開銷和批次寫入，不同環境下的預期吞吐：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>環境&lt;/th>
 &lt;th>單筆&lt;/th>
 &lt;th>批次（100/tx）&lt;/th>
 &lt;th>適合場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Mac M1/M2 NVMe + pure Go&lt;/td>
 &lt;td>~5,000/sec&lt;/td>
 &lt;td>~30,000/sec&lt;/td>
 &lt;td>開發機&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Linux VPS SATA SSD&lt;/td>
 &lt;td>~3,000/sec&lt;/td>
 &lt;td>~20,000/sec&lt;/td>
 &lt;td>小型部署&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Raspberry Pi 4 SD card&lt;/td>
 &lt;td>~200/sec&lt;/td>
 &lt;td>~1,000/sec&lt;/td>
 &lt;td>邊緣設備&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="和事件產生速率的對照">和事件產生速率的對照&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>預估 events/sec&lt;/th>
 &lt;th>SQLite 批次能撐嗎&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>自用 1 個 app&lt;/td>
 &lt;td>&amp;lt; 10&lt;/td>
 &lt;td>遠超需求&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>小團隊 5 人各跑 1 個 app&lt;/td>
 &lt;td>&amp;lt; 50&lt;/td>
 &lt;td>綽綽有餘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10 SDK 同時 flush&lt;/td>
 &lt;td>100-1000 burst&lt;/td>
 &lt;td>批次 INSERT 撐得住&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>100+ 使用者持續活躍&lt;/td>
 &lt;td>500+ 持續&lt;/td>
 &lt;td>邊界 — 觀察 database is locked&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>burst 和持續的差異在於：burst 是短暫的高峰（flush batch 到達後數秒內消化完），持續是長時間的穩定高流量。SQLite 的 WAL mode 對 burst 容忍度高（write lock 等待時間短），對持續高流量容忍度有限（write lock 等待累積）。&lt;/p></description><content:encoded><![CDATA[<p>SQLite Backend 的效能受三個因素影響：儲存裝置（SSD vs HDD vs SD card）、Go driver 選擇（modernc.org/sqlite pure Go vs mattn/go-sqlite3 CGO）、並發模型（WAL mode + single-writer）。本章根據 SQLite 的技術特性和業界基準推導預期效能範圍，並提供實測方法讓使用者在自己的環境驗證。所有數字是預期範圍而非實測值 — 實際效能依硬體和 workload 而定。</p>
<h2 id="寫入吞吐">寫入吞吐</h2>
<p>寫入吞吐決定 collector 每秒能消化多少事件。SQLite 的寫入效能主要受 fsync 頻率和 WAL checkpoint 影響。</p>
<h3 id="單筆-insert">單筆 INSERT</h3>
<p>每筆 INSERT 獨立一個 transaction 時，每次 commit 都會 fsync。WAL mode 的 fsync 成本比 journal mode 低（append-only），但仍是寫入的主要瓶頸。</p>
<table>
  <thead>
      <tr>
          <th>儲存裝置</th>
          <th>單筆 INSERT 延遲</th>
          <th>理論上限</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>NVMe SSD</td>
          <td>10-30 μs</td>
          <td>30,000-100,000 inserts/sec</td>
      </tr>
      <tr>
          <td>SATA SSD</td>
          <td>30-50 μs</td>
          <td>20,000-30,000 inserts/sec</td>
      </tr>
      <tr>
          <td>HDD</td>
          <td>50-200 μs</td>
          <td>5,000-20,000 inserts/sec</td>
      </tr>
      <tr>
          <td>SD card</td>
          <td>500-2000 μs</td>
          <td>500-2,000 inserts/sec</td>
      </tr>
  </tbody>
</table>
<p>modernc.org/sqlite（pure Go）的效能約為 CGO driver（mattn/go-sqlite3）的 60-80%。上表數字基於 CGO driver，pure Go 需打八折。Go HTTP handler 的開銷（JSON 解碼、schema 驗證、goroutine 調度）再扣 10-20%。</p>
<h3 id="批次-insert">批次 INSERT</h3>
<p>一個 transaction 包裹多筆 INSERT，只做一次 fsync。Collector 接收 SDK 的 flush batch（一個 HTTP request 帶一批事件）天然適合批次寫入。</p>
<p>吞吐提升幅度和批次大小的關係：</p>
<table>
  <thead>
      <tr>
          <th>批次大小</th>
          <th>相對單筆的吞吐提升</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 筆/tx</td>
          <td>3-5x</td>
      </tr>
      <tr>
          <td>100 筆/tx</td>
          <td>5-10x</td>
      </tr>
      <tr>
          <td>1000 筆/tx</td>
          <td>8-15x</td>
      </tr>
  </tbody>
</table>
<p>提升來自 fsync 次數從「每筆一次」降到「每批一次」。超過 100 筆/tx 後邊際收益遞減。</p>
<h3 id="實際預期">實際預期</h3>
<p>結合 pure Go driver、HTTP handler 開銷和批次寫入，不同環境下的預期吞吐：</p>
<table>
  <thead>
      <tr>
          <th>環境</th>
          <th>單筆</th>
          <th>批次（100/tx）</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mac M1/M2 NVMe + pure Go</td>
          <td>~5,000/sec</td>
          <td>~30,000/sec</td>
          <td>開發機</td>
      </tr>
      <tr>
          <td>Linux VPS SATA SSD</td>
          <td>~3,000/sec</td>
          <td>~20,000/sec</td>
          <td>小型部署</td>
      </tr>
      <tr>
          <td>Raspberry Pi 4 SD card</td>
          <td>~200/sec</td>
          <td>~1,000/sec</td>
          <td>邊緣設備</td>
      </tr>
  </tbody>
</table>
<h3 id="和事件產生速率的對照">和事件產生速率的對照</h3>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>預估 events/sec</th>
          <th>SQLite 批次能撐嗎</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自用 1 個 app</td>
          <td>&lt; 10</td>
          <td>遠超需求</td>
      </tr>
      <tr>
          <td>小團隊 5 人各跑 1 個 app</td>
          <td>&lt; 50</td>
          <td>綽綽有餘</td>
      </tr>
      <tr>
          <td>10 SDK 同時 flush</td>
          <td>100-1000 burst</td>
          <td>批次 INSERT 撐得住</td>
      </tr>
      <tr>
          <td>100+ 使用者持續活躍</td>
          <td>500+ 持續</td>
          <td>邊界 — 觀察 database is locked</td>
      </tr>
  </tbody>
</table>
<p>burst 和持續的差異在於：burst 是短暫的高峰（flush batch 到達後數秒內消化完），持續是長時間的穩定高流量。SQLite 的 WAL mode 對 burst 容忍度高（write lock 等待時間短），對持續高流量容忍度有限（write lock 等待累積）。</p>
<h2 id="查詢延遲">查詢延遲</h2>
<p>查詢延遲決定 dashboard 的刷新體驗。SQLite 的查詢效能取決於索引覆蓋和掃描行數。</p>
<h3 id="有索引的查詢">有索引的查詢</h3>
<p>建議的索引（見 <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a> 的建議索引段）覆蓋 dashboard 的核心查詢模式。有索引時的預期延遲：</p>
<table>
  <thead>
      <tr>
          <th>查詢模式</th>
          <th>10 萬筆</th>
          <th>50 萬筆</th>
          <th>100 萬筆</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>等值查詢（WHERE session_id = ?）</td>
          <td>&lt; 1ms</td>
          <td>&lt; 1ms</td>
          <td>&lt; 1ms</td>
      </tr>
      <tr>
          <td>範圍查詢（WHERE ts BETWEEN ? AND ?）</td>
          <td>&lt; 10ms</td>
          <td>10-50ms</td>
          <td>50-100ms</td>
      </tr>
      <tr>
          <td>GROUP BY name</td>
          <td>10-50ms</td>
          <td>50-200ms</td>
          <td>200-500ms</td>
      </tr>
      <tr>
          <td>COUNT DISTINCT session_id</td>
          <td>50-100ms</td>
          <td>200-500ms</td>
          <td>500ms-1s</td>
      </tr>
      <tr>
          <td>JOIN + window function</td>
          <td>100ms-1s</td>
          <td>1-3s</td>
          <td>3-10s</td>
      </tr>
  </tbody>
</table>
<h3 id="無索引的查詢">無索引的查詢</h3>
<p>無索引時 SQLite 做全表掃描。掃描速度約 50-100 MB/sec（SSD）、10-30 MB/sec（HDD）。</p>
<table>
  <thead>
      <tr>
          <th>資料量</th>
          <th>預估大小</th>
          <th>SSD 全掃延遲</th>
          <th>HDD 全掃延遲</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬筆</td>
          <td>~40 MB</td>
          <td>200-500ms</td>
          <td>1-3s</td>
      </tr>
      <tr>
          <td>100 萬筆</td>
          <td>~400 MB</td>
          <td>2-5s</td>
          <td>10-30s</td>
      </tr>
      <tr>
          <td>300 萬筆</td>
          <td>~1.2 GB</td>
          <td>5-15s</td>
          <td>30-90s</td>
      </tr>
  </tbody>
</table>
<p>超過 100 萬筆無索引查詢會超出 dashboard 可接受的刷新延遲 — 這是 day-one 就建索引的理由。</p>
<h3 id="dashboard-刷新頻率-vs-查詢延遲">Dashboard 刷新頻率 vs 查詢延遲</h3>
<p>Dashboard 的每個視圖有不同的刷新間隔和可接受延遲。查詢延遲超過可接受值時，dashboard 體驗變差（等待轉圈、資料過時）。</p>
<table>
  <thead>
      <tr>
          <th>Dashboard 視圖</th>
          <th>刷新間隔</th>
          <th>可接受延遲</th>
          <th>10 萬筆有索引</th>
          <th>100 萬筆有索引</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即時狀態卡</td>
          <td>1-5 秒</td>
          <td>&lt; 100ms</td>
          <td>滿足</td>
          <td>滿足</td>
      </tr>
      <tr>
          <td>Error 列表</td>
          <td>5-10 秒</td>
          <td>&lt; 500ms</td>
          <td>滿足</td>
          <td>滿足</td>
      </tr>
      <tr>
          <td>趨勢圖（最近 24h）</td>
          <td>30 秒</td>
          <td>&lt; 1s</td>
          <td>滿足</td>
          <td>邊界</td>
      </tr>
      <tr>
          <td>長期聚合（最近 30 天）</td>
          <td>5 分鐘</td>
          <td>&lt; 3s</td>
          <td>滿足</td>
          <td>需要預聚合</td>
      </tr>
  </tbody>
</table>
<p>「需要預聚合」代表原始事件的聚合查詢超過可接受延遲，應該依賴分層保留策略中的 hourly_summary / daily_summary 表（見 <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a> 的分層保留段）。</p>
<h2 id="資源消耗">資源消耗</h2>
<h3 id="記憶體">記憶體</h3>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>佔用</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go HTTP server</td>
          <td>20-50 MB</td>
          <td>基礎開銷</td>
      </tr>
      <tr>
          <td>SQLite page cache</td>
          <td>2 MB（預設）</td>
          <td><code>PRAGMA cache_size</code> 可調</td>
      </tr>
      <tr>
          <td>寫入 buffer（channel）</td>
          <td>1-10 MB</td>
          <td>取決於 channel 容量和事件大小</td>
      </tr>
      <tr>
          <td>查詢結果暫存</td>
          <td>和結果集成正比</td>
          <td>GROUP BY 10 萬筆 ~10 MB</td>
      </tr>
      <tr>
          <td><strong>Collector 整體</strong></td>
          <td><strong>50-100 MB</strong></td>
          <td>自用場景</td>
      </tr>
  </tbody>
</table>
<p>Raspberry Pi（1 GB RAM）上建議把 page cache 調小（<code>PRAGMA cache_size = -512</code> = 512 KB），避免大結果集查詢（加 LIMIT），dashboard 刷新頻率降低。</p>
<h3 id="cpu">CPU</h3>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>CPU 使用</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>INSERT（寫入）</td>
          <td>可忽略</td>
          <td>I/O bound，CPU 不是瓶頸</td>
      </tr>
      <tr>
          <td>SELECT（查詢）</td>
          <td>和掃描行數正比</td>
          <td>有索引時可忽略</td>
      </tr>
      <tr>
          <td>Downsample（每小時）</td>
          <td>短暫 spike &lt; 1s</td>
          <td>處理最近一小時的事件</td>
      </tr>
      <tr>
          <td>Purge（每天）</td>
          <td>短暫 spike 1-3s</td>
          <td>分批 DELETE</td>
      </tr>
      <tr>
          <td><strong>整體</strong></td>
          <td><strong>&lt; 5%</strong></td>
          <td>自用場景</td>
      </tr>
  </tbody>
</table>
<h3 id="磁碟">磁碟</h3>
<table>
  <thead>
      <tr>
          <th>日事件量</th>
          <th>原始資料/天</th>
          <th>原始資料/月</th>
          <th>含索引/月</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1,000（極低）</td>
          <td>0.3-0.5 MB</td>
          <td>9-15 MB</td>
          <td>11-18 MB</td>
      </tr>
      <tr>
          <td>10,000（自用）</td>
          <td>3-5 MB</td>
          <td>90-150 MB</td>
          <td>110-180 MB</td>
      </tr>
      <tr>
          <td>100,000（小團隊）</td>
          <td>30-50 MB</td>
          <td>0.9-1.5 GB</td>
          <td>1.1-1.8 GB</td>
      </tr>
  </tbody>
</table>
<p>WAL 檔案通常 &lt; 10 MB（auto-checkpoint 在 WAL 達到 1000 pages 時觸發）。分層保留策略下，原始事件只保留 7 天，長期佔用由聚合摘要表決定（遠小於原始事件）。</p>
<h2 id="邊緣設備場景">邊緣設備場景</h2>
<p>Raspberry Pi、低配 VPS（1 核 / 1 GB RAM）、甚至 NAS 上跑 collector 時的特殊考量：</p>
<p><strong>SD card 的隨機寫入</strong>：SD card 的隨機寫入 IOPS 極低（100-500 IOPS），WAL mode 的 checkpoint（把 WAL 內容合併回主資料庫檔案）可能卡住 1-5 秒。期間新的寫入等待 checkpoint 完成。建議調高 <code>wal_autocheckpoint</code> 的閾值（如 5000 pages），讓 checkpoint 頻率降低但每次時間更長 — 在非活躍時段（凌晨）手動觸發 <code>PRAGMA wal_checkpoint(TRUNCATE)</code>。</p>
<p><strong>1 GB RAM</strong>：cache_size 調小（512 KB）、避免 <code>SELECT *</code> 不帶 LIMIT、GROUP BY 的結果集用 HAVING 條件過濾減少暫存。Dashboard 的長期聚合直接查 hourly_summary 表而非原始事件。</p>
<p><strong>ARM CPU</strong>：pure Go SQLite driver（modernc.org/sqlite）在 ARM 上的效能差距可能比 x86 更大（pure Go 的 C-to-Go 翻譯在 ARM 的指令最佳化較少）。實測確認。</p>
<p><strong>建議配置</strong>：邊緣設備上 collector 的 dashboard 刷新頻率從預設值降低（即時狀態卡 5 秒 → 30 秒，趨勢圖 30 秒 → 5 分鐘），降採樣 job 頻率從每小時改為每 6 小時。</p>
<h2 id="實測方法指引">實測方法指引</h2>
<p>教學的預期數字是推導值，實際效能取決於使用者的硬體和 workload。Collector 提供內建的 benchmark 命令讓使用者在自己的環境實測。</p>
<h3 id="寫入-benchmark">寫入 benchmark</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 單筆寫入：10000 筆，每筆獨立 transaction</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">./collector benchmark write --events<span class="o">=</span><span class="m">10000</span> --batch<span class="o">=</span><span class="m">1</span> --storage<span class="o">=</span>sqlite
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 批次寫入：10000 筆，每 100 筆一個 transaction</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">./collector benchmark write --events<span class="o">=</span><span class="m">10000</span> --batch<span class="o">=</span><span class="m">100</span> --storage<span class="o">=</span>sqlite</span></span></code></pre></div><p>輸出：total duration、events/sec、p50/p95/p99 latency per event。</p>
<h3 id="查詢-benchmark">查詢 benchmark</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><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">./collector benchmark seed --events<span class="o">=</span><span class="m">100000</span> --storage<span class="o">=</span>sqlite
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 跑查詢 benchmark</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">./collector benchmark query --type<span class="o">=</span>error --group-by<span class="o">=</span>name --storage<span class="o">=</span>sqlite
</span></span><span class="line"><span class="ln">6</span><span class="cl">./collector benchmark query --session-id<span class="o">=</span>random --storage<span class="o">=</span>sqlite</span></span></code></pre></div><p>輸出：query duration、rows scanned、rows returned。</p>
<h3 id="production-觀察指標">Production 觀察指標</h3>
<p>部署後用 DevOps dashboard（見 <a href="/blog/monitoring/04-collector/dashboard-devops/" data-link-title="DevOps Dashboard 設計" data-link-desc="Collector 和 SDK 是否健康 — 日常監控的服務狀態卡、吞吐量曲線、儲存用量，以及告警觸發後的排障視圖">DevOps Dashboard 設計</a>）觀察 collector 自身的效能 metric：</p>
<ul>
<li><code>collector.storage.write_duration_ms</code>：每次寫入的延遲。P95 超過 100ms 是瓶頸訊號。</li>
<li><code>collector.storage.query_duration_ms</code>：每次查詢的延遲。P95 超過 dashboard 刷新間隔是瓶頸訊號。</li>
<li><code>collector.storage.db_size_bytes</code>：資料庫大小。接近磁碟可用空間的 80% 時觸發 purge 或擴容。</li>
<li><code>collector.storage.wal_size_bytes</code>：WAL 檔案大小。持續 &gt; 50 MB 代表 checkpoint 跟不上寫入速度。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>切換到 PostgreSQL 的觸發條件 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>SQLite 和 PostgreSQL 的功能分層 → <a href="/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇</a></li>
<li>Ingestion 端的擴展設計 → <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL SQL Features：PG 早就有的、MySQL 8.0 才補的、PG 仍領先的</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/sql-features-baseline/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/sql-features-baseline/</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>SQL features baseline&lt;/em> — PG 早期就有的、MySQL 8.0 才補的、PG 仍領先的、給從 MySQL 評估 PG 的讀者 reference。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="pg-sql-工程深度的歷史錨點">PG SQL 工程深度的歷史錨點&lt;/h2>
&lt;p>PG 在 SQL feature 上長期領先 MySQL：&lt;/p>
&lt;ul>
&lt;li>2009 (PG 8.4)：CTE / window function / recursive query&lt;/li>
&lt;li>2013 (PG 9.3)：lateral derived table / materialized view&lt;/li>
&lt;li>2014 (PG 9.4)：JSONB / partial index 早就有 / GIN index&lt;/li>
&lt;li>2015 (PG 9.5)：UPSERT (&lt;code>ON CONFLICT&lt;/code>)&lt;/li>
&lt;li>2017 (PG 10)：declarative partitioning / logical replication / multi-column statistics&lt;/li>
&lt;/ul>
&lt;p>MySQL 8.0（2018）才補 CTE / window / lateral / JSON_TABLE / hash join — &lt;em>PG 早 9 年起步&lt;/em>。&lt;/p>
&lt;p>對 &lt;em>從 MySQL 評估 PG&lt;/em> 的讀者來說、PG 的 SQL 工程深度不只是「該有的都有」、更多是「PG 結構性領先的特性 + MySQL 8.0 補了哪些 + PG 仍領先哪些」。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/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&lt;/a> 對比視角：&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>SQL features baseline</em> — PG 早期就有的、MySQL 8.0 才補的、PG 仍領先的、給從 MySQL 評估 PG 的讀者 reference。</p></blockquote>
<hr>
<h2 id="pg-sql-工程深度的歷史錨點">PG SQL 工程深度的歷史錨點</h2>
<p>PG 在 SQL feature 上長期領先 MySQL：</p>
<ul>
<li>2009 (PG 8.4)：CTE / window function / recursive query</li>
<li>2013 (PG 9.3)：lateral derived table / materialized view</li>
<li>2014 (PG 9.4)：JSONB / partial index 早就有 / GIN index</li>
<li>2015 (PG 9.5)：UPSERT (<code>ON CONFLICT</code>)</li>
<li>2017 (PG 10)：declarative partitioning / logical replication / multi-column statistics</li>
</ul>
<p>MySQL 8.0（2018）才補 CTE / window / lateral / JSON_TABLE / hash join — <em>PG 早 9 年起步</em>。</p>
<p>對 <em>從 MySQL 評估 PG</em> 的讀者來說、PG 的 SQL 工程深度不只是「該有的都有」、更多是「PG 結構性領先的特性 + MySQL 8.0 補了哪些 + PG 仍領先哪些」。</p>
<p>跟 <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>
<ul>
<li>MySQL 8.0 視角：「我終於補齊 + 跟 PG 對比」</li>
<li>PG 視角：「我長期領先 + MySQL 8.0 才追上某些、其他我仍領先」</li>
</ul>
<h2 id="pg-結構性領先特性mysql-沒對應--弱對應">PG 結構性領先特性（MySQL 沒對應 / 弱對應）</h2>
<h3 id="1-materialized-view">1. Materialized View</h3>
<p>PG 9.3+ 內建 materialized view：</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="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">orders_summary</span><span class="w"> </span><span class="k">AS</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">SELECT</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><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">order_count</span><span class="p">,</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">total</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">orders</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">user_id</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">-- 手動 refresh
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">REFRESH</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">orders_summary</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="c1">-- 或 concurrent refresh（PG 9.4+、不 lock read）
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="n">REFRESH</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">CONCURRENTLY</span><span class="w"> </span><span class="n">orders_summary</span><span class="p">;</span></span></span></code></pre></div><p>用途：</p>
<ul>
<li>預計算複雜 aggregation、查詢時極快</li>
<li>Concurrent refresh 不 lock read</li>
<li>可建 index on materialized view</li>
</ul>
<p><strong>MySQL 對應</strong>：沒原生 materialized view。常見替代：</p>
<ul>
<li>Trigger + summary table（手動維護）</li>
<li>Application 層 caching layer</li>
<li>用 view + cache layer（不是 materialization）</li>
</ul>
<p>MySQL 8.0+ 仍無原生 materialized view。</p>
<h3 id="2-partial-index">2. Partial Index</h3>
<p>PG 預設支援 partial index — 對 <em>滿足條件的 row</em> 才建 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 user 建 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_users_active_email</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="p">(</span><span class="n">email</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">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">-- Index size 比 full index 小很多、query 性能跟 full index 一樣
</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">users</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">email</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;x@y.com&#39;</span><span class="p">;</span></span></span></code></pre></div><p>用途：</p>
<ul>
<li><em>Soft-delete</em> 場景：對 <code>deleted_at IS NULL</code> 建 partial index</li>
<li><em>Hot subset</em> 場景：對 <code>status = 'pending'</code> 等熱資料建 partial</li>
<li>Index 大小 / 寫入成本大降</li>
</ul>
<p><strong>MySQL 對應</strong>：MySQL 沒原生 partial index。MySQL 8.0+ 有 <em>functional index</em> 但跟 partial 不同。MySQL 替代：</p>
<ul>
<li>Generated column + index（接近、但維護複雜）</li>
<li>或接受 full index cost</li>
</ul>
<h3 id="3-foreign-data-wrapper-fdw">3. Foreign Data Wrapper (FDW)</h3>
<p>PG FDW 讓 query 跨外部資料源：</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="n">EXTENSION</span><span class="w"> </span><span class="n">postgres_fdw</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="k">CREATE</span><span class="w"> </span><span class="n">SERVER</span><span class="w"> </span><span class="n">remote_db</span><span class="w"> </span><span class="k">FOREIGN</span><span class="w"> </span><span class="k">DATA</span><span class="w"> </span><span class="n">WRAPPER</span><span class="w"> </span><span class="n">postgres_fdw</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">OPTIONS</span><span class="w"> </span><span class="p">(</span><span class="k">host</span><span class="w"> </span><span class="s1">&#39;remote.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">dbname</span><span class="w"> </span><span class="s1">&#39;analytics&#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="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="n">MAPPING</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="n">localuser</span><span class="w"> </span><span class="n">SERVER</span><span class="w"> </span><span class="n">remote_db</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">OPTIONS</span><span class="w"> </span><span class="p">(</span><span class="k">user</span><span class="w"> </span><span class="s1">&#39;remoteuser&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">password</span><span class="w"> </span><span class="s1">&#39;...&#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="k">CREATE</span><span class="w"> </span><span class="k">FOREIGN</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">remote_orders</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="p">...)</span><span class="w"> </span><span class="n">SERVER</span><span class="w"> </span><span class="n">remote_db</span><span class="w"> </span><span class="k">OPTIONS</span><span class="w"> </span><span class="p">(</span><span class="k">table_name</span><span class="w"> </span><span class="s1">&#39;orders&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- 在 local PG query remote table
</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">remote_orders</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">100</span><span class="p">;</span></span></span></code></pre></div><p>支援 FDW：<code>postgres_fdw</code> / <code>mysql_fdw</code> / <code>oracle_fdw</code> / <code>mongo_fdw</code> / <code>file_fdw</code> / <code>redis_fdw</code> 等。</p>
<p><strong>MySQL 對應</strong>：MySQL 8.0+ 有 FEDERATED engine（受限、不推薦）。實務上 MySQL 跨 DB query 用 application 層處理。</p>
<h3 id="4-jsonb--gin-indexpg-結構性優勢">4. JSONB + GIN Index（PG 結構性優勢）</h3>
<p>PG JSONB 是 <em>binary 儲存</em> + 可 <em>直接 GIN index</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">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 over JSONB
</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 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">-- 快 query
</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="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">11</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="s1">&#39;$.variants[*].price &gt; 100&#39;</span><span class="p">;</span></span></span></code></pre></div><p><strong>MySQL 對應</strong>：MySQL 8.0 JSON_TABLE 是 SQL standard、但 <em>index 必須 generated column workaround</em>（不能 GIN index over JSON）。</p>
<p>詳見 <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 PG JSONB 對比段。</p>
<h3 id="5-range-types--exclusion-constraints">5. Range Types + Exclusion Constraints</h3>
<p>PG range types + exclusion constraints 防止 <em>時間範圍重疊</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">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">reservations</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">room_id</span><span class="w"> </span><span class="nb">INT</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="n">during</span><span class="w"> </span><span class="n">TSRANGE</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="n">EXCLUDE</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">GIST</span><span class="w"> </span><span class="p">(</span><span class="n">room_id</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="o">=</span><span class="p">,</span><span class="w"> </span><span class="n">during</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="o">&amp;&amp;</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="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- INSERT 重疊 booking 自動 reject
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">reservations</span><span class="w"> </span><span class="p">(</span><span class="n">room_id</span><span class="p">,</span><span class="w"> </span><span class="n">during</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;[2026-05-19 10:00, 2026-05-19 12:00)&#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">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">reservations</span><span class="w"> </span><span class="p">(</span><span class="n">room_id</span><span class="p">,</span><span class="w"> </span><span class="n">during</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;[2026-05-19 11:00, 2026-05-19 13:00)&#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 class="c1">-- ERROR: conflicting key value violates exclusion constraint</span></span></span></code></pre></div><p><strong>MySQL 對應</strong>：完全沒對應、必須 application 層 enforce。</p>
<h3 id="6-check-constraint--domain-type">6. CHECK Constraint + Domain Type</h3>
<p>PG <code>CHECK</code> constraint 真執行（MySQL 8.0 才補）+ user-defined <code>DOMAIN</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">CREATE</span><span class="w"> </span><span class="k">DOMAIN</span><span class="w"> </span><span class="n">positive_int</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nb">INT</span><span class="w"> </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">VALUE</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</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="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </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">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">4</span><span class="cl"><span class="w">    </span><span class="n">quantity</span><span class="w"> </span><span class="n">positive_int</span><span class="w"> </span><span class="k">NOT</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">5</span><span class="cl"><span class="w">    </span><span class="n">amount</span><span class="w"> </span><span class="nb">DECIMAL</span><span class="w"> </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">amount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">0</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="p">);</span></span></span></code></pre></div><p><strong>MySQL 對應</strong>：8.0+ 有 CHECK constraint enforcement（5.7 可寫但不執行）。沒 user-defined DOMAIN。</p>
<h3 id="7-extension-ecosystem">7. Extension Ecosystem</h3>
<p>PG extension 是 <em>結構優勢</em>：</p>
<ul>
<li><code>pg_partman</code>：自動 partition lifecycle</li>
<li><code>pg_repack</code>：online table rewrite</li>
<li><code>pg_stat_statements</code>：query stats</li>
<li><code>pgvector</code>：vector similarity search</li>
<li><code>pg_cron</code>：scheduled job</li>
<li><code>PostGIS</code>：GIS</li>
<li><code>TimescaleDB</code>：time-series</li>
<li><code>Citus</code>：sharding</li>
</ul>
<p><strong>MySQL 對應</strong>：MySQL plugin 機制有、生態遠遠不如。詳見 <em>PG Extension Ecosystem</em> 篇（待寫）。</p>
<h2 id="mysql-80-補齊的-pg-既有特性">MySQL 8.0 補齊的 PG 既有特性</h2>
<table>
  <thead>
      <tr>
          <th>特性</th>
          <th>PG 推出</th>
          <th>MySQL 推出</th>
          <th>差異後說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CTE</td>
          <td>8.4 (2009)</td>
          <td>8.0 (2018)</td>
          <td>MySQL 補語法、行為 PG 12+ 跟 MySQL 接近</td>
      </tr>
      <tr>
          <td>Window function</td>
          <td>8.4 (2009)</td>
          <td>8.0 (2018)</td>
          <td>兩家都標準、frame spec 細節有差</td>
      </tr>
      <tr>
          <td>Lateral derived table</td>
          <td>9.3 (2013)</td>
          <td>8.0.14 (2019)</td>
          <td>MySQL 後加、planner 不如 PG 成熟</td>
      </tr>
      <tr>
          <td>Hash join</td>
          <td>早就有</td>
          <td>8.0.18 (2019)</td>
          <td>MySQL 受限（equality on indexed column）</td>
      </tr>
      <tr>
          <td>JSON_TABLE</td>
          <td>17 (2024)</td>
          <td>8.0 (2018)</td>
          <td>MySQL 較早、PG 17+ 補進、PG 自己有 JSONB 路線</td>
      </tr>
      <tr>
          <td>CHECK constraint</td>
          <td>早就有</td>
          <td>8.0 (2018)</td>
          <td>MySQL 5.7 可寫但不執行</td>
      </tr>
      <tr>
          <td>Role-based auth</td>
          <td>早就有</td>
          <td>8.0 (2018)</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Atomic DDL</td>
          <td>早就有</td>
          <td>8.0 (2018)</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Common keyword</td>
          <td>完整</td>
          <td>8.0 補</td>
          <td>MySQL 5.7 缺很多 (window/rank/lateral 等)</td>
      </tr>
  </tbody>
</table>
<p>MySQL 8.0 是 <em>補齊 9 年 SQL standard 落後</em>、不是 <em>新領先 PG</em>。</p>
<h2 id="pg-仍領先的特性">PG 仍領先的特性</h2>
<p>對應「MySQL 8.0 補了 → PG 仍沒輸」的視角。以下 14 條中、<em>production 影響最大</em> 的是 Materialized view / Partial index / JSONB GIN / Full-text search 跟 Range / Exclusion constraints（schema-level expressiveness）；<em>次要但常用</em> 的是 Multi-column statistics 跟 Procedural language；<em>非典型但 niche 重要</em> 的是 User-defined DOMAIN / Generic table inheritance（讀者不必然知道、但 ORM 跟 schema migration 工具會用）：</p>
<table>
  <thead>
      <tr>
          <th>PG 領先特性</th>
          <th>MySQL 對應狀態</th>
          <th>補充</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Materialized view</td>
          <td>無原生</td>
          <td>application-side 重算成本高</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td>無（functional index 不等同）</td>
          <td>對 boolean / status column 救 storage</td>
      </tr>
      <tr>
          <td>FDW</td>
          <td>弱（FEDERATED engine 不推薦）</td>
          <td>跨 DB query escape hatch</td>
      </tr>
      <tr>
          <td>JSONB GIN index</td>
          <td>無（generated column workaround）</td>
          <td>JSON workload 結構性差</td>
      </tr>
      <tr>
          <td>Range types</td>
          <td>無</td>
          <td>booking / availability schema 救命</td>
      </tr>
      <tr>
          <td>Exclusion constraints</td>
          <td>無</td>
          <td>range overlap 防護</td>
      </tr>
      <tr>
          <td>User-defined DOMAIN</td>
          <td>無</td>
          <td>column-level type constraint</td>
      </tr>
      <tr>
          <td>Extension ecosystem</td>
          <td>弱</td>
          <td>pgvector / TimescaleDB / PostGIS</td>
      </tr>
      <tr>
          <td>Full-text search 成熟</td>
          <td>InnoDB FTS 較弱</td>
          <td>tsvector + GIN + pg_trgm 三層</td>
      </tr>
      <tr>
          <td>Multi-column statistics</td>
          <td>8.0 histograms 部分對應、PG 更廣</td>
          <td>planner 更準</td>
      </tr>
      <tr>
          <td>Procedural language</td>
          <td>PL/pgSQL + 多語言（PL/Python / PL/Perl 等）</td>
          <td>Stored procedure（不擴語言）</td>
      </tr>
      <tr>
          <td>Recursive CTE 深度</td>
          <td>Unlimited</td>
          <td>1000（cte_max_recursion_depth）</td>
      </tr>
      <tr>
          <td>LSN-based replication</td>
          <td>簡潔</td>
          <td>binlog file+position（GTID 緩解）</td>
      </tr>
      <tr>
          <td>Generic table inheritance</td>
          <td>早就有</td>
          <td>無（multi-tenant schema 結構用）</td>
      </tr>
  </tbody>
</table>
<h2 id="對從-mysql-評估-pg的讀者">對「從 MySQL 評估 PG」的讀者</h2>
<p>讀者通常從 MySQL 8.0 過來、問題是 <em>「PG 比 MySQL 強在哪、弱在哪」</em>：</p>
<h3 id="pg-比-mysql-強">PG 比 MySQL 強</h3>
<ul>
<li><em>SQL 工程深度</em>：上面列的 7 個結構優勢</li>
<li><em>Extension ecosystem</em>：pgvector / TimescaleDB / Citus / pg_partman 等</li>
<li><em>Optimizer</em>：planner 對複雜 query 更成熟</li>
<li><em>Concurrency model</em>：MVCC + 少 lock（<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>）</li>
</ul>
<h3 id="pg-比-mysql-弱">PG 比 MySQL 弱</h3>
<ul>
<li><em>Replication 機制簡潔度</em>：MySQL GTID 比 PG WAL + replication slot 配置簡單（<a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>）</li>
<li><em>Sharding ecosystem</em>：Vitess / PlanetScale 比 Citus 規模驗證高</li>
<li><em>Operational tooling 廣度</em>：pt-toolkit / gh-ost / Orchestrator 等</li>
<li><em>VACUUM 維護</em>：PG MVCC 必須 VACUUM、autovacuum 配錯議題多（<a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>）</li>
</ul>
<h3 id="選-pg-的核心-driver">選 PG 的核心 driver</h3>
<p>對 SQL 工程深度、extension、複雜 query / OLAP-style workload 的場景、PG 仍是首選。對純簡單 OLTP + 大規模 sharding、MySQL + Vitess 仍 competitive。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<ul>
<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 對比">MVCC + Lock Model</a>：PG MVCC 是 SQL feature 的並行控制基礎</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 對比">Query Optimization</a>：PG planner 對 window / CTE / hash join 成熟</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/citus-distributed/" data-link-title="PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster" data-link-desc="Citus 是 PG extension、把單機 PG 變成 *coordinator &#43; worker* sharded cluster、保留 PG SQL &#43; 加 distributed table &#43; reference table &#43; columnar storage。本文走 Citus 架構（coordinator / worker / distribution column）、3 種 table type（distributed / reference / local）、配置 step-by-step、5 production 踩雷（distribution column 選錯 / cross-shard transaction / reference table 過大 / colocate 不對齊 / worker failover）、跟 MySQL Vitess sharding sibling 對比">Citus Distributed</a>：extension 之一、體現 extension 生態</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>：MVCC 代價、跟 SQL feature 並行控制相關</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/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>（concurrency 基礎）</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>（planner 成熟度）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/citus-distributed/" data-link-title="PostgreSQL Citus Distributed：用 extension 把 PG 變成 sharded cluster" data-link-desc="Citus 是 PG extension、把單機 PG 變成 *coordinator &#43; worker* sharded cluster、保留 PG SQL &#43; 加 distributed table &#43; reference table &#43; columnar storage。本文走 Citus 架構（coordinator / worker / distribution column）、3 種 table type（distributed / reference / local）、配置 step-by-step、5 production 踩雷（distribution column 選錯 / cross-shard transaction / reference table 過大 / colocate 不對齊 / worker failover）、跟 MySQL Vitess sharding sibling 對比">PG Citus Distributed</a>（extension example）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">PG Autovacuum Tuning</a>（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>（sibling、反向視角）</li>
<li>官方：<a href="https://www.postgresql.org/about/featurematrix/">PostgreSQL Features</a></li>
</ul>
]]></content:encoded></item></channel></rss>