<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Team Tiger Data</title>
    <description>The latest articles on DEV Community by Team Tiger Data (@tigerdata_dev).</description>
    <link>https://dev.to/tigerdata_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2547418%2F1859b44f-d7f7-47c9-9ca2-082bae60b949.png</url>
      <title>DEV Community: Team Tiger Data</title>
      <link>https://dev.to/tigerdata_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tigerdata_dev"/>
    <language>en</language>
    <item>
      <title>TimescaleDB 2.28: Faster Queries, Lighter Operations, and Better Schema Evolution</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:30:13 +0000</pubDate>
      <link>https://dev.to/tigerdata/timescaledb-228-faster-queries-lighter-operations-and-better-schema-evolution-54hb</link>
      <guid>https://dev.to/tigerdata/timescaledb-228-faster-queries-lighter-operations-and-better-schema-evolution-54hb</guid>
      <description>&lt;p&gt;Time-series analytics at scale creates operational friction. When you're running continuous aggregates, columnar storage, and complex analytical patterns, each new metric, query pattern, and configuration tuning attempt adds complexity. Refreshes block each other, configuration changes require rebuilds, and new aggregates mean recomputing entire rollups.&lt;/p&gt;

&lt;p&gt;Over recent &lt;a href="https://github.com/timescale/timescaledb/releases" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB releases&lt;/u&gt;&lt;/a&gt;, we've prioritized minimizing these hurdles by leveraging bloom filters to bypass redundant processing during high-volume operations on columnar storage. We also expanded vectorized execution across more query patterns and simplified continuous aggregate workflows by combining refresh and compression.&lt;/p&gt;

&lt;p&gt;Now with &lt;a href="https://github.com/timescaledb/releases/tag/2.28.0" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB 2.28&lt;/u&gt;&lt;/a&gt;, we're making common analytical queries faster without code changes, making continuous aggregate operations less disruptive and more flexible, and eliminating friction when evolving your schema and configuration. The result is faster queries, lighter operations, and the ability to evolve your analytics alongside your application as it scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lighter, more flexible continuous aggregates:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ADD COLUMN&lt;/code&gt; on CAggs: Add new aggregations in place without rebuilding&lt;/li&gt;
&lt;li&gt;Fine-grained locking: Refreshes no longer serialize unrelated operations&lt;/li&gt;
&lt;li&gt;Incremental refresh batching: Break large refreshes into batches instead of one heavy operation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ANALYZE&lt;/code&gt; and &lt;code&gt;VACUUM&lt;/code&gt;: Maintenance commands now work directly on continuous aggregates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Faster queries on compressed data:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metadata-driven first() and last() queries: Answer from batch metadata without decompression&lt;/li&gt;
&lt;li&gt;Vectorized &lt;code&gt;CASE&lt;/code&gt; expressions: Conditional logic stays on the fast path&lt;/li&gt;
&lt;li&gt;Batch Sorted Merge for more queries: Lighter-weight sorting on compressed data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Better operational flexibility:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sparse index retrofitting: Update configuration on existing chunks without recompression&lt;/li&gt;
&lt;li&gt;Compression settings clarity: Warnings prevent misconfiguration&lt;/li&gt;
&lt;li&gt;GUC for bulk loads: Skip invalidation tracking during migrations&lt;/li&gt;
&lt;li&gt;Nullable &lt;code&gt;ORDER BY&lt;/code&gt; safety: Fallback to correct compression path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Also:&lt;/strong&gt; PostgreSQL 15 support ends with 2.28. Plan your upgrade to PG16, PG17, or PG18.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema evolution: ADD COLUMN on CAggs
&lt;/h2&gt;

&lt;p&gt;Schema evolution lets you add new aggregated columns without dropping and rebuilding. Before 2.28, adding a metric meant recreating the entire CAgg, recomputing all historical data (hours on large datasets), and breaking downstream consumers. Now you can &lt;code&gt;ADD COLUMN&lt;/code&gt; with &lt;code&gt;GENERATED ALWAYS AS&lt;/code&gt; and backfill incrementally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Add a new metric to an existing CAgg&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;conditions_summary_hourly&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;max_temp&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;
  &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Backfill historical data&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;refresh_continuous_aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'conditions_summary_hourly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;force&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How we made continuous aggregates operational at scale
&lt;/h2&gt;

&lt;p&gt;Real-time dashboards on operational data used to force a choice: refresh frequently and block bulk loads, or refresh less often and accept stale data. Every continuous aggregate refresh acquired a table-wide lock on its materialized hypertable, blocking concurrent operations. In 2.28, we switched to row-level locking on the continuous aggregate catalog entry. Only one refresh processes a CAgg's invalidation log at a time, but the materialized table is free for concurrent operations. Refreshes and bulk loads now run in parallel.&lt;/p&gt;

&lt;p&gt;We also made continuous aggregates more flexible. &lt;code&gt;ADD COLUMN&lt;/code&gt; on CAggs enables you to evolve your analytical schema in production by adding new aggregations as you learn what data you need, without downtime or rebuilds. Your dashboard design no longer locks you into upfront predictions. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9uuz9qj7v2kk2rzduv2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9uuz9qj7v2kk2rzduv2.jpg" alt="TimescaleDB 2.28 - Continuous Aggregates" width="799" height="519"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Before 2.28: refresh acquired table-wide lock&lt;/span&gt;
&lt;span class="c1"&gt;-- After 2.28: refresh acquires only catalog row lock&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;refresh_continuous_aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'metrics_hourly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'2026-02-01'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Other refreshes, bulk loads, and DDL can proceed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2.28 also adds two capabilities that make continuous aggregates evolve with your application:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental refresh batching&lt;/strong&gt; processes large time windows in smaller batches instead of single long-running transactions. Before 2.28, &lt;code&gt;refresh_continuous_aggregate()&lt;/code&gt; materialized the entire window atomically, holding resources and blocking vacuums. Large windows could take hours and fail mid-way. Now you can batch them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="c1"&gt;-- Refresh a 30-day window in smaller batches&lt;/span&gt;
   &lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;refresh_continuous_aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="s1"&gt;'metrics_hourly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
     &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{
       "buckets_per_batch": 10,
       "max_batches_per_execution": 20
     }'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;
   &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Schema evolution&lt;/strong&gt; lets you add new aggregated columns without dropping and rebuilding. Before 2.28, adding a metric meant recreating the entire CAgg, recomputing all historical data (hours on large datasets), and breaking downstream consumers. Now you can &lt;code&gt;ADD COLUMN&lt;/code&gt; with &lt;code&gt;GENERATED ALWAYS AS&lt;/code&gt; and backfill incrementally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Add a new metric to an existing CAgg&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;conditions_summary_hourly&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;max_temp&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;
  &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Backfill historical data&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;refresh_continuous_aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'conditions_summary_hourly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;force&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ANALYZE&lt;/code&gt; and &lt;code&gt;VACUUM&lt;/code&gt; now work on continuous aggregates directly. Maintenance commands automatically maintain accurate planner statistics across the materialized hypertable and all its chunks, ensuring query plans stay optimal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Faster queries on compressed data
&lt;/h2&gt;

&lt;p&gt;One of the most common queries in time-series workloads is "give me the latest value per series." Dashboards and monitoring systems run it constantly. It's a simple pattern: find the first or last value in a time range. But on compressed data, answering that query used to require decompressing entire batches just to find values that are already in the metadata. Time-sorted batches store first and last values as metadata. But before 2.28, the database decompressed anyway to answer those queries.&lt;/p&gt;

&lt;p&gt;In 2.28, TimescaleDB extracts first(value, time) and last(value, time) aggregates directly from batch sparse indexes. No decompression. For "latest reading" queries that consume significant resources at scale, that means meaningful speedup with zero query changes. Here's what that looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2sva9t6mujgkx8rgh8gk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2sva9t6mujgkx8rgh8gk.jpg" alt="TimescaleDB 2.28 - Metadata Optimization" width="800" height="621"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Define firstlast sparse index&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;tsdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'firstlast(temperature)'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Note: Use rebuild_sparse_index() to retrofit existing chunks&lt;/span&gt;
&lt;span class="c1"&gt;-- New chunks now answer this query from metadata alone&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beyond first/last, we also extended vectorized execution to cover &lt;code&gt;CASE&lt;/code&gt; expressions. Before 2.28, conditional logic in aggregations forced columnar queries to fall back to row-by-row processing, making identical queries perform very differently depending on whether a &lt;code&gt;CASE&lt;/code&gt; expression was present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This query now stays fully vectorized&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;error_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;slow_avg_latency&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pivot-style queries, error rate calculations, and conditional metrics all stay on the fast vectorized path. For workloads that use conditional aggregations on compressed history, that eliminates the surprise performance cliffs.&lt;/p&gt;

&lt;p&gt;Finally, Batch Sorted Merge now applies to more query patterns. &lt;code&gt;ORDER BY&lt;/code&gt; queries on unordered compressed chunks without segmentation no longer fall back to expensive external sorts. The planner now replaces those sorts with lightweight metadata merges over pre-sorted batches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better operational flexibility and efficiency
&lt;/h2&gt;

&lt;p&gt;Tuning time-series workloads means iterating on compression strategy. But historically, every configuration change (adding a sparse index, adjusting &lt;code&gt;segmentby&lt;/code&gt;, optimizing &lt;code&gt;orderby&lt;/code&gt;) only applied to future compressions. Existing chunks kept their old settings, creating inconsistent performance and confusion about why newly tuned queries behaved differently.&lt;/p&gt;

&lt;p&gt;2.28 eliminates that friction. &lt;code&gt;rebuild_sparse_index&lt;/code&gt; lets you retrofit sparse index configuration to existing chunks without recompressing. Sparse indexes are metadata. Updating them shouldn't require rewriting data. Below is the actual code snippet you can leverage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Change sparse index settings on the hypertable&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;tsdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'minmax(value)'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Retrofit existing chunks without recompression&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;_timescaledb_functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rebuild_sparse_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'_timescaledb_internal._hyper_1_42_chunk'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Compression settings changes now emit warnings&lt;/strong&gt; clarifying that new settings apply only to future compressions. This closes the gap: users assume &lt;code&gt;ALTER TABLE&lt;/code&gt; sets configuration across the entire dataset, but it only affects new chunks.&lt;/p&gt;

&lt;p&gt;For correctness, &lt;strong&gt;nullable ORDER BY columns now safely fall back&lt;/strong&gt; to decompress-compress during recompression, preventing silent incorrect results when min/max metadata doesn't account for &lt;code&gt;NULL&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gapfill row count estimation&lt;/strong&gt; improved so &lt;code&gt;time_bucket_gapfill&lt;/code&gt; queries get more accurate planner statistics and better query plans.&lt;/p&gt;

&lt;p&gt;For bulk migrations, a new GUC &lt;strong&gt;timescaledb.skip_cagg_invalidation&lt;/strong&gt; suppresses continuous aggregate invalidation tracking during bulk loads. Migration tools no longer generate useless invalidation entries that trigger expensive refresh storms.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL 15: Final Release and Migration Path
&lt;/h2&gt;

&lt;p&gt;PostgreSQL 15 support ends with TimescaleDB 2.28. Going forward, only PostgreSQL 16, 17, and 18 are supported.&lt;/p&gt;

&lt;p&gt;If you're on PG15, plan your upgrade to PG17 or PG18 now. We'll begin upgrading instances in production the week of September 15th, so you should plan your migration path over the next two months to avoid downtime. Postgres upgrades are typically non-disruptive (seconds to minutes of downtime using logical replication or physical backup/restore), and newer Postgres versions bring improvements in query parallelism, vector search optimization, and compression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrade to 2.28 today
&lt;/h2&gt;

&lt;p&gt;For workloads running continuous aggregates at scale, columnar queries with conditional logic, or iterating on compression configuration, 2.28 removes operational friction: refreshes don't serialize, queries stay vectorized, and tuning doesn't require rewrites.&lt;/p&gt;

&lt;p&gt;2.28 is available now. To learn more, &lt;a href="https://github.com/timescale/timescaledb/releases/tag/2.28.0" rel="noopener noreferrer"&gt;&lt;u&gt;check out the full release notes&lt;/u&gt;&lt;/a&gt; for a complete list of improvements, or &lt;a href="https://console.cloud.tigerdata.com/signup" rel="noopener noreferrer"&gt;&lt;em&gt;&lt;u&gt;try Tiger Cloud for free&lt;/u&gt;&lt;/em&gt;&lt;/a&gt; and experience TimescaleDB 2.28 on your largest hypertables. We welcome your feedback on &lt;a href="https://github.com/timescale/timescaledb" rel="noopener noreferrer"&gt;&lt;u&gt;GitHub&lt;/u&gt;&lt;/a&gt;. Please note that for Tiger Cloud customers, all improvements are live immediately. For self-hosted deployments, download the&lt;a href="https://github.com/timescale/timescaledb/releases/tag/2.28.0" rel="noopener noreferrer"&gt;&lt;u&gt;latest release&lt;/u&gt;&lt;/a&gt; and follow the&lt;a href="https://www.tigerdata.com/docs/reference/timescaledb/install/" rel="noopener noreferrer"&gt;&lt;u&gt;upgrade guide&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>announcementsrelease</category>
      <category>timescaledb</category>
      <category>database</category>
    </item>
    <item>
      <title>The Hidden Cost of Postgres Constraints at Scale</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Fri, 24 Jul 2026 14:30:49 +0000</pubDate>
      <link>https://dev.to/tigerdata/the-hidden-cost-of-postgres-constraints-at-scale-1ge6</link>
      <guid>https://dev.to/tigerdata/the-hidden-cost-of-postgres-constraints-at-scale-1ge6</guid>
      <description>&lt;p&gt;Your ingest workers are queuing. &lt;a href="https://www.tigerdata.com/learn/5-ways-to-monitor-your-postgresql-database" rel="noopener noreferrer"&gt;&lt;u&gt;&lt;code&gt;pg_stat_activity&lt;/code&gt;&lt;/u&gt;&lt;/a&gt; shows lock waits. The blocked query is not a slow SELECT. It's your bulk INSERT, waiting on the &lt;code&gt;devices&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;You added a FOREIGN KEY there months ago. You added a UNIQUE constraint on the readings table to catch duplicates. Both were the right call. At 100 devices and 10,000 rows, you never felt them. At 50K inserts per second, they've become the ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What Postgres actually executes on every insert to enforce &lt;code&gt;FOREIGN KEY&lt;/code&gt; and &lt;code&gt;UNIQUE&lt;/code&gt; constraints&lt;/li&gt;
&lt;li&gt;Why this overhead is invisible during a PoC and destructive at production ingest rates&lt;/li&gt;
&lt;li&gt;Four concrete approaches that preserve data integrity without paying the full constraint cost on every row&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;Postgres Optimization Treadmill&lt;/u&gt;&lt;/a&gt; describes how high-frequency time-series workloads hit architectural ceilings despite correct tuning. &lt;a href="https://www.tigerdata.com/blog/mvcc-feature-youre-paying-for-but-not-using" rel="noopener noreferrer"&gt;&lt;u&gt;MVCC overhead&lt;/u&gt;&lt;/a&gt;, index write amplification, and &lt;a href="https://www.tigerdata.com/blog/write-amplification-in-postgres-the-3-4x-tax-on-every-insert" rel="noopener noreferrer"&gt;&lt;u&gt;WAL volume&lt;/u&gt;&lt;/a&gt; all compound as data grows. Constraint enforcement layers on top of all of that, and it compounds in the same direction.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;FOREIGN KEY&lt;/code&gt; fires an index lookup against the referenced table on every insert. At 50K inserts/sec, that's 50K random reads per second competing directly with your write path. Every &lt;code&gt;UNIQUE&lt;/code&gt; constraint fires an index scan before every insert, on an append-only table where duplicates shouldn't ever occur. Both generate additional WAL records and hold row-level locks during execution. Together, they quietly consume the ingest safety margin between "running well" and "falling behind."&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracing a single constrained insert
&lt;/h2&gt;

&lt;p&gt;A vanilla insert into a plain table performs two operations: a heap tuple write and a WAL commit record.&lt;/p&gt;

&lt;p&gt;A constrained insert does five:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Heap write.&lt;/strong&gt; The row is written to the 8KB heap page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B-tree insertion.&lt;/strong&gt; &lt;a href="https://www.tigerdata.com/blog/indexing-your-way-into-a-performance-bottleneck" rel="noopener noreferrer"&gt;&lt;u&gt;Every index on the table receives a new entry&lt;/u&gt;&lt;/a&gt;, traversing from root to leaf and splitting pages as needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FK shared-lock acquisition.&lt;/strong&gt; Postgres acquires a &lt;code&gt;FOR KEY SHARE&lt;/code&gt; on the referenced row in the parent table (&lt;code&gt;devices&lt;/code&gt;) to verify it exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNIQUE index scan.&lt;/strong&gt; Postgres scans the unique index to confirm no matching entry already exists before writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WAL commit record.&lt;/strong&gt; The constraint checks generate WAL in addition to the row write itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Under concurrent write load at 50K inserts/sec, step 3 is where it breaks down — not through mutual blocking, since FOR KEY SHARE locks are compatible with each other, but through MultiXacts. With only a handful of device rows referenced by thousands of concurrent transactions, each parent row is locked FOR KEY SHARE by many transactions at once, and Postgres must track that shared ownership with a MultiXactID. At this concurrency the churn saturates the MultiXact SLRU caches. pg_stat_activity surfaces this as MultiXact LWLock wait events (only for PostgreSQL 16-18; event names may differ on earlier versions), not as a query performance problem.&lt;/p&gt;

&lt;p&gt;Not all constraints carry the same cost. &lt;code&gt;NOT NULL&lt;/code&gt; and &lt;code&gt;CHECK&lt;/code&gt; constraints evaluate against the row being inserted with no external lookups. They're near-free. &lt;code&gt;FOREIGN KEY&lt;/code&gt; and &lt;code&gt;UNIQUE&lt;/code&gt; are where the overhead lives, because both require reads against external state on every single insert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying the problem
&lt;/h2&gt;

&lt;p&gt;Run this query during your next peak ingest window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wait_event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wait_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;query_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait_event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'LWLock'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;wait_event&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'MultiXact%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- PostgreSQL 16–18: SLRU wait-event names verified on these versions&lt;/span&gt;
     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait_event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Lock'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;wait_event&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'transactionid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tuple'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All MultiXact wait events share the MultiXact prefix on PostgreSQL 16-18, so the LIKE 'MultiXact%' filter captures all SLRU contention events regardless of minor version. Look for rows where wait_event starts with MultiXact. That indicates MultiXact SLRU contention from FK checks on hot parent rows - the mechanism described above. Rows where wait_event is transactionid or tuple indicate true row-lock waits from a concurrent update or delete on the parent, a less common but related failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four approaches to reduce constraint overhead
&lt;/h2&gt;

&lt;p&gt;These options are ordered by risk and invasiveness. Start with option 1 if you have existing constraints and need a minimal-change fix. Move to option 2 if your duplication window is bounded to recent data. Use options 3 or 4 only if you own the full write path end to end and can enforce integrity outside the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Defer FK checks to commit time
&lt;/h3&gt;

&lt;p&gt;Postgres supports &lt;a href="https://www.postgresql.org/docs/current/sql-set-constraints.html" rel="noopener noreferrer"&gt;&lt;u&gt;deferring constraint checks&lt;/u&gt;&lt;/a&gt; to commit time rather than row time. Inside a &lt;a href="https://www.tigerdata.com/learn/testing-postgres-ingest-insert-vs-batch-insert-vs-copy" rel="noopener noreferrer"&gt;&lt;u&gt;bulk-insert transaction&lt;/u&gt;&lt;/a&gt;, the FK lookup runs once per batch instead of once per row.&lt;/p&gt;

&lt;p&gt;First, declare the constraint as deferrable. This is backward-compatible: the constraint still enforces row-by-row in any transaction that does not explicitly defer it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
  &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;sensor_readings_device_id_fkey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;sensor_readings_device_id_fkey&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;DEFERRABLE&lt;/span&gt; &lt;span class="k"&gt;INITIALLY&lt;/span&gt; &lt;span class="k"&gt;IMMEDIATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then defer it inside each bulk-insert transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINTS&lt;/span&gt; &lt;span class="n"&gt;sensor_readings_device_id_fkey&lt;/span&gt; &lt;span class="k"&gt;DEFERRED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;staging_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a batch of 1,000 rows, this turns 1,000 FK lookups into one check at commit. The integrity guarantee is unchanged: if any &lt;code&gt;device_id&lt;/code&gt; in the batch does not exist in &lt;code&gt;devices&lt;/code&gt;, the commit fails and the batch rolls back. Transactions that do not call &lt;code&gt;SET CONSTRAINTS ... DEFERRED&lt;/code&gt; continue to enforce row-by-row, so this change does not affect other callers.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Scope the UNIQUE check to your live data window
&lt;/h3&gt;

&lt;p&gt;If your primary concern is duplicate prevention during a historical bulk load or replay, a &lt;a href="https://www.tigerdata.com/learn/postgresql-performance-tuning-optimizing-database-indexes" rel="noopener noreferrer"&gt;&lt;u&gt;partial index&lt;/u&gt;&lt;/a&gt; eliminates the UNIQUE check for any row whose timestamp falls outside the live window.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
  &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;sensor_readings_unique_reading&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_sensor_readings_recent_unique&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres evaluates the partial index predicate at INSERT time. A row inserted with &lt;code&gt;ts = now()&lt;/code&gt; satisfies &lt;code&gt;ts &amp;gt; now() - 7 days&lt;/code&gt; and gets the UNIQUE check. A row inserted during a historical backfill with &lt;code&gt;ts = '2024-01-15'&lt;/code&gt; does not satisfy the predicate in 2026 and skips the check entirely. That is the primary benefit: bulk loads of historical data avoid the UNIQUE scan completely.&lt;/p&gt;

&lt;p&gt;For ongoing ingestion of fresh data, each new row is added to the partial index as it's inserted, so the index grows over time. To reclaim the size advantage, schedule a weekly rebuild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;REINDEX&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;idx_sensor_readings_recent_unique&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 2 years of retention (730 days), a freshly rebuilt 7-day partial index covers roughly 1% of the dataset: 7 / 730 = 0.0096. The index is approximately 1/100th the size of a full-table UNIQUE index on the same columns, which reduces both scan time and per-insert write amplification by the same factor.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Validate FK references in the application layer
&lt;/h3&gt;

&lt;p&gt;For workloads where the device set is stable and well-known, validating &lt;code&gt;device_id&lt;/code&gt; in the application before inserting removes the per-row database lookup completely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="c1"&gt;# Cache valid device IDs at startup; refresh on a schedule
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_valid_devices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id FROM devices;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize the cache
&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbname=mydb user=postgres host=localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;valid_devices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_valid_devices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_readings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Filter invalid device IDs before they reach the database
&lt;/span&gt;    &lt;span class="n"&gt;valid_batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;device_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;valid_devices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;invalid_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid_batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;invalid_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Log or alert; this signals an upstream data quality issue
&lt;/span&gt;        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dropped &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invalid_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows with unknown device_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO sensor_readings (ts, device_id, value) VALUES (%s, %s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;device_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;valid_batch&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This trades a database-level guarantee for an application-level guarantee. It works when the application owns the write path and invalid &lt;code&gt;device_id&lt;/code&gt; values represent an upstream data quality problem rather than a concurrent-write race condition. Once this pattern is in place, the database-level FK is redundant, which makes option 4 available.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Drop the FK constraint
&lt;/h3&gt;

&lt;p&gt;If your ingest pipeline already validates &lt;code&gt;device_id&lt;/code&gt; before writing to Postgres (as shown in option 3), the database-level FK enforces a guarantee the pipeline already provides. Removing it cuts the per-insert lock acquisition entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
  &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;sensor_readings_device_id_fkey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates the shared-lock acquisition on &lt;code&gt;devices&lt;/code&gt; for every insert and reduces WAL records by removing the per-row constraint check entries. Combined with the partial UNIQUE index from option 2, this recovers measurable ingest headroom without changing hardware. The &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;Postgres Optimization Treadmill article&lt;/u&gt;&lt;/a&gt; shows that a 50K inserts/sec workload with five indexes already generates 25-50MB/sec of WAL from heap and index writes. Dropping the FK removes additional per-row overhead sitting on top of that baseline.&lt;/p&gt;

&lt;p&gt;The tradeoff is real: no FK means no database-level catch for data quality bugs that slip through the pipeline. Only drop the constraint if option 3 is in place and you have monitoring to detect upstream device ID mismatches before they reach the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating the fix
&lt;/h2&gt;

&lt;p&gt;After applying option 1 or option 4, rerun the detection query during peak load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wait_event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wait_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;query_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait_event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'LWLock'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;wait_event&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'MultiXact%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- PostgreSQL 16–18&lt;/span&gt;
     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait_event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Lock'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;wait_event&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'transactionid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tuple'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MultiXact LWLock waits and any transactionid/tuple row-lock waits tied to the devices table should drop to near zero (MultiXact events only visible on PostgreSQL 16-18). If they persist after applying option 1, confirm the constraint was successfully altered to include &lt;code&gt;DEFERRABLE&lt;/code&gt; before the transaction runs &lt;code&gt;SET CONSTRAINTS ... DEFERRED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After applying option 2, confirm the partial index exists and the full constraint is gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indpred&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_indexes&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_index&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sensor_readings'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;indpred&lt;/code&gt; column contains the partial index predicate as text. A non-null value confirms the index is partial. The index size should reflect only the data that has been inserted since the last REINDEX. At 2 years of retention with a 7-day partial index freshly rebuilt, expect a size approximately 1/100th that of a full-table index on the same columns.&lt;/p&gt;

&lt;p&gt;To verify WAL reduction after option 4, compare &lt;code&gt;wal_bytes&lt;/code&gt; from &lt;code&gt;pg_stat_wal&lt;/code&gt; before and after dropping the FK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;wal_records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wal_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wal_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;wal_size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_wal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After dropping the FK, &lt;code&gt;wal_bytes&lt;/code&gt; growth rate should decrease measurably within a few minutes of sustained ingest. On older Postgres versions, check &lt;code&gt;pg_stat_bgwriter&lt;/code&gt; for write activity trends instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next step
&lt;/h2&gt;

&lt;p&gt;Run the &lt;code&gt;pg_stat_activity&lt;/code&gt; lock detection query during your next peak ingest window. If you see lock waits pointing at your &lt;code&gt;devices&lt;/code&gt; table, apply option 1 first: alter the FK to be deferrable and add &lt;code&gt;SET CONSTRAINTS ... DEFERRED&lt;/code&gt; to your bulk-insert transactions. It's a two-statement change with no impact on the integrity guarantee and no risk to other callers.&lt;/p&gt;

&lt;p&gt;If your ingest rate is still climbing and you're already on the optimization treadmill, the &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;Tiger Data free trial&lt;/u&gt;&lt;/a&gt; lets you validate ingest headroom on your own data.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>developers</category>
    </item>
    <item>
      <title>What's New in Tiger Cloud: Bigger Performance Gains, Wider Platform Reach, Better Visibility</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:51:12 +0000</pubDate>
      <link>https://dev.to/tigerdata/whats-new-in-tiger-cloud-bigger-performance-gains-wider-platform-reach-better-visibility-2pe0</link>
      <guid>https://dev.to/tigerdata/whats-new-in-tiger-cloud-bigger-performance-gains-wider-platform-reach-better-visibility-2pe0</guid>
      <description>&lt;p&gt;This year, we've focused on improving three areas that define the Tiger Cloud experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scale without splitting your architecture:&lt;/strong&gt; Compression becomes a performance advantage. &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; on compressed data run up to 160x faster, summary queries up to 70x faster. Storage scales to 80,000 IOPS and 64 TB on demand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend less time configuring, more time shipping:&lt;/strong&gt; Tiger Console auto-tunes hypertables, the &lt;a href="https://www.tigerdata.com/docs/integrate/connectors/source/sync-from-postgres" rel="noopener noreferrer"&gt;&lt;u&gt;PostgreSQL Source Connector&lt;/u&gt;&lt;/a&gt; moves data to Tiger Cloud without custom pipelines, and &lt;code&gt;pg_textsearch&lt;/code&gt; brings production-ready BM25 search natively in Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production-grade reliability, without the DIY tax:&lt;/strong&gt; Tiger Cloud handles data residency, network isolation, disaster recovery, and visibility so you don't have to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This past quarter we shipped deeper query engine optimizations in TimescaleDB, new regions, new enterprise networking options, and a long list of smaller improvements to how Tiger Console works day-to-day. Instead of listing every Tiger Cloud release on its own, here's what it adds up to, and why it matters: you can stay on Postgres as you scale, you'll spend less time configuring and more time shipping, and you get the reliability and visibility that time-series workloads actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale without splitting your architecture
&lt;/h2&gt;

&lt;p&gt;The moment analytical queries start competing with transactional ones, teams feel pressure to bolt on a separate analytical database. This quarter's TimescaleDB releases and storage upgrades ensure Postgres keeps scaling for time-series and analytical workloads instead of becoming the reason you re-architect. Here's what shipped, and why it matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run queries and writes directly on compressed data, without the performance tax
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.tigerdata.com/docs/build/how-to/basic-compression" rel="noopener noreferrer"&gt;&lt;u&gt;Compression&lt;/u&gt;&lt;/a&gt; used to mean a trade-off: a smaller footprint for slower access. With the release of &lt;a href="https://github.com/timescale/timescaledb/releases/tag/2.26.0" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB v2.26&lt;/u&gt;&lt;/a&gt;, that trade-off keeps shrinking. Aggregate queries like &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, &lt;code&gt;MAX&lt;/code&gt;, and &lt;code&gt;FIRST&lt;/code&gt;/&lt;code&gt;LAST&lt;/code&gt; now read straight from compressed metadata instead of decompressing full batches, up to 70x faster. Grouping with time_bucket() runs roughly 3.5x faster. Multi-column filters push down directly into compressed scans, cutting unnecessary decompression by half or more.&lt;/p&gt;

&lt;p&gt;Writes get the same treatment. &lt;a href="https://github.com/timescale/timescaledb/releases/tag/2.27.0" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB v2.27&lt;/u&gt;&lt;/a&gt; lets &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, and &lt;code&gt;UPSERT&lt;/code&gt; on compressed chunks skip decompressing data that can't match, so selective write operations run up to 160x faster. Query rewriting can automatically route matching aggregations to a continuous aggregate, and continuous aggregate refreshes can compress chunks as part of the same job instead of needing a separate policy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.tigerdata.com/docs/learn/continuous-aggregates" rel="noopener noreferrer"&gt;&lt;u&gt;Continuous aggregates&lt;/u&gt;&lt;/a&gt; are now more reliable at scale. We fixed three stability issues that were constraining them: a memory leak, query correctness edge cases, and a deadlock during concurrent refreshes. As a result, you can now push continuous aggregates harder without operational workarounds or special handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add full-text search without adding a search engine
&lt;/h3&gt;

&lt;p&gt;As part of the &lt;a href="https://github.com/timescale/pg_textsearch/releases#release-v1.0.0" rel="noopener noreferrer"&gt;&lt;u&gt;pg_textsearch v1.0.0&lt;/u&gt;&lt;/a&gt; release, &lt;a href="https://www.tigerdata.com/docs/deploy/tiger-cloud/tiger-cloud-aws/tiger-cloud-extensions/pg-textsearch" rel="noopener noreferrer"&gt;&lt;u&gt;BM25 full-text search&lt;/u&gt;&lt;/a&gt; now runs natively inside Postgres, and is production-ready. Add relevance-ranked search to your application without standing up and syncing a separate Elasticsearch cluster. In benchmarks at 138 million documents, &lt;code&gt;pg_textsearch&lt;/code&gt; ran up to 6.5x faster than ParadeDB on typical multi-word queries and sustained 8.7x higher concurrent throughput. It ships with an &lt;code&gt;&amp;lt;@&amp;gt;&lt;/code&gt; query syntax, a &lt;code&gt;bm25_force_merge()&lt;/code&gt; function for segment consolidation, and support for Postgres 17 and 18. One less system in your stack to operate and keep in sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scale storage on demand instead of provisioning for a peak that may not come
&lt;/h3&gt;

&lt;p&gt;Scale plan services can now choose between 16,000 and 40,000 IOPS with up to 1,500 MB/s of throughput. Enterprise plans go up to 80,000 IOPS and 2,000 MB/s, with total capacity up to 64 TB. Changes apply without downtime, and you pay only for the IOPS you use. &lt;a href="https://www.tigerdata.com/docs/build/data-management/storage/manage-storage#high-performance-storage-tier" rel="noopener noreferrer"&gt;&lt;u&gt;Size up as your workload grows instead of guessing at peak load today.&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Spend less time configuring, more time shipping
&lt;/h2&gt;

&lt;p&gt;None of the above matters much if half your week still goes to console configuration instead of building. The following updates hand more of that time back to you so you can focus on what matters: building your product, not configuring your database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build hypertables in a few clicks, without writing SQL
&lt;/h3&gt;

&lt;p&gt;Define hypertable columns directly in Tiger Console instead of writing SQL. Configure a columnstore in the same step. For the best performance, you can enable&lt;a href="https://www.tigerdata.com/docs/build/performance-optimization/improve-hypertable-performance#automated-tuning" rel="noopener noreferrer"&gt;&lt;u&gt;automated chunk tuning&lt;/u&gt;&lt;/a&gt; afterward, so you won't have to manually set chunk intervals.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdzegt3jgvt8fnb68oah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdzegt3jgvt8fnb68oah.png" alt="Build hypertables in a few clicks, without writing SQL" width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Move data from Postgres into Tiger Cloud without building your own pipeline
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://www.tigerdata.com/docs/integrate/connectors/source/sync-from-postgres" rel="noopener noreferrer"&gt;&lt;u&gt;PostgreSQL Source Connector&lt;/u&gt;&lt;/a&gt; is now stable and ready for production use. Replicate an existing Postgres database into Tiger Cloud without hand-rolling a migration or sync pipeline. It supports a configurable worker count for the initial data copy, table selection by publication or direct selection, SSH tunneling, and bulk updates for table and schema mappings. Everything you need to bring production data over reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-grade reliability, without the DIY tax
&lt;/h2&gt;

&lt;p&gt;Data residency, network isolation, disaster recovery, and visibility into what's happening inside your database are table stakes for any fully-managed platform. The whole point of choosing Tiger Cloud is that you shouldn't have to design, build, and maintain that infrastructure yourself. Here's what shipped this quarter that takes more of that off your plate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Meet data residency requirements in more places
&lt;/h3&gt;

&lt;p&gt;Tiger Cloud is now available in two additional Azure regions: Germany West Central (Frankfurt) and Southeast Asia (Singapore). Teams with GDPR-sensitive workloads can now keep EU data in-region, and Asia-Pacific teams get local data residency and lower latency, without moving off Azure. For a &lt;a href="https://www.tigerdata.com/docs/learn/tiger-cloud/regions" rel="noopener noreferrer"&gt;&lt;u&gt;list of all available regions, click here&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep database traffic off the public internet
&lt;/h3&gt;

&lt;p&gt;Private endpoint support is now generally available across every supported AWS and Azure region. &lt;a href="https://www.tigerdata.com/docs/deploy/tiger-cloud/tiger-cloud-aws/security/aws-privatelink" rel="noopener noreferrer"&gt;&lt;u&gt;AWS PrivateLink&lt;/u&gt;&lt;/a&gt; connects from your VPC over the AWS private network. &lt;a href="https://www.tigerdata.com/docs/deploy/tiger-cloud/tiger-cloud-azure/security/azure-privatelink" rel="noopener noreferrer"&gt;&lt;u&gt;Azure Private Link&lt;/u&gt;&lt;/a&gt; does the same from your VNet over Microsoft's private backbone. Both are configured directly in Tiger Console and included on Scale and Enterprise plans, so database traffic never has to touch the public internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recover easily when a region goes down
&lt;/h3&gt;

&lt;p&gt;Cross-region backup already copies your data to a geographically distant region. Now Enterprise customers can restore directly from that backup in Tiger Console, closing the loop so a regional outage doesn't mean opening a support ticket to get your data back. &lt;a href="https://www.tigerdata.com/docs/deploy/tiger-cloud/tiger-cloud-aws/service-management/fork-services#pitr-forks" rel="noopener noreferrer"&gt;&lt;u&gt;To learn more, click here.&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Catch problems in the tools you already use, before they escalate
&lt;/h3&gt;

&lt;p&gt;Visibility should be simple. You shouldn't have to wait for a problem to surface before you can see it coming. The Tiger Cloud status page now lives at &lt;a href="https://status.tigerdata.com" rel="noopener noreferrer"&gt;&lt;u&gt;status.tigerdata.com&lt;/u&gt;&lt;/a&gt;, tied directly into incident response, so you can subscribe and get notified the moment an incident is created, updated, or resolved instead of finding out from a support thread.&lt;/p&gt;

&lt;p&gt;Inside the &lt;a href="https://www.tigerdata.com/docs/deploy/tiger-cloud/tiger-cloud-aws/monitoring#metrics" rel="noopener noreferrer"&gt;&lt;em&gt;&lt;u&gt;Metrics&lt;/u&gt;&lt;/em&gt;&lt;/a&gt; tab in Tiger Console, a new Queries per Second graph gives a real-time view of throughput, making it easier to spot spikes or drops in query volume.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob69rtvlctg8rom5g7ox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob69rtvlctg8rom5g7ox.png" alt="Inside the Metrics tab in Tiger Console, a new Queries per Second graph gives a real-time view of throughput" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://www.tigerdata.com/docs/deploy/tiger-cloud/tiger-cloud-aws/monitoring#insights" rel="noopener noreferrer"&gt;&lt;em&gt;&lt;u&gt;Insights&lt;/u&gt;&lt;/em&gt;&lt;/a&gt; tab, the query deep dive page now tracks CPU, memory, and storage IO (read and write) over time, so you can catch a query's resource footprint trending the wrong way and see its downstream impact on system health before it turns into a bigger problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71yn98pm1y6vmxpom68n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71yn98pm1y6vmxpom68n.png" alt="In the Insights tab, the query deep dive page now tracks CPU, memory, and storage IO (read and write) over time" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, you can check the Chunk timeline in the &lt;a href="https://www.tigerdata.com/docs/deploy/tiger-cloud/tiger-cloud-aws/service-management/service-explorer" rel="noopener noreferrer"&gt;&lt;em&gt;&lt;u&gt;Explorer&lt;/u&gt;&lt;/em&gt;&lt;/a&gt; tab to see how your data is organized across chunks. You can inspect sizes, time ranges, and whether each chunk is in the &lt;a href="https://www.tigerdata.com/docs/learn/columnar-storage/understand-hypercore" rel="noopener noreferrer"&gt;&lt;u&gt;rowstore or columnstore&lt;/u&gt;&lt;/a&gt;. This lets you spot organization issues and monitor columnstore job health without running system queries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqhu3ucab8dh1m0z75l0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqhu3ucab8dh1m0z75l0.png" alt="check the Chunk timeline in the Explorer tab to see how your data is organized across chunks" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lastly, if you already monitor infrastructure elsewhere, Tiger Cloud now exports telemetry to &lt;a href="https://www.tigerdata.com/docs/integrate/observability-alerting/azure-monitor" rel="noopener noreferrer"&gt;&lt;u&gt;Azure Monitor&lt;/u&gt;&lt;/a&gt;, plus PostgreSQL-specific metrics (replication, cache usage, background activity) to Amazon CloudWatch, Datadog, and Prometheus, with system-level disk IO and throughput metrics exported by default. Wherever you already look for problems, Tiger Cloud's data is there too. For a &lt;a href="https://www.tigerdata.com/docs/integrate/observability-alerting/exported-metrics" rel="noopener noreferrer"&gt;&lt;u&gt;full list of available metrics you can export with Tiger Cloud exporters, click here&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Tiger Cloud for free
&lt;/h2&gt;

&lt;p&gt;All of these features are live on Tiger Cloud. If you're already a customer, &lt;a href="https://console.cloud.timescale.com/login" rel="noopener noreferrer"&gt;&lt;u&gt;sign in&lt;/u&gt;&lt;/a&gt; and check out the latest TimescaleDB releases on your compressed hypertables and the new IOPS options if you're storage-bound.&lt;/p&gt;

&lt;p&gt;If you're new to Tiger Cloud, &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;start a free trial&lt;/u&gt;&lt;/a&gt; and see what a Postgres-native operational analytics database looks like.&lt;/p&gt;

</description>
      <category>tigercloud</category>
      <category>tigerdata</category>
      <category>platforms</category>
    </item>
    <item>
      <title>The Data Layer for the AI Data Center</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:47:46 +0000</pubDate>
      <link>https://dev.to/tigerdata/the-data-layer-for-the-ai-data-center-4k5k</link>
      <guid>https://dev.to/tigerdata/the-data-layer-for-the-ai-data-center-4k5k</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part II of a two-part series on the AI data center stack. Part I,&lt;/em&gt; &lt;a href="https://www.tigerdata.com/blog/how-ai-rewired-the-data-center" rel="noopener noreferrer"&gt;&lt;em&gt;&lt;u&gt;AI's Physical Constraints: How AI Rewired the Data Center&lt;/u&gt;&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, explains why AI capacity has become constrained by the physical data center stack: accelerators, memory, cooling, power, time, and water.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Physical Plant Telemetry Is Now a Data-Layer Requirement
&lt;/h2&gt;

&lt;p&gt;In an AI data center, the workload no longer stops at the server boundary. A synchronized training job can move power, cooling, battery response, and power electronics in patterns that look less like conventional facility load and more like part of the compute system itself. The physical plant has become coupled to the computer's execution profile.&lt;/p&gt;

&lt;p&gt;This paper starts from the operational consequence: once those systems are coupled, their telemetry has to be correlated, retained, and queried together.&lt;/p&gt;

&lt;p&gt;That coupling changes what operational telemetry has to do.&lt;/p&gt;

&lt;p&gt;The plant is still operated through established OT patterns: PLCs, protection systems, BMS, PMS, CDU controllers, SCADA, message brokers, and layered networks. None of that goes away. What changes is the correlation requirement. Operators now need to ask questions that cross systems and timescales: what happened to facility load during a training phase transition, which cooling loop reacted first, which racks drove the phase imbalance, whether battery or UPS behavior aligned with GPU power movement, and how a hall-level event propagated into the campus electrical posture.&lt;/p&gt;

&lt;p&gt;Those are not dashboard-only questions. They are data-layer questions. The system has to ingest the raw signal, preserve its context, retain it long enough to matter, and make it queryable without moving the data out of the environment where operations run.&lt;/p&gt;

&lt;p&gt;The volume is assumed. NVIDIA DCGM exposes GPU telemetry through &lt;a href="https://docs.nvidia.com/datacenter/dcgm/latest/dcgm-api/dcgm-api-field-ids.html" rel="noopener noreferrer"&gt;&lt;u&gt;field identifiers&lt;/u&gt;&lt;/a&gt; and exporter paths covering clocks, power, thermals, energy, and fabric health, with third-party collector coverage describing thousands of metrics across GPU, MIG, NVLink, NVSwitch, and CPU scopes. The facility side adds liquid-cooling loop temperatures, pressures, flow, pump state, CDU status, leak detection, phase-level power, UPS and battery behavior, vibration, generator state, and grid-interface telemetry. &lt;a href="https://www.opencompute.org/documents/ocp-wp-dcf-improve-data-center-cooling-facility-efficiency-through-platform-power-telemetryr1-0-final-update-pdf" rel="noopener noreferrer"&gt;&lt;u&gt;OCP telemetry work&lt;/u&gt;&lt;/a&gt; catalogs base-building and data-hall points across utility, generation, central plant, power monitoring, cooling, environmental, and liquid-cooling systems.&lt;/p&gt;

&lt;p&gt;The harder issue is not only volume. It is a timescale mismatch. GPU power can change in seconds or less. Electrical protection events can unfold in cycles. Cooling loops respond more slowly. Building systems often trend at minute cadence. The data layer has to preserve those cadences without flattening them into averages that erase the sequence operators need to reconstruct. Traditional facility telemetry was often sampled at minute-level cadence because the use case was monitoring, trending, and capacity planning. OCP notes that minute-level power sampling was commonly used to reduce network impact. AI workload telemetry, by contrast, is routinely collected at one-second or finer resolution, and the workload itself moves in synchronized phases. A &lt;a href="https://arxiv.org/abs/2604.04745" rel="noopener noreferrer"&gt;&lt;u&gt;756-GPU academic cluster study&lt;/u&gt;&lt;/a&gt; collected 162 GB of per-second telemetry over 31 days. &lt;a href="https://newsletter.semianalysis.com/p/ai-training-load-fluctuations-at-gigawatt-scale-risk-of-power-grid-blackout" rel="noopener noreferrer"&gt;&lt;u&gt;SemiAnalysis&lt;/u&gt;&lt;/a&gt;, citing Meta's Llama 3 infrastructure, describes tens of megawatts of instantaneous power fluctuation from synchronized GPU behavior on a 24,000-H100-class cluster with about 30 MW of IT capacity. &lt;a href="https://engineering.fb.com/2024/03/12/data-center-engineering/building-metas-genai-infrastructure/" rel="noopener noreferrer"&gt;&lt;u&gt;Meta separately documented two 24,576-GPU clusters&lt;/u&gt;&lt;/a&gt; used for Llama 3 training. &lt;a href="https://www.nerc.com/globalassets/our-work/reports/event-reports/incident_review_large_load_loss.pdf" rel="noopener noreferrer"&gt;&lt;u&gt;NERC's July 10, 2024 incident review&lt;/u&gt;&lt;/a&gt; documented approximately 1,500 MW of customer-initiated, voltage-sensitive load reduction after a 230 kV transmission fault sequence, six faults in an 82-second period.&lt;/p&gt;

&lt;p&gt;The operational result is simple and uncomfortable: slow facility telemetry and fast workload telemetry now have to live in one data layer, with enough fidelity to be queried together. The data model has to respect OT boundaries, preserve local autonomy, and still support building, campus, and enterprise rollup. It has to fit the systems already in place rather than ask the facility to reorganize itself around a database.&lt;/p&gt;

&lt;p&gt;That is the architecture this paper lays out: a PostgreSQL-native time-series layer, implemented with &lt;a href="https://www.tigerdata.com/docs" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB&lt;/u&gt;&lt;/a&gt;, present at each Purdue scope, behind SCADA and the operational applications rather than in place of them, integrated through OT protocols, compressed and rolled up the hierarchy on premises, and optionally synchronized to a managed cloud only when policy allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data-Layer Requirements
&lt;/h2&gt;

&lt;p&gt;The reference architecture starts with requirements, because the failure mode is rarely a missing connector or a single slow query. It is a data layer that satisfies one requirement while violating another. For AI data center operations, the following have to hold together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sustained high-frequency, high-cardinality ingest.&lt;/strong&gt; The ingest path must support continuous streams from GPUs, power chain, cooling chain, BMS, PMS, CDU, leak detection, vibration, battery, and grid-interface systems, and keep insert behavior stable as tags, devices, tenants, halls, and derived metrics grow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Years of history online and affordable.&lt;/strong&gt; Operators need recent high-resolution data for incident response, but capacity planning, model-based optimization, energy analysis, and failure prediction depend on long histories. Aging data cannot disappear into cold archives that need a separate restore path before they can answer a question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrent real-time and analytical access.&lt;/strong&gt; The same operational data estate has to serve live dashboards, alarm investigation, root-cause analysis, fleet comparison, efficiency studies, and long-running analytical queries. Isolation matters, but copying data into disconnected stores for every audience defeats the point of a facility-wide operating record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge-to-enterprise rollup with local autonomy.&lt;/strong&gt; Data is born locally. A hall must keep recording if the building rollup is unavailable, and a building must keep operating if the campus link is down. Rollup should be delayed, replayed, and reconciled without changing the identity or semantics of the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reliability and durability as baseline behavior.&lt;/strong&gt; High availability, automatic failover, incremental backup, point-in-time restore, and operational visibility are not convenience features in this environment. They are part of the minimum viable design for operational infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment where the data lives.&lt;/strong&gt; The primary target is on-premises, at the edge, in the data center, or in a customer-managed environment. Air-gapped and intermittently connected sites must be first-class designs, not exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Architecture: A Time-Series Data Layer at Every Purdue Scope
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorage.ghost.io%2Fc%2F6b%2Fcb%2F6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e%2Fcontent%2Fimages%2F2026%2F07%2Fdiagram-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorage.ghost.io%2Fc%2F6b%2Fcb%2F6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e%2Fcontent%2Fimages%2F2026%2F07%2Fdiagram-1.png" alt="AI data center telemetry stack across Purdue operating scopes" width="800" height="646"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;The AI Data Center Telemetry Stack. A time-series data layer sits behind SCADA, HMI, MQTT, and operational applications at the hall and building scopes. Deterministic control stays local. Operational data rolls up on premises from hall to building to campus to enterprise, with optional managed-cloud synchronization shown as a separate path. Purdue layers are logical operating scopes; actual network segmentation varies by site.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The reference architecture places a time-series data layer at each operational scope of the Purdue model. Treat those scopes as logical, not as strict network boundaries. In many industrial environments Purdue Layers 2 and 3 share a subnet or operational network, so the layers describe responsibility and rollup position, not mandatory physical segmentation. This does not move control into the database. Deterministic control loops stay in PLCs, controllers, protection systems, BMS, PMS, CDU controllers, and SCADA. The database observes, stores, aggregates, and serves. It does not sit in the deterministic control path.&lt;/p&gt;

&lt;p&gt;The key architectural choice is consistency: the same time-series technology runs at each scope, scaled to that scope's responsibility, with data rolling up the hierarchy. Rollup becomes a native data architecture instead of a chain of one-off translations between incompatible stores.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layers 0 and 1: instrumentation, control, and local capture
&lt;/h3&gt;

&lt;p&gt;Field devices and control assets produce the raw state: meters, breaker monitors, rPDUs, UPS telemetry, CDUs, leak sensors, valve positions, pump speeds, pressure transducers, flow meters, temperature and vibration sensors, PLC tags, BMCs, and GPU node telemetry. Collection happens through industrial gateways, collectors, or local agents adjacent to the control network.&lt;/p&gt;

&lt;p&gt;Accepted ingress patterns include OPC UA for structured industrial data, Modbus and BACnet where building and equipment systems already expose them, MQTT for brokered publish and subscribe, Redfish for server and hardware management telemetry, and SNMP where power and network equipment still emit it. &lt;a href="https://www.opencompute.org/documents/ocp-wp-dcf-improve-data-center-cooling-facility-efficiency-through-platform-power-telemetryr1-0-final-update-pdf" rel="noopener noreferrer"&gt;&lt;u&gt;OCP's telemetry guidance&lt;/u&gt;&lt;/a&gt; explicitly covers MQTT, Redfish, TLS and mTLS, push and pull, publish and subscribe, Modbus, BACnet, segmentation, and secure data exchange across the OT and IT boundary.&lt;/p&gt;

&lt;p&gt;At this layer the local database footprint can be small. Its job is short-horizon buffering, timestamp integrity, store-and-forward behavior, and protection against upstream loss. It should run on industrial PCs or local edge servers without introducing a dependency into the control loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: hall operations and the hall historian
&lt;/h3&gt;

&lt;p&gt;Layer 2 is the hall-level operating environment. This is where the supervisory and application systems live: SCADA and HMIs, Ignition, MQTT brokers, and the hall-level applications operators work day to day. The time-series data layer does not replace any of them. It sits behind and alongside them as &lt;a href="https://www.tigerdata.com/learn/scada-data-management-at-scale-architecture-historians-and-the-modern-database" rel="noopener noreferrer"&gt;&lt;u&gt;the hall historian&lt;/u&gt;&lt;/a&gt;: the durable, queryable record of what the hall did.&lt;/p&gt;

&lt;p&gt;A hall-level time-series store is the reference pattern, because it gives each hall a complete, durable local record and lets it stay available on its own. Not every deployment starts there. Some facilities run the data layer at the building scope and above and aggregate hall telemetry there, then add hall-level stores as availability requirements, complexity, and the need for local autonomy grow. The architecture supports both: the hall tier scales in where those requirements justify it, while the rollup hierarchy stays the same.&lt;/p&gt;

&lt;p&gt;Where deployed, each hall-level time-series store ingests high-frequency telemetry for that hall, receiving it through Ignition gateways, MQTT brokers, OPC UA bridges, protocol gateways, collectors, and application connectors. SCADA remains the supervisory environment, the HMIs remain the operator interface, and MQTT remains the brokered transport where it is already used. Against that, TimescaleDB provides durable high-resolution history, SQL access, compression, continuous aggregates, and local query performance. It serves historical queries behind the local dashboards and preserves operational history during disconnection from upper scopes: if the link to building or campus is unavailable, Layer 2 keeps recording. SCADA and the HMIs remain the operator's real-time supervisory surface; the historian is what they query when the question is what happened, when, and in what order. Order matters here, and so does context: because TimescaleDB is PostgreSQL, writes land in ACID transactions, and the historian can preserve the recorded sequence with the timestamp, source, and quality context needed to reconstruct event order.&lt;/p&gt;

&lt;p&gt;This is where high-cardinality ingest matters most. A hall can contain thousands of accelerators, dense liquid-cooling loops, high-frequency power instrumentation, and multiple local systems previously operated in separate views. The Layer 2 historian should answer immediate operator questions: what changed in this hall, which CDU loop moved first, which racks saw the phase imbalance, what the GPU power profile did, and which protection or backup systems responded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layers 2.5 and 3: sub-zone, building, and SCADA adjacency
&lt;/h3&gt;

&lt;p&gt;Layers 2.5 and 3 aggregate across halls, rooms, mechanical zones, electrical lineups, and building systems. In practice these scopes often share an operational network with Layer 2 rather than sitting on a separate tier; the distinction is one of rollup responsibility, not necessarily of subnet. This is the integration point for SCADA, BMS, PMS, DCIM, MQTT brokers, message buses, and operational applications.&lt;/p&gt;

&lt;p&gt;The data layer sits alongside SCADA, not in place of it. SCADA remains the supervisory interface and control environment at both the hall and building scopes. The time-series layer is the durable, queryable substrate behind real-time views, historical analysis, external reporting, and analytics.&lt;/p&gt;

&lt;p&gt;The strongest proof point for this fit is the &lt;a href="https://www.tigerdata.com/newsroom/inductive-automation-and-tiger-data-collaborate-to-modernize-the-industrial-historian-market" rel="noopener noreferrer"&gt;&lt;u&gt;strategic alliance between Inductive Automation and Tiger Data&lt;/u&gt;&lt;/a&gt;, the company behind TimescaleDB, announced in April 2026 to modernize the industrial historian. Inductive Automation makes Ignition, a widely deployed industrial application and SCADA platform; &lt;a href="https://www.tigerdata.com/newsroom/tiger-data-launches-timescaledb-enterprise-a-self-managed-time-series-database-built-for-on-premises-and-edge-deployment" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB Enterprise&lt;/u&gt;&lt;/a&gt; (in early access; &lt;a href="http://design-partner-signup-tbd" rel="noopener noreferrer"&gt;&lt;u&gt;sign up to become a design partner&lt;/u&gt;&lt;/a&gt;) is an on-prem PostgreSQL-based time-series database that serves as the historian behind it, for on-premises and edge deployment. For operators already standardizing on Ignition, that makes the database an integrated historian path rather than a parallel system the operations team has to glue together alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layers 4 and 5+: campus, enterprise, and cross-site analytics
&lt;/h3&gt;

&lt;p&gt;At Layer 4 and Layer 5+, data rolls up for campus operations, cross-building comparison, capacity planning, reporting, fleet analytics, and AI or machine-learning workflows. This scope does not need every raw sample forever at the hottest resolution. It needs governed rollups, selected raw windows, and enough fidelity to reconstruct operational behavior when an incident crosses halls, buildings, or grid boundaries.&lt;/p&gt;

&lt;p&gt;The rollup itself is the heart of the architecture, and it runs on continuous synchronization between scopes: hall to building, building to campus, campus to enterprise. Where supported, planned synchronization capabilities move selected raw streams, rollups, and metadata upward between TimescaleDB stores, so an upper scope holds a faithful, current view without the lower scope losing autonomy or identity. Critically, this rollup stays inside the operator's own infrastructure. It does not require the cloud.&lt;/p&gt;

&lt;p&gt;Moving data from the on-premises hierarchy to Tiger Cloud is a separate concern. For organizations that want managed cross-site analytics, planned synchronization capabilities can provide an optional path to Tiger Cloud, where &lt;a href="https://www.tigerdata.com/docs/learn/data-lifecycle/storage/about-storage-tiers" rel="noopener noreferrer"&gt;&lt;u&gt;tiered storage&lt;/u&gt;&lt;/a&gt; keeps frequently queried data in a high-performance tier and moves older data to object storage. Tiger Cloud documentation describes up to 64 TB in the high-performance tier depending on plan, with a low-cost object storage tier behind it. That path is optional and cloud-bound. The on-premises hierarchy is complete without it: for sites that cannot send operational data out, the full edge-to-enterprise rollup still runs, entirely self-managed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operator Scenario: Reconstructing a Load Event
&lt;/h2&gt;

&lt;p&gt;A training job enters a synchronized phase. GPU power draw rises across a hall, phase-level power telemetry moves, one CDU loop responds before the others, and UPS or battery telemetry shows a corresponding event. The hall historian preserves the high-resolution sequence locally. The building rollup shows whether the response crossed halls or stayed local. The campus view shows whether the event aligned with broader electrical posture. The operator does not need five disconnected exports to reconstruct the event. The data layer preserves source, timestamp, unit, quality, and lineage so the question can be asked across systems without losing the order of operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why TimescaleDB Fits This Architecture
&lt;/h2&gt;

&lt;p&gt;The requirements above point to a single system. The data layer has to speak SQL, keep time-series data bounded as it grows, compress years of history into affordable storage, roll up cleanly from edge to enterprise, and run reliably on hardware the operator controls. TimescaleDB is a PostgreSQL extension that does all of that in one engine.&lt;/p&gt;

&lt;p&gt;Because it is an extension and not a fork, it keeps standard PostgreSQL clients, drivers, and SQL, and adds hypertables, continuous aggregates, compression, and retention. The data layer fits the tooling operations and analytics teams already run: drivers, BI tools, Grafana, backup tooling, access-control patterns, and the operational knowledge already in the building. There is no proprietary query language to learn and no silo that only one vendor's tools can read.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.tigerdata.com/docs/learn/hypertables/understand-hypertables" rel="noopener noreferrer"&gt;&lt;u&gt;Hypertables&lt;/u&gt;&lt;/a&gt; answer the volume problem. They partition by time, so ingest, retention, compression, and queries operate over chunks instead of one ever-growing table that eventually degrades. &lt;a href="https://www.tigerdata.com/docs/learn/continuous-aggregates" rel="noopener noreferrer"&gt;&lt;u&gt;Continuous aggregates&lt;/u&gt;&lt;/a&gt; answer the rollup requirement. They maintain incrementally refreshed views, including rollups over rollups, which maps directly onto the Purdue hierarchy: raw hall data rolls into minute, hourly, and daily views; building aggregates roll into campus views; selected metrics feed enterprise models.&lt;/p&gt;

&lt;p&gt;Compression is what makes long online retention practical. Tiger Data's &lt;a href="https://www.tigerdata.com/blog/how-timescaledb-expands-postgresql-iiot-performance-envelope" rel="noopener noreferrer"&gt;&lt;u&gt;IIoT performance work&lt;/u&gt;&lt;/a&gt; reports common compression ratios of 80 to 95 percent, with a terabyte of raw data compressing to roughly 50 to 100 GB. &lt;a href="https://www.tigerdata.com/docs/learn/columnar-storage/understand-hypercore" rel="noopener noreferrer"&gt;&lt;u&gt;Hypercore&lt;/u&gt;&lt;/a&gt;, the hybrid row and columnar engine behind this, lands new data in a row-oriented path for ingest and updates, then moves older data into columnar storage for compression and analytical scans. One table serves both the operator querying the last hour and the analyst scanning the last year.&lt;/p&gt;

&lt;p&gt;On ingest, the honest claim is sustained production scale, not a record for peak rows per second on a narrow benchmark. The partitioning model exists to prevent the failure mode where a single PostgreSQL table grows until inserts and queries fall over. &lt;a href="https://www.tigerdata.com/blog/introducing-direct-compress-up-to-40x-faster-leaner-data-ingestion-for-developers-tech-preview" rel="noopener noreferrer"&gt;&lt;u&gt;Direct Compress&lt;/u&gt;&lt;/a&gt;, a TimescaleDB 2.21 tech preview from September 2025, compresses data in memory during COPY ingestion and reports up to 40x faster ingestion in its benchmark scenario, with the usual caveat that schema, batching, storage, and workload shape change the result. AI data center telemetry is won by keeping ingest, SQL access, compression, rollup, retention, security, and operations in one reliable system, not by a drag race.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability Where Operations Actually Run
&lt;/h2&gt;

&lt;p&gt;The guarantees you were promised, on your own hardware.&lt;/p&gt;

&lt;p&gt;Operations teams have spent years being told that critical data infrastructure should behave like a managed service: high availability, automatic failover, incremental backup, point-in-time restore, operational dashboards, controlled upgrades, and recovery that does not depend on a hero at 3 a.m. The requirement is right. The deployment assumption is wrong. Many AI data center environments cannot make a managed public cloud service the dependency of record for OT data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.tigerdata.com/newsroom/tiger-data-launches-timescaledb-enterprise-a-self-managed-time-series-database-built-for-on-premises-and-edge-deployment" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB Enterprise&lt;/u&gt;&lt;/a&gt; is the self-managed answer to that constraint: the open-source TimescaleDB engine plus the operations layer, licensed for on-premises, edge, and customer-managed cloud, and built to run air-gapped. The operations layer is the point. High-availability clustering, automatic failover, fully incremental backups, and point-in-time recovery put managed-grade behavior on hardware the operator owns. A web-based admin console and pre-configured Grafana dashboards make that behavior visible instead of tribal.&lt;/p&gt;

&lt;p&gt;Because the engine is the same at every scope, each layer can be operated to the criticality of its function. A Layer 2 hall historian runs locally and stays durable through an upstream outage. A Layer 3 building store runs with replicas and failover. A Layer 4 campus store aggregates across buildings without forcing every hall to depend on the campus link. Backup and restore are part of the operating model, not an export script someone has to remember to run.&lt;/p&gt;

&lt;p&gt;Two data paths leave each site, and the distinction matters. On-premises rollup moves selected raw streams, rollups, and metadata up the hierarchy while each local system stays complete on its own. Cloud synchronization is a separate, optional path for organizations that want cross-site analytics in Tiger Cloud. For air-gapped sites, regulated sites, and facilities where policy, latency, or operational independence keeps OT data local, the rollup path runs without the cloud path ever being enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operating Model and Guardrails
&lt;/h2&gt;

&lt;p&gt;The architecture should be deployed with clear boundaries.&lt;/p&gt;

&lt;p&gt;First, keep the database out of the control loop. It can record telemetry and serve queries, but a database issue must never affect trip logic, PLC scans, protective relays, CDU control, or BMS control loops.&lt;/p&gt;

&lt;p&gt;Second, keep the storage technology consistent across scopes. When the same time-series engine runs at the hall, the building, the campus, and the enterprise, a query written against a hall historian stays valid against a campus rollup, and identities, units, and retention policies carry upward without reinterpretation. Consistency at this level is what turns rollup from an integration project into a property of the system.&lt;/p&gt;

&lt;p&gt;Third, keep identities stable across rollup. A tag, device, rack, CDU, UPS, hall, tenant zone, or GPU should not get a new identity at every layer. The rollup path should preserve source, timestamp, quality, unit, and lineage.&lt;/p&gt;

&lt;p&gt;Fourth, design for replay. Local stores need bounded queues and retention windows that let them backfill upstream systems after a link outage. The system should assume disconnection, not merely tolerate it.&lt;/p&gt;

&lt;p&gt;Fifth, separate raw, operational, and analytical views. Operators need recent raw and high-resolution data; building and campus teams need rollups; enterprise analytics needs governed datasets. Hypertables, compression policies, retention policies, and continuous aggregates provide the database-level primitives for that separation.&lt;/p&gt;

&lt;p&gt;Finally, make reliability visible. HA state, replica lag, backup freshness, restore validation, ingest lag, compression job health, disk headroom, and query saturation belong on the operational dashboard. A database holding operational telemetry becomes part of the facility's own instrumentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Operator's Goal
&lt;/h2&gt;

&lt;p&gt;The data layer is not the goal. Energizing the build is the goal. Keeping the facility stable through synchronized workload behavior is the goal. Running power and cooling close to the real operating envelope, without sacrificing margin blindly, is the goal. Planning the next tranche of capacity with evidence instead of guesswork is the goal.&lt;/p&gt;

&lt;p&gt;A time-series layer earns its place only if it serves those outcomes. It has to fit the SCADA and protocol stack already in place. It has to keep years of history online without making storage economics impossible. It has to serve real-time and analytical access without splitting the operating record. It has to roll up from edge to enterprise while letting every local layer keep running when the link drops. And it has to deliver reliability and durability on hardware the operator controls.&lt;/p&gt;

&lt;p&gt;That is the reference architecture: TimescaleDB as the PostgreSQL-native time-series data layer, present at each Purdue scope, sitting behind SCADA and the operational applications rather than replacing them, integrated through OT protocols, compressed and rolled up the hierarchy on premises from hall to building to campus to enterprise, operated locally, and optionally synchronized to Tiger Cloud only when policy allows.&lt;/p&gt;

&lt;p&gt;The physical plant is now part of the computer. The data layer has to be built like that is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Interested in managed time-series analytics? Start a free &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;Tiger Cloud trial&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Running on-premises, at the edge, or in an air-gapped environment? TimescaleDB Enterprise is built for those deployments and is accepting &lt;a href="https://www.tigerdata.com/timescaledb-enterprise#form-section" rel="noopener noreferrer"&gt;&lt;u&gt;design partners&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>datacenters</category>
      <category>developers</category>
      <category>telemetry</category>
    </item>
    <item>
      <title>AI's Physical Constraints: How AI Rewired the Data Center</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:34:34 +0000</pubDate>
      <link>https://dev.to/tigerdata/ais-physical-constraints-how-ai-rewired-the-data-center-4odj</link>
      <guid>https://dev.to/tigerdata/ais-physical-constraints-how-ai-rewired-the-data-center-4odj</guid>
      <description>&lt;p&gt;For most of the cloud era, a server rack was a five to twenty kilowatt object. You could fill a room with them, move air across the front, and the building stayed an ordinary building. A single current AI rack, NVIDIA's GB300 NVL72, draws about 132 to 140 kilowatts, with the GPUs alone accounting for more than a hundred. That is close to an order of magnitude more power in the same floor space as those old racks, and it lands as heat in the same small volume. Past roughly a hundred kilowatts per rack, air stops being able to carry the heat out, and the rack has to be plumbed for liquid. The compute got denser and the building changed with it.&lt;/p&gt;

&lt;p&gt;This pattern repeats all the way out to the grid. For about fifteen years, getting more computing power felt like turning a dial. You needed more, you asked for more, and a few seconds later it was there. Spin up a hundred servers for a traffic spike, spin them back down when it passes. Capacity behaved like something continuous, instant, and reversible, a knob you turned rather than a thing you built. A generation of software was designed on that assumption, and it held, because for ordinary workloads the power and hardware involved were small against what the world could supply.&lt;/p&gt;

&lt;p&gt;That has changed. Across AI infrastructure projects, the same moment now repeats. A team asks for more, and the answer comes back no. Not "no, that costs more," which everyone understands, but a harder no. "No, those GPUs are not available this quarter." "No, that region has no more power, and will not for years." You can order a GPU in a day; the date that a few hundred megawatts arrives at a site can be four or five years out, and no amount of money moves it sooner. The request that used to be a billing question has become a physical one.&lt;/p&gt;

&lt;p&gt;Anyone who has designed, built, or run a data center knows the physical layer was always there. The people who design and build these facilities sized the transformers, ordered the switchgear, planned the cooling, and waited on the utility. What is new is the scale at which AI hits the physical layer. The data centers going up for AI are a different class of build: denser, hotter, hungrier, and more tightly coupled to the grid than the ones that came before.&lt;/p&gt;

&lt;p&gt;Data centers always needed chips, memory, cooling, power, and water. Most cloud workloads before the AI surge kept those requirements in a range the existing build could absorb. AI pushes them past the thresholds where the old assumptions hold. It does not create a new kind of physics. It removes the buffer that made the physics easy to ignore.&lt;/p&gt;

&lt;p&gt;Each of these limits has been written about on its own. What gets missed is how they &lt;em&gt;connect&lt;/em&gt;, and why AI makes them arrive together. AI scaling moves through a physical dependency chain: more accelerators require scarce chip packaging and memory; more memory and compute concentrate heat; concentrated heat changes the rack and the building; the building then needs power the grid may take years to deliver; at that scale, the power itself may have to be buffered inside the facility; and the cooling choices made along the way determine where water becomes a problem. The limits arrive in a predictable order, and the order is the story:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;GPUs.&lt;/strong&gt; The first visible shortage is accelerators, the GPUs that do the AI computation, but the real bottleneck sits &lt;em&gt;around&lt;/em&gt; the chip, not &lt;em&gt;in&lt;/em&gt; it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory.&lt;/strong&gt; The accelerator depends on high-bandwidth memory, which pulls on the same finite wafer base as ordinary memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cooling.&lt;/strong&gt; More compute and memory in the same space means more heat in the same rack, past what air can carry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power and time.&lt;/strong&gt; Liquid cooling moves heat, but every watt still has to come from the grid. First you wait for power to arrive; then, at AI scale, you may need to buffer the workload's own power swings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Water.&lt;/strong&gt; Not the national catastrophe the headlines suggest, but a local siting constraint shaped by cooling design.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's start with the one everyone already knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  GPUs: The Bottleneck Is Not the Chip
&lt;/h2&gt;

&lt;p&gt;The first wall everyone notices is GPUs. You cannot get them, you cannot get enough, or the price to rent them has climbed since last year. The figures are not subtle. H100 rental prices rose roughly 40 percent off their late-2025 lows in a matter of months, &lt;a href="https://newsletter.semianalysis.com/p/the-great-gpu-shortage-rental-capacity" rel="noopener noreferrer"&gt;&lt;u&gt;from about $1.70 to $2.35 per GPU-hour on one-year contracts&lt;/u&gt;&lt;/a&gt; between October 2025 and March 2026, and on-demand capacity is effectively sold out across GPU types. The pressure reaches the workstation end too. In June 2026 NVIDIA listed its &lt;a href="https://www.tomshardware.com/pc-components/gpus/nvidia-raises-rtx-pro-6000-blackwell-gpu-pricing-to-usd13-250-55-percent-increase-over-msrp-in-a-years-time" rel="noopener noreferrer"&gt;&lt;u&gt;RTX Pro 6000 Blackwell at $13,250&lt;/u&gt;&lt;/a&gt;, a 55 percent jump over the $8,565 launch price a year earlier, and the reason it gave was the 96 gigabytes of memory on the card in a market where memory is scarce.&lt;/p&gt;

&lt;p&gt;The obvious reading is that NVIDIA cannot make enough chips, but that is not where the bottleneck lives. A modern AI accelerator is not one chip but a package: the processor die, stacks of high-bandwidth memory (HBM), and an interposer that wires them together at enormous bandwidth. A faster processor does not help if it cannot be packaged with memory, and advanced packaging capacity is finite, specifically the chip-on-wafer-on-substrate (CoWoS) lines at TSMC. The same &lt;a href="https://newsletter.semianalysis.com/p/the-great-gpu-shortage-rental-capacity" rel="noopener noreferrer"&gt;&lt;u&gt;analysis that tracked the rental spike&lt;/u&gt;&lt;/a&gt; named CoWoS packaging and HBM, not the processor, as the choke points. The lead times that stretch GPU orders toward a year are gated there. The chip is not the scarce thing; what surrounds it is.&lt;/p&gt;

&lt;p&gt;So the GPU shortage is really a packaging and memory shortage wearing a GPU label. Packaging capacity can expand, but it expands on a manufacturing clock. Memory is the harder half, and the reasons it stays scarce are the next wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory: Why the Price Will Not Come Down
&lt;/h2&gt;

&lt;p&gt;When a component spikes in price, the reflex is to wait. Shortages end, factories ramp, the price comes back down. That reflex is wrong here, and the reason is structural, not cyclical.&lt;/p&gt;

&lt;p&gt;The memory AI systems need comes in two kinds. HBM is the fast, expensive memory stacked right next to the accelerator, where the model's working data lives during computation. Dynamic random-access memory (DRAM) is the ordinary system memory around it. The binding shortage is HBM, and the pressure spills into DRAM because both pull on the same finite wafer base. SK Hynix, the leading maker of HBM, &lt;a href="https://www.notebookcheck.net/SK-hynix-sells-out-its-DRAM-NAND-and-HBM-chip-supply-to-Nvidia-through-2026-as-AI-demand-outpaces-Samsung-and-Micron-s-capacity.1151402.0.html" rel="noopener noreferrer"&gt;&lt;u&gt;locked up its 2026 HBM output&lt;/u&gt;&lt;/a&gt; well ahead of the year, and Micron has likewise reported its 2026 HBM sold out. Memory has gone from a rounding error in a machine's bill of materials to the single largest driver of the price on a high-end card.&lt;/p&gt;

&lt;p&gt;So why not just make more? Essentially all the world's DRAM comes from &lt;a href="https://www.techtimes.com/articles/318052/20260609/samsung-leads-dram-market-share-386-sk-hynix-trails-revenue-tops-profit-margins.htm" rel="noopener noreferrer"&gt;&lt;u&gt;three companies&lt;/u&gt;&lt;/a&gt;: Samsung and SK Hynix in South Korea, and Micron in the United States. When all three make the same allocation call at once, that is the global supply. There is no fourth maker at comparable scale waiting to undercut them.&lt;/p&gt;

&lt;p&gt;Adding supply means building a fabrication plant, and a fab is not a factory you stand up in a quarter. It takes years of cleanroom construction, tool installation, and qualification before a single sellable chip comes out. HBM makes the squeeze worse, not better: each gigabyte of it uses about three times the wafer capacity of DDR5, today's standard volume DRAM, so every wafer redirected to the scarce thing makes the common thing scarcer still, and HBM already consumes &lt;a href="https://tech-insider.org/memory-chip-shortage-2026-ai-consumer-electronics/" rel="noopener noreferrer"&gt;&lt;u&gt;roughly a quarter of all DRAM wafer output&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is a second reason, and it is a choice rather than a constraint. The makers are steering wafers toward high-margin AI and enterprise memory and away from everything else, because that is where the money is. A new fab does not automatically reverse that, because the same margin logic governs what it chooses to build. IDC, the market-research firm, &lt;a href="https://tech-insider.org/memory-chip-shortage-2026-ai-consumer-electronics/" rel="noopener noreferrer"&gt;&lt;u&gt;projects 2026 DRAM supply growth of only about 16 percent year over year&lt;/u&gt;&lt;/a&gt;, well below the 20 to 30 percent that was historically normal, even as demand for the AI variety grows far faster. The people running these companies are saying so directly. Intel's chief executive, Lip-Bu Tan, relayed in February 2026 what two of the key memory makers had told him: there is &lt;a href="https://www.bloomberg.com/news/articles/2026-02-03/intel-ceo-says-there-s-no-relief-on-memory-shortage-until-2028" rel="noopener noreferrer"&gt;&lt;u&gt;no relief until 2028&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The makers could produce more, but it takes years, only three companies do it at global scale, the AI memory eats three times the wafers, and they earn far more selling to AI than to you. Part of the shortage is physics, the supply is simply years out. Part of it is choice: the capacity that does exist is being pointed at AI, not at you. You have not been priced out for a quarter. &lt;strong&gt;You have been outbid.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cooling: Why the Rack Changed Shape
&lt;/h2&gt;

&lt;p&gt;GPUs and memory are still things you buy, even when you have to wait. The next wall is different. The same density that makes AI systems powerful, more transistors on the die and more memory stacked beside it, becomes heat the moment the hardware enters a building.&lt;/p&gt;

&lt;p&gt;Every watt of power a machine draws comes back out as heat. Whatever goes in has to come out, or the machine cooks itself. It sounds too simple to matter, and it is the whole reason the rack changed shape.&lt;/p&gt;

&lt;p&gt;For most of the history of computing, taking the heat out meant moving air. Servers in a rack, cool air through the room, and that was enough. At the rack densities of the CPU era, even at the top of the range, the airflow was manageable enough that the building stayed recognizably the same kind of building: rows of racks, cold aisles, hot aisles, chillers, and fans. Anyone who built those rooms knows the envelope.&lt;/p&gt;

&lt;p&gt;AI did not make heat new. It made heat &lt;em&gt;dense&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;All the capability crammed into the die and the memory turns into heat in the same small volume. The &lt;a href="https://www.nvidia.com/en-us/data-center/gb300-nvl72/" rel="noopener noreferrer"&gt;&lt;u&gt;rack densities from the opening&lt;/u&gt;&lt;/a&gt;, an order of magnitude higher than the CPU era, are really heat-removal figures: every one of those kilowatts has to be carried back out. The next generation on the roadmap, the Vera Rubin systems, is projected to push per-rack density several times higher again, and &lt;a href="https://www.tomshardware.com/pc-components/cooling/cooling-system-for-a-single-nvidia-blackwell-ultra-nvl72-rack-costs-a-staggering-usd50-000-set-to-increase-to-usd56-000-with-next-generation-nvl144-racks" rel="noopener noreferrer"&gt;&lt;u&gt;cooling vendors are already designing for the increase&lt;/u&gt;&lt;/a&gt;. At those densities, air cooling stops being practical. Bigger fans do not solve the volume problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorage.ghost.io%2Fc%2F6b%2Fcb%2F6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e%2Fcontent%2Fimages%2F2026%2F07%2Fdiagram-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorage.ghost.io%2Fc%2F6b%2Fcb%2F6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e%2Fcontent%2Fimages%2F2026%2F07%2Fdiagram-2.png" alt="Rack power density by generation, air vs. liquid cooling threshold" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Figure 1: Rack power density by generation. Past roughly 100 kilowatts, air cooling stops working and liquid becomes mandatory. The Rubin Ultra figure is a roadmap projection.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;So the machine changed shape. A given volume of air can carry only so much heat away before it has to move faster than is practical, while water carries roughly three to four thousand times as much heat per unit volume as air. Past roughly a hundred kilowatts per rack, that gap stops being an efficiency question and becomes a hard limit, and the model flips: from moving air through a room to carrying heat away in liquid piped directly to the chip. The newest systems do not offer an air-cooled option at all. The GB300 NVL72 is &lt;a href="https://www.nvidia.com/en-us/data-center/gb300-nvl72/" rel="noopener noreferrer"&gt;&lt;u&gt;fully liquid-cooled&lt;/u&gt;&lt;/a&gt;. The rack is no longer just electrical equipment. It now has plumbing.&lt;/p&gt;

&lt;p&gt;Capital helps with procurement and retrofits. It does not change the thermal limits of air. This wall is geometry and thermodynamics. And it has a consequence even experienced operators feel: the hardware no longer fits in most existing buildings. A data center built for air, even one finished a couple of years ago, often cannot host these racks without being substantially rebuilt, retrofitted for liquid distribution, higher floor loading, and the plumbing that comes with it. You cannot simply drop the latest GPUs into the footprint you already have. For an operator who has spent a career optimizing airflow, that is the moment it becomes clear this is a different kind of building.&lt;/p&gt;

&lt;p&gt;Liquid cooling changes how heat leaves the chip. It does not change where the energy comes from. Every watt still starts at the grid, and that is where the slowest constraint appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Power and Time: The Wall Underneath the Walls
&lt;/h2&gt;

&lt;p&gt;What limits AI in the end is not chips, and it is not cooling. It is electricity, and specifically it is time. You can buy a GPU in a day. You cannot buy the specific date on which a few hundred megawatts will be delivered to a site. That is set by a physical system that moves on a timescale of &lt;em&gt;years&lt;/em&gt;. Money can fund equipment and alternatives, but it does not make shared grid capacity appear on software time.&lt;/p&gt;

&lt;p&gt;The building can be designed and built on one clock; the grid upgrades that let it draw full power often run on a longer one. Before a site can draw full power, the utility has to study and approve the load, the transmission system has to support it, and the substations and lines that feed it have to exist. The industry calls this &lt;strong&gt;time-to-power&lt;/strong&gt; : the interval between choosing a site and being able to draw the load you planned around. For large AI sites, that interval can define the project. The upstream grid work resists money in a way the other constraints do not, because the transmission lines and substations are shared infrastructure that serves everyone on the grid, so a new load cannot simply pay to skip the queue without the wires actually being built. You can finish the building and then wait to turn it all the way on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorage.ghost.io%2Fc%2F6b%2Fcb%2F6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e%2Fcontent%2Fimages%2F2026%2F07%2Fdiagram-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorage.ghost.io%2Fc%2F6b%2Fcb%2F6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e%2Fcontent%2Fimages%2F2026%2F07%2Fdiagram-3.png" alt="Time-to-power gap: GPU purchase vs. site energization timeline" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;The time-to-power gap. You can buy a GPU in a day, but energizing a site takes years, and the build can finish while the power wait continues.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;There is another clock running alongside the approval queue: the equipment itself. Even once a project is cleared to connect, the high-voltage transformers and switchgear that tie it to the grid can be in shortage. Lead times for large power transformers have stretched from roughly two years before 2020 to as long as five years now, and industry estimates suggest a &lt;a href="https://finance.yahoo.com/sectors/technology/articles/half-planned-us-data-center-150928890.html" rel="noopener noreferrer"&gt;&lt;u&gt;meaningful share of planned 2026 data-center capacity could slip&lt;/u&gt;&lt;/a&gt; for want of power equipment and grid connections. Electrical gear is not the biggest line item in a data center. It can still decide when the building turns on, a reversal any operator who has waited on a transformer order will recognize.&lt;/p&gt;

&lt;p&gt;The grid backlog around these projects is large. At the end of 2025, more than 2,000 gigawatts of generation and storage capacity were waiting in line to connect to the US grid, roughly twice the entire installed US power fleet. More waiting to connect than currently exists. That queue is not the same thing as a data-center load request, but it shows the condition of the shared infrastructure every large new load depends on: the wires, substations, studies, and upgrades are all moving on a multi-year clock. &lt;a href="https://emp.lbl.gov/publications/queued-2025-edition-characteristics" rel="noopener noreferrer"&gt;&lt;u&gt;Lawrence Berkeley National Laboratory&lt;/u&gt;&lt;/a&gt;, which tracks those queues, finds the median time from request to commercial operation has roughly doubled, from under two years for projects built in the early 2000s to four to five years now.&lt;/p&gt;

&lt;p&gt;This is not abstract, and it is not only an American problem. Ireland is the cleanest example. Dublin had become one of Europe's great data center hubs until the grid could not keep up, and in 2021 the grid operator EirGrid and the Commission for Regulation of Utilities imposed what amounted to a &lt;a href="https://www.iiea.com/blog/data-centres-in-ireland-the-state-of-play" rel="noopener noreferrer"&gt;&lt;u&gt;moratorium on new connections in the Dublin area&lt;/u&gt;&lt;/a&gt;. One Amazon project and two Microsoft projects were among those &lt;a href="https://www.datacenterdynamics.com/en/news/microsoft-aws-equinix-join-list-of-companies-pausing-data-center-projects-in-dublin/" rel="noopener noreferrer"&gt;&lt;u&gt;turned away and relocated to London, Frankfurt, and Madrid&lt;/u&gt;&lt;/a&gt;. By 2024, data centers were drawing &lt;a href="https://www.iiea.com/blog/data-centres-in-ireland-the-state-of-play" rel="noopener noreferrer"&gt;&lt;u&gt;around 21 percent of all the electricity in the country&lt;/u&gt;&lt;/a&gt;. The moratorium &lt;a href="https://www.bloomberg.com/news/articles/2025-12-12/ireland-set-to-end-moratorium-on-new-power-links-to-data-centers" rel="noopener noreferrer"&gt;&lt;u&gt;eased only in December 2025&lt;/u&gt;&lt;/a&gt;, and the new terms show where things are headed: a new facility now has to bring its own power generation or storage rather than simply draw from the grid.&lt;/p&gt;

&lt;p&gt;The power industry itself is reorganizing around this demand. In May 2026, NextEra Energy announced a roughly &lt;a href="https://www.cnbc.com/2026/05/18/nextera-nee-dominion-energy-d-data-center-ai.html" rel="noopener noreferrer"&gt;&lt;u&gt;$67 billion all-stock plan to acquire Dominion Energy&lt;/u&gt;&lt;/a&gt;, the utility behind northern Virginia's data center corridor.&lt;/p&gt;

&lt;p&gt;That is the first half of the power story: getting electricity to the site. The second half starts once it arrives, and it is where AI looks least like the loads the grid grew up serving. A large training run is synchronized. Tens of thousands of accelerators compute, pause together to exchange results, and compute again. Power draw follows that loop. A single H100-class GPU draws far less at idle than under compute, so when tens of thousands switch states together, the facility's load can swing by tens of megawatts in seconds or less. Meta reported &lt;a href="https://newsletter.semianalysis.com/p/ai-training-load-fluctuations-at-gigawatt-scale-risk-of-power-grid-blackout" rel="noopener noreferrer"&gt;&lt;u&gt;swings around 30 megawatts on a 24,000-GPU cluster&lt;/u&gt;&lt;/a&gt; training Llama 3.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorage.ghost.io%2Fc%2F6b%2Fcb%2F6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e%2Fcontent%2Fimages%2F2026%2F07%2Fdiagram.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorage.ghost.io%2Fc%2F6b%2Fcb%2F6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e%2Fcontent%2Fimages%2F2026%2F07%2Fdiagram.png" alt="Training cluster compute-vs-pause load pattern" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;A synchronized training cluster swings between compute and pause many times a second. The grid is built to follow the smooth aggregate of many independent users, not one correlated load moving in lockstep.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The grid was built around load diversity, where thousands of independent homes and businesses average into something smooth and predictable. A synchronized training cluster is neither diverse nor smooth, and that is the part that is new even to people who have planned power for a living. The stability problem is broader than training-loop swings: large data-center loads can also behave unexpectedly during grid disturbances. In July 2024, a transmission fault in Northern Virginia caused &lt;a href="https://www.nerc.com/globalassets/our-work/reports/event-reports/incident_review_large_load_loss.pdf" rel="noopener noreferrer"&gt;&lt;u&gt;roughly 1,500 megawatts of data-center load to disconnect itself within 82 seconds&lt;/u&gt;&lt;/a&gt;, an event the North American Electric Reliability Corporation (NERC), which sets and enforces reliability standards for the North American bulk power system, said the system had never seen at that magnitude.&lt;/p&gt;

&lt;p&gt;The fix moves on-site. xAI's Colossus cluster in Memphis installed &lt;a href="https://www.datacenterdynamics.com/en/news/xai-deploys-168-tesla-megapacks-to-power-its-colossus-supercomputer-in-memphis/" rel="noopener noreferrer"&gt;&lt;u&gt;about 150 megawatts of grid-scale battery storage&lt;/u&gt;&lt;/a&gt; alongside its power infrastructure. The point of that storage is not how much energy it holds but how fast it can absorb and deliver power. A small, fast store placed in front of a slower supply is a cache. Here the slow backing store is the grid, and the fast store is local batteries and power electronics. Batteries are no longer only backup equipment. In these designs, they can become part of workload control. At AI scale, power stops being merely an input to the computer. It becomes part of the computer's design.&lt;/p&gt;

&lt;p&gt;And a design has to be operated. Once batteries, power electronics, cooling loops, and GPUs act as a single system, someone has to watch them as one: how power draw tracks compute, how the batteries answer a training swing, how heat follows the load. Those measurements arrive every second, from equipment that used to belong to three different teams. Read after the fact, they tell you what broke. Read live, they keep the loop stable.&lt;/p&gt;

&lt;p&gt;Power is where the earlier constraints converge. The GPU you could not get, the region that was full, the building that needed rebuilding, the batteries now sitting between the workload and the grid: each one traces back to the same place, a power system that has to be built and buffered, on the grid's schedule, not the software team's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Water: In Proportion
&lt;/h2&gt;

&lt;p&gt;Power is the hardest wall because it sets the clock and, through on-site batteries and power electronics, becomes part of the machine's own design. Water is different. It sits downstream of cooling design and geography, which makes it more local, more variable, and more solvable than the public debate suggests. AI makes the siting choice more visible because the facilities are larger and denser, but the water problem still depends on design. That matters because water draws the most public attention of any of these constraints, and some of the least accurate reporting.&lt;/p&gt;

&lt;p&gt;One distinction matters before any number makes sense: water withdrawn is not water consumed. Withdrawal is what a facility takes in; consumption is what it uses up, mostly through evaporation. A facility can withdraw a large volume and return most of it, or consume nearly all of what it takes, depending entirely on the cooling design.&lt;/p&gt;

&lt;p&gt;Nationally, the figure is modest. As of 2021, all US data centers combined accounted for &lt;a href="https://ketos.co/data-centers-water-usage-myths" rel="noopener noreferrer"&gt;&lt;u&gt;roughly 449 million gallons of water a day&lt;/u&gt;&lt;/a&gt;, about three to four tenths of one percent of total US water withdrawals, far below agriculture or power generation. The headline framing of data centers draining the country's water is not supported by the national figures.&lt;/p&gt;

&lt;p&gt;The real issue is local. &lt;a href="https://ketos.co/data-centers-water-usage-myths" rel="noopener noreferrer"&gt;&lt;u&gt;Roughly 40 percent of US data centers sit in areas of high or extreme water stress&lt;/u&gt;&lt;/a&gt;, so even a small national share can land hard on a particular community. Stated that way, it is a siting problem, real and solvable, rather than an indictment of the technology.&lt;/p&gt;

&lt;p&gt;The cooling design is what sets the consumption. On-site consumption ranges from nearly nothing, for an air-cooled or closed-loop facility, to as much as &lt;a href="https://ketos.co/data-centers-water-usage-myths" rel="noopener noreferrer"&gt;&lt;u&gt;70 to 80 percent of what was withdrawn&lt;/u&gt;&lt;/a&gt;, for an open evaporative one. A single large evaporative facility can use something like &lt;a href="https://www.brookings.edu/articles/ai-data-centers-and-water/" rel="noopener noreferrer"&gt;&lt;u&gt;five million gallons a day, comparable to a town of fifty thousand people&lt;/u&gt;&lt;/a&gt;. The same facility, built closed-loop, can use almost none. The high number and the low number describe the same building with two different cooling choices.&lt;/p&gt;

&lt;p&gt;This is the constraint the industry is most actively engineering away. Closed-loop systems fill once and recirculate rather than evaporate. The same shift to liquid and direct-to-chip cooling described earlier can cut water needs dramatically, &lt;a href="https://www.eesi.org/articles/view/data-centers-and-water-consumption" rel="noopener noreferrer"&gt;&lt;u&gt;by up to 95 percent in some designs&lt;/u&gt;&lt;/a&gt;, and immersion cooling can eliminate evaporative water use altogether. Reclaimed wastewater is increasingly used in place of drinking water. Of the five walls in this piece, water is the one where the engineering response is furthest along, which is exactly why it deserves to be described accurately rather than dramatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reserve Ran Out
&lt;/h2&gt;

&lt;p&gt;The five walls are not five problems. They are one fact seen from five &lt;em&gt;angles&lt;/em&gt;. None of this is new physics: the power was always physical, the heat was always real, capacity always took years to build. What changed is that the cloud era ran on a deep reserve of capacity built ahead of demand, and as long as that reserve lasted, the limits underneath stayed out of view. You turned a dial and the reserve answered. AI has drawn that reserve down, and at a scale the old infrastructure was never built to carry, so the limits are back in view all at once.&lt;/p&gt;

&lt;p&gt;That is why the data centers rising for AI are a different class of build, and why the people who built the last generation look at the numbers and recognize that the rules they worked under have moved. The accelerator depends on packaging and memory, the rack depends on liquid cooling, and the building depends on power. At this scale, the power itself needs a buffer. The site depends on grid capacity, water choices, and time. The next time a capacity question lands on your desk, ask where it will physically live and how long the power takes, before you ask what it costs. The cloud used to be an abstraction. It has an address now.&lt;/p&gt;

&lt;p&gt;The five walls are physical. Operating inside them is not. Once the facility and the computer are one coupled system, running it means reading it as one: GPU power draw, cooling response, battery state, and grid posture, measured together and fast enough to act while the numbers are still true. That is not a facilities dashboard on a five-minute refresh. It is a &lt;a href="https://www.tigerdata.com/blog/tiger-lake-a-new-architecture-for-real-time-analytical-systems-and-agents" rel="noopener noreferrer"&gt;&lt;u&gt;live, correlated, high-frequency record&lt;/u&gt;&lt;/a&gt; of a machine that now runs from the silicon to the substation. Capturing that record, and &lt;a href="https://www.tigerdata.com/blog/real-time-analytics-for-time-series-continuous-aggregates" rel="noopener noreferrer"&gt;&lt;u&gt;querying it before it goes stale&lt;/u&gt;&lt;/a&gt;, is its own problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Operational telemetry only helps if you can query it while it is still true, at the rate it arrives. That is the workload Tiger Data is built for: &lt;a href="https://www.tigerdata.com/learn/guide-to-postgresql-scaling" rel="noopener noreferrer"&gt;&lt;u&gt;time-series and event data on Postgres&lt;/u&gt;&lt;/a&gt;, fresh and correct, &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;without splitting into a second system&lt;/u&gt;&lt;/a&gt;. &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;Start a free Tiger Cloud trial&lt;/u&gt;&lt;/a&gt;. Running on-premises, at the edge, or air-gapped? &lt;a href="https://www.tigerdata.com/newsroom/tiger-data-launches-timescaledb-enterprise-a-self-managed-time-series-database-built-for-on-premises-and-edge-deployment" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB Enterprise&lt;/u&gt;&lt;/a&gt; is built for those deployments and is taking design partners.&lt;/p&gt;

</description>
      <category>thoughtleadership</category>
      <category>ai</category>
      <category>datacenters</category>
      <category>developers</category>
    </item>
    <item>
      <title>What We Heard: Three Patterns from Spring 2026 Events</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Wed, 01 Jul 2026 16:21:19 +0000</pubDate>
      <link>https://dev.to/tigerdata/what-we-heard-three-patterns-from-spring-2026-events-4j5m</link>
      <guid>https://dev.to/tigerdata/what-we-heard-three-patterns-from-spring-2026-events-4j5m</guid>
      <description>&lt;p&gt;Spring 2026 was relentless. Tiger Data teams covered ten events across Europe and the US: GrafanaCON in Barcelona, Hannover Messe and AWS Summit Hamburg in Germany, AWS Summit London, ETHDenver in Colorado, the Offshore Technology Conference in Houston, Data Driven Oil &amp;amp; Gas in Texas, Sensors Converge and IOT Tech Expo in the Bay Area, and AWS Summit Los Angeles. Hundreds of conversations with platform engineers, SREs, industrial automation teams, oil and gas operators, blockchain infrastructure builders, and sensor hardware teams. That's a lot of badge scans.&lt;/p&gt;

&lt;p&gt;Three patterns kept showing up across every audience, and they're really different angles on the same shift: the market has moved past category education and into implementation-level scrutiny. Here's what that looked like on the ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Postgres is becoming the default for industrial and operational telemetry
&lt;/h2&gt;

&lt;p&gt;Five years ago, telemetry workloads at scale defaulted to purpose-built time-series databases. InfluxDB. Prometheus. Specialized vertical stacks. The conversation at the booth has changed.&lt;/p&gt;

&lt;p&gt;At Hannover Messe, the world's largest industrial trade show, the most common question from automation and manufacturing teams was some variant of: "we have tens of thousands of sensors writing constantly, our current system is buckling, what do you do differently." Not "should we use a time-series database." Not "what is TimescaleDB." Those teams had already chosen Postgres before they walked up. They wanted to know if Tiger Data could keep them on it at the volumes they were already running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5fnk9f0abmikvwu0zpdw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5fnk9f0abmikvwu0zpdw.png" width="800" height="503"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Tiger Data team at Hannover Messe, Europe's largest industrial trade show.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At Sensors Converge, the audience skews closer to the hardware end of the IIoT pipeline: sensor manufacturers, embedded systems engineers, and the people writing firmware for the devices feeding everyone else's databases. The questions were specific. How does storage behave when you're sampling at 100 Hz across 50,000 endpoints. What happens when a fleet of devices comes back online after a network outage and dumps a backlog. Workload questions, not category questions.&lt;/p&gt;

&lt;p&gt;At the Offshore Technology Conference in Houston, the energy sector followed the same shape. &lt;a href="https://www.tigerdata.com/learn/iiot-database-requirements" rel="noopener noreferrer"&gt;SCADA telemetry&lt;/a&gt;. Well monitoring. Equipment digital twins. Lifecycle analytics on multi-decade asset histories. Teams that already had Postgres in production wanted to know how far they could push it before having to reach for another system. The answer, increasingly, is further than they had assumed.&lt;/p&gt;

&lt;p&gt;The pull toward Postgres in these markets is consistent. Existing Postgres in production. Existing SQL skills on the team. No appetite for a second on-call rotation. Industrial software is conservative for good reasons, and the system that already runs the business is a much better starting point than a green-field rewrite. Customers like &lt;a href="https://www.tigerdata.com/blog/how-mechademy-cut-hybrid-digital-twin-infrastructure-costs" rel="noopener noreferrer"&gt;Mechademy&lt;/a&gt;, &lt;a href="https://www.tigerdata.com/blog/how-flogistix-by-flowco-reduced-infrastructure-management-costs-by-66-with-tiger-data" rel="noopener noreferrer"&gt;Flogistix&lt;/a&gt;, and &lt;a href="https://www.tigerdata.com/case-studies/axpo" rel="noopener noreferrer"&gt;Axpo&lt;/a&gt; are public references for what this looks like in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: Split-architecture fatigue is becoming an explicit conversation
&lt;/h2&gt;

&lt;p&gt;The second pattern builds directly on the first. Teams who split their architecture into "Postgres for transactions, ClickHouse or a warehouse for analytics" are increasingly explicit about regretting it, something we have been arguing internally for years and are now hearing back from the field.&lt;/p&gt;

&lt;p&gt;At AWS Summit London, the booth was dense with platform engineers running on RDS or Aurora and feeling the analytical wall closing in. The question was usually some version of: "we are about to add a separate analytics database. Is there a path that does not require us to do that." A few years ago, the answer most teams accepted was no. They added the second system, built the pipeline, accepted the lag, and moved on. The conversation now starts from a different premise. Teams have either watched a peer team go through the split and pay the operational tax, or they have done it themselves and want out.&lt;/p&gt;

&lt;p&gt;At Hannover and OTC, the same pattern appeared on a longer timeline. Industrial teams that built their architectures a decade ago, with operational data in one place and analytics in another, are increasingly looking at consolidation. Multi-system data plumbing is one of the largest hidden line items in their engineering budget. The pitch that operational analytics can stay on the source of truth, with no pipeline, no drift, no second backup strategy, is landing differently than it did even two years ago.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.tigerdata.com/blog/how-speedcast-built-a-global-communications-network-on-tiger-lake" rel="noopener noreferrer"&gt;Speedcast's story&lt;/a&gt; is the cleanest public version of the before-and-after. They stitched together Kafka, Flink, and custom code to stream data from Postgres to Iceberg, and &lt;a href="https://www.tigerdata.com/blog/tiger-lake-a-new-architecture-for-real-time-analytical-systems-and-agents" rel="noopener noreferrer"&gt;Tiger Lake&lt;/a&gt; replaced all of it. As Kevin Otten, their Director of Technical Architecture, put it: "It's the architecture we wish we had from day one."&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: The questions have gotten harder, and that's the clearest evidence of the first two
&lt;/h2&gt;

&lt;p&gt;Across all ten events, the conversations got deeper than at any event series we have run. The questions skipped the basics, and that alone says something: teams don't ask about compression ratios and failure modes when they're still deciding on a category. They ask once they've already made the decisions in Patterns 1 and 2, and they're just checking the implementation.&lt;/p&gt;

&lt;p&gt;At GrafanaCON, booth visitors asked about &lt;a href="https://www.tigerdata.com/blog/real-time-analytics-for-time-series-continuous-aggregates" rel="noopener noreferrer"&gt;continuous aggregates&lt;/a&gt;, compression ratios, and how &lt;a href="https://www.tigerdata.com/blog/hypercore-a-hybrid-row-storage-engine-for-real-time-analytics" rel="noopener noreferrer"&gt;Hypercore&lt;/a&gt; behaves under high-cardinality dashboards. These are the questions teams ask when they are comparing implementations. The Grafana community is one of the most technically opinionated audiences we talk to, and the depth of the questions has been climbing event over event.&lt;/p&gt;

&lt;p&gt;At Sensors Converge, the questions ran the same way at the hardware level. Specific sample rates. Specific endpoint counts. Specific failure modes. Nobody wanted a feature tour. They wanted to know whether the system would behave the way they expected under their workload.&lt;/p&gt;

&lt;p&gt;The share of conversations that started with "what is a time-series database" or "what is TimescaleDB" was lower than at any event series we have run. The market has moved past category education. People know what they need, and they are picking implementations.&lt;/p&gt;

&lt;p&gt;That's the throughline across all three patterns: teams have already decided Postgres can carry this weight, they're done paying the split-architecture tax, and now they're evaluating implementations instead of categories. That's a good problem for a database company to have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you'll find us next
&lt;/h2&gt;

&lt;p&gt;The calendar doesn't really do summer breaks. If you want to keep the conversation going, here's where we'll be through the end of the year:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.tigerdata.com/events/from-mqtt-to-dashboard-workshop-2026" rel="noopener noreferrer"&gt;Online Workshop: From MQTT to Dashboard: Real-Time IIoT Pipelines with Tiger Cloud&lt;/a&gt;, July 22.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.imts.com/" rel="noopener noreferrer"&gt;IMTS&lt;/a&gt;, Chicago, September 14-19. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://inductiveautomation.com/events/ignition-community-conference-sacramento-ca-09-22-2026" rel="noopener noreferrer"&gt;Ignition Community Conference&lt;/a&gt;, Sacramento, September 22-24.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ase.isa.org/sponsorship" rel="noopener noreferrer"&gt;ISA Automation Summit &amp;amp; Expo&lt;/a&gt;, Florida, September 27-29.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.enlit-europe.com/" rel="noopener noreferrer"&gt;Enlit Europe&lt;/a&gt;, Vienna, November 10-12.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;More events coming&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We're still locking in the rest of the year. Keep an eye out on our &lt;a href="https://www.tigerdata.com/events" rel="noopener noreferrer"&gt;events page&lt;/a&gt;, or &lt;a href="https://www.linkedin.com/newsletters/tiger-data-newsletter-7342694485203554305/" rel="noopener noreferrer"&gt;subscribe to the newsletter&lt;/a&gt; if you'd rather have updates come to you.&lt;/p&gt;

&lt;p&gt;Want to connect at an event not on this list? &lt;a href="https://www.tigerdata.com/contact" rel="noopener noreferrer"&gt;Reach out.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we are not coming to your city, the &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;Tiger Cloud trial&lt;/a&gt; is open whenever you are.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>iot</category>
      <category>devops</category>
      <category>database</category>
    </item>
    <item>
      <title>How Float Runs an AI Energy Company on a 3-Person Team with Tiger Data</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Tue, 30 Jun 2026 11:00:05 +0000</pubDate>
      <link>https://dev.to/tigerdata/how-float-runs-an-ai-energy-company-on-a-3-person-team-with-tiger-data-2ldl</link>
      <guid>https://dev.to/tigerdata/how-float-runs-an-ai-energy-company-on-a-3-person-team-with-tiger-data-2ldl</guid>
      <description>&lt;p&gt;&lt;em&gt;Danish startup achieves 99.3% compression on 1Hz smart meter data, powering real-time appliance-level energy analytics for hundreds of homes.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://float.energy" rel="noopener noreferrer"&gt;&lt;em&gt;&lt;u&gt;Float&lt;/u&gt;&lt;/em&gt;&lt;/a&gt; &lt;em&gt;is a Danish AI and energy startup that collects 1Hz smart meter data from hundreds of homes and disaggregates it into per-appliance consumption using a proprietary ML model. The entire system depends on one architectural bet: that compression on their time-series database would be high enough to make the storage economics work at scale. Co-founders Jens Brandt Nellegaard (CEO) and Victor Grabow (CTO) share how they evaluated every major time-series database, why 90% compression was the hard floor for viability, and what happened when they hit 99.3% on Tiger Data.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About Float
&lt;/h2&gt;

&lt;p&gt;Most people have no idea what their individual appliances cost to run. In Denmark, most energy providers send a monthly PDF bill, and that is the entire customer experience. The market has seen no meaningful innovation in 10 to 15 years. Fraud and a lack of transparency have been so widespread that the Danish government introduced new consumer protection regulation effective January 2026.&lt;/p&gt;

&lt;p&gt;The underlying data to solve this problem actually exists. Most European smart meters have a standardized consumer interface that can output total household electrical load down to one-second resolution. But total load is not very interesting on its own. What consumers need is a breakdown by individual appliance - to understand which one is wasting electricity, which one is running at peak price, which one is approaching the capacity limit of a fuse. Academic research on energy disaggregation goes back to 1992. Nobody had solved it in a commercially scalable way.&lt;/p&gt;

&lt;p&gt;Float built three things to close this gap: a proprietary hardware module that plugs into the consumer port on a European smart meter, a signal processing and neural net pipeline that classifies appliance-level consumption from the raw waveform, and a consumer-facing app with a proactive AI energy agent. The system collects roughly 15 measurements per second per household, each at 1Hz resolution. One second is the hard floor - Victor explains: &lt;em&gt;"At one-minute resolution, the model would break, because what we track are the changes. If there are too many appliances turning on within the same minute, it would be very hard to differentiate them."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Jens Brandt Nellegaard and Victor Grabow co-founded Float in 2022. After nearly three years of R&amp;amp;D, they achieved a proof of concept in December 2024, secured their energy provider license in December 2025, and are now rolling out a private beta to 350 pre-vetted customers. The company has just three people.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Float started on Azure managed Postgres with the TimescaleDB extension. The team had Azure credits early, so it made sense at the time. But the Apache version available on Azure did not include compression, and that turned out to be a dealbreaker.&lt;/p&gt;

&lt;p&gt;Every customer generates roughly 15 measurements per second. Float samples voltage, frequency, and total load across each phase entering the home. At 1,000 homes, that is 15,000 data points per second, continuously. Without compression, the storage cost alone would break their business model. “&lt;em&gt;We are an energy company with a flat-rate subscription fee,”&lt;/em&gt; says Jens. &lt;em&gt;“We pass through the spot market price one-to-one with no markup. If storage cost per user exceeds what the subscription supports, the economics collapse.&lt;/em&gt;” &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;We also tried InfluxDB before settling on TimescaleDB. We ran into ingestion issues, and we needed SQL.&lt;/em&gt; &lt;em&gt;When you are a three-person team building an asset-centric microservice platform, you cannot afford a database that requires a proprietary query language and limits how you join and query data across domains. - Victor Grabow, CTO, Float&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On top of the storage problem, Float needed continuous aggregates. The Danish DSO delivers settlement data at 15-minute resolution. Float collects data every second. To generate a live energy bill and compare it against the grid operator's numbers, the system needs to aggregate raw 1Hz data down to 15-minute windows constantly. On managed Postgres without TimescaleDB's full feature set, that meant writing and maintaining batch jobs - more infrastructure overhead for a team that was already stretched thin across hardware, ML, and a licensed energy company.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Tiger Data
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;We tested pretty much every time-series database on the market. We think Tiger Data is the best solution for our use case. - Victor Grabow, CTO, Float&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The team researched time-series solutions extensively and discovered TimescaleDB through the compression and continuous aggregate features. The compression was compelling enough that it was just a matter of time before they needed the full capability. When they found Tiger Data - the company behind TimescaleDB - the managed cloud service made the path clear.&lt;/p&gt;

&lt;p&gt;Two features drove the decision. First, &lt;strong&gt;&lt;em&gt;Compression&lt;/em&gt;&lt;/strong&gt;. The team had modeled the unit economics and needed at least 90% compression on the time-series data for the subscription model to work. Anything below that and storage costs per user would exceed what the flat-rate fee could support. Second, &lt;strong&gt;&lt;em&gt;Continuous Aggregates&lt;/em&gt;&lt;/strong&gt; - materialized views that update incrementally as new data arrives. Float runs aggregations constantly, converting 1Hz readings to 15-minute settlement windows, calculating threshold-based alerts on voltage and frequency, detecting outages, and triggering duration-based notifications like flagging an oven that has been running for four hours. Continuous aggregates handle all of this without batch jobs or scheduled pipelines.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;We chose Tiger Cloud, the fully managed service on Azure, because it was a question of speed. We needed to get up and running fast and offload infrastructure management entirely.&lt;/em&gt; &lt;a href="https://encore.dev" rel="noopener noreferrer"&gt;&lt;em&gt;&lt;u&gt;Encore&lt;/u&gt;&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, our DevOps platform, provides ephemeral environments on Google Cloud, and Tiger Cloud's database branching fits naturally into that workflow. - Victor Grabow, CTO, Float&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Float Energy Data Stack
&lt;/h2&gt;

&lt;p&gt;Data starts at the smart meter. Float’s IoT module plugs into the standardized consumer interface port and captures voltage, frequency, and total load across each phase at 1Hz resolution. The module sends readings to Azure IoT Hub, which the team kept from the original Azure setup as a stable ingestion endpoint for all devices.&lt;/p&gt;

&lt;p&gt;From there, a bridge connector forwards the stream into Google Cloud, where Encore deploys Float’s microservices. The team moved off Azure Event Hub eventually because it was expensive. Google Cloud streaming services handle the same throughput at lower cost. The streaming layer batches incoming measurements from all households every second and inserts them per batch into Tiger Data.&lt;/p&gt;

&lt;p&gt;Tiger Data stores the raw 1Hz time-series readings and runs continuous aggregates for threshold-based monitoring: voltage spikes, frequency changes, mean and max calculations, outage detection, and duration-based appliance alerts. All raw data is retained for ML training purposes through the private beta phase, with tiered storage planned as the fleet scales.&lt;/p&gt;

&lt;p&gt;The Float app reads processed data to show customers their real-time energy breakdown per appliance. New customers see total wattage immediately on connection. Appliance-level breakdown takes roughly three to four weeks as the model trains on their home's specific patterns. The agentic orchestration layer on top handles billing, onboarding, customer service, and proactive notifications - flagging forgotten ovens and irons, inefficient appliances, and dangerous load conditions approaching fuse limits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkwc923nxtiqalmxiubfg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkwc923nxtiqalmxiubfg.png" alt="Float's data architecture" width="799" height="371"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Float's data architecture: 1Hz readings flow from the IoT module through Azure IoT Hub and Google Cloud streaming into Tiger Data, which serves the ML pipeline, consumer app, and agentic platform.
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Compression Enabled
&lt;/h2&gt;

&lt;p&gt;On Tiger Data, Float is seeing 99.3% compression on its time-series data. Victor puts it directly: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Compression needed to be in the high nineties range to not break our business model. So that was a great outcome. - Victor Grabow, CTO, Float&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That number unlocked three things that would not have been possible at lower compression ratios.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Business Model Works
&lt;/h3&gt;

&lt;p&gt;At 15,000 data points per second across 1,000 homes, uncompressed storage would generate terabytes of raw time-series data per year. Float passes through the spot market electricity price to customers at cost with no markup. Revenue comes from a flat-rate subscription fee. If storage cost per user climbs above what that fee can support, the entire model collapses. At 99.3% compression, it does not. The subscription covers infrastructure with margin to spare, and that margin holds as the fleet scales.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full Data Retention for ML Training
&lt;/h3&gt;

&lt;p&gt;Float's disaggregation model needs weeks of 1Hz training data per household to learn each home's specific appliance signatures. At lower compression, the team would face a choice: retain all raw data for model training or keep storage costs viable. At 99.3%, they retain everything. All raw 1Hz readings from the entire private beta fleet are available for the ML pipeline, with tiered storage planned only as the fleet scales past the beta phase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-Time Billing Without Batch Infrastructure
&lt;/h3&gt;

&lt;p&gt;The Danish grid operates on 15-minute settlement windows. Float collects data every second. Continuous aggregates bridge that gap, converting 1Hz readings into the 15-minute intervals the DSO requires for bill reconciliation. Danish energy prices swing up to 80% between peak and off-peak hours, which makes the freshness of the aggregation directly valuable to customers. Because continuous aggregates update incrementally as new data arrives, Float's live energy bill is always current, i.e., no scheduled batch jobs, no pipeline maintenance, no lag.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Three-Person Team Running a Licensed Energy Company
&lt;/h3&gt;

&lt;p&gt;Float holds an energy provider license in Denmark. That means billing, customer service, onboarding, regulatory compliance - operational overhead that traditional energy companies staff with dozens of people. Tiger Cloud's managed infrastructure is part of what makes this possible. The team does not manage database operations, storage provisioning, or aggregation pipelines. That overhead is handled. When asked about team size, Jens's answer was simple: &lt;em&gt;"Three. We have three people... and an army of agents. This is the future."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;Float is targeting 1,000 additional private beta customers within the next 12 months, with a seed round, two additional hardware variants for full Danish grid coverage, and expansion into two more countries.&lt;/p&gt;

&lt;p&gt;The next major integration is EV charging - starting with Tesla's telemetry API, enabling smart charging during cheap price windows. The bigger thesis is that a fleet of homes measured at 1Hz resolution can trade power on the spot market more efficiently than any energy company operating at 15-minute resolution with a 24 to 48-hour delay. As Jens puts it: &lt;em&gt;"Ultimately we are trying to make the home not a burden for the grid, but a partner of the grid."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The architecture decision that compounds as Float scales is not the compression ratio itself. It is that everything runs on a single Tiger Data instance: the raw 1Hz readings, the continuous aggregates for billing, the training data for the ML pipeline, the anomaly detection queries. No split architecture to maintain, no query paths to reconcile as the fleet grows from 350 homes to 1,000 and beyond. The data model does not change - it just gets bigger.&lt;/p&gt;

</description>
      <category>devqa</category>
      <category>ai</category>
      <category>casestudy</category>
      <category>energy</category>
    </item>
    <item>
      <title>How Small Postgres Metadata Tables Quietly Throttle Your Largest Queries</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Mon, 29 Jun 2026 13:34:12 +0000</pubDate>
      <link>https://dev.to/tigerdata/how-small-postgres-metadata-tables-quietly-throttle-your-largest-queries-3507</link>
      <guid>https://dev.to/tigerdata/how-small-postgres-metadata-tables-quietly-throttle-your-largest-queries-3507</guid>
      <description>&lt;p&gt;Most time-series performance work starts with the obvious suspect: the fact table. Five hundred million rows of sensor readings, hundreds of daily partitions, an autovacuum worker fighting write load. That is where engineers go first. That is where the &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;Optimization Treadmill&lt;/u&gt;&lt;/a&gt; lives. But after reading this guide, you'll know how to find a different class of slowdown hiding in the opposite direction, detect it in under five minutes with &lt;a href="https://www.tigerdata.com/learn/explaining-postgresql-explain" rel="noopener noreferrer"&gt;&lt;u&gt;EXPLAIN&lt;/u&gt;&lt;/a&gt;, and apply a same-day fix that requires no schema migration.&lt;/p&gt;

&lt;p&gt;A 10,000-row device metadata table, rarely updated, sitting in the &lt;code&gt;FROM&lt;/code&gt; clause of every dashboard query you run, can drag your entire time-series query path into the wrong performance tier. The time-series scan looks fast in &lt;code&gt;EXPLAIN&lt;/code&gt;. The join node is where the time goes. The fix requires looking at a table that almost nobody thinks to audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;Postgres triggers ANALYZE on a table based on &lt;code&gt;autovacuum_analyze_scale_factor&lt;/code&gt;, which defaults to 0.2. That means 20% of the table's rows need to change before autovacuum schedules a statistics refresh. On a 10,000-row &lt;code&gt;devices&lt;/code&gt; table, that threshold is 2,000 rows. If your device metadata only sees occasional inserts as you onboard new sensors or record location changes, &lt;code&gt;ANALYZE&lt;/code&gt; might have run once in the last quarter.&lt;/p&gt;

&lt;p&gt;Meanwhile, &lt;code&gt;pg_statistic&lt;/code&gt; still describes the distribution of &lt;code&gt;device_id&lt;/code&gt; values your planner saw six months ago. Your 10,000-row table looks like a 600-row table to the query planner. Every query joining it runs a plan built for a much smaller dataset, and that plan doesn't improve on its own.&lt;/p&gt;

&lt;p&gt;The symptom compounds constantly. Every dashboard refresh, every alert evaluation, every hourly export job runs this join. Stale statistics on a small metadata table are not a one-time cost. They tax your entire query workload invisibly, on every execution. The decision about how to execute that join is made by the query planner once, based on what &lt;code&gt;pg_statistic&lt;/code&gt; describes at plan time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Postgres picks a join strategy
&lt;/h2&gt;

&lt;p&gt;When the planner receives a &lt;a href="https://www.tigerdata.com/learn/postgresql-join-type-theory" rel="noopener noreferrer"&gt;&lt;u&gt;query with a join&lt;/u&gt;&lt;/a&gt;, it chooses between execution strategies based on estimated row counts from &lt;code&gt;pg_statistic&lt;/code&gt;. A nested loop join works well when the inner table is small and the join condition hits an index. A hash join builds an in-memory hash table and scans both relations once.&lt;/p&gt;

&lt;p&gt;When your &lt;code&gt;devices&lt;/code&gt; table has 10,000 rows but &lt;code&gt;pg_statistic&lt;/code&gt; thinks it has 600, the planner's cost model produces the wrong answer. It may choose a nested loop when the dataset is large enough to warrant a hash join, or allocate an undersized hash table that spills to disk. The direction of the error matters less than the consequence: a fast indexed scan on your fact table becomes the cheaper half of a misplanned join, and the join node pays the penalty.&lt;/p&gt;

&lt;p&gt;This pushes your query from the indexed access tier, where performance scales with log(n), into the linear tier, where it scales with the full dataset the planner miscalculated. A join that ran in 20ms when the planner had accurate statistics can degrade to 4 seconds as the mismatch grows, with no change to the fact table, the indexes, or the query itself. The &lt;a href="https://www.tigerdata.com/blog/the-iiot-postgresql-performance-envelope" rel="noopener noreferrer"&gt;&lt;u&gt;IIoT performance envelope&lt;/u&gt;&lt;/a&gt; maps this degradation curve in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the problem
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; on any query that joins your fact table to a metadata table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;any&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;joins&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;facility&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'plant-a'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the join node in the output. Postgres shows two numbers for every node: rows=X (estimated) and actual rows=Y. A ratio of actual to estimated above 5:1 in either direction generally indicates the planner is working with stale information.&lt;/p&gt;

&lt;p&gt;A real mismatch looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hash Join (cost=45.00..12890.43 rows=620 width=48)
           (actual time=0.821..2847.332 rows=9841 loops=1)
  -&amp;gt; Seq Scan on devices (cost=0.00..42.20 rows=620 width=24)
                           (actual time=0.015..0.441 rows=9841 loops=1)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The planner expected 620 rows from &lt;code&gt;devices&lt;/code&gt;. It got 9,841. That gap is why the hash table bucketing is wrong, why memory allocation is undersized, and why a join that should take 150ms ran for 2.8 seconds. The fact table scan was fast. The join node absorbed all the cost. &lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: Force fresher statistics
&lt;/h2&gt;

&lt;p&gt;The fastest fix lowers the scale factor on the metadata table so &lt;code&gt;ANALYZE&lt;/code&gt; fires more aggressively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;autovacuum_analyze_scale_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autovacuum_analyze_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these settings, &lt;code&gt;ANALYZE&lt;/code&gt; fires after roughly 150 rows change rather than 2,000. For a table that sees occasional updates as you onboard new sensors, this keeps statistics current without adding meaningful autovacuum load. The &lt;code&gt;devices&lt;/code&gt; table is not your hot write path.&lt;/p&gt;

&lt;p&gt;Run a manual &lt;code&gt;ANALYZE&lt;/code&gt; immediately to rebuild statistics from current data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then re-run the &lt;code&gt;EXPLAIN&lt;/code&gt; query. The estimated rows on the join node should now match actual rows closely, and the join strategy will likely change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: Audit all small metadata tables
&lt;/h2&gt;

&lt;p&gt;The same problem applies to any metadata table that grows slowly but gets joined to your large fact tables (e.g. a &lt;code&gt;facilities&lt;/code&gt; table, an &lt;code&gt;equipment_types&lt;/code&gt; table, a &lt;code&gt;tag_groups&lt;/code&gt; table). This query finds the candidates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_live_tup&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;estimated_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_analyze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_autoanalyze&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n_live_tup&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;last_analyze&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
      &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;last_analyze&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n_live_tup&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any metadata table with more than a week between &lt;code&gt;ANALYZE&lt;/code&gt; runs and fewer than 100,000 rows is worth checking. Identify which ones appear in joins to large fact tables, then apply the scale factor adjustment to each. This is a one-time change with no ongoing maintenance burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: Denormalize for the highest-volume joins
&lt;/h2&gt;

&lt;p&gt;When a join runs on every query in your workload, the most durable fix is to remove it entirely. For stable metadata like &lt;code&gt;location&lt;/code&gt; or &lt;code&gt;facility&lt;/code&gt;, you can store the value directly on the &lt;code&gt;readings&lt;/code&gt; row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Add the column to the fact table&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Backfill from the current join&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original query then becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'plant-a'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No join. No statistics dependency. No planning variability. The tradeoff is keeping &lt;code&gt;location&lt;/code&gt; current when a device moves. The simplest pattern is a trigger on &lt;code&gt;devices&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;sync_device_location&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;devices_location_sync&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;sync_device_location&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well when device location changes are infrequent. If &lt;code&gt;readings&lt;/code&gt; has hundreds of millions of rows and devices move regularly, an application-layer update run as a background job is safer: it keeps the writes out of the transaction that updates &lt;code&gt;devices&lt;/code&gt; and avoids locking large row sets. A chunked loop limits I/O pressure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;rows_updated&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_inherits&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pg_inherits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inhrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pg_inherits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inhparent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'readings'&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;
    &lt;span class="n"&gt;LOOP&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'UPDATE %I r
             SET location = d.location
             FROM devices d
             WHERE r.device_id = d.id
               AND r.location IS DISTINCT FROM d.location'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;part&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;GET&lt;/span&gt; &lt;span class="k"&gt;DIAGNOSTICS&lt;/span&gt; &lt;span class="n"&gt;rows_updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ROW_COUNT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;IS DISTINCT FROM&lt;/code&gt; check skips rows where the value already matches, so the loop terminates cleanly even if it runs across a restart. Either way, the application code that previously ran the join query gets simpler, not more complex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating the fix
&lt;/h2&gt;

&lt;p&gt;After applying Fix 1 or Fix 3, re-run the original &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt;. A correctly-planned query shows estimated rows close to actual on the join node and a shorter total execution time. Here is what the output looks like after &lt;code&gt;ANALYZE&lt;/code&gt; with corrected statistics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hash Join (cost=178.00..13210.55 rows=9930 width=48)
           (actual time=1.243..312.881 rows=9841 loops=1)
  -&amp;gt; Seq Scan on devices (cost=0.00..172.30 rows=9930 width=24)
                           (actual time=0.018..0.892 rows=9841 loops=1)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The estimated rows now match actual rows closely (9,930 vs. 9,841). Total execution time dropped from 2.8 seconds to 312ms. The hash table was allocated correctly from the start.&lt;/p&gt;

&lt;p&gt;For a before-and-after comparison across your full query workload, use &lt;code&gt;pg_stat_statements&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mean_exec_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_exec_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_sec&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'%devices%'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'%readings%'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;mean_exec_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this before the fix, then again 24 hours later once autovacuum has refreshed statistics and the query plan cache has turned over. The mean execution time change for your slowest join queries is your signal. &lt;/p&gt;

&lt;h2&gt;
  
  
  Your next step
&lt;/h2&gt;

&lt;p&gt;Open psql and run the audit query from Fix 2. It takes under a minute, requires no permissions beyond &lt;code&gt;SELECT&lt;/code&gt; on &lt;code&gt;pg_stat_user_tables&lt;/code&gt;, and identifies every underanalyzed metadata table in your schema at once. For each table that comes back, check whether it joins to a large fact table in your workload, then apply the scale factor fix from Fix 1. No maintenance window, no schema migration. The new statistics take effect the moment &lt;code&gt;ANALYZE&lt;/code&gt; completes.&lt;/p&gt;

&lt;p&gt;If the audit returns nothing but your join queries are still slow, the bottleneck has moved. The &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;Optimization Treadmill&lt;/u&gt;&lt;/a&gt; maps the next layer of performance constraints in high-volume Postgres workloads and what to do when the metadata tables aren't the problem. Start a &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;Tiger Data free trial&lt;/u&gt;&lt;/a&gt; today to validate these fixes on your own data.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>developers</category>
      <category>database</category>
    </item>
    <item>
      <title>What You’re Really Owning When You Self-Host TimescaleDB</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Thu, 25 Jun 2026 13:51:54 +0000</pubDate>
      <link>https://dev.to/tigerdata/what-youre-really-owning-when-you-self-host-timescaledb-j19</link>
      <guid>https://dev.to/tigerdata/what-youre-really-owning-when-you-self-host-timescaledb-j19</guid>
      <description>&lt;p&gt;Written by Matty Stratton, Brandon Purcell, Noah Hein, Hien Phan&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why operating TimescaleDB for mission-critical applications becomes a sustained platform engineering investment. Written by the team that builds TimescaleDB and operates Tiger Cloud across thousands of production deployments.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;Most engineers who evaluate TimescaleDB believe they are making a database decision. By the time customers depend on the application in production, they discover they made a platform ownership decision. The gap between those two decisions is the subject of this paper.&lt;/p&gt;

&lt;p&gt;Availability, recoverability, scalability, security, and lifecycle management originate in the requirements of the application, and they land on the database team as operational systems that must be designed, built, staffed, and maintained for as long as the application runs. None of that work is beyond a capable engineering team. The question this paper raises is whether owning that platform is the highest-leverage use of the platform engineers who could otherwise be building the product the database supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics on Live Operational Data Is Part of the Application
&lt;/h2&gt;

&lt;p&gt;Analytics that runs overnight against a reporting database is important, and it is separate from the application. Analytics that plant operators watch continuously, that customers open before starting their workday, or that triggers an automated intervention before a defect is manufactured has moved onto the critical path of the business. Once analytics moves onto the critical path, the database supporting it moves there too.&lt;/p&gt;

&lt;p&gt;One deployment carries most of this paper. An automotive supplier operates 120 robotic welding lines ingesting billions of sensor measurements per day. Plant engineers use the platform to detect abnormal operating patterns before they become equipment failures. A welding line showing early signs of a calibration drift or a sensor trending out of range may be a maintenance issue today; missed for thirty minutes, it can become an unplanned shutdown that halts production and costs the business more in rework than the platform team's quarterly budget. Every minute of latency is a minute a problem propagates across a line. We will follow this platform from its first twenty instrumented lines through its third year of operation, because the operational story of a self-hosted deployment is a story that unfolds over years, and it is easier to understand through one platform than across a survey of many. Where the same failure arrives from a different direction, we will bring in other deployments we have worked with: a connected-equipment OEM, a food processor, a specialty chemicals manufacturer. Different industries, different latency tolerances, the same dynamic underneath.&lt;/p&gt;

&lt;p&gt;TimescaleDB is still TimescaleDB and PostgreSQL is still PostgreSQL. What changes is the operational commitment required to deliver the availability, recovery, scalability, and governance the application now demands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Database Inherits the Application's Requirements
&lt;/h2&gt;

&lt;p&gt;Nobody is thinking about the database when the automotive supplier's operations organization commits to detecting equipment degradation before it stops a line, when plant management sets uptime expectations for the dashboards shift supervisors watch, or when compliance defines how long production records backing warranty claims must be retained. The database team inherits the requirements anyway and the inheritance changes the nature of the work.&lt;/p&gt;

&lt;p&gt;A 99.9% availability target becomes a systems problem: replicas, failover orchestration, monitoring, and on-call coverage. A point-in-time recovery requirement becomes a tested runbook with validated restore times at production volume. Years of telemetry under compliance obligations becomes legal exposure that accumulates with every year of data the team holds.&lt;/p&gt;

&lt;p&gt;Early in an application's life these requirements are loose. An outage of a few minutes is an inconvenience; a day of missing data is embarrassing but recoverable. As more users depend on the application and other teams build their own workflows on its data, the requirements tighten. Closing that gap is continuous work, and it falls on your platform engineers for as long as the application runs. The rest of this paper is what that work actually looks like, and each section raises the stakes a step: from lost engineer hours, to business risk, to commercial liability, to legal exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downtime Becomes a Product Problem Before the Architecture Catches Up
&lt;/h2&gt;

&lt;p&gt;During the automotive supplier's early deployment, with twenty welding lines instrumented and a handful of engineers as the only users, a database restart is an inconvenience. Dashboards go blank for two minutes. Engineers wait. Nobody escalates.&lt;/p&gt;

&lt;p&gt;Twelve months later, all 120 lines are instrumented and shift supervisors in three facilities depend on those dashboards. An equipment problem that goes undetected for thirty minutes because the platform was down costs more in rework than the platform team's quarterly budget. The database has the same configuration it had twelve months ago. The application does not.&lt;/p&gt;

&lt;p&gt;The response is redundancy. &lt;a href="https://www.tigerdata.com/blog/how-timescale-replication-works-enabling-postgres-ha" rel="noopener noreferrer"&gt;&lt;u&gt;PostgreSQL streaming replication&lt;/u&gt;&lt;/a&gt; keeps a replica synchronized with the primary, and if the primary fails, the replica is promoted with minimal data loss. This is well-understood architecture and it works. It is also where the engineering commitment begins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31hu1azsv43gkukuygzr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31hu1azsv43gkukuygzr.png" width="800" height="572"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: Single-instance deployment versus production HA architecture: primary, HA replica, read replica, connection pooler, and monitoring as distinct operational layers, with failure paths and failover direction&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Replication configuration is a decision with real trade-offs: &lt;a href="https://www.tigerdata.com/learn/best-practices-for-postgres-database-replication" rel="noopener noreferrer"&gt;&lt;u&gt;synchronous replication eliminates lag but constrains ingest throughput; asynchronous replication preserves throughput but can lose data in a failover&lt;/u&gt;&lt;/a&gt;. Someone who understands the application has to make that call and own it as the workload changes. More importantly, someone has to watch replication continuously. A replica that has quietly fallen minutes behind provides a false sense of safety: stale as a read source, lossy as a failover target. Silent replication drift is one of the most common ways HA architectures fail to deliver the guarantees they appear to provide. We have seen it on deployments that had every structural component in place. The monitoring was the missing piece.&lt;/p&gt;

&lt;p&gt;Beyond replication, HA is a stack of components that each need owners: failover automation that has been tested under simulated failure, not just configured; a connection pooler that shortens the error window and brings its own failure modes; rolling maintenance procedures rehearsed before they're needed under time pressure; and a monitoring layer that covers replication health, failover state, and background jobs, with alert thresholds tuned to the application and runbooks kept current as the system changes.&lt;/p&gt;

&lt;p&gt;Availability is a standing allocation: a senior engineer's judgment on replication and failover, recurring hours for monitoring and rehearsal, and a permanent slot in the on-call rotation. Every one of those hours comes from the same platform engineers the roadmap is counting on. The question is whether it is the work you hired them to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery Objectives Are Set by the Business and Tested by Almost Nobody
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdbzomoy8piyxluti67g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdbzomoy8piyxluti67g.png" width="800" height="632"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2: Recovery architecture showing full weekly backups, daily incrementals, continuous WAL archiving, and the RPO/RTO envelope they define, with restoration time as a function of data volume&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.tigerdata.com/blog/database-backups-and-disaster-recovery-in-postgresql-your-questions-answered" rel="noopener noreferrer"&gt;&lt;u&gt;Most backups are running&lt;/u&gt;&lt;/a&gt;. The question organizations rarely ask before they need to is whether the restore completes within the window the business requires, from the point in time the business requires, at the volume the database has actually reached. That gap is where recovery risk lives, and the food processor is standing in it the day a customer reports a potential contamination event.&lt;/p&gt;

&lt;p&gt;The investigation needs sensor records for one production line during a two-hour window eighteen months ago. The data was backed up. None of that answers the only question that matters: whether &lt;a href="https://www.tigerdata.com/blog/making-postgresql-backups-100x-faster-via-ebs-snapshots-and-pgbackrest" rel="noopener noreferrer"&gt;&lt;u&gt;restoring eighteen months of production telemetry at current volumes&lt;/u&gt;&lt;/a&gt; completes inside the investigation's clock. Nobody has ever run that restore. The procedure that exists is a hypothesis, and the incident is the wrong time to run the experiment.&lt;/p&gt;

&lt;p&gt;That is the ownership gap. Not the backup. The untested restore.&lt;/p&gt;

&lt;p&gt;As data volume grows, restore drills become harder, slower, and more important. A backup that exists is not the same as a recovery process that works.&lt;/p&gt;

&lt;p&gt;The food processor's contamination event is the urgent version of this failure; the chemical manufacturer's corrupted migration is the irreversible one. A restore that &lt;em&gt;completes&lt;/em&gt; is not necessarily a restore that &lt;em&gt;worked&lt;/em&gt;. Validating that distinction at production scale is a recurring drill, measured in engineer-days per quarter, and someone has to own the calendar invite. That is before the platform has grown. The next problem is a system that succeeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Success Rewrites the Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmur2mdtw11art8ojqda1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmur2mdtw11art8ojqda1.png" width="799" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3: The same deployment scaling from hundreds to tens of thousands of assets: storage growth, backup window duration, query p95 latency, and concurrent connections as separate axes over time&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The deployment that shows what success costs is an industrial equipment OEM whose customer-facing dashboards are a contracted product feature. A database outage there is a commercial event, logged against an SLA and escalated to an account manager. The OEM instructed a few hundred connected assets at launch and reached tens of thousands two years later. It did not build a system that broke. It built a system that succeeded, and the success invalidated the original architecture one assumption at a time. Nothing failed. The application simply outran the operational decisions made around it.&lt;/p&gt;

&lt;p&gt;Storage growth is the most visible dimension and the least costly; &lt;a href="https://www.tigerdata.com/blog/how-to-optimize-postgresql-cloud-costs-with-tiered-storage" rel="noopener noreferrer"&gt;&lt;u&gt;disk is cheap to add&lt;/u&gt;&lt;/a&gt;. The downstream effects are the expensive part: longer backup windows, longer restores, more expensive maintenance, slower schema changes across thousands of chunks.&lt;/p&gt;

&lt;p&gt;Retention policy management becomes load-bearing, and the hard part is rarely the configuration. The OEM's customers, now benchmarking equipment against peers on historical trends, reject the retention window set two years ago. The food processor meets the same moving target from the regulatory side: entering a new jurisdiction rewrites the retention obligations it launched with. The policy must stay aligned with requirements that keep moving, and someone has to confirm it is actually running.&lt;/p&gt;

&lt;p&gt;Growth changes the operating model across every dimension: more assets, more users, more historical data, and more customer expectations. Query concurrency grows from a handful of internal analysts to thousands of customer-facing users, bringing read replicas, replication lag, and connection routing into scope. Configuration that was correct at two hundred assets is reconsidered at twenty thousand, but corrections don't take effect instantly. They migrate into effect gradually as new data arrives, which means the team is managing transitions, not flipping switches. These solutions work. They also need owners, and more of them as the application grows.&lt;/p&gt;

&lt;p&gt;None of this stays solved. Each revisit lands on the same team being asked to ship features and support customers. Growth in the application is growth in the platform team's backlog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Platform Never Stops Evolving
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyf94ef1wfjn1uyqz6mf5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyf94ef1wfjn1uyqz6mf5.png" width="800" height="574"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4: Version lifecycle timeline: PostgreSQL major version cadence, TimescaleDB release cadence, support windows, and the upgrade planning cycles they impose&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is a common expectation, particularly among teams building their first production database platform, that operations stabilize once the deployment is running. It does not. Ownership never ends.&lt;/p&gt;

&lt;p&gt;PostgreSQL ships a major version each year; TimescaleDB tracks those releases. Running past end-of-life means running without security patches, which is untenable for any system holding customer data or regulated production records. Minor version patches require only a brief service restart. A major PostgreSQL version upgrade is a coordinated migration process: it requires a staging environment that mirrors production in data volume, query workload, aggregate configuration, and columnstore state; a post-upgrade validation suite checked against pre-upgrade baselines; a tested rollback plan; and a coordinated maintenance window. For the chemicals manufacturer's five years of reactor telemetry, that is two to four engineer-weeks of work, recurring roughly annually, for as long as the platform operates.&lt;/p&gt;

&lt;p&gt;Runbooks are the connective tissue holding the rest together, and they decay by default. Return to the automotive supplier, now three years in. The platform looks substantially different from what launched: additional lines instrumented, aggregates added, retention adjusted, the HA configuration changed after a failover exposed a gap in the original design, the chunk interval re-tuned after ingest grew. Each change was made for a good reason. None made it back into the runbook, and the engineer who made most of them has moved to a different team. This is how operational debt accumulates: not through negligence, but through the ordinary pressure of a team moving fast and treating documentation as something to get to later. If you are reading this and thinking it would not happen on your team, it is worth asking when your runbooks were last tested against the system they describe. Later arrives during incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every Capability Arrives With an Owner Attached
&lt;/h2&gt;

&lt;p&gt;The preceding sections walk through these systems one at a time, as they arrive in the life of a deployment. Here is the full surface area in one place: each system with its own configuration, monitoring requirements, failure modes, and cadence of ongoing work. In aggregate, they make up the platform.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;What it delivers&lt;/th&gt;
&lt;th&gt;How it fails quietly&lt;/th&gt;
&lt;th&gt;The ongoing work&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HA &amp;amp; failover (replicas, pooler, promotion automation)&lt;/td&gt;
&lt;td&gt;The availability SLA&lt;/td&gt;
&lt;td&gt;Silent replication drift; failover automation that was never rehearsed&lt;/td&gt;
&lt;td&gt;Lag monitoring, failover drills, pooler tuning, rolling maintenance coordination&lt;/td&gt;
&lt;td&gt;Continuous monitoring; drills quarterly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup &amp;amp; point-in-time recovery&lt;/td&gt;
&lt;td&gt;The RPO/RTO commitment&lt;/td&gt;
&lt;td&gt;Restores never run at production volume; broken WAL archive chains&lt;/td&gt;
&lt;td&gt;Restore validation against real objectives; post-restore verification before returning to service&lt;/td&gt;
&lt;td&gt;Restore drills quarterly; re-scoped at each growth step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.tigerdata.com/learn/what-is-data-retention-policy" rel="noopener noreferrer"&gt;Retention policies&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bounded storage; compliance windows&lt;/td&gt;
&lt;td&gt;Background job fails silently; policy on the wrong relation deletes data that should have been kept; aggregates outlive the raw data they were built from&lt;/td&gt;
&lt;td&gt;Policy validation and job monitoring; alignment with dependent systems and changing business requirements&lt;/td&gt;
&lt;td&gt;Monthly job audit; review on every regulatory or contract change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.tigerdata.com/blog/hypercore-a-hybrid-row-storage-engine-for-real-time-analytics" rel="noopener noreferrer"&gt;Hypercore columnstore&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;90–98% storage reduction; faster analytical queries&lt;/td&gt;
&lt;td&gt;Conversion boundary set too early adds overhead to hot data; too late, and the economics of long retention erode&lt;/td&gt;
&lt;td&gt;Boundary tuning against access patterns; resource planning for large backfills (e.g., post-calibration corrections)&lt;/td&gt;
&lt;td&gt;Semiannual review; per backfill event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.tigerdata.com/blog/materialized-views-the-timescale-way" rel="noopener noreferrer"&gt;Continuous aggregates&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Dashboard latency at production scale&lt;/td&gt;
&lt;td&gt;A failed refresh serves stale data with no user-visible error&lt;/td&gt;
&lt;td&gt;Refresh policy tuning; job failure alerting so stale data is caught before users report it&lt;/td&gt;
&lt;td&gt;Weekly alert review; retuned with each workload shift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.tigerdata.com/blog/timescale-cloud-tips-testing-your-chunk-size" rel="noopener noreferrer"&gt;Chunk configuration&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ingest throughput and memory health&lt;/td&gt;
&lt;td&gt;A misconfigured interval degrades writes precisely during peak load&lt;/td&gt;
&lt;td&gt;Interval review as ingest rates change; corrections migrate gradually into effect&lt;/td&gt;
&lt;td&gt;Quarterly review or per major ingest change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.tigerdata.com/learn/guide-to-postgresql-security" rel="noopener noreferrer"&gt;Security &amp;amp; governance&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Breach containment, auditability, contractual compliance&lt;/td&gt;
&lt;td&gt;Long-lived over-permissioned credentials; audit logs retained where nobody looks; unreviewed production changes&lt;/td&gt;
&lt;td&gt;Credential scoping and rotation rehearsed outside of incidents; audit log review and tamper protection; change control with staging validation and rollback plans&lt;/td&gt;
&lt;td&gt;Rotation per policy; review gates on every production change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version lifecycle&lt;/td&gt;
&lt;td&gt;Security patches and support coverage&lt;/td&gt;
&lt;td&gt;Running past end-of-life, unpatched, while holding customer or regulated data&lt;/td&gt;
&lt;td&gt;Minor patch windows; major upgrades validated against a staging environment that mirrors production volume, workload, aggregate configuration, and columnstore state&lt;/td&gt;
&lt;td&gt;Minor on a rolling cadence; major roughly annually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runbooks &amp;amp; institutional knowledge&lt;/td&gt;
&lt;td&gt;Incident response speed&lt;/td&gt;
&lt;td&gt;Documentation describing the system as it was at launch&lt;/td&gt;
&lt;td&gt;Updating with every configuration change; testing procedures against the live system&lt;/td&gt;
&lt;td&gt;With every change; tested quarterly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Losing the Database Stops Being an Outage
&lt;/h2&gt;

&lt;p&gt;We have seen audit configurations that were technically correct but practically invisible: logs retained in a system nobody had access to, alerts wired to a distribution list that no longer existed. The mechanism was in place. The ownership was not.&lt;/p&gt;

&lt;p&gt;Every section so far has priced platform ownership in engineer hours and business risk. There is a point in a platform's life where the currency changes, where losing the database stops being an outage and starts being regulatory action, contractual liability, or litigation. The chemicals manufacturer lives past that point. Its five years of reactor telemetry is the empirical foundation for yield improvement decisions that took years to accumulate, and it is also an evidentiary record. The corrupted migration from earlier in this paper does more than destroy operational knowledge; it puts the team in the position of reconstructing that record under potential legal scrutiny, explaining to lawyers what a background job did and why nobody caught it. Some of these failures are irreversible by construction. A retention policy that accidentally drops a week of compliance data cannot be undone. A breach of customer telemetry arrives with notification obligations and suddenly load-bearing contract language.&lt;/p&gt;

&lt;p&gt;This work rarely looks dramatic on a task list. It is credential scoping, rotation drills, tamper-protected audit logs, staging validation, change control, and rollback plans for every production change.&lt;/p&gt;

&lt;p&gt;But the failure mode is different. Availability gaps cost minutes. Recovery gaps cost hours or days. Governance gaps can cost the company things that cannot simply be restored.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Costs, and What It Buys
&lt;/h2&gt;

&lt;p&gt;Sustaining this platform at the standard a critical application demands lands between 1.5 and 3 full-time platform engineers, growing with the application, because every dimension of the work scales with data volume, concurrency, and the criticality of the guarantees. The on-call rotation requires three to four people to staff sustainably, independent of how much of their time the platform consumes. A major version upgrade is two to four engineer-weeks a year. Restore drills, failover rehearsals, policy audits, and runbook maintenance each claim recurring days per quarter. These numbers are rough by necessity and conservative by experience.&lt;/p&gt;

&lt;p&gt;Capability was never the question. The teams that do this well go in with eyes open, staffed for the work, treating the platform as a product in its own right. The real question is the counterfactual: what would those same engineers build if they were pointed at the application instead, at the ingestion pipelines, the product features, the things customers actually pay for, while the guarantees are delivered by the team that builds the database?&lt;/p&gt;

&lt;p&gt;Most engineers who evaluate TimescaleDB believe they are making a database decision. By the time the application is in production and customers depend on it, they discover they were deciding which parts of a platform they want to own. Every team owns the application. The decision is how much of the platform they want to own alongside it. This paper is designed to make that decision visible before it is made. For a framework to make it deliberately, see &lt;a href="https://tigerdata.com/blog/self-hosted-timescaledb-vs-tiger-cloud-decision" rel="noopener noreferrer"&gt;&lt;u&gt;Self-Hosted TimescaleDB or Tiger Cloud: A Framework for the Decision&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>timescaledb</category>
      <category>tigercloud</category>
      <category>developers</category>
      <category>database</category>
    </item>
    <item>
      <title>Great Models Aren't Enough for Physical AI</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Thu, 18 Jun 2026 17:52:23 +0000</pubDate>
      <link>https://dev.to/tigerdata/great-models-arent-enough-for-physical-ai-5g4e</link>
      <guid>https://dev.to/tigerdata/great-models-arent-enough-for-physical-ai-5g4e</guid>
      <description>&lt;p&gt;&lt;em&gt;Notes from our Physical AI dinner&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What should a drone do when a police helicopter approaches it?&lt;/p&gt;

&lt;p&gt;We heard that question at a dinner we recently hosted for engineering leaders and founders working in Physical AI: the AI behind robots, drones, autonomous vehicles, and other machines that sense and act in the real world, along with the infrastructure that keeps them running. Nobody at the table had a clean answer. The people in that room deploy these machines for a living, and the question that stumped them was about safety, regulation, and operations, not model quality.&lt;/p&gt;

&lt;p&gt;That was the theme of the whole evening: Physical AI is constrained by the physical world. Progress depends not only on better models, but on solving the problems around them: regulation, safety, operations, and data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling takes more than a better model
&lt;/h2&gt;

&lt;p&gt;Despite rapid model progress, truly large-scale autonomous deployments still feel distant. The gap is the long tail of situations nobody puts in a pitch deck. How should a system respond when an animal starts interacting with the equipment? What kicks in when hardware behaves in ways nobody anticipated? Who is accountable when it does?&lt;/p&gt;

&lt;p&gt;This is the unglamorous work that gates adoption. The physical world doesn't behave like a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  The physical world plays by rules a model can't change
&lt;/h2&gt;

&lt;p&gt;Physical AI companies routinely operate inside regulatory frameworks written for older technologies. EV charging companies have had to navigate gas-station rules, including public price displays and printed receipts. Drone operators face aviation requirements designed for crewed aircraft. These constraints sit outside the model entirely, and a team has to clear them before a deployment is legal, let alone good.&lt;/p&gt;

&lt;p&gt;Society sets its own rule on top: machines face a higher bar than humans. A battery fire or an autonomous accident draws disproportionate attention compared to an equivalent human-caused incident. That's the reality, and the teams that win will design for it early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surviving the physical world is a data problem
&lt;/h2&gt;

&lt;p&gt;The edge cases, the regulations, the higher bar: you handle all of them through the data the machines produce. You catch an edge case because something in the telemetry looked wrong. You prove you met a regulation because you kept the records. The physical world is messy, and data is how you get a grip on it.&lt;/p&gt;

&lt;p&gt;So &lt;a href="https://www.tigerdata.com/blog/why-iot-data-breaks-traditional-databases-what-to-do-instead" rel="noopener noreferrer"&gt;&lt;u&gt;data becomes its own hard problem&lt;/u&gt;&lt;/a&gt;. Machines in the field generate enormous volumes of telemetry, and every team deploying them wrestles with the same five decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What does the system need in real time?&lt;/li&gt;
&lt;li&gt;What stays at the edge?&lt;/li&gt;
&lt;li&gt;What gets &lt;a href="https://www.tigerdata.com/blog/how-to-build-an-iot-pipeline-for-real-time-analytics-in-postgresql" rel="noopener noreferrer"&gt;&lt;u&gt;shipped to the cloud&lt;/u&gt;&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;What's worth retaining for training?&lt;/li&gt;
&lt;li&gt;And what must be kept for regulators, sometimes for decades?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nuclear applications can carry 30-year retention requirements, a timescale that makes most storage strategies look quaint.&lt;/p&gt;

&lt;p&gt;At fleet scale, &lt;a href="https://www.tigerdata.com/blog/best-practices-for-building-iiot-energy-monitoring-applications" rel="noopener noreferrer"&gt;&lt;u&gt;monitoring, observability, and automation become critical infrastructure&lt;/u&gt;&lt;/a&gt;, increasingly run with agentic copilots that help operators watch and triage while humans stay accountable for the rare edge cases.&lt;/p&gt;

&lt;p&gt;Most teams haven't felt this yet, because most aren't at fleet scale. The ones who treat the telemetry layer as core infrastructure before they get there are the ones who won't be rebuilding it under load later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The work is surviving reality, not beating a benchmark
&lt;/h2&gt;

&lt;p&gt;The model is what everyone watches. The deployment is decided by everything around it: the regulation, the safety bar, the operations, and the data that ties them together.&lt;/p&gt;

&lt;p&gt;That's the work the people at our dinner do every day, and it's why we'll keep bringing them together. It's also the work we do: helping teams capture, store, and make sense of the data their machines produce, so the operational layer is ready when deployment scales. If you're building machines that operate beyond the lab, &lt;a href="mailto:dinners@tigerdata.com"&gt;reach out&lt;/a&gt;. We'd love to have you at the table.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>iot</category>
      <category>thoughtleadership</category>
      <category>robotics</category>
    </item>
    <item>
      <title>When PostgreSQL Isn't the Right Fit: Recognizing Workloads That Need Different Architecture</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Fri, 12 Jun 2026 12:00:47 +0000</pubDate>
      <link>https://dev.to/tigerdata/when-postgresql-isnt-the-right-fit-recognizing-workloads-that-need-different-architecture-5871</link>
      <guid>https://dev.to/tigerdata/when-postgresql-isnt-the-right-fit-recognizing-workloads-that-need-different-architecture-5871</guid>
      <description>&lt;p&gt;When PostgreSQL isn't the right fit, the signs don't announce themselves clearly. Postgres is the right database for roughly 90% of workloads, such as SaaS backends, CRUD applications, and transactional systems with mixed read/write access on shared rows. But there's a narrow 10% where those same strengths become overhead: high-frequency append-only ingestion, time-ordered data accumulating at sustained rates, analytical scans over hundreds of millions of rows. If that sounds like your system, this post is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Will Learn
&lt;/h2&gt;

&lt;p&gt;If you've added indexes, implemented partitioning, tuned autovacuum, and upgraded hardware only to watch performance degrade again on the same trajectory, the problem likely isn't your configuration. By the end of this post, you'll know whether your workload is in Postgres's 10%, how to confirm it with a single diagnostic query, and what the first concrete step toward the right architecture looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;An optimization problem and an architecture problem look identical in the early stages. Both show up as slow queries. Both respond to the same fixes: indexes, partitioning, autovacuum tuning, hardware upgrades. The divergence happens later, when the fixes stop holding and performance degrades on the same trajectory regardless of what you change.&lt;/p&gt;

&lt;p&gt;This is what’s known as the &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;optimization treadmill&lt;/u&gt;&lt;/a&gt;: a predictable sequence of phases that each buy three to six months of relief without changing the underlying trajectory. &lt;a href="https://www.tigerdata.com/blog/mvcc-feature-youre-paying-for-but-not-using" rel="noopener noreferrer"&gt;&lt;u&gt;MVCC overhead&lt;/u&gt;&lt;/a&gt;, row-oriented storage, B-tree index maintenance, WAL volume. These aren't bugs. They're architectural tradeoffs that work well for 90% of workloads and work poorly for the 10%.&lt;/p&gt;

&lt;p&gt;Knowing which problem you have determines whether you should keep tuning or make a different decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Postgres Was Designed For
&lt;/h2&gt;

&lt;p&gt;Postgres's architecture is built around concurrent access to shared rows. Multiple transactions read and write the same data at the same time, and MVCC handles the isolation. B-tree indexes find specific rows by key. Row-oriented storage assumes that when you retrieve a row, you want most of the columns in it.&lt;/p&gt;

&lt;p&gt;For an e-commerce backend, a user authentication system, or a multi-tenant SaaS product, these are exactly the right tradeoffs. Transactions need isolation. Point lookups by user ID are the dominant query pattern. Write rates track user activity, which gives the database natural breathing room between peaks. The question isn't whether Postgres is good. It's whether the workload you're running matches the patterns its architecture was designed to serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workload That Breaks the Match
&lt;/h2&gt;

&lt;p&gt;Three characteristics, when they appear together, put a workload outside what Postgres handles well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Append-only or append-heavy writes.&lt;/strong&gt; Rows are written once and never, or almost never, updated. Sensor readings, financial transactions, log entries, event streams. Every row still pays the full &lt;a href="https://www.tigerdata.com/blog/mvcc-feature-youre-paying-for-but-not-using" rel="noopener noreferrer"&gt;&lt;u&gt;MVCC cost&lt;/u&gt;&lt;/a&gt;: a 23-byte tuple header tracking transaction visibility, hint-bit dirtying on reads, and autovacuum running continuously to freeze tuples and update the visibility map. None of that overhead produces value on data that will never be touched again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sustained high write rates.&lt;/strong&gt; Not burst traffic that settles. Continuous ingestion at thousands to hundreds of thousands of rows per second, around the clock. The table grows without pause, B-tree index maintenance adds overhead with every insert, and that cost compounds with row volume, so there is no quiet window for &lt;a href="https://www.tigerdata.com/blog/preventing-silent-spiral-table-bloat" rel="noopener noreferrer"&gt;&lt;u&gt;autovacuum to catch up&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analytical query patterns.&lt;/strong&gt; The queries are aggregations over time ranges: averages, counts, percentiles, &lt;code&gt;GROUP BY&lt;/code&gt; time bucket. Row-oriented storage forces Postgres to read all columns of every matching row even when the query needs two. On a 30-column table, that's fifteen times the I/O a &lt;a href="https://www.tigerdata.com/blog/hypercore-a-hybrid-row-storage-engine-for-real-time-analytics" rel="noopener noreferrer"&gt;&lt;u&gt;columnar layout would require&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Any one of these is manageable. All three together is the combination that Postgres handles well at one million rows and struggles with at one hundred million.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Optimization Treadmill in Practice
&lt;/h2&gt;

&lt;p&gt;The pattern is predictable. Queries slow down as the table grows. You add indexes, and reads get faster. Write performance drops because index maintenance scales with row volume. You upgrade the instance. Performance stabilizes and costs go up. You implement partitioning. Recent-data queries get faster. Partition management becomes its own maintenance burden. You tune autovacuum settings. Things stabilize for a while. Data volume increases. The cycle repeats.&lt;/p&gt;

&lt;p&gt;Each step is individually correct. The problem is that the sequence never ends. You're working around an architectural mismatch instead of running a workload the architecture was designed to serve.&lt;/p&gt;

&lt;p&gt;The engineering cost accumulates in ways that are harder to see on a dashboard. The senior engineer spending a week on partition strategy is not shipping product features. The on-call rotation starts treating "database is slow again" as a recurring incident category. Quarterly planning includes a database scalability line item, every quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Know Which 10% You're In
&lt;/h2&gt;

&lt;p&gt;The answer is already in your table statistics. Not in &lt;code&gt;EXPLAIN&lt;/code&gt; plans or monitoring dashboards, but in the counters tracking exactly how rows have been written, updated, and cleaned up over the table's lifetime. Run this against your highest-traffic tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;N&lt;/span&gt;
&lt;span class="n"&gt;_live_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_dead_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_tup_ins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_tup_upd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_tup_upd&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_tup_ins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;update_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_autovacuum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_autoanalyze&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n_tup_ins&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an example of what a flagged table looks like next to a healthy one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;table_name&lt;/th&gt;
&lt;th&gt;n_tup_ins&lt;/th&gt;
&lt;th&gt;n_tup_upd&lt;/th&gt;
&lt;th&gt;update_pct&lt;/th&gt;
&lt;th&gt;last_autovacuum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;device_metrics&lt;/td&gt;
&lt;td&gt;84,729,3041&lt;/td&gt;
&lt;td&gt;24,892&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;2025-06-01 14:22:11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user_accounts&lt;/td&gt;
&lt;td&gt;184,203&lt;/td&gt;
&lt;td&gt;91,843&lt;/td&gt;
&lt;td&gt;49.86&lt;/td&gt;
&lt;td&gt;2025-05-29 08:14:03&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;device_metrics&lt;/code&gt; is in the 10%: 847 million inserts, near-zero updates, and autovacuum fired three minutes ago on a table that has never had a meaningful &lt;code&gt;UPDATE&lt;/code&gt; run against it. &lt;code&gt;user_accounts&lt;/code&gt; is not: nearly half its rows are updated, and autovacuum runs only when it actually needs to.&lt;/p&gt;

&lt;p&gt;Look for &lt;code&gt;update_pct&lt;/code&gt; under 5% and &lt;code&gt;last_autovacuum&lt;/code&gt; timestamps within the last few minutes on tables with near-zero deletes. That's the overhead the &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;companion piece&lt;/u&gt;&lt;/a&gt; documents in detail: a cleanup process running non-stop on data you never modify, because the storage engine generates that work regardless of your intent.&lt;/p&gt;

&lt;p&gt;Pair those numbers against the broader pattern. Your sustained write rate exceeds 10,000 rows per second. Your most common queries aggregate over time ranges, not point lookups by row identifier. You added partitioning specifically to control table size. You upgraded your instance specifically for query performance, not connection headroom.&lt;/p&gt;

&lt;p&gt;Three or more of those conditions, and you're in the 10%. The optimization treadmill will keep running, but the trajectory won't change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 10% Actually Needs
&lt;/h2&gt;

&lt;p&gt;If you've confirmed you're in the 10%, migrating your highest-traffic table starts with a single function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'device_metrics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;by_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This converts the table to a TimescaleDB hypertable, which does automatic time-based chunking without cron jobs or partition management scripts. From there, you can enable columnar storage on your chunks. This format reads only the columns a query requests, not full rows, and compresses historical data by 10 to 20x, bringing time-range aggregation performance in line with what the workload demands. The &lt;a href="https://www.tigerdata.com/blog/how-to-migrate-your-data-to-timescale" rel="noopener noreferrer"&gt;&lt;u&gt;migration post&lt;/u&gt;&lt;/a&gt; walks through the full process, including zero-downtime options for production tables.&lt;/p&gt;

&lt;p&gt;You keep the same SQL, the same connection strings, the same ecosystem tooling. This isn't a replacement for Postgres. It's Postgres with the storage primitives your specific workload actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Postgres is not the problem. Running the wrong workload class through an architecture designed for a different problem is. The distinction matters because one has a tuning fix and the other has a structural fix, and those two paths look identical for the first several months.&lt;/p&gt;

&lt;p&gt;The most expensive version of this recognition happens after 18 months of optimization effort. The cheapest version happens now.&lt;/p&gt;

&lt;p&gt;Run the diagnostic query above. If the numbers land where you expect, read the &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;full architectural breakdown&lt;/u&gt;&lt;/a&gt;. If you're ready to test on your own data, &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;start a free Tiger Data trial&lt;/u&gt;&lt;/a&gt; today.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>architecture</category>
      <category>developers</category>
    </item>
    <item>
      <title>Row vs Columnar Storage for Analytics: Why PostgreSQL Scans Are Slower Than They Should Be</title>
      <dc:creator>Team Tiger Data</dc:creator>
      <pubDate>Fri, 05 Jun 2026 12:48:04 +0000</pubDate>
      <link>https://dev.to/tigerdata/row-vs-columnar-storage-for-analytics-why-postgresql-scans-are-slower-than-they-should-be-59ee</link>
      <guid>https://dev.to/tigerdata/row-vs-columnar-storage-for-analytics-why-postgresql-scans-are-slower-than-they-should-be-59ee</guid>
      <description>&lt;p&gt;Here's a query that runs on most time-series tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query needs two columns: &lt;code&gt;ts&lt;/code&gt; and &lt;code&gt;temperature&lt;/code&gt;. The table has 15 columns. Postgres reads all 15 columns for every row that matches the &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;That's not a bug. It's how row-oriented storage works. Each row is stored as a contiguous block of bytes on disk, called a heap tuple, and Postgres reads the entire tuple to access any column within it. For point lookups on individual records, this is efficient. You want the whole row, and it's stored together. For analytical scans over millions of rows where you need two columns out of fifteen, it's the dominant source of wasted I/O.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;Understanding Postgres Performance Limits for Analytics on Live Data&lt;/u&gt;&lt;/a&gt;, row-oriented storage was identified as one of four architectural constraints that compound under high-frequency ingestion. That whitepaper maps the pattern at a system level. This post goes deeper on the physical mechanism: exactly how pages work, how read amplification accumulates, and why the usual fixes don't reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Will Learn
&lt;/h2&gt;

&lt;p&gt;By the end of this post, you'll have a concrete diagnostic formula: the read amplification ratio. It tells you whether your storage layout is the dominant I/O bottleneck for analytical queries on any table you own. You'll also understand why indexes can't fix this class of problem and how a hybrid row-columnar storage layout changes the math. This post assumes working familiarity with Postgres page layout and B-tree indexes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Row Storage Actually Works in Postgres
&lt;/h2&gt;

&lt;p&gt;Postgres stores data in 8KB pages. Each page holds multiple heap tuples. Each tuple contains every column value for that row, stored sequentially, preceded by a 23-byte header that carries transaction visibility metadata.&lt;/p&gt;

&lt;p&gt;A table with 15 columns averaging 200 bytes per row fits roughly 35 to 40 rows per page, after accounting for headers, alignment padding, and page overhead.&lt;/p&gt;

&lt;p&gt;When Postgres runs a sequential scan, it reads pages from disk in order. Each page load brings all the rows on that page into &lt;code&gt;shared_buffers&lt;/code&gt;, with all 15 columns per row intact. The executor then evaluates the &lt;code&gt;WHERE&lt;/code&gt; clause and pulls the needed columns from what was already loaded into memory.&lt;/p&gt;

&lt;p&gt;The I/O cost is proportional to total table size, not to the size of the queried columns. A query that needs 12 bytes of data per row still reads 200 bytes from disk. The remaining 188 bytes load into the buffer cache and get discarded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Read Amplification Math
&lt;/h2&gt;

&lt;p&gt;The number that makes this concrete is the read amplification ratio: total row width divided by the width of the columns the query actually needs.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;sensor_readings&lt;/code&gt;, the calculation is direct. The &lt;code&gt;ts&lt;/code&gt; column is a &lt;code&gt;timestamptz&lt;/code&gt; at 8 bytes. The temperature column is a &lt;code&gt;float4&lt;/code&gt; at 4 bytes. Together they represent 12 bytes of useful data per row. The full row is 200 bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read amplification ratio: 200 ÷ 12 = 16.7x&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For every byte the query uses, Postgres reads 16.7 bytes from disk.&lt;/p&gt;

&lt;p&gt;At 100 million rows covering seven days, that ratio stops being abstract. The query needs 100M x 12 bytes = 1.14 GB. Postgres reads 100M x 200 bytes = 18.6 GB. At a 500 MB/sec sequential read rate, the scan takes approximately 38 seconds. Reading only the needed columns would take roughly 2.3 seconds. That 16x gap is pure storage model overhead.&lt;/p&gt;

&lt;p&gt;No index changes this number. No configuration setting changes it. Partitioning reduces scope. Fewer pages get scanned by cutting the time range, but within each partition the same per-row read cost applies. The storage layout determines the I/O, and the storage layout is fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try This Now: Measure Your Read Amplification
&lt;/h2&gt;

&lt;p&gt;You can calculate the ratio for any table you own. Run these two queries to get the byte widths you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Full row weight&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;row_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Queried column weight&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;queried_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Divide &lt;code&gt;row_bytes&lt;/code&gt; by &lt;code&gt;queried_bytes&lt;/code&gt;. If the ratio is above 5x, the storage model is your largest I/O bottleneck for analytical queries on that table. No index or configuration change will close that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Indexes Don’t Solve This
&lt;/h2&gt;

&lt;p&gt;When a query is slow, the instinctive response is to add an index. For OLTP workloads, that instinct is correct. B-tree indexes excel at row selection: they find specific rows in &lt;code&gt;O(log n)&lt;/code&gt; time, and for a lookup like &lt;code&gt;SELECT * FROM users WHERE id = 123&lt;/code&gt;, the index locates the target row in microseconds.&lt;/p&gt;

&lt;p&gt;For analytical queries that touch millions of rows, row selection is not the bottleneck. Finding the rows is fast. Reading the data from those rows is slow. An index scan on a million-row result set still reads the full heap tuple for every matching row to extract the needed columns.&lt;/p&gt;

&lt;p&gt;The one exception is a covering index, which stores column values inside the index itself so Postgres can satisfy the query without touching the heap. But covering indexes for analytical queries become impractical at scale. When queries involve aggregations across high-frequency writes, wide covering indexes impose substantial write overhead, compounding exactly the index maintenance costs described in the &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill" rel="noopener noreferrer"&gt;&lt;u&gt;optimization treadmill post&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;B-tree indexes optimize for row selection (which rows to read). Analytical query performance is dominated by row width (how much data per row). These are different problems, and solving one leaves the other intact. For a broader look at what this means for your schema design, see &lt;a href="https://www.tigerdata.com/learn/postgresql-data-analysis-best-practices" rel="noopener noreferrer"&gt;&lt;u&gt;Best Practices for PostgreSQL Data Analysis&lt;/u&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  How Columnar Storage Changes the Equation
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://www.tigerdata.com/learn/columnar-databases-vs-row-oriented-databases-which-to-choose" rel="noopener noreferrer"&gt;&lt;u&gt;columnar storage&lt;/u&gt;&lt;/a&gt;, data is organized by column instead of by row. All values for &lt;code&gt;ts&lt;/code&gt; live together in one stream on disk. All values for &lt;code&gt;temperature&lt;/code&gt; live together in another. When the query needs those two columns, it reads two streams. The other 13 columns are never touched.&lt;/p&gt;

&lt;p&gt;Same query, same 100 million rows: data read drops to 100M x 12 bytes = 1.14 GB. With typical 10 to 20x compression for time-series data, that compresses to approximately 60 to 120 MB. At 500 MB/sec, the same scan completes in roughly 0.12 to 0.24 seconds.&lt;/p&gt;

&lt;p&gt;The compression benefit stacks on top of the I/O reduction. Because all values in a column share the same data type, compression algorithms work far more effectively. Sequential timestamps delta-encode to near-zero storage overhead. Floating-point sensor values compress with XOR-based techniques derived from &lt;a href="https://www.vldb.org/pvldb/vol8/p1816-teller.pdf" rel="noopener noreferrer"&gt;&lt;u&gt;Facebook's Gorilla algorithm&lt;/u&gt;&lt;/a&gt;. Row-oriented heap storage can't apply any of these because values from different columns are interleaved on every page. There's no contiguous column stream to compress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypercore: Row and Columnar in One Table
&lt;/h2&gt;

&lt;p&gt;The tradeoff with pure columnar storage is write performance. Every new row appends to each column file separately, which adds overhead for high-frequency ingestion. You get the read benefit but give up write throughput. Tiger Data's Hypercore solves this with a &lt;a href="https://www.tigerdata.com/blog/hypercore-a-hybrid-row-storage-engine-for-real-time-analytics" rel="noopener noreferrer"&gt;&lt;u&gt;hybrid layout that keeps both&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Recent data stays in row-oriented storage for fast ingestion. Older data converts automatically to columnar format based on a compression policy you configure. The application writes standard SQL to one table. The storage format changes by age without any application-layer involvement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Enable Hypercore on a hypertable with a 7-day row storage window&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sensor_readings&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compress_segmentby&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'device_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compress_orderby&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ts DESC'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;add_compression_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sensor_readings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New rows land in row format and ingest quickly. Data older than seven days converts to columnar chunks. To verify the behavior immediately without waiting for the policy schedule, compress a chunk manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;compress_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;show_chunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sensor_readings'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; on the aggregation query to see the difference in buffer reads (representative output on a 100M-row dataset):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Before: row storage sequential scan
Seq Scan on sensor_readings
  Buffers: shared read=2375000 -- 18.6 GB read from disk
  Execution Time: 38142.2 ms

-- After: Hypercore columnar scan
Custom Scan (ColumnarScan) on sensor_readings
  Buffers: shared read=10240 -- 80 MB read from disk
  Execution Time: 196.4 ms

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same &lt;code&gt;SELECT&lt;/code&gt; statement works against both storage formats. The query planner handles the difference transparently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Row storage reads every column to access any column. For analytical queries that scan millions of rows and need only a few, this is the largest source of I/O overhead. It doesn't yield to &lt;a href="https://www.tigerdata.com/learn/postgres-performance-best-practices" rel="noopener noreferrer"&gt;&lt;u&gt;index tuning&lt;/u&gt;&lt;/a&gt;, partitioning, or hardware upgrades.&lt;/p&gt;

&lt;p&gt;Calculate the read amplification ratio for your most common analytical queries using the &lt;code&gt;pg_column_size&lt;/code&gt; queries above. If the ratio is above 5x, &lt;a href="https://www.tigerdata.com/docs/reference/timescaledb/hypercore" rel="noopener noreferrer"&gt;&lt;u&gt;Hypercore&lt;/u&gt;&lt;/a&gt; is the direct fix. Start a &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;free Tiger Data trial&lt;/u&gt;&lt;/a&gt; today to enable the hybrid storage model on your tables.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>realtimeanalytics</category>
      <category>storage</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
