<?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: James Blackwood-Sewell</title>
    <description>The latest articles on DEV Community by James Blackwood-Sewell (@james_blackwoodsewell_58).</description>
    <link>https://dev.to/james_blackwoodsewell_58</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2710737%2F2ee1a993-a308-45e1-abea-3403dc3ea2b0.png</url>
      <title>DEV Community: James Blackwood-Sewell</title>
      <link>https://dev.to/james_blackwoodsewell_58</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/james_blackwoodsewell_58"/>
    <language>en</language>
    <item>
      <title>Benchmarking Databases for Real-Time Analytics Applications</title>
      <dc:creator>James Blackwood-Sewell</dc:creator>
      <pubDate>Wed, 26 Mar 2025 16:11:45 +0000</pubDate>
      <link>https://dev.to/tigerdata/benchmarking-databases-for-real-time-analytics-applications-f2d</link>
      <guid>https://dev.to/tigerdata/benchmarking-databases-for-real-time-analytics-applications-f2d</guid>
      <description>&lt;p&gt;Choosing the right database for analytics is hard. With many options available, each is optimized for different use cases.&lt;/p&gt;

&lt;p&gt;Some databases are built for real-time analytics in customer-facing applications, where low-latency queries and high-ingest performance are essential. Others are designed for internal BI and reporting and optimized for large-scale aggregations and batch processing. Some databases are general-purpose, handling both transactions and analytics, while others specialize in analytical workloads.&lt;/p&gt;

&lt;p&gt;Benchmarks can help—but only if they reflect your actual workload.&lt;/p&gt;

&lt;p&gt;Several benchmarks, such as ClickBench, TPC-H, and TPC-DS, evaluate the performance of databases for analytics. However, they are not representative of real-time analytics.&lt;/p&gt;

&lt;p&gt;To fill this gap, we’ve created RTABench, a new benchmark to assist developers in evaluating the performance of different databases in real-time analytics scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key patterns in real-time analytics include:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multi-table Joins: Quickly combining data from several tables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Selective Filtering: Fast lookups for the most recent and specific data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pre-aggregated Results: Using pre-calculated materialized views for prompt responses.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While denormalizing data can speed up queries, it complicates management and raises costs. Real-time applications favor normalized schemas and joining data at query time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introducing RTABench&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To meet the need for real-time analytics workloads, we developed RTABench, a benchmark designed to test databases with these specific requirements. RTABench focuses on essential query patterns such as joins, filtering, and pre-aggregations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How RTABench Works&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RTABench models an order tracking system with normalized tables to mimic real-time analytics applications. It uses around 171 million order records and evaluates databases with 40 different queries, including basic counts, selective filtering, multi-table joins, and pre-aggregated queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RTABench categorizes databases into:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;General-purpose databases: e.g., PostgreSQL and MySQL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Real-time analytics databases: Optimized for quick insights, often secondary databases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Batch analytics databases: Primarily for historical data and excluded from real-time benchmarks.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Benchmark Results: What We Learned&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RTABench results are published at rtabench.com. While performance varies based on workload characteristics, this benchmark reveals some interesting insights:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;General-purpose databases perform better on RTABench than on ClickBench.&lt;/strong&gt; That’s expected—RTABench uses a normalized schema similar to real applications, while ClickBench is based on a denormalized dataset optimized for batch analytics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TimescaleDB is 1.9x faster than ClickHouse on RTABench, even though it’s 6.8x slower on ClickBench.&lt;/strong&gt; This is likely because TimescaleDB is optimized for real-time analytics applications, which often rely on normalized schemas and selective aggregations, while ClickHouse shines in denormalized, columnar analytics with large-scale aggregations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Incremental materialized views offer massive speedups.&lt;/strong&gt; They deliver up to hundreds or even thousands of times faster performance than querying the raw data (from seconds to a few milliseconds), demonstrating their value for real-time analytics. However, among the databases tested, only ClickHouse and TimescaleDB support them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ClickHouse is the leader in data loading and storage efficiency.&lt;/strong&gt; It’s 4.8x faster at loading data and uses 1.7x less disk than the next best database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PostgreSQL was the fastest general-purpose database.&lt;/strong&gt; The most popular database among developers demonstrates its versatility. With indexing, it’s only 4.1x slower than TimescaleDB on raw queries—but it can’t match the performance of incremental materialized views, which PostgreSQL doesn’t support.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2F1gyv8gkj3sx9mlzlx575.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.amazonaws.com%2Fuploads%2Farticles%2F1gyv8gkj3sx9mlzlx575.png" alt=" " width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check out the full results &lt;a href="https://rtabench.com/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community contributions to RTABench are encouraged!&lt;/strong&gt; Whether it's adding new databases, improving existing queries, or making configuration tweaks, we hope you’ll contribute to the repo. All tools, datasets, and results are available on GitHub, with the latest version &lt;a href="https://rtabench.com/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>database</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Speed Up Triggers by 7x With Transition Tables</title>
      <dc:creator>James Blackwood-Sewell</dc:creator>
      <pubDate>Thu, 20 Mar 2025 13:13:38 +0000</pubDate>
      <link>https://dev.to/tigerdata/speed-up-triggers-by-7x-with-transition-tables-347l</link>
      <guid>https://dev.to/tigerdata/speed-up-triggers-by-7x-with-transition-tables-347l</guid>
      <description>&lt;p&gt;TimescaleDB 2.18 introduces transition table support for hypertables, a &lt;a href="https://github.com/timescale/timescaledb/issues/1084" rel="noopener noreferrer"&gt;&lt;u&gt;long-requested and upvoted feature&lt;/u&gt;&lt;/a&gt; (issue from 2019!) that enhances trigger functionality by allowing bulk access to affected rows during &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt; operations.&lt;/p&gt;

&lt;p&gt;This sidesteps the inefficiencies of row-by-row trigger processing, making operations that aggregate metadata about devices or batches significantly more efficient: we’ve seen a &lt;strong&gt;7x performance improvement&lt;/strong&gt; for statement-level triggers with transition tables.&lt;/p&gt;

&lt;p&gt;Like the support for &lt;a href="https://www.timescale.com/blog/postgresql-indexes-for-columnstore" rel="noopener noreferrer"&gt;&lt;u&gt;PostgreSQL indexes in our columnstore&lt;/u&gt;&lt;/a&gt;, this feature brings TimescaleDB closer to standard PostgreSQL behavior, making integration into existing workflows smoother. More importantly, it unlocks new, previously impractical bulk processing capabilities due to performance constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Transition Tables Work in PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Transition tables were introduced in PostgreSQL 10 to give &lt;code&gt;AFTER&lt;/code&gt; triggers access to all affected rows in a single operation. Normally, when a trigger fires, it operates on each row one at a time—meaning that if a statement inserts, updates, or deletes multiple rows, the trigger function must run once per row, which is very inefficient.&lt;/p&gt;

&lt;p&gt;With transition tables, &lt;a href="https://www.postgresql.org/docs/current/sql-createtrigger.html" rel="noopener noreferrer"&gt;PostgreSQL allows trigger functions&lt;/a&gt; to see all modified rows at once in a temporary table-like structure. This means a trigger can process changes in bulk rather than row-by-row. As you can guess, it’s a lot faster!&lt;/p&gt;

&lt;p&gt;These transition tables are available in &lt;code&gt;AFTER INSERT&lt;/code&gt;, &lt;code&gt;AFTER UPDATE&lt;/code&gt;, and &lt;code&gt;AFTER DELETE&lt;/code&gt; triggers. They are referenced using the &lt;code&gt;REFERENCING NEW TABLE AS&lt;/code&gt; or &lt;code&gt;REFERENCING OLD TABLE AS&lt;/code&gt; clauses, which let the trigger function treat the affected rows like a normal table inside the function.&lt;/p&gt;

&lt;p&gt;Until TimescaleDB 2.18, &lt;a href="https://docs.timescale.com/use-timescale/latest/hypertables/about-hypertables/" rel="noopener noreferrer"&gt;hypertables&lt;/a&gt; lacked support for this feature, which meant that bulk processing of changes had to be handled outside of trigger logic or row by row with a statement-level trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Transition Tables Took Us So Long
&lt;/h2&gt;

&lt;p&gt;I’m a developer advocate, not a developer, but I came up against the need for transition tables often enough that I brushed up my C skills and attacked the issue. At first, I assumed there was a deep technical reason why TimescaleDB didn't support transition tables. But as I dug into the code and started working on the &lt;a href="https://github.com/timescale/timescaledb/issues/1084" rel="noopener noreferrer"&gt;&lt;u&gt;initial pull request&lt;/u&gt;&lt;/a&gt;, it became clear that it wasn’t a technical limitation—it was probably just left out of the original implementation to reduce scope.&lt;/p&gt;

&lt;p&gt;Hypertables add complexity compared to regular PostgreSQL tables because of chunking and our time-series planner optimizations. Handling transition tables across all these cases took a lot of testing, but fundamentally, it wasn’t impossible—it just hadn’t been prioritized yet.&lt;/p&gt;

&lt;p&gt;I opened the&lt;a href="https://github.com/timescale/timescaledb/pull/6901" rel="noopener noreferrer"&gt;&lt;u&gt; initial PR&lt;/u&gt;&lt;/a&gt;, but, like many open-source contributions, it took some extra polish to get it production-ready. &lt;a href="https://www.timescale.com/blog/author/mats" rel="noopener noreferrer"&gt;&lt;u&gt;Mats&lt;/u&gt;&lt;/a&gt; later picked it up and refined the implementation, making sure it was solid and well-integrated into TimescaleDB. Huge shoutout to Mats for seeing it through to completion!&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases for Transition Tables
&lt;/h2&gt;

&lt;p&gt;Now that we have transition tables, they unlock a bunch of cool use cases for hypertables. Here are a few examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tracking first and last appearances of an entity&lt;/strong&gt; : Keep a metadata table that records when an entity (e.g., a generator or sensor) first and last appears in the dataset. You could even track the total number of records associated with it and any other metadata your application needs (even the full last state). This can then be used for ultra-fast lookups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch validation and anomaly detection&lt;/strong&gt; : Flag bad data—like missing values or extreme readings—for further analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch tracking:&lt;/strong&gt; Track the size, timing, and metadata (minimum and maximum timestamps) for each batch insert. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tracking device metadata
&lt;/h3&gt;

&lt;p&gt;One of the best use cases for transition tables is tracking per-ID metadata, like when a device or sensor first and last appears in the dataset. While this data is available in the main table, maintaining a separate metadata table can speed up finding records in the hypertable, especially when you have many hypertable chunks and all devices aren’t constantly reporting.&lt;/p&gt;

&lt;p&gt;The following SQL can be used to set this up.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Table schema&lt;/strong&gt;
&lt;/h4&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;power_generation&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;generator_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;power_output_kw&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;voltage&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;current&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;frequency&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&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;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'power_generation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s1"&gt;'timestamp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="n"&gt;chunk_time_interval&lt;/span&gt;&lt;span class="o"&gt;=&amp;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="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;create_default_indexes&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;false&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;INDEX&lt;/span&gt; &lt;span class="n"&gt;power_generation_generator_id_timestamp_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;power_generation&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generator_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&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;h4&gt;
  
  
  &lt;strong&gt;Metadata table&lt;/strong&gt;
&lt;/h4&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;generator_metadata&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;generator_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&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;first_seen&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_seen&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;Trigger function&lt;/strong&gt;
&lt;/h4&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;update_generator_metadata&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;generator_metadata&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generator_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_seen&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;generator_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&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="nb"&gt;timestamp&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;new_table&lt;/span&gt; &lt;span class="n"&gt;n&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;generator_id&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generator_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;last_seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GREATEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generator_metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;first_seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LEAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generator_metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_seen&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;NULL&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;Trigger statement&lt;/strong&gt;
&lt;/h4&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;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;generator_metadata_trigger&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;power_generation&lt;/span&gt;
&lt;span class="k"&gt;REFERENCING&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_table&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;STATEMENT&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;update_generator_metadata&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation ensures the metadata table automatically keeps accurate &lt;code&gt;first_seen&lt;/code&gt; and &lt;code&gt;last_seen&lt;/code&gt; timestamps for each &lt;code&gt;generator_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It can be used to &lt;a href="https://www.timescale.com/blog/scaling-real-time-analytics-in-postgres" rel="noopener noreferrer"&gt;speed up real-time analytics&lt;/a&gt;, querying directly for the last record with this SQL:&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="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;power_generation&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;generator_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&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;last_record&lt;/span&gt; 
                 &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;power_generation_metadata&lt;/span&gt;
                 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;generator_id&lt;/span&gt; &lt;span class="o"&gt;=&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;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Adding a trigger to track generator metadata is useful, but what about performance? Running row-by-row updates in a trigger is notoriously slow, so let’s compare a row-level trigger, a statement-level trigger (using transition tables), and an import without any trigger.&lt;/p&gt;

&lt;p&gt;I used the &lt;a href="https://github.com/jamessewell/pgingester/" rel="noopener noreferrer"&gt;&lt;u&gt;pgingester&lt;/u&gt;&lt;/a&gt; command, running it with the following settings:&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;pgingester&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="nb"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;copy&lt;/span&gt; 
                    &lt;span class="c1"&gt;--threads 1  &lt;/span&gt;
                    &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;~/&lt;/span&gt;&lt;span class="n"&gt;power_generation_10m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2F1h86tqgiujvv7qkcc48y.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.amazonaws.com%2Fuploads%2Farticles%2F1h86tqgiujvv7qkcc48y.png" width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For an import of 10 million records using just a single thread, the results were clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A row-level trigger processing each record individually took 370 seconds, achieving only 27,000 rows/sec. This was almost 9x slower than the baseline import.&lt;/li&gt;
&lt;li&gt;A statement-level trigger (using transition tables) processed the same data in 53 seconds, handling 187,000 rows/sec—a 1.28x slowdown compared to no trigger but still an order of magnitude faster than row-by-row processing.&lt;/li&gt;
&lt;li&gt;A baseline import with no trigger completed in 42 seconds at 239,000 rows/sec.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The takeaway? Row-by-row triggers are effectively unusable at scale, while transition tables offer a reasonable trade-off between functionality and performance.&lt;/p&gt;

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

&lt;p&gt;Transition tables in TimescaleDB 2.18 are a huge step forward for efficient bulk processing in hypertables. They make maintaining ID and device metadata tables much more practical while keeping TimescaleDB more in sync with standard PostgreSQL behavior.&lt;/p&gt;

&lt;p&gt;Yes, there’s some overhead compared to not using triggers at all, but &lt;strong&gt;statement-level triggers with&lt;/strong&gt;  &lt;strong&gt;transition tables are 7x faster&lt;/strong&gt; than row-by-row processing. If you’ve been avoiding triggers because of performance concerns, this feature makes them viable again.&lt;/p&gt;

&lt;p&gt;I’m excited to see how people use this feature—whether for real-time monitoring, metadata tracking, or something completely unexpected. Got a cool use case? &lt;a href="https://slack.timescale.com" rel="noopener noreferrer"&gt;&lt;u&gt;Let us know&lt;/u&gt;&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;If you want to try this performance boost for yourself and supercharge your real-time analytics workloads, the easiest way to get started is to &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;create a free Timescale Cloud account&lt;/u&gt;&lt;/a&gt;. You can also &lt;a href="https://docs.timescale.com/self-hosted/latest/install/" rel="noopener noreferrer"&gt;&lt;u&gt;install TimescaleDB on your machine&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>database</category>
      <category>news</category>
      <category>postgres</category>
    </item>
    <item>
      <title>10 psql commands to boost productivity (with examples)</title>
      <dc:creator>James Blackwood-Sewell</dc:creator>
      <pubDate>Tue, 11 Mar 2025 12:38:17 +0000</pubDate>
      <link>https://dev.to/tigerdata/10-psql-commands-to-boost-productivity-4a42</link>
      <guid>https://dev.to/tigerdata/10-psql-commands-to-boost-productivity-4a42</guid>
      <description>&lt;p&gt;Did you know that despite all the fancy GUI tools available, the humble command-line &lt;code&gt;psql&lt;/code&gt; remains the &lt;a href="https://www.timescale.com/state-of-postgres/2024" rel="noopener noreferrer"&gt;most popular way to interact with PostgreSQL&lt;/a&gt; databases? &lt;/p&gt;

&lt;p&gt;But let's be honest - &lt;code&gt;psql&lt;/code&gt; has a bit of a learning curve. The real magic lies in its meta-commands (or slash commands), which start with a backslash (&lt;code&gt;\&lt;/code&gt;). These commands can dramatically improve your workflow, &lt;strong&gt;but only if you know they exist&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;Let me walk you through my top 10 most-used &lt;code&gt;psql&lt;/code&gt; meta-commands that will make your database interactions much smoother. &lt;/p&gt;

&lt;p&gt;📚 &lt;a href="https://www.timescale.com/blog/10-psql-commands-that-will-make-your-life-easier" rel="noopener noreferrer"&gt;Jump to the full article&lt;/a&gt;&lt;br&gt;
🔧 &lt;a href="https://www.timescale.com/blog/how-to-install-psql-on-mac-ubuntu-debian-windows" rel="noopener noreferrer"&gt;Install &lt;code&gt;psql&lt;/code&gt; on Mac, Ubuntu, Debian, Windows&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  1. &lt;code&gt;\d&lt;/code&gt; - Describe Relations
&lt;/h2&gt;

&lt;p&gt;This command shows you all the relations (tables, views, sequences) your current session can access:&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;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;
         &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;relations&lt;/span&gt;
 &lt;span class="k"&gt;Schema&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Type&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="k"&gt;Owner&lt;/span&gt;   
&lt;span class="c1"&gt;--------+-------+-------+-----------&lt;/span&gt;
 &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;power&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;tsdbadmin&lt;/span&gt;
 &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;tsdbadmin&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&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;Pro tip:&lt;/strong&gt; There are many variants of this command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;\dt&lt;/code&gt; - Shows only tables&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;\di&lt;/code&gt; - Shows only indexes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;\du&lt;/code&gt; - Shows roles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;\dn&lt;/code&gt; - Shows schemas&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;\l&lt;/code&gt; - Shows databases (not \db as you might expect!)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As an added bonus, try &lt;code&gt;\d+&lt;/code&gt; to also see the &lt;strong&gt;size&lt;/strong&gt; of each table, or &lt;code&gt;\l+&lt;/code&gt; for database sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;\d&lt;/code&gt; table - Describe a Specific Relation
&lt;/h2&gt;

&lt;p&gt;Need the details of a specific table? Use &lt;code&gt;\d&lt;/code&gt; followed by the table name:&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;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt;
                         &lt;span class="k"&gt;Table&lt;/span&gt; &lt;span class="nv"&gt;"public.small"&lt;/span&gt;
  &lt;span class="k"&gt;Column&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="k"&gt;Type&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Collation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Nullable&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Default&lt;/span&gt; 
&lt;span class="c1"&gt;----------+--------------------------+-----------+----------+---------&lt;/span&gt;
 &lt;span class="n"&gt;ts&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt; 
 &lt;span class="n"&gt;sensorid&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;                  &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt; 
 &lt;span class="n"&gt;value&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt; 
&lt;span class="n"&gt;Indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"small_sensorid_ts_idx"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sensorid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&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;This gives you columns, data types, constraints, and indexes all in one view.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;\e&lt;/code&gt; - Edit Query Buffer
&lt;/h2&gt;

&lt;p&gt;Working on a complex query? Every time you run a query in &lt;code&gt;psql&lt;/code&gt;, it goes into your query buffer. The &lt;code&gt;\e&lt;/code&gt; command opens your default editor with that buffer loaded so you can edit it properly.&lt;/p&gt;

&lt;p&gt;This is a lifesaver for complex queries where one-line edits in the terminal become tedious!&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;\ef&lt;/code&gt; function - Edit Function
&lt;/h2&gt;

&lt;p&gt;Similar to &lt;code&gt;\e&lt;/code&gt;, but this opens your editor with an existing function definition loaded. When you save and exit, it runs the SQL automatically.&lt;/p&gt;

&lt;p&gt;Remember to include &lt;code&gt;CREATE OR REPLACE&lt;/code&gt; if you're updating an existing function!&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;\x&lt;/code&gt; - Toggle Expanded Output
&lt;/h2&gt;

&lt;p&gt;This command changes how results are displayed. Instead of columns stretching across your screen, each row becomes a vertical block:&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;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="c1"&gt;-- Expanded display is on.&lt;/span&gt;
&lt;span class="n"&gt;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;power&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="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;RECORD&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;------------------------&lt;/span&gt;
&lt;span class="n"&gt;ts&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt; &lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;76672&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;v01&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;8043&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;284272117919&lt;/span&gt;
&lt;span class="n"&gt;v02&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;6896&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;529731366893&lt;/span&gt;
&lt;span class="n"&gt;v03&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;460&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;279921892548&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is invaluable when dealing with wide tables or columns containing JSON, geospatial data, or long text.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. &lt;code&gt;\timing&lt;/code&gt; - Toggle Command Timing
&lt;/h2&gt;

&lt;p&gt;Enable timing mode to see how long each query takes:&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;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;timing&lt;/span&gt; 
&lt;span class="c1"&gt;-- Timing is on.&lt;/span&gt;
&lt;span class="n"&gt;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&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;v01&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;power&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="n"&gt;RECORD&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;——&lt;/span&gt;
&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;9999&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99927364142&lt;/span&gt;

&lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;635&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just be aware that this includes network round-trip time, which can be significant when connecting to remote databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;\c&lt;/code&gt; database - Connect to Database
&lt;/h2&gt;

&lt;p&gt;Need to switch databases without disconnecting? Use \c followed by the database name:&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;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="n"&gt;another_database&lt;/span&gt;
&lt;span class="n"&gt;You&lt;/span&gt; &lt;span class="k"&gt;are&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="nv"&gt;"another_database"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="nv"&gt;"tsdbadmin"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. &lt;code&gt;\copy&lt;/code&gt; - Perform SQL Copy
&lt;/h2&gt;

&lt;p&gt;PostgreSQL's COPY command is great for bulk loading data, but it runs server-side. The &lt;code&gt;\copy&lt;/code&gt; meta-command performs the same function but runs client-side, meaning it can access files on your local machine:&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;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;copy&lt;/span&gt; &lt;span class="n"&gt;my_table&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="s1"&gt;'/path/to/local/file.csv'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CSV&lt;/span&gt; &lt;span class="n"&gt;HEADER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful when working with cloud databases where you can't easily upload files to the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. &lt;code&gt;\i&lt;/code&gt; file - Read SQL from File
&lt;/h2&gt;

&lt;p&gt;Have a SQL script you want to run? Use &lt;code&gt;\i&lt;/code&gt; to execute commands from a file:&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;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;my_script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All commands will run in sequence, and errors will be visible but won't stop execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. &lt;code&gt;\?&lt;/code&gt; - The PSQL Meta-Command Cheat Sheet
&lt;/h2&gt;

&lt;p&gt;Forgot how a meta-command works? Type &lt;code&gt;\?&lt;/code&gt; to see all available commands:&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;tsdb&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;While there are many great GUI tools out there (like pgAdmin, DataGrip, or PopSQL), mastering &lt;code&gt;psql&lt;/code&gt; meta-commands can make you significantly more productive when working with PostgreSQL.&lt;/p&gt;

&lt;p&gt;If you need more quick tips, check our &lt;a href="https://www.timescale.com/learn/postgres-cheat-sheet" rel="noopener noreferrer"&gt;cheat sheet for interacting with your PostgreSQL&lt;/a&gt; database.&lt;/p&gt;

&lt;p&gt;What are your favorite &lt;code&gt;psql&lt;/code&gt; tricks? Share them in the comments below!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want more PostgreSQL tips? Follow for more database insights and practical guides—save for later!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>productivity</category>
      <category>database</category>
    </item>
    <item>
      <title>8 Reasons Postgres for IoT just makes sense</title>
      <dc:creator>James Blackwood-Sewell</dc:creator>
      <pubDate>Wed, 19 Feb 2025 11:52:00 +0000</pubDate>
      <link>https://dev.to/tigerdata/8-reasons-postgres-for-iot-just-makes-sense-3186</link>
      <guid>https://dev.to/tigerdata/8-reasons-postgres-for-iot-just-makes-sense-3186</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; PostgreSQL solves the IoT data puzzle by unifying time-series and relational data in one database. Engineers can now query rich context alongside sensor readings, while TimescaleDB handles massive data volumes. From location tracking to custom data types, PostgreSQL delivers the reliability that IoT demands—all without juggling multiple databases. 👉 &lt;a href="https://www.timescale.com/blog/storing-iot-data-why-you-should-use-postgresql?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=dev-2025&amp;amp;utm_content=8-reasons-postgres-for-iot" rel="noopener noreferrer"&gt;Jump to the full article&lt;/a&gt; &lt;/p&gt;




&lt;p&gt;IoT generates massive streams of sensor data from connected physical systems. Teams often split this between SQL and NoSQL databases, creating needless complexity. PostgreSQL with TimescaleDB offers a simpler path: one database that handles both time-series and relational data at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Scaling PostgreSQL for Sensor Data (With Timescale)
&lt;/h2&gt;

&lt;p&gt;IoT backends need to support high data ingest rates, while writes on PostgreSQL can often slow to a crawl as your dataset grows.&lt;/p&gt;

&lt;p&gt;🤯 &lt;em&gt;Did you know&lt;/em&gt; you can scale PostgreSQL to ingest millions of rows per second, storing billions of rows, even on a single node with a modest amount of RAM (if your database is architected the right way).&lt;/p&gt;

&lt;p&gt;How do we know this? Because we did it. 😎 For one of our features, we &lt;a href="https://www.timescale.com/learn/guide-to-postgresql-scaling" rel="noopener noreferrer"&gt;scaled PostgreSQL to petabytes of data&lt;/a&gt; (and counting), with over 800 billion new records per day. We flew past 100 trillion raw records in a single table recently. &lt;/p&gt;

&lt;h2&gt;
  
  
  2. A Simplified Stack: One IoT Database vs. Two
&lt;/h2&gt;

&lt;p&gt;TimescaleDB lets PostgreSQL handle both your time-series and relational IoT data in a single database.&lt;/p&gt;

&lt;p&gt;This eliminates the complexity of maintaining separate NoSQL and SQL databases with messy glue code between them. For backup scenarios, this is crucial - no more risk of time-series data pointing to missing device metadata because your databases were backed up at different times. From edge computing to large-scale deployments, one unified database makes system design dramatically simpler, echoed by CTO of a unicorn startup: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Aggressively consolidating our tech stack lets us sharpen our engineering team's focus."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. More Context for Your IoT Data
&lt;/h2&gt;

&lt;p&gt;A single database for time-series data and metadata does more than just simplify your stack: it also lets you add context to your sensor data. Because what is the point of collecting data if you don’t know what you’re measuring?&lt;/p&gt;

&lt;p&gt;With TimescaleDB and PostgreSQL, they can keep their metadata normalized and add the necessary context to their time-series data at query time (via a &lt;a href="https://www.timescale.com/learn/sql-joins-summary" rel="noopener noreferrer"&gt;SQL JOIN&lt;/a&gt;). For example, via a query like this:&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;-- Show me the average temperature by machine, for machines of a &lt;/span&gt;
&lt;span class="c1"&gt;-- given type on a particular line, by 5 minute intervals&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;'5 minutes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;measurements&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;as&lt;/span&gt; &lt;span class="n"&gt;five_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;machine_id&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;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;machines&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;machine_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;machines&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;JOIN&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;machines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&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;machines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'extruder'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'production'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;.&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;'36 hours'&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;five_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machine_id&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;five_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machine_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Power and Ease of Use
&lt;/h2&gt;

&lt;p&gt;Let’s take a step back and keep in mind the value of an IoT project: to collect, analyze, and act on data to improve utility/efficiencies, reduce downtime/waste, and provide better products and services. In other words, you need more than a data store: you also need an easy way to wring actionable insights out of your data.&lt;/p&gt;

&lt;p&gt;This is where SQL comes in. While it’s been quite fashionable in the past several years to denounce SQL and praise NoSQL, the truth is that SQL is quite powerful and is starting to make a comeback (which is one reason why “NoSQL” is now getting “backronymed” to “Not only SQL”).&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;-- Plot the change in temperature for ceiling sensors &lt;/span&gt;
&lt;span class="c1"&gt;-- on linear and logarithmic scales, by 10 second intervals&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ten_second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;lead&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="n"&gt;OVER&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;temperature_rise_linear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;lead&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="n"&gt;OVER&lt;/span&gt; &lt;span class="k"&gt;data&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;temperature_rise_log&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'10 seconds'&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;as&lt;/span&gt; &lt;span class="n"&gt;ten_second&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;as&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;sensors&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sensor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensors&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;sensors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ceiling'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;.&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="s1"&gt;'2017-06-01'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;window&lt;/span&gt; &lt;span class="k"&gt;data&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;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ten_second&lt;/span&gt; &lt;span class="k"&gt;asc&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;SQL has another advantage:&lt;/strong&gt; people across your organization already know it. You won’t need to train your engineers to learn a new specialized query language (or hire new ones), and non-technical users won’t need to rely as heavily on engineering (and engineering release cycles) to answer questions about the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Broad Data Type Support (Including JSON)
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;JSON in a relational database? Eat your heart out, MongoDB.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When you start building your IoT product, you may not know what data you will care about, nor have a specific data schema in mind. Later on, you may need very specific data structures (e.g., arrays).&lt;/p&gt;

&lt;p&gt;Fortunately, PostgreSQL supports a broad spectrum of data types. It allows for semi-structured data (via JSON / JSONB support) but also a variety of other data types, including many numeric types, geometric types, arrays, range types, and date/time types.&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;-- Storing and querying JSON data in PostgreSQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sensor_id&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nb"&gt;time&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sensor_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;             &lt;span class="n"&gt;readings&lt;/span&gt;             
&lt;span class="c1"&gt;---------------+-----------+------+----------------------------------&lt;/span&gt;
 &lt;span class="mi"&gt;1499789565000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;330&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"occupancy"&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="nv"&gt;"lights"&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="mi"&gt;1499789565000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;440&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;74&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="nv"&gt;"humidity"&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;81&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="mi"&gt;1499789565000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;441&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;72&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="nv"&gt;"humidity"&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;78&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="mi"&gt;1499789560000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;330&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"occupancy"&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="nv"&gt;"lights"&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="mi"&gt;1499789560000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;440&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"humidity"&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;81&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="mi"&gt;1499789560000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;441&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;72&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="nv"&gt;"humidity"&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;79&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="mi"&gt;1499789555000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;330&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"occupancy"&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="nv"&gt;"lights"&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="mi"&gt;1499789555000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;440&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"humidity"&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;80&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="mi"&gt;1499789555000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;441&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;72&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="nv"&gt;"humidity"&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;78&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡 &lt;em&gt;Tips on &lt;a href="https://www.timescale.com/blog/best-practices-for-picking-postgresql-data-types" rel="noopener noreferrer"&gt;picking data types in Postgres&lt;br&gt;
&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Geospatial Support for Your IoT DataPostgreSQL with You can run many Postgres extensions in TimescaleDB.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One Powerful extension is PostGIS, and it tackles a core IoT challenge:&lt;/strong&gt; tracking things through space and time. Fleet vehicles, delivery routes, and mobile assets all need both location and temporal data. Instead of juggling separate systems, these extensions turn PostgreSQL into a spatiotemporal database that handles it all - where things are, where they've been, and when.&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.amazonaws.com%2Fuploads%2Farticles%2Fkhqwz15osn9imfdsc4up.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fkhqwz15osn9imfdsc4up.gif" alt=" " width="560" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Plenty of Integration Opportunities
&lt;/h2&gt;

&lt;p&gt;Time-series databases need a rich ecosystem of connectors to tools like Kafka, Spark, and Tableau. While newer databases lack this support, PostgreSQL's 30-year head start means TimescaleDB inherits a vast network of ready-to-use integrations. If PostgreSQL connects to it, TimescaleDB does too.&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.amazonaws.com%2Fuploads%2Farticles%2F18ctrbty0djkcz6psx0h.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.amazonaws.com%2Fuploads%2Farticles%2F18ctrbty0djkcz6psx0h.png" alt=" " width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Proven Reliability
&lt;/h2&gt;

&lt;p&gt;IoT databases need to be dependable from day one. A factory line can't pause while you debug database issues at 3 AM. Period.&lt;/p&gt;

&lt;p&gt;PostgreSQL has spent 30 years running mission-critical systems, and TimescaleDB inherits this stability along with built-in tools for replication and failover.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try Timescale Today (Free)
&lt;/h2&gt;

&lt;p&gt;There is a lot of noise in the IoT world right now, and deciding what database to use for your IoT project can be hard. &lt;/p&gt;

&lt;p&gt;Sometimes, the &lt;em&gt;best&lt;/em&gt; option is the &lt;em&gt;boring&lt;/em&gt; option: the database that just works. &lt;/p&gt;

&lt;p&gt;That’s PostgreSQL, which now (thanks to Timescale) finally scales to handle IoT workloads and optimizes SQL for time-series data.&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.amazonaws.com%2Fuploads%2Farticles%2Fmvv6xb0ipcfl15oo0gae.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.amazonaws.com%2Fuploads%2Farticles%2Fmvv6xb0ipcfl15oo0gae.png" alt="How PostgreSQL and Timescale solve the challenges of IoT data.&amp;lt;br&amp;gt;
" width="800" height="521"&gt;&lt;/a&gt;&lt;br&gt;
If you’re building a new IoT project or currently wrestling with a complex IoT stack, choose PostgreSQL—but faster. &lt;a href="https://console.cloud.timescale.com/signup/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=dev-2025&amp;amp;utm_content=timescale-sign-up-page/" rel="noopener noreferrer"&gt;Create a free Timescale account&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need help with anything, feel free to &lt;a href="https://timescaledb.slack.com/join/shared_invite/zt-2z21o39m9-tYKeANBvt8bA7TVdoC9YhQ#/shared-invite/email" rel="noopener noreferrer"&gt;join our Community on Slack&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>devops</category>
      <category>datascience</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Calculating Dwell Time in Postgres+TimescaleDB</title>
      <dc:creator>James Blackwood-Sewell</dc:creator>
      <pubDate>Thu, 30 Jan 2025 03:52:49 +0000</pubDate>
      <link>https://dev.to/james_blackwoodsewell_58/calculating-dwell-time-in-postgrestimescaledb-40pp</link>
      <guid>https://dev.to/james_blackwoodsewell_58/calculating-dwell-time-in-postgrestimescaledb-40pp</guid>
      <description>&lt;p&gt;When you're working with IOT or OT data you often want to look at a series of state transitions for sensors or devices and work out the amount of time spent in each state. Each device can only be in a single state at a time, so when a new state is started, the old one is closed off.&lt;/p&gt;

&lt;p&gt;This is often used for billing or metering (think power meters, devices connected to a network, or even a complex state machine) and can be referred to as dwell time or connection time. In this case, the desired output is ( &lt;code&gt;device_id, state, start_time, dwell_time&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Let's consider the following table which tracks states for sensors.&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;testdata&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&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;state&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&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;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can populate the table with some test 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="c1"&gt;-- Create some test data&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;testdata&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&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;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 12:01:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 12:02:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 12:05:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 12:10:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 12:20:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 12:20:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 12:22:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 13:00:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 13:02:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 13:06:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 13:12:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 14:00:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 14:12:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 14:14:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 14:30:00'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2019-Nov-01 14:35:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can solve this with plain SQL (I'll leave that as a exercise for the reader😉), but if you're using PostgreSQL + &lt;a href="https://www.timescale.com/" rel="noopener noreferrer"&gt;TimescaleDB&lt;/a&gt; then you can use one of the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/" rel="noopener noreferrer"&gt;hyperfunctions&lt;/a&gt; which come with the &lt;code&gt;timescaledb-toolkit&lt;/code&gt; extension.&lt;/p&gt;

&lt;p&gt;The final solution looks like this:&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;WITH&lt;/span&gt; &lt;span class="n"&gt;states&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;SELECT&lt;/span&gt; 
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state_timeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state_agg&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;state&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="n"&gt;sid&lt;/span&gt; 
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;testdata&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;sid&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;sid&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;start_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="n"&gt;dwell_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;states&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;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which gives the following results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sid     state   start_time              dwell_time
1       a       2019-11-01 12:01:00+00  00:09:00
1       b       2019-11-01 12:10:00+00  00:15:00
1       a       2019-11-01 12:25:00+00  02:10:00
2       a       2019-11-01 12:20:00+00  00:02:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The magic lies in the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/state-tracking/state_agg/#state_agg" rel="noopener noreferrer"&gt;&lt;code&gt;state_agg&lt;/code&gt;&lt;/a&gt; aggregate, which tracks transitions between states within our grouping set (in this case the state column), and also in the &lt;code&gt;state_timeline&lt;/code&gt; function, which takes the output of &lt;code&gt;state_agg&lt;/code&gt; and creates a timeline of all state changes with the time they were entered and exited.&lt;/p&gt;

&lt;p&gt;With that information in hand, it’s easy to work out the &lt;code&gt;dwell_time&lt;/code&gt; using &lt;code&gt;end_time - start_time&lt;/code&gt;. If you're impressed by this check out the other things &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/" rel="noopener noreferrer"&gt;hyperfunctions&lt;/a&gt; can do!&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>iot</category>
      <category>sql</category>
    </item>
    <item>
      <title>TimescaleDB in 2024: Making Postgres Faster</title>
      <dc:creator>James Blackwood-Sewell</dc:creator>
      <pubDate>Wed, 15 Jan 2025 09:56:20 +0000</pubDate>
      <link>https://dev.to/tigerdata/timescaledb-in-2024-making-postgres-faster-32f7</link>
      <guid>https://dev.to/tigerdata/timescaledb-in-2024-making-postgres-faster-32f7</guid>
      <description>&lt;p&gt;If I had to summarize 2024 for Timescale, I’d call it &lt;em&gt;the year of Postgres for AI&lt;/em&gt;. From game-changing open-source launches like &lt;a href="https://github.com/timescale/pgvectorscale" rel="noopener noreferrer"&gt;&lt;u&gt;pgvectorscale&lt;/u&gt;&lt;/a&gt; (a performance booster for large production vector workloads with PostgreSQL + &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;&lt;u&gt;pgvector&lt;/u&gt;&lt;/a&gt;) to &lt;a href="https://github.com/timescale/pgai" rel="noopener noreferrer"&gt;&lt;u&gt;pgai&lt;/u&gt;&lt;/a&gt; (which integrates Postgres with LLMs for AI app development), we pushed the boundaries of what developers can achieve with Postgres. But AI wasn’t the only story. TimescaleDB’s time-series capabilities also evolved dramatically during 2024, making Postgres even faster for real-time analytics.&lt;/p&gt;

&lt;p&gt;For those of you who don’t know, &lt;a href="https://github.com/timescale/timescaledb" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB&lt;/u&gt;&lt;/a&gt; is a PostgreSQL extension for high-performance real-time analytics on time series and event data. It is available as an &lt;a href="https://docs.timescale.com/self-hosted/latest/" rel="noopener noreferrer"&gt;&lt;u&gt;open-source extension&lt;/u&gt;&lt;/a&gt; or fully managed on &lt;a href="https://console.cloud.timescale.com/signup" rel="noopener noreferrer"&gt;&lt;u&gt;Timescale Cloud&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I love TimescaleDB (I was a user well before I worked at Timescale ❤️), so in the spirit of sharing that love, here are my top five TimescaleDB features from 2024 and why you should care (or at least why I do 😅).&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Chunk-Skipping Indexes on Compressed Hypertables
&lt;/h2&gt;

&lt;p&gt;This feature introduces a way to add min/max indexes on compressed &lt;a href="https://docs.timescale.com/use-timescale/latest/hypertables/about-hypertables/" rel="noopener noreferrer"&gt;hypertable&lt;/a&gt; chunks. The planner uses these to perform chunk exclusion on columns other than the default time-based one. &lt;/p&gt;

&lt;p&gt;This can be useful in two main cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’ve got a column that is correlated to the time-based partition in some way, and you’d like to use it efficiently in queries. The classic example is an order ID. As time increases, so does the order ID, so the min/max value will be tightly bound per chunk. Adding chunk skipping would allow you to use order ID in your &lt;code&gt;WHERE&lt;/code&gt; clause and have all chunks but the matching one excluded.&lt;/li&gt;
&lt;li&gt;You have a column in which you want to search for outliers that don’t occur often. Say you have a temperature column, and you want to find all rows that exceed a temperature threshold. You could enable chunk skipping and only have to scan the chunks that exceed the outlier value in your &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s take a look at how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Create the orders hypertable and enable compression
CREATE TABLE orders (ts timestamptz, orderid serial, orderdata JSONB);
SELECT create_hypertable ('orders', by_range ('ts'));
ALTER TABLE orders SET (timescaledb.compress);

-- Insert some data, this will make 52 chunks (one per week)
INSERT INTO orders 
  SELECT generate_series(now() - INTERVAL '1 year', now(), '1 minute');

-- Enable chunk skipping for the orders column then compress all the data
SET timescaledb.enable_chunk_skipping = on;
SELECT enable_chunk_skipping('orders', 'orderid');
SELECT compress_chunk(c) FROM show_chunks('orders') c;

-- Query by orderid alone, this would normally hit all 52 chunks :)
EXPLAIN ANALYZE SELECT * FROM orders WHERE orderid=1;

Custom Scan (DecompressChunk) on _hyper_95_10464_chunk  (cost=0.03..15.60 rows=560000 width=44) (actual time=0.032..0.033 rows=1 loops=1)
  Vectorized Filter: (orderid = 1)
  Rows Removed by Filter: 5605
  -&amp;gt;  Seq Scan on compress_hyper_96_10517_chunk  (cost=0.00..15.60 rows=560 width=116) (actual time=0.003..0.004 rows=6 loops=1)
Planning Time: 1.436 ms
Execution Time: 0.064 ms

-- You can see the query above only hit a single chunk!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As with most new features in TimescaleDB, chunk skipping is disabled by default at launch, so remember to set &lt;code&gt;timescaledb.enable_chunk_skipping&lt;/code&gt; to true (either globally or per session) to let the planner know you’re using it. I’d recommend only enabling the feature for queries that you think will use it.&lt;/p&gt;

&lt;p&gt;Performance gains are pretty data-dependent, but we have seen &lt;a href="https://www.timescale.com/blog/boost-postgres-performance-by-7x-with-chunk-skipping-indexes#the-importance-of-correlation-in-postgresql-partitioning" rel="noopener noreferrer"&gt;&lt;u&gt;up to 7x when testing normal situations.&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Faster Real-Time Continuous Aggregates and Continuous Aggregate Policies
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.timescale.com/use-timescale/latest/continuous-aggregates/about-continuous-aggregates/" rel="noopener noreferrer"&gt;&lt;u&gt;Continuous aggregates&lt;/u&gt;&lt;/a&gt; (CAggs) are one of my favorite features in TimescaleDB. We already give you lightning-fast analytical queries out of the box, but CAggs take it a step further by taking a SQL query and automatically materializing it in the background. I’m cheating here by squashing two CAgg features into the same item, but I’m sure you can live with that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Faster real-time continuous aggregates
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.timescale.com/use-timescale/latest/continuous-aggregates/real-time-aggregates/" rel="noopener noreferrer"&gt;&lt;u&gt;Real-time continuous aggregates&lt;/u&gt;&lt;/a&gt; allow the computation of results for newly inserted data on the fly (before the background job has materialized). That data is then immediately contained in queries to the continuous aggregate alongside the already materialized data to give a full, up-to-date snapshot.&lt;/p&gt;

&lt;p&gt;In Timescale 2.14.0, we made a change that increased the performance of real-time continuous aggregates by up to &lt;a href="https://www.timescale.com/blog/how-we-made-real-time-data-aggregation-in-postgres-faster-by-50-000" rel="noopener noreferrer"&gt;&lt;u&gt;50,000x &lt;/u&gt;&lt;/a&gt;(that’s not a typo). For those who want the technical details we constifyied our internal &lt;code&gt;cagg_watermark&lt;/code&gt; function call, which unlocked plan-time chunk exclusion (as opposed to run-time) and made everything faster (read the link above for the full debrief).&lt;/p&gt;

&lt;p&gt;If you haven't tried real-time (or even normal) CAggs, I’d encourage you to give them a go. Check out our newly rewritten &lt;a href="https://github.com/timescale/timescaledb#create-continuous-aggregates" rel="noopener noreferrer"&gt;&lt;u&gt;GitHub Readme&lt;/u&gt;&lt;/a&gt; for a quick how-to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Faster continuous aggregate policies
&lt;/h3&gt;

&lt;p&gt;In the past, continuous aggregates could sometimes be a little I/O hungry when updates flowed in for older chunks. Before version 2.17.0, we would recalculate and rewrite each chunk with changes, but now we use an efficient merge policy to only write the changes to storage. Overall, continuous aggregate policies are lighter, use fewer system resources in the background, and complete faster.&lt;/p&gt;

&lt;p&gt;For anyone building dashboards or running real-time analytics with late-arriving data, this is a game-changer.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Hypercore Introduction and Enhancements
&lt;/h2&gt;

&lt;p&gt;We’ve had a hybrid row-columnar storage engine for a long time (it powers our compression), but this year we gave it an official name: &lt;a href="https://www.timescale.com/blog/hypercore-a-hybrid-row-storage-engine-for-real-time-analytics" rel="noopener noreferrer"&gt;&lt;u&gt;hypercore&lt;/u&gt;&lt;/a&gt;. That wasn’t enough for the team, so during the year, they also knocked out some great hypercore improvements. My favorites were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimized defaults&lt;/strong&gt; : Timescale now recommends the best &lt;code&gt;segment_by&lt;/code&gt; and &lt;code&gt;order_by&lt;/code&gt; settings based on table configuration and statistics. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smarter decompression&lt;/strong&gt; : The query planner can now evaluate more &lt;code&gt;WHERE&lt;/code&gt; conditions before decompressing data, reducing unnecessary I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Min/max sparse indexes&lt;/strong&gt; : Columns with &lt;code&gt;btree&lt;/code&gt; indexes can now benefit from min/max sparse indexes after compression. This is similar to chunk skipping but operates on individual batches within chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vectorized filters&lt;/strong&gt; : Queries with text equality or &lt;code&gt;LIKE&lt;/code&gt; filters are now vectorized for faster execution.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.timescale.com/blog/bridging-the-gap-between-compressed-and-uncompressed-data-in-postgres" rel="noopener noreferrer"&gt;&lt;strong&gt;Compressed tuple filtering&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;:&lt;/strong&gt; Up to 500x faster updates and deletes by avoiding the need to decompress and materialize irrelevant batches.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.timescale.com/blog/how-we-made-postgresql-upserts-300x-faster-on-compressed-data" rel="noopener noreferrer"&gt;&lt;strong&gt;Faster upserts&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;:&lt;/strong&gt; Improved upsert performance by more than 300x on compressed data by forcing B-tree index scans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also continued our work on vectorization for real-time analytics queries, but that deserves its own mention.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. SIMD Vectorization for Blazing Fast Analytical Queries
&lt;/h2&gt;

&lt;p&gt;Real-time analytics over columnar (compressed) data got a massive performance boost this year thanks to more work on &lt;strong&gt;SIMD (Single Instruction, Multiple Data) vectorization&lt;/strong&gt;. This lets us take advantage of modern CPUs when processing vectors (or arrays) of values, which hypercore can conveniently provide for each column. If you’re running queries that group by &lt;code&gt;segment_by&lt;/code&gt; columns and use aggregate functions like &lt;code&gt;sum&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;avg&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, or &lt;code&gt;max&lt;/code&gt;, you’ll notice major speedups.&lt;/p&gt;

&lt;p&gt;This is an area we are continuing to invest heavily in, you’ll hear a lot more about this in the coming months. If you’re after more information &lt;strong&gt;right now&lt;/strong&gt; , then have a read of my post on the &lt;a href="https://www.timescale.com/blog/teaching-postgres-new-tricks-simd-vectorization-for-faster-analytical-queries" rel="noopener noreferrer"&gt;&lt;u&gt;start of our vectorization journey&lt;/u&gt;&lt;/a&gt; from last year.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Foreign Keys on Hypertables
&lt;/h2&gt;

&lt;p&gt;For years, developers have asked for support for foreign keys on hypertables. I can even remember asking for this shortly after TimescaleDB launched!&lt;/p&gt;

&lt;p&gt;This year, we delivered: You can now add foreign keys from regular tables to hypertables, with full support for cascading actions. While it's not going to change the world, it's one of those little developer experience bumps that make your life a little easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrap-Up
&lt;/h2&gt;

&lt;p&gt;What a year for TimescaleDB! 🚀 If you’re working with time-series data or want to keep enjoying Postgres familiarity but with a significant performance boost, &lt;a href="https://github.com/timescale/timescaledb" rel="noopener noreferrer"&gt;give TimescaleDB a try&lt;/a&gt;. With these new features, you can optimize your queries, reduce your storage footprint, and make real-time analytics more efficient. Set it up, test it out, and see how it fits into your projects.&lt;/p&gt;

&lt;p&gt;Got questions? Join the &lt;a href="https://slack.timescale.com" rel="noopener noreferrer"&gt;Timescale Slack community&lt;/a&gt;, where like-minded devs and Timescale team members share their tips and will help you get started.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>opensource</category>
      <category>database</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
