<?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>Timescaledb on Tarragon</title><link>https://tarrragon.github.io/blog/tags/timescaledb/</link><description>Recent content in Timescaledb 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/timescaledb/index.xml" rel="self" type="application/rss+xml"/><item><title>TimescaleDB Deep Dive：Hypertable / Continuous Aggregate / Compression 把 PG 變 Time-Series DB</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/timescaledb-deep-dive/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/timescaledb-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>TimescaleDB extension&lt;/em> — 用 PG 解 time-series workload 的路徑、跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem&lt;/a> 是 &lt;em>單一 extension 細節 vs ecosystem 全景&lt;/em> 的關係。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="timescaledb-是-pg-的-time-series-specialization">TimescaleDB 是 PG 的 &lt;em>Time-Series Specialization&lt;/em>&lt;/h2>
&lt;p>TimescaleDB 不是獨立 DB、是 PG extension：&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="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">timescaledb&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加完後、PG 多三個 time-series 專屬機制：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Hypertable&lt;/strong>：對 time column 自動 partition、應用層看是一張表&lt;/li>
&lt;li>&lt;strong>Continuous aggregate&lt;/strong>：incremental refresh 的 materialized view&lt;/li>
&lt;li>&lt;strong>Compression&lt;/strong>：對舊 chunk 壓縮（columnar-like format）&lt;/li>
&lt;/ol>
&lt;p>跟專業 time-series DB（InfluxDB / Prometheus / VictoriaMetrics）對比、TimescaleDB 的賣點不是「最快」而是「PG ecosystem 一致」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>TimescaleDB&lt;/th>
 &lt;th>InfluxDB&lt;/th>
 &lt;th>Prometheus&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Query 語言&lt;/td>
 &lt;td>標準 SQL&lt;/td>
 &lt;td>InfluxQL / Flux&lt;/td>
 &lt;td>PromQL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入效能&lt;/td>
 &lt;td>中（10-100K rows/s）&lt;/td>
 &lt;td>高（500K+ rows/s）&lt;/td>
 &lt;td>中（pull-based scrape）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>壓縮&lt;/td>
 &lt;td>90%+（columnar compression）&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Join&lt;/td>
 &lt;td>完整 SQL join&lt;/td>
 &lt;td>弱&lt;/td>
 &lt;td>不支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跟既有 PG schema&lt;/td>
 &lt;td>同一個 DB、可 join&lt;/td>
 &lt;td>獨立&lt;/td>
 &lt;td>獨立&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生態&lt;/td>
 &lt;td>完整 PG ecosystem&lt;/td>
 &lt;td>自家 ecosystem&lt;/td>
 &lt;td>自家 ecosystem&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Open source&lt;/td>
 &lt;td>Apache 2.0（部分功能 TSL license）&lt;/td>
 &lt;td>MIT&lt;/td>
 &lt;td>Apache 2.0&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>何時選 TimescaleDB&lt;/strong>：&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>TimescaleDB extension</em> — 用 PG 解 time-series workload 的路徑、跟 <a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a> 是 <em>單一 extension 細節 vs ecosystem 全景</em> 的關係。</p></blockquote>
<hr>
<h2 id="timescaledb-是-pg-的-time-series-specialization">TimescaleDB 是 PG 的 <em>Time-Series Specialization</em></h2>
<p>TimescaleDB 不是獨立 DB、是 PG extension：</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">timescaledb</span><span class="p">;</span></span></span></code></pre></div><p>加完後、PG 多三個 time-series 專屬機制：</p>
<ol>
<li><strong>Hypertable</strong>：對 time column 自動 partition、應用層看是一張表</li>
<li><strong>Continuous aggregate</strong>：incremental refresh 的 materialized view</li>
<li><strong>Compression</strong>：對舊 chunk 壓縮（columnar-like format）</li>
</ol>
<p>跟專業 time-series DB（InfluxDB / Prometheus / VictoriaMetrics）對比、TimescaleDB 的賣點不是「最快」而是「PG ecosystem 一致」：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>TimescaleDB</th>
          <th>InfluxDB</th>
          <th>Prometheus</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 語言</td>
          <td>標準 SQL</td>
          <td>InfluxQL / Flux</td>
          <td>PromQL</td>
      </tr>
      <tr>
          <td>寫入效能</td>
          <td>中（10-100K rows/s）</td>
          <td>高（500K+ rows/s）</td>
          <td>中（pull-based scrape）</td>
      </tr>
      <tr>
          <td>壓縮</td>
          <td>90%+（columnar compression）</td>
          <td>高</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Join</td>
          <td>完整 SQL join</td>
          <td>弱</td>
          <td>不支援</td>
      </tr>
      <tr>
          <td>跟既有 PG schema</td>
          <td>同一個 DB、可 join</td>
          <td>獨立</td>
          <td>獨立</td>
      </tr>
      <tr>
          <td>生態</td>
          <td>完整 PG ecosystem</td>
          <td>自家 ecosystem</td>
          <td>自家 ecosystem</td>
      </tr>
      <tr>
          <td>Open source</td>
          <td>Apache 2.0（部分功能 TSL license）</td>
          <td>MIT</td>
          <td>Apache 2.0</td>
      </tr>
  </tbody>
</table>
<p><strong>何時選 TimescaleDB</strong>：</p>
<ul>
<li>Application 已用 PG、不想多管一套 time-series DB</li>
<li>需要 join time-series 跟 application 表（user / device metadata）</li>
<li>不需 InfluxDB 級寫入速度（&lt; 100K rows/s）</li>
<li>Team SQL 熟、PromQL / Flux 學習成本不想付</li>
</ul>
<p><strong>何時選 InfluxDB / Prometheus（不選 TimescaleDB）</strong>：</p>
<ul>
<li>High-cardinality metric（10M+ unique series）— TSDB-purpose-built engine 在 cardinality 跟 retention 上比 hypertable 高效</li>
<li>Pull-based scrape model（Prometheus）跟 alerting / Grafana 生態深整合</li>
<li>PromQL operator（<code>rate()</code> / <code>histogram_quantile()</code>）對 metric query 比 SQL 直覺</li>
<li>TSL license 不能接受（TimescaleDB 部分功能在 Timescale License、不是純 Apache 2.0）</li>
<li>Operational team 已熟 InfluxDB / Prometheus、不想多學 PG 維運</li>
</ul>
<h2 id="hypertable自動-time-based-partitioning">Hypertable：自動 Time-based Partitioning</h2>
<p>普通 PG 表變 hypertable：</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">sensor_data</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">time</span><span class="w">        </span><span class="n">TIMESTAMPTZ</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">3</span><span class="cl"><span class="w">    </span><span class="n">sensor_id</span><span class="w">   </span><span class="nb">INTEGER</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">4</span><span class="cl"><span class="w">    </span><span class="n">temperature</span><span class="w"> </span><span class="n">DOUBLE</span><span class="w"> </span><span class="k">PRECISION</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">humidity</span><span class="w">    </span><span class="n">DOUBLE</span><span class="w"> </span><span class="k">PRECISION</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">-- 變 hypertable、按 time 自動 partition
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">create_hypertable</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;time&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Hypertable 機制：</p>
<ul>
<li>後台自動拆 <em>chunk</em>（child partition）by time interval（預設 7 天）</li>
<li>Application 看到的是 <code>sensor_data</code> 一張表、實際資料分散在 <code>_timescaledb_internal._hyper_*_chunk</code> 表</li>
<li>Query 自動 chunk pruning（只掃命中時間範圍的 chunk）</li>
</ul>
<p><strong>Chunk interval 選擇</strong>很關鍵：</p>
<table>
  <thead>
      <tr>
          <th>Chunk interval</th>
          <th>適用</th>
          <th>問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 小時</td>
          <td>高頻 metrics（每秒 100+ row）</td>
          <td>Chunk 太多、catalog 膨脹</td>
      </tr>
      <tr>
          <td>1 天</td>
          <td>中高頻（每秒 10-100 row）</td>
          <td>OK</td>
      </tr>
      <tr>
          <td>7 天（預設）</td>
          <td>中頻（每分鐘 row）</td>
          <td>OK</td>
      </tr>
      <tr>
          <td>30 天</td>
          <td>低頻（每小時 row）</td>
          <td>OK</td>
      </tr>
  </tbody>
</table>
<p>通用原則：<em>每個 chunk 25% RAM</em>、超過退化 disk IO。Production 監控 <code>chunk_size</code> 跟 <code>shared_buffers</code> ratio 自動調。</p>
<p><strong>Multi-dimensional hypertable</strong>（time + space partition）：</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">-- 按 time + device_id 雙維 partition
</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">create_hypertable</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;time&#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="n">partitioning_column</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="s1">&#39;sensor_id&#39;</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">number_partitions</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="mi">16</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>適用 sensor 數 1000+ 的 IoT workload、單 chunk 太大時用 space partition 拆。</p>
<h2 id="continuous-aggregatecaggincremental-materialized-view">Continuous Aggregate（CAGG）：Incremental Materialized View</h2>
<p>普通 PG materialized view 是 <em>全量重算</em>、TimescaleDB CAGG 是 <em>incremental refresh</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="c1">-- 1 小時粒度聚合
</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="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">sensor_hourly</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">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">timescaledb</span><span class="p">.</span><span class="n">continuous</span><span class="p">)</span><span class="w"> </span><span class="k">AS</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">SELECT</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">time_bucket</span><span class="p">(</span><span class="s1">&#39;1 hour&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">time</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">hour</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="n">sensor_id</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="k">avg</span><span class="p">(</span><span class="n">temperature</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">avg_temp</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="k">max</span><span class="p">(</span><span class="n">temperature</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">max_temp</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">min</span><span class="p">(</span><span class="n">temperature</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">min_temp</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">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">sample_count</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">sensor_data</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">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">hour</span><span class="p">,</span><span class="w"> </span><span class="n">sensor_id</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">-- 加 refresh policy（每 30 分鐘 refresh 過去 1 天）
</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="n">add_continuous_aggregate_policy</span><span class="p">(</span><span class="s1">&#39;sensor_hourly&#39;</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 class="n">start_offset</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;1 day&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="n">end_offset</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;30 minutes&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="n">schedule_interval</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;30 minutes&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><p>CAGG 機制：</p>
<ul>
<li>記錄哪些 time bucket 已 materialize、哪些 stale</li>
<li>Refresh 時只重算 stale bucket、不全量</li>
<li>Query CAGG 自動 fallback 到原 hypertable 補最新資料（real-time aggregation）</li>
</ul>
<p><strong>CAGG vs 普通 MV 對比</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>TimescaleDB CAGG</th>
          <th>普通 PG MV</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Refresh 模式</td>
          <td>Incremental</td>
          <td>全量重算</td>
      </tr>
      <tr>
          <td>Refresh 時間</td>
          <td>秒級</td>
          <td>表大時數十分鐘</td>
      </tr>
      <tr>
          <td>Real-time fallback</td>
          <td>自動補最新</td>
          <td>不支援、需手動 union</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>多一份 aggregated</td>
          <td>多一份 aggregated</td>
      </tr>
      <tr>
          <td>Policy</td>
          <td>內建排程</td>
          <td>需 pg_cron / 外部排程</td>
      </tr>
  </tbody>
</table>
<p><strong>CAGG hierarchy</strong>（多層聚合）：</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">-- 從 1 hour CAGG 再聚合到 1 day
</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="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">sensor_daily</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">WITH</span><span class="w"> </span><span class="p">(</span><span class="n">timescaledb</span><span class="p">.</span><span class="n">continuous</span><span class="p">)</span><span class="w"> </span><span class="k">AS</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">SELECT</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">time_bucket</span><span class="p">(</span><span class="s1">&#39;1 day&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">hour</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">day</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="n">sensor_id</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="k">avg</span><span class="p">(</span><span class="n">avg_temp</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">daily_avg</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">sensor_hourly</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">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">day</span><span class="p">,</span><span class="w"> </span><span class="n">sensor_id</span><span class="p">;</span></span></span></code></pre></div><p>Application query 不同時間範圍時自動命中對應粒度、不必每次掃原始資料。</p>
<h2 id="compression把舊-chunk-壓-90">Compression：把舊 Chunk 壓 90%+</h2>
<p>舊 chunk 可以開啟 compression：</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">-- 開啟 compression（必須先設定 segment by）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">sensor_data</span><span class="w"> </span><span class="k">SET</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">timescaledb</span><span class="p">.</span><span class="n">compress</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">timescaledb</span><span class="p">.</span><span class="n">compress_segmentby</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;sensor_id&#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="n">timescaledb</span><span class="p">.</span><span class="n">compress_orderby</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;time DESC&#39;</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">-- 自動壓縮 policy：7 天前 chunk 壓
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">add_compression_policy</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;7 days&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Compression 機制：</p>
<ul>
<li>把 chunk 內 row 按 <code>segmentby</code> 分組</li>
<li>每組內按 <code>orderby</code> 排序後、把每 column 變成 <em>columnar array</em></li>
<li>對 array 用 type-specific 壓縮（Gorilla for float / delta-of-delta for timestamp / dictionary for string）</li>
</ul>
<p>實際壓縮率：</p>
<table>
  <thead>
      <tr>
          <th>Workload</th>
          <th>壓縮率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IoT sensor（重複值多）</td>
          <td>95-98%</td>
      </tr>
      <tr>
          <td>Application metrics</td>
          <td>90-95%</td>
      </tr>
      <tr>
          <td>Trade tick（隨機浮點）</td>
          <td>70-85%</td>
      </tr>
      <tr>
          <td>Log line（高 cardinality string）</td>
          <td>50-70%</td>
      </tr>
  </tbody>
</table>
<p><strong>Compression 限制</strong>（重要）：</p>
<ul>
<li>壓縮後 chunk <strong>不能 UPDATE / DELETE 單 row</strong>（要先 decompress）</li>
<li>壓縮後 chunk <strong>不能加 column</strong>（要 decompress 所有 chunk）</li>
<li>壓縮後 chunk 只能 <em>append new row</em>、不能改舊 row</li>
<li>DDL 變更（加 column / 改 index）需 decompress</li>
</ul>
<p>實務：compression 是 <em>write-once cold data</em> 的工具、active OLTP chunk 不開。</p>
<h2 id="retention-policy自動刪舊資料">Retention Policy：自動刪舊資料</h2>





<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">-- 1 年前 chunk 自動刪
</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">add_retention_policy</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;1 year&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Retention drop 整個 chunk（不是 DELETE row）、O(1) 操作、不產生 bloat。</p>
<p>CAGG 有獨立 retention：</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">-- 原始資料只留 30 天、aggregated 留 5 年
</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">add_retention_policy</span><span class="p">(</span><span class="s1">&#39;sensor_data&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;30 days&#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">SELECT</span><span class="w"> </span><span class="n">add_retention_policy</span><span class="p">(</span><span class="s1">&#39;sensor_hourly&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;5 years&#39;</span><span class="p">);</span></span></span></code></pre></div><p>這是 TimescaleDB 跟普通 PG partitioning 最大的價值差 — 普通 PG 要自己寫 cron drop partition、TimescaleDB policy 內建。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="case-1chunk-size-不對catalog-膨脹">Case 1：Chunk size 不對、catalog 膨脹</h3>
<p><strong>情境</strong>：sensor 每秒寫 10 row、chunk_interval 設 1 小時、一年產 8760 chunk、<code>pg_class</code> 撐到 200 萬 row、planner 變慢。</p>
<p>修法：</p>
<ul>
<li>Chunk 數量上限 ~10000、超過 catalog overhead 出現</li>
<li>重設 chunk_interval：<code>SELECT set_chunk_time_interval('sensor_data', INTERVAL '1 day');</code></li>
<li>已存在 chunk 不會自動 merge、要靠 retention drop 自然消化</li>
</ul>
<h3 id="case-2cagg-refresh-落後-real-time">Case 2：CAGG refresh 落後 real-time</h3>
<p><strong>情境</strong>：CAGG refresh policy 每 1 小時跑、application 期待「即時 dashboard」、看到的數字落後 1 小時。</p>
<p>修法：</p>
<ul>
<li>縮短 <code>schedule_interval</code>（5 分鐘）</li>
<li>用 <code>real-time aggregation</code>（預設 ON、CAGG 自動 union 原始資料）</li>
<li>確認 <code>materialized_only = false</code>（real-time aggregation 開啟）</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="n">MATERIALIZED</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">sensor_hourly</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="p">(</span><span class="n">timescaledb</span><span class="p">.</span><span class="n">materialized_only</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">false</span><span class="p">);</span></span></span></code></pre></div><h3 id="case-3compression-後想-update">Case 3：Compression 後想 UPDATE</h3>
<p><strong>情境</strong>：發現某個歷史 row 數值錯、想 UPDATE、報錯 <em>cannot update/delete from compressed chunk</em>。</p>
<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">-- 找到該 chunk 並 decompress
</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">decompress_chunk</span><span class="p">(</span><span class="k">c</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">show_chunks</span><span class="p">(</span><span class="s1">&#39;sensor_data&#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="n">older_than</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;7 days&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="p">::</span><span class="nb">text</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;%_5_chunk&#39;</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">-- UPDATE 完再 compress 回去
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">sensor_data</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">temperature</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">22</span><span class="p">.</span><span class="mi">5</span><span class="w"> </span><span class="k">WHERE</span><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 class="k">SELECT</span><span class="w"> </span><span class="n">compress_chunk</span><span class="p">(...);</span></span></span></code></pre></div><p>或設計階段就避免 — compression 用在 <em>immutable data</em>、有可能改的留未壓。</p>
<h3 id="case-4hypertable-不能加-fk-到-non-hypertable">Case 4：Hypertable 不能加 FK 到 non-hypertable</h3>
<p><strong>情境</strong>：想對 <code>sensor_data</code> 加 FK 到 <code>sensors</code> 表、報錯 <em>foreign key constraints with hypertables are not supported</em>。</p>
<p>修法：</p>
<ul>
<li>Application 層維護 referential integrity</li>
<li>或反過來：<code>sensors</code> 可以 FK 到 hypertable（特定方向支援）</li>
<li>TimescaleDB 2.11+ 部分支援 FK from hypertable、但限制多</li>
</ul>
<h3 id="case-5timescaledb-跟-pg-主版本對齊">Case 5：TimescaleDB 跟 PG 主版本對齊</h3>
<p><strong>情境</strong>：PG 升級 14 → 16、TimescaleDB extension 沒對應升級、PG 啟動 fail。</p>
<p>TimescaleDB 跟 PG 版本對齊矩陣：</p>
<table>
  <thead>
      <tr>
          <th>TimescaleDB</th>
          <th>支援 PG version</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2.11+</td>
          <td>13, 14, 15</td>
          <td></td>
      </tr>
      <tr>
          <td>2.13+</td>
          <td>13, 14, 15, 16</td>
          <td>加 PG 16 支援</td>
      </tr>
      <tr>
          <td>2.15.x</td>
          <td>13, 14, 15, 16</td>
          <td>最後支援 PG 13 的 minor</td>
      </tr>
      <tr>
          <td>2.16+</td>
          <td>14, 15, 16</td>
          <td>PG 13 drop</td>
      </tr>
      <tr>
          <td>2.17+</td>
          <td>14, 15, 16, 17</td>
          <td>PG 17 加入（需 17.2+ binary 對齊）</td>
      </tr>
      <tr>
          <td>2.18+</td>
          <td>14, 15, 16, 17</td>
          <td>PG 17 完整支援</td>
      </tr>
      <tr>
          <td>2.23+</td>
          <td>14, 15, 16, 17, 18</td>
          <td>PG 18 加入</td>
      </tr>
  </tbody>
</table>
<p>修法：</p>
<ul>
<li>升 PG 前先升 TimescaleDB 到支援目標 PG 版本的 extension</li>
<li>Production 升級順序：TimescaleDB minor upgrade → PG major upgrade → TimescaleDB final upgrade</li>
<li>Cloud managed（Timescale Cloud）自動處理</li>
</ul>
<h2 id="跟-pg-原生-partitioning-對比">跟 PG 原生 Partitioning 對比</h2>
<p>PG 10+ 有 declarative partitioning、不一定要 TimescaleDB：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>TimescaleDB hypertable</th>
          <th>PG declarative partitioning</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自動建 chunk</td>
          <td>是</td>
          <td>否（需手動或 pg_partman）</td>
      </tr>
      <tr>
          <td>Chunk pruning</td>
          <td>自動</td>
          <td>自動（需 partition key）</td>
      </tr>
      <tr>
          <td>Retention 內建</td>
          <td>是</td>
          <td>否（pg_partman 或自寫 cron）</td>
      </tr>
      <tr>
          <td>Compression</td>
          <td>內建 columnar</td>
          <td>否</td>
      </tr>
      <tr>
          <td>Continuous aggregate</td>
          <td>內建</td>
          <td>否（自寫 incremental refresh）</td>
      </tr>
      <tr>
          <td>跨 chunk index</td>
          <td>統一 management</td>
          <td>Per-partition index</td>
      </tr>
      <tr>
          <td>Cardinality limit</td>
          <td>10000+ chunk OK</td>
          <td>1000+ partition 就慢</td>
      </tr>
  </tbody>
</table>
<p>何時用原生 partitioning（不用 TimescaleDB）：</p>
<ul>
<li>不需要 compression / CAGG</li>
<li>Partition 數 &lt; 1000</li>
<li>已用 pg_partman 不想換</li>
<li>公司禁用 TSL license（TimescaleDB 部分功能受限）</li>
</ul>
<p>何時用 TimescaleDB：</p>
<ul>
<li>高頻 time-series（compression 必要）</li>
<li>需要 CAGG（手寫 incremental MV 成本高）</li>
<li>Partition 數 &gt; 1000</li>
<li>IoT / metrics / observability workload</li>
</ul>
<p>詳細 partitioning 機制看 <a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">declarative-partitioning</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a>：PG extension 全景</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">declarative-partitioning</a>：原生 partitioning</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/jsonb-deep-dive/" data-link-title="PostgreSQL JSONB Deep Dive：Binary Storage &#43; GIN Index 為什麼是結構性優勢" data-link-desc="PG JSONB（9.4&#43;）是 *binary 儲存的 JSON*、可直接 GIN index、是 PG 在 JSON workload 的結構性優勢、跟 MongoDB / MySQL 8.0 JSON_TABLE 比仍領先。本文走 JSON vs JSONB 差異、GIN index 機制（jsonb_ops vs jsonb_path_ops）、operator &#43; path query、partial JSONB indexing、5 production 踩雷（大 JSONB 跟 TOAST / nested update / index 選錯 op class / jsonb_path_query 跟 jsonb_path_exists 行為差 / partial index 條件搞錯）、何時用 JSONB vs 拆 column">jsonb-deep-dive</a>：IoT payload 用 JSONB 儲存</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>：hypertable autovacuum 行為</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">major-version-upgrade</a>：TimescaleDB + PG 升級順序</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>看 <a href="/blog/backend/01-database/vendors/postgresql/extension-ecosystem/" data-link-title="PostgreSQL Extension Ecosystem：把 PG 變成 vector DB / time-series / sharded 的 plugin 生態" data-link-desc="PG 的 extension 機制不只是 plugin、是 *結構性產品線擴張* — pgvector 讓 PG 變 vector DB、TimescaleDB 變 time-series、Citus 變 sharded、PostGIS 變 GIS。本文走 PG extension lifecycle、6 個 production-critical extension（pg_stat_statements / pg_partman / pg_repack / pgvector / TimescaleDB / PostGIS）、5 production 踩雷（extension version 跟 PG version 對齊 / managed PG 限制 / upgrade order / shared_preload_libraries 衝突 / extension 跟 logical replication 互動）、cloud vendor 對 extension 的限制">extension-ecosystem</a> 了解其他 PG 擴展選項</li>
<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 overview</a> 看全圖</li>
</ul>
]]></content:encoded></item></channel></rss>