<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[Tiger Data Blog]]></title>
        <description><![CDATA[Insights, product updates, and tips from TigerData (Creators of TimescaleDB) engineers on Postgres, time series & AI. IoT, crypto, and analytics tutorials & use cases.]]></description>
        <link>https://www.tigerdata.com/blog</link>
        <image>
            <url>https://www.tigerdata.com/icon.ico</url>
            <title>Tiger Data Blog</title>
            <link>https://www.tigerdata.com/blog</link>
        </image>
        <generator>RSS for Node</generator>
        <lastBuildDate>Tue, 07 Apr 2026 10:15:19 GMT</lastBuildDate>
        <atom:link href="https://www.tigerdata.com/blog" rel="self" type="application/rss+xml"/>
        <ttl>60</ttl>
        <item>
            <title><![CDATA[PostgreSQL + TimescaleDB: 1,000x Faster Queries, 90 % Data Compression, and Much More]]></title>
            <description><![CDATA[TimescaleDB expands PostgreSQL query performance by 1,000x, reduces storage utilization by 90%, and provides time-saving features for time-series and analytical applications—while still being 100% Postgres.]]></description>
            <link>https://www.tigerdata.com/blog/postgresql-timescaledb-1000x-faster-queries-90-data-compression-and-much-more</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/postgresql-timescaledb-1000x-faster-queries-90-data-compression-and-much-more</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[General]]></category>
            <category><![CDATA[Engineering]]></category>
            <category><![CDATA[Benchmarks & Comparisons]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Thu, 22 Sep 2022 15:32:44 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/10/Screenshot-2023-10-11-at-7.24.23-PM.png">
            </media:content>
            <content:encoded><![CDATA[
<!--kg-card-begin: html-->
<div class="highlight">
	
    <p class="highlight__text">
        <svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
	</svg>
<b> Compared to PostgreSQL alone, TimescaleDB can dramatically improve query performance by 1,000x or more, reduce storage utilization by 90 %, and provide features essential for time-series and analytical applications. Some of these features even benefit non-time-series data–increasing query performance just by loading the extension. </b> 
    </p>
</div>

<!--kg-card-end: html-->
<p>PostgreSQL is today’s most advanced and most popular open-source relational database. We believe this as much today as we did <a href="https://timescale.ghost.io/blog/when-boring-is-awesome-building-a-scalable-time-series-database-on-postgresql-2900ea453ee2/">five years ago</a> when we chose PostgreSQL as the foundation of TimescaleDB because of its longevity, extensibility, and rock-solid architecture.</p><p>By loading the TimescaleDB extension into a PostgreSQL database, you can effectively “supercharge” PostgreSQL, empowering it to excel for both time-series workloads and classic transactional ones. </p><p>This article highlights how TimescaleDB improves PostgreSQL query performance at scale, increases storage efficiency (thus lowering costs), and provides developers with the tools necessary for building modern, innovative, and cost-effective time-series applications—all while retaining access to the full Postgres feature set and ecosystem.</p><p>(To show our work, this article also presents the benchmarks that compare query performance and data ingestion for one billion rows of time-series data between PostgreSQL 14.4 and TimescaleDB 2.7.2.  For PostgreSQL, we benchmarked both using a single-table and declarative partitioning)</p><h2 id="better-performance-at-scale">Better Performance at Scale</h2><p>With orders of magnitude better performance at scale, TimescaleDB enables developers to build on top of PostgreSQL <em>and </em>“future-proof” their applications.</p><h3 id="1000x-faster-performance-for-time-series-queries">1,000x faster performance for time-series queries</h3><p>The core concept in TimescaleDB is the notion of the “hypertable”: seamless partitioning of data while presenting the abstraction of a single, virtual table across all your data. </p><p>This partitioning enables faster queries by quickly excluding irrelevant data, as well as enabling enhancements to the query planner and execution process. In this way, a hypertable looks and feels just like a normal PostgreSQL table but enables a lot more.</p><p>For example, one recent query planner improvement excludes data more efficiently for relative <code>now()</code>-based queries (e.g., <code>WHERE time &gt;= now()-’1 week’::interval</code>). To be even more specific, <a href="https://timescale.ghost.io/blog/how-we-fixed-long-running-postgresql-now-queries/">these relative time predicates are constified</a> at planning time to ignore chunks that don't have data to satisfy the query. Furthermore, as the number of partitions increases, planning times can be reduced by 100x or more over vanilla PostgreSQL for the same number of partitions.</p><p>When hypertables are compressed, the amount of data that queries need to read is reduced, leading to dramatic increases in performance of 1000x or more. For more information (including a discussion of this bar chart), keep reading the benchmark below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/09/single-query-latency-milliseconds.png" class="kg-image" alt="" loading="lazy" width="2000" height="1053" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/09/single-query-latency-milliseconds.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/09/single-query-latency-milliseconds.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/09/single-query-latency-milliseconds.png 1600w, https://timescale.ghost.io/blog/content/images/2022/09/single-query-latency-milliseconds.png 2070w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Query latency comparison (ms) between TimescaleDB and PostgreSQL 14.4. To see the complete query, scroll down below.</span></figcaption></figure><p>Other enhancements in TimescaleDB apply to both hypertables and normal PostgreSQL tables, e..g, SkipScan, which <a href="https://timescale.ghost.io/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql/">dramatically improves DISTINCT queries on any PostgreSQL table</a> with a matching B-tree index regardless of whether you have time-series data or not.</p><h3 id="reduce-commonly-run-queries-to-milliseconds-even-when-the-original-query-took-minutes-or-hours">Reduce commonly run queries to milliseconds (even when the original query took minutes or hours)<br></h3><p>Today, nearly every time-series application reaches for rolling aggregations to query and analyze data more efficiently. The raw data could be saved per second, minute, or hour (and a plethora of other permutations in between), but what most applications display are time-based aggregates. </p><p>What's more, most time-series data applications are append-only, which means that aggregate queries return the same values over and over based on the unchanged raw data. It's much more efficient to store the results of the aggregate query and use those for analytic reporting and analysis most of the time. </p><p>Often, developers try materialized views in vanilla PostgreSQL to help, however, they have two main problems with fast-changing time-series data:</p><ul><li>Materialized views <em>recreate the entire view every time the materialization process runs, </em>even if little or no data has changed.</li><li>Materialized views don't provide any data retention management. Any time you delete raw data and update the materialized view, the aggregated data is removed as well.</li></ul><p>In contrast, <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/about-continuous-aggregates/">TimescaleDB’s continuous aggregates</a> solve both of these problems. They are updated automatically on the <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/refresh-policies/">schedule you configure</a>, they can have data <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/data-retention/data-retention-with-continuous-aggregates/#about-data-retention-with-continuous-aggregates">retention policies applied separately from the underlying hypertable</a>, and they only update the portions of new data that have been modified since the last materialization was run.</p><p>When we compare using a continuous aggregate to querying the data directly, customers often see queries that might take minutes or even hours drop to milliseconds. When that query is powering a dashboard or a web page, this can be the difference between snappy and unusable.</p><h2 id="lower-storage-costs">Lower Storage Costs</h2><p>The number one driver of cost for modern time-series applications is storage. Even when storage is cheap, time-series data piles up quickly. TimescaleDB provides two methods to reduce the amount of data being stored, compression and downsampling using continuous aggregates.</p><h3 id="90-or-more-storage-savings-via-best-in-class-compression-algorithms">90&nbsp;% or more storage savings via best-in-class <a href="https://www.tigerdata.com/blog/time-series-compression-algorithms-explained" rel="noreferrer">compression algorithms</a></h3><p>The TimescaleDB hypertable is data heavily partitioned into many, many smaller partitions called “chunks.” TimescaleDB provides <a href="https://www.tigerdata.com/blog/building-columnar-compression-in-a-row-oriented-database" rel="noreferrer">native columnar compression</a> on this per-chunk basis. </p><p>As we show in the benchmark results (and as we see often in production databases), compression reduced disk consumption by over 90% compared to the same data in vanilla PostgreSQL. </p><p>Even better, TimescaleDB doesn't change anything about the PostgreSQL storage system to achieve this level of compression. Instead, TimescaleDB utilizes PostgreSQL storage features, namely TOAST, to transition historical data from row-store to column-store, a key component for querying long-term aggregates over individual columns.</p><p>To demonstrate the effectiveness of compression, here’s a comparison of the total size of the CPU table and indexes in TimescaleDB and in PostgreSQL.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/09/image-5.png" class="kg-image" alt="" loading="lazy" width="2000" height="1053" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/09/image-5.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/09/image-5.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/09/image-5.png 1600w, https://timescale.ghost.io/blog/content/images/2022/09/image-5.png 2070w" sizes="(min-width: 720px) 720px"></figure><p><br>With the proper <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/compression/about-compression/#enable-compression">compression policy in place</a>, hypertable chunks will be compressed automatically once all data in the chunk has aged beyond the specified time interval. </p><p>In practice, this means that a hypertable can store data as row-oriented for newer data and column-oriented for older data simultaneously. Having the data stored as both row and column store also matches the typical query patterns of time-series applications to help improve overall query performance—again, something we see in the benchmark results.</p><p>This reduces the storage footprint and improves query performance even further for many time-series aggregate queries. Compression is also automatic: users set a compression horizon, and then data is automatically compressed as it ages.</p><p>This also means that users can save significant costs using cloud services that provide separation of compute and storage—such as Tiger Cloud (formerly Timescale Cloud)—so that larger machines aren’t needed just for more storage. </p><h3 id="more-storage-savings-by-easily-removing-or-downsampling-data">More storage savings by easily removing or downsampling data</h3><p>With TimescaleDB, automated <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/data-retention/about-data-retention/">data retention </a>is achieved with <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/data-retention/create-a-retention-policy/">one SQL command</a>:</p><pre><code class="language-SQL">SELECT add_retention_policy('cpu', INTERVAL '7 days');
</code></pre><p>There's no further setup or extra extensions to install or configure. Each day any partitions older than 7 days will be dropped automatically. If you were to implement this in vanilla PostgreSQL you’d need to use DELETE to remove records, which is a very costly operation as it needs to scan for the data to remove. Even if you were using PostgreSQL declarative partitioning, you’d still need to automate the process yourself, wasting precious developer time, adding additional requirements, and implementing bespoke code that needs to be supported moving forward.</p><p>One can also combine continuous aggregates and data retention policies to downsample data and then drop the raw measurements, thus saving even more data storage. </p><p>Using this architecture, you can retain higher-level rollup values for a longer period of time, even after the raw data has been dropped from the database. This allows multiple different levels of granularity to be stored in the database, and provides even more ways to control storage costs.</p><h2 id="more-features-to-speed-up-development-time">More Features to Speed Up Development Time</h2><p>TimescaleDB includes more features that speed up development time. This includes a library of over 100 hyperfunctions, which make complex time-series analysis easy using SQL, such as count approximations, statistical aggregates, and more. TimescaleDB also includes a built-in, multi-purpose job scheduling engine for setting up automated workflows.</p><h3 id="library-of-over-100-hyperfunctions-that-make-complex-analysis-easy">Library of over 100 hyperfunctions that make complex analysis easy</h3><p>TimescaleDB hyperfunctions make data analysis in SQL easy. This library includes <a href="https://docs.timescale.com/api/latest/hyperfunctions/time-weighted-averages/">time-weighted averages</a>, <a href="https://docs.timescale.com/api/latest/hyperfunctions/gapfilling-interpolation/locf/">last observation carried forward</a>, and <a href="https://docs.timescale.com/api/latest/hyperfunctions/downsample/">downsampling with LTTP or ASAP algorithms</a>, <a href="https://docs.timescale.com/api/latest/hyperfunctions/time_bucket/">time_bucket()</a>, and <a href="https://docs.timescale.com/api/latest/hyperfunctions/gapfilling-interpolation/time_bucket_gapfill/">time_bucket_gapfill()</a>. </p><p>As an example, one could get the average temperature every day for each device over the last seven days, carrying forward the last value for missing readings with the following SQL.</p><pre><code class="language-SQL">SELECT
  time_bucket_gapfill('1 day', time) AS day,
  device_id,
  avg(temperature) AS value,
  locf(avg(temperature))
FROM metrics
WHERE time &gt; now () - INTERVAL '1 week'
GROUP BY day, device_id
ORDER BY day;
</code></pre><p>For more information on the extensive list of hyperfunctions in TimescaleDB, please visit our <a href="https://docs.timescale.com/api/latest/hyperfunctions/">API documentation</a>.</p><h3 id="built-in-job-scheduler-for-workflow-automation">Built-in job scheduler for workflow automation </h3><p>TimescaleDB provides the ability to schedule the execution of custom stored procedures with <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/user-defined-actions/">user-defined actions</a>. This feature provides access to the same job scheduler that TimescaleDB uses to run all of the native automation jobs for compression, continuous aggregates, data retention, and more. </p><p>This provides a similar functionality as a third-party scheduler like<code>pg_cron</code> without needing to maintain multiple <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extensions</a> or databases.</p><p>We see users doing all sorts of neat stuff with user-defined actions, from calculating complex SLAs to sending event emails based on data correctness to polling tables.</p><h2 id="still-100-postgresql-and-sql">Still 100&nbsp;% PostgreSQL and SQL</h2><p>Notably, because TimescaleDB is packaged as a <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extension</a>, it achieves these results without forking or breaking PostgreSQL.</p><h3 id="extending-postgresql%E2%80%94not-forking-or-cloning">Extending PostgreSQL—not forking or cloning</h3><p>Postgres is popular at the moment, but a lot of that popularity is with ‘Postgres compatible’ products which might look like Postgres, or talk like Postgres, or query somewhat like Postgres - but aren’t Postgres under the hood (and are sometimes closed-source). </p><p>TimescaleDB is just PostgreSQL. One can install other extensions, make full use of the type system, and benefit from the incredibly diverse Postgres ecosystem.</p><h3 id="100-sql">100&nbsp;% SQL</h3><p>Any product that can connect to PostgreSQL can query time-series data stored with TimescaleDB using the same SQL it normally would. While we provide helper functions for working with data, we do not restrict the SQL features one can use. Once in the database, users can combine <a href="https://www.tigerdata.com/blog/time-series-introduction" rel="noreferrer">time series</a> and business data as necessary.</p><h3 id="rock-solid-foundations-thanks-to-postgresql">Rock-solid foundations thanks to PostgreSQL</h3><p>PostgreSQL is not a new database: it has years of production deployments under its belt. High availability, backup and restore, and load-balancing are all solved problems. As we mentioned earlier, we chose Postgres because it was reliable, and TimescaleDB inherits that reliability.</p><h2 id="benchmarking-setup-and-results">Benchmarking Setup and Results</h2><p>This section provides details about how we tested TimescaleDB against vanilla PostgreSQL. Feel free to download the <a href="https://github.com/timescale/tsbs">Time-Series Benchmarking Suite</a> and run it for yourself. If you'd like to get started with TimescaleDB quickly, you can use Tiger Cloud, which lets you <a href="https://console.cloud.timescale.com/signup">sign up for a free 30-day trial</a>.</p><h3 id="benchmark-configuration">Benchmark configuration</h3><p>For this benchmark, all tests were run on the same m5.2xlarge EC2 instance in AWS us-east-1 with the following configuration and software versions. </p><ul><li>Versions: TimescaleDB version 2.7.2, community edition, and PostgreSQL 14.4</li><li>One remote client machine running TSBS, one database server, both in the same cloud data center</li><li>TSBS Client Instance: EC2 m5.4xlarge  with 16 vCPU and 64&nbsp;GB memory</li><li>Database server instance: EC2 m5.2xlarge  with 8 vCPU and 32&nbsp;GB memory</li><li>OS: both server and client machines ran Ubuntu 20.04</li><li>Disk size: 1&nbsp;TB of EBS GP2 storage</li><li>TSBS config: Dev-ops profile, 4,000 devices recording metrics every 10 seconds over one month.</li></ul><p>We also deliberately chose to use EBS (elastic block storage) volumes rather than attached SSDs. While benchmark performance would certainly improve with SSDs, the baseline performance using EBS is illustrative of what many self-hosted users could expect while saving some expenses by using elastic storage.</p><h3 id="database-configuration">Database configuration</h3><p>We ran only one PostgreSQL cluster on the EC2 database instance. The TimescaleDB extension was loaded via <code>shared_preload_libraries</code> but not installed into the PostgreSQL-only database.</p><p>To set sane defaults for the PostgreSQL cluster, we ran <code>timescaledb-tune</code> and set<code>synchronous_commit=off</code> in postgresql.conf. This is a common performance configuration for write-heavy workloads while still maintaining transactional, logged integrity. <strong>All configuration changes applied to both PostgreSQL and TimescaleDB benchmarks alike.</strong></p><h3 id="the-dataset">The dataset</h3><p>As we mentioned earlier, for this benchmark, we used the <a href="https://github.com/timescale/tsbs">Time-Series Benchmarking Suite </a>and generated data for 4,000 devices, recording metrics every 10 seconds, for one month. This generated just over one billion rows of data. Because TimescaleDB is a PostgreSQL extension, we could use the same data file and ingestion process, ensuring identical data in each database.</p><h3 id="timescaledb-setup">TimescaleDB setup</h3><p>TimescaleDB uses an abstraction called <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertables</a> which splits large tables into smaller chunks, increasing performance and greatly easing management of large amounts of time-series data.</p><p>We also enabled native compression on TimescaleDB. We compressed everything but the most recent chunk of data, leaving it uncompressed. This configuration is a commonly recommended one where raw, uncompressed data is kept for recent time periods and older data is compressed, enabling greater query efficiency. The parameters we used to enable compression are as follows: we segmented by the <code>tags_id</code> columns and ordered by time descending and <code>usage_user</code> columns.</p><p><strong><em>All benchmark results were performed on a single PostgreSQL table and on an empty TimescaleDB hypertable created with four-hour chunks.</em></strong></p><p>(And for those thinking that we also need to compare TimescaleDB with PostgreSQL Declarative Partitioning, please read on to the end; we discuss that as well.)</p><h2 id="query-latency-deep-dive">Query Latency Deep Dive</h2><p>For this benchmark, we inserted one billion rows of data and then ran a set of queries 100 times each against the respective database. The data, indexes, and queries are exactly the same for both databases. The only difference is that the TimescaleDB queries use the <code>time_bucket()</code> function for doing arbitrary interval bucketing, whereas the PostgreSQL queries use the new <code>date_bin()</code> function, introduced in PostgreSQL 13.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/09/Query-latency-deep-dive--1--1.png" class="kg-image" alt="" loading="lazy" width="2000" height="1639" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/09/Query-latency-deep-dive--1--1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/09/Query-latency-deep-dive--1--1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/09/Query-latency-deep-dive--1--1.png 1600w, https://timescale.ghost.io/blog/content/images/2022/09/Query-latency-deep-dive--1--1.png 2070w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Query latency comparison between PostgreSQL and TimescaleDB, for different queries</span></figcaption></figure><p>The results are clear and consistently reproducible. For one billion rows of data spanning one month of time (with four-hour partitions), <strong><em>TimescaleDB consistently outperformed a vanilla PostgreSQL database running 100 queries at a time.</em></strong> </p><p>There are two main reasons for TimescaleDB's consistent query performance.</p><h3 id="compression-smaller-storage-less-work">Compression = smaller storage + less work</h3><p>In PostgreSQL (and many other databases), table data is stored in an 8&nbsp;Kb page (sometimes called a block). If a query has to read 1,000 pages to satisfy it, it reads ~8&nbsp;Mb of data. If some of that data had to be retrieved from disk, then the query will usually be slower than if all of the data was found in memory (the reserved space known as <em>shared buffers</em> in PostgreSQL, if you’re looking for some insight into PostgreSQL caching we have <a href="https://timescale.ghost.io/blog/database-scaling-postgresql-caching-explained/">a blog on that</a>).</p><p>With TimescaleDB compression, queries that return the same results have to read significantly fewer pages of data (this is both because of the actual compression and because it can return single columns rather than whole rows). For all of our benchmarking queries, this also translates into higher concurrency for the benchmark duration.</p><p>Stated another way, compression typically impacts fetching historical data most because TimescaleDB can query individual columns rather than entire rows. Because less I/O is occurring for each query, TimescaleDB can handle more queries with a lower standard deviation than vanilla PostgreSQL.</p><p>Let's look at two examples of how this plays out between the two databases using two queries above, <code>cpu-max-all-1</code> and <code>single-groupby-1-1-12</code>.</p><h3 id="single-groupby-1-1-12"><code>single-groupby-1-1-12</code></h3><p>We selected one of the queries from the benchmark and ran it on both databases. Recall that each database has the exact same data and indexes on uncompressed data. TimescaleDB has the advantage of being able to segment and order compressed data in a way that's beneficial to typical application queries.</p><pre><code class="language-SQL">EXPLAIN (ANALYZE,BUFFERS)
SELECT time_bucket('1 minute', time) AS minute,
        max(usage_user) as max_usage_user
        FROM cpu
        WHERE tags_id IN (
          SELECT id FROM tags WHERE hostname IN ('host_249')
        ) 
        AND time &gt;= '2022-08-03 06:16:22.646325 +0000' 
        AND time &lt; '2022-08-03 18:16:22.646325 +0000'
        GROUP BY minute ORDER BY minute;</code></pre><p>When we run the <code>EXPLAIN</code> on this query and ask for <code>BUFFERS</code> to be returned, we start to get a hint of what's happening.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/09/single-query-latency-single-groupby-1-1-12.png" class="kg-image" alt="" loading="lazy" width="2000" height="1053" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/09/single-query-latency-single-groupby-1-1-12.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/09/single-query-latency-single-groupby-1-1-12.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/09/single-query-latency-single-groupby-1-1-12.png 1600w, https://timescale.ghost.io/blog/content/images/2022/09/single-query-latency-single-groupby-1-1-12.png 2070w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Query latency vs volume of data that has to be read to satisfy the query in TimescaleDB and PostgreSQL.</span></figcaption></figure><p></p><p>Two things quickly jump out when I view these results. First, the execution times are significantly lower than the benchmarking results above. Individually, these queries execute pretty fast, but PostgreSQL has to read approximately 27x more data to satisfy the query. When 16 workers request data across the time range, PostgreSQL has to do a lot more I/O, which consumes resources. TimescaleDB can simply handle a higher concurrency for the same workload. </p><h3 id="cpu-max-all-1"><code>cpu-max-all-1</code></h3><p>Again we can clearly see the impact of compression on the ability for TimescaleDB to handle a higher concurrent load when compared to vanilla PostgreSQL for time-series queries.</p><pre><code class="language-SQL">EXPLAIN (ANALYZE, buffers) 
SELECT
   time_bucket('3600 seconds', time) AS hour,
   max(usage_user) AS max_usage_user,
   max(usage_system) AS max_usage_system,
   max(usage_idle) AS max_usage_idle,
   max(usage_nice) AS max_usage_nice,
   max(usage_iowait) AS max_usage_iowait,
   max(usage_irq) AS max_usage_irq,
   max(usage_softirq) AS max_usage_softirq,
   max(usage_steal) AS max_usage_steal,
   max(usage_guest) AS max_usage_guest,
   max(usage_guest_nice) AS max_usage_guest_nice 
FROM cpu 
WHERE  
   tags_id IN (
      SELECT id FROM tags WHERE hostname IN ('host_249')
   )
   AND time &gt;= '2022-08-08 18:16:22.646325 +0000' 
   AND time &lt; '2022-08-09 02:16:22.646325 +0000' 
GROUP BY HOUR 
ORDER BY HOUR;</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/09/single-query-latency-cpu-max-all-1.png" class="kg-image" alt="" loading="lazy" width="2000" height="1053" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/09/single-query-latency-cpu-max-all-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/09/single-query-latency-cpu-max-all-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/09/single-query-latency-cpu-max-all-1.png 1600w, https://timescale.ghost.io/blog/content/images/2022/09/single-query-latency-cpu-max-all-1.png 2070w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Query latency vs volume of data that has to be read to satisfy the query, in TimescaleDB and PostgreSQL</span></figcaption></figure><p>With compression, TimescaleDB does significantly less work to retrieve the same data, resulting in faster queries and higher query concurrency.</p><h3 id="time-ordered-queries-just-work-better">Time-ordered queries just work better</h3><p>TimescaleDB hypertables require a time column to partition the data. Because time is an essential (and known) part of each row and chunk, TimescaleDB can intelligently improve how the query is planned and executed to take advantage of the time component of the data.</p><p>For example, let's query for the maximum CPU usage for each minute for the last 10 minutes.</p><pre><code class="language-SQL">EXPLAIN (ANALYZE,BUFFERS)        
SELECT time_bucket('1 minute', time) AS minute, 
  max(usage_user) 
FROM cpu 
WHERE time &gt; '2022-08-14 07:12:17.568901 +0000' 
GROUP BY minute 
ORDER BY minute DESC 
LIMIT 10;
</code></pre><p>Because TimescaleDB understands that this query is aggregating on time and the result is ordered by the time column (something each chunk is already ordering by in an index), it can use the ChunkAppend custom execution node. In contrast, PostgreSQL plans five workers to scan all partitions before sorting the results and finally doing a <code>GroupAggregate</code> on the time column.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/09/single-query-latency-chunkappend-1.png" class="kg-image" alt="" loading="lazy" width="2000" height="1053" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/09/single-query-latency-chunkappend-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/09/single-query-latency-chunkappend-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/09/single-query-latency-chunkappend-1.png 1600w, https://timescale.ghost.io/blog/content/images/2022/09/single-query-latency-chunkappend-1.png 2070w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Query latency vs volume of data that has to be read to satisfy the query, in TimescaleDB and PostgreSQL</span></figcaption></figure><p>TimescaleDB scans fewer data and doesn't need to spend time re-sorting the data that it knows is already sorted in the chunk. For time-series data with a known order and constraints, TimescaleDB works better for most queries than vanilla PostgreSQL.</p><h2 id="ingest-performance">Ingest Performance</h2><p>Intriguingly, ingest performance for both TimescaleDB and PostgreSQL are nearly identical, a dramatic improvement for PostgreSQL given the <a href="https://timescale.ghost.io/blog/timescaledb-vs-6a696248104e/">results five years ago with PostgreSQL 9.6</a>. However, TimscaleDB still consistently finished with an average rate of 3,000 to 4,000 rows/second higher than a single PostgreSQL table.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/09/insert-performance-of-1-billion-rows--1--1.png" class="kg-image" alt="" loading="lazy" width="2000" height="1349" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/09/insert-performance-of-1-billion-rows--1--1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/09/insert-performance-of-1-billion-rows--1--1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/09/insert-performance-of-1-billion-rows--1--1.png 1600w, https://timescale.ghost.io/blog/content/images/2022/09/insert-performance-of-1-billion-rows--1--1.png 2070w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Insert performance comparison between TimescaleDB 2.7.2 and PostgreSQL 14.4</span></figcaption></figure><p>This shows that while vast improvements have been made in PostgreSQL, TimescaleDB hypertables also continue to perform exceptionally well. As well as the rate, the other characteristics of ingest performance are nearly identical between TimescaleDB and PostgreSQL. Modifying the batch size for the number of rows to insert at a time impacts each database the same: small batch sizes or a few hundred rows significantly hinder ingest performance, while batch sizes of 10,000 to 15,000 rows seem to be about optimal for this dataset.</p><h2 id="declarative-partitioning">Declarative Partitioning</h2><p>In the benchmarks above, we tested TimescaleDB against a single PostgreSQL table simply because that’s the default option that most people end up using. PostgreSQL also has support for native declarative partitioning, which has also been maturing over the past few years. </p><p>For the sake of completeness, we also tested TimescaleDB against native declarative partitioning. As the graphic below shows, TimescaleDB is still 1,000x faster for some queries, with strong performance gains still showing across the board. Ingest performance was similar between TimescaleDB and declarative partitioning.</p><p>In fact, if anything, the takeaway from these tests was that while declarative partitioning has matured, the gap between using a single table and declarative partitioning has shrunk.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/04/image.png" class="kg-image" alt="" loading="lazy" width="2000" height="1639" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/04/image.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/04/image.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/04/image.png 1600w, https://timescale.ghost.io/blog/content/images/2023/04/image.png 2070w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Query latency comparison between TimescaleDB and PostgreSQL with declarative partitioning</span></figcaption></figure><p>Using declarative partitioning is also harder. One needs to manually pre-create partitions, ensure there are no data gaps, ensure no data is inserted outside of your partition ranges, and create more partitions as time moves on.</p><p>In contrast, with TimescaleDB, one does not need any of this. Instead, a single <code>create_hypertable</code> command is used to convert a standard table into a hypertable, and TimescaleDB takes care of the rest.</p><h2 id="conclusion">Conclusion</h2><p>TimescaleDB harnesses the power of the extension framework to supercharge PostgreSQL for time-series and analytical applications. With additional features like compression and continuous aggregates, TimescaleDB provides not only the most performant way of using time-series data in PostgreSQL but also the best developer experience. </p><p>When compared to traditional PostgreSQL, TimescaleDB enables 1,000x faster time-series queries, compresses data by 90&nbsp;%, and provides access to advanced time-series analysis tools and operational features specifically designed to ease data management. TimescaleDB also provides benefits for other types of queries with features like SkipScan—just by installing the extension.</p><p>In short, TimescaleDB extends PostgreSQL to enable developers to continue to use the database they love for time series, perform better at scale, spend less, and stream data analysis and operations. </p><p>If you’re looking to expand your <a href="https://www.tigerdata.com/learn/building-a-scalable-database" rel="noreferrer">database scalability</a>, try our hosted service, <a href="https://www.timescale.com/cloud">Tiger Cloud</a>. You will get the PostgreSQL you know and love with extra features for time series (<a href="https://timescale.ghost.io/blog/how-we-made-data-aggregation-better-and-faster-on-postgresql-with-timescaledb-2-7/">continuous aggregation</a>, <a href="https://docs.timescale.com/api/latest/compression/">compression</a>, <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/data-retention/about-data-retention/#drop-data-by-chunk">automatic retention policies</a>, <a href="https://timescale.ghost.io/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-2/">hyperfunctions</a>). Plus, a platform with <a href="https://timescale.ghost.io/blog/how-high-availability-works-in-our-cloud-database/">automated backups, high availability</a>, automatic upgrades, and much more. <a href="https://console.cloud.timescale.com/signup">You can use it for free for 30 days; no credit card required</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[State of PostgreSQL 2022—13 Tools That Aren't psql]]></title>
            <description><![CDATA[Performance and tooling are frequently debated in the State of PostgreSQL survey, and this year was no exception. With psql remaining the number one tool for querying and admin among PostgreSQL users, we decided to compile a list of tools— that aren’t psql—to broaden your options.]]></description>
            <link>https://www.tigerdata.com/blog/state-of-postgresql-2022-13-tools-that-arent-psql</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/state-of-postgresql-2022-13-tools-that-arent-psql</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[State of PostgreSQL]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Tue, 26 Jul 2022 13:26:48 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2022/07/Blog-Hero.png">
            </media:content>
            <content:encoded><![CDATA[<p>The State of PostgreSQL 2022 survey closed a few weeks ago, and we're hard at work cleaning and analyzing the data to provide the best insights we can for the PostgreSQL community.</p><p>In the database community, however, there are usually two things that drive lots of <em>discussion</em> year after year: performance and tooling. During this year's survey, we modified the questions slightly so that we could focus on three specific use cases and the PostgreSQL tools that the community finds most helpful for each: querying and administration, development, and data visualization.</p><h2 id="postgresql-tools-what-do-we-have-against-psql">PostgreSQL Tools: What Do We Have Against psql?</h2><p>Absolutely nothing! As evidenced by the majority of respondents (69.4 %) that mentioned using psql for querying and administration, it's the ubiquitous choice for so many PostgreSQL users and there is already good documentation and community contributed resources (<a href="https://psql-tips.org/">https://psql-tips.org/</a> by <a href="https://www.youtube.com/watch?v=j2gHN0ItUq8">Leatitia Avrot</a> is a great example) to learn more about it.</p><p>So that got us thinking. What other tools did folks bring up often for interacting with PostgreSQL along the three use cases mentioned above?</p><p>I'm glad we asked. 😉</p><h2 id="postgresql-querying-and-administration">PostgreSQL Querying and Administration</h2><p>As we just said, psql is by far the most popular tool for interacting with PostgreSQL. 🎉</p><p>It's clear, however, that many users with all levels of experience do trust other tools as well.</p><h3 id="query-and-administration-tools">Query and administration tools</h3><p>pgAdmin (35 %), DBeaver (26 %), Datagrip (13 %), and IntelliJ (10 %) IDEs received the most mentions. Most of these aren't surprising if you've been working with databases, PostgreSQL or not. The most popular GUIs (pgAdmin and DBeaver) are open source and freely available to use. The next more popular GUIs (Datagrip and IntelliJ) are licensed per seat. However, if your company or team already uses JetBrain's tools, you might have access to these popular tools.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Blog---10-tools-that-aren-t-psql.png" class="kg-image" alt loading="lazy" width="2000" height="1310" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Blog---10-tools-that-aren-t-psql.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Blog---10-tools-that-aren-t-psql.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Blog---10-tools-that-aren-t-psql.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Blog---10-tools-that-aren-t-psql.png 2000w" sizes="(min-width: 720px) 720px"></figure><p>What I was more interested in were the mentions that happened just after the more popular tools I expected to see. Often, it's this next set of PostgreSQL tools that has gained enough attention from community members that there's obviously a value proposition to investigate further. If they can be helpful to my (or your) development workflow in certain situations, I think it's worth digging a little deeper.</p><h3 id="pgcli">pgcli</h3><!--kg-card-begin: markdown--><p>First on the list is <a href="https://www.pgcli.com/">pgcli</a>, a Python-based command-line tool and one of many <a href="https://github.com/dbcli">dbcli</a> tools created for various databases. Although this is not a replacement for <code>psql</code>, it provides an interactive, auto-complete interface for writing SQL and getting results. Syntax highlighting and some basic support for psql backslash commands are included. If you love to stay in the terminal but want a little more interactivity, the dbcli tools have been around for quite some time, have a nice community of support, and might make database exploration just a little bit easier sometimes.</p>
<!--kg-card-end: markdown--><h3 id="azure-data-studio">Azure Data Studio</h3><p>Introduced as a beta in December 2017 by the Microsoft database tooling team, <a href="https://docs.microsoft.com/en-us/sql/azure-data-studio/download-azure-data-studio?view=sql-server-ver16">Azure Data Studio</a> has been built on top of the same Electron platform as Visual Studio Code. Although the primary feature set is currently geared towards SQL Server (for obvious reasons), the ability to connect to PostgreSQL has been available since 2019.</p><p>There are a couple of unique features in Azure Data Studio (ADS) that work with both SQL Server and PostgreSQL connections that I think are worth mentioning. </p><p>First, ADS includes the ability to create and run SQL-based Jupyter Notebooks. Typically you'd have to wrap your SQL inside of another runtime like Python, but ADS provides the option to select the "SQL" kernel and deals with the connection and SQL wrapping behind the scenes.</p><p>Second, ADS provides the ability to export query results to Excel without any plugins needed. While there are (seemingly) a thousand ways to quickly get a result set into CSV, producing a correctly formatted Excel file requires a plugin with almost any other tool. Regardless of how you feel about Excel, it is still the tool of choice for many data analysts, and being able to provide an Excel file easily does help sometimes.</p><p>Finally, ADS also provides some basic charting capabilities using query results. There's no need to set up a notebook and use a <a href="https://plotly.com/python/">charting library like plotly</a> if you just need to get some quick visualizations on the data. I've had a few hiccups with the capabilities (it's certainly not intended as a serious data analytics tool), but it can be helpful to get some quick chart images to share while exploring query data.</p><h3 id="postico">Postico</h3><p>For anyone using MacOS, <a href="https://eggerapps.at/postico/">Postico</a> is a GUI application that's been recommended in many of my circles. Many folks prefer the native MacOS feel, and some of the unique usability and editing features that make working with PostgreSQL simple and intuitive.</p><h3 id="up-and-coming">Up and coming</h3><p>We'll leave it to you to look through the data and see what other GUI/query tools fellow PostgreSQL users are also using that might be of interest to you, but there are a few that were mentioned multiple times and even caused me to hit Google a few times to find out more. Some are free and open source, while others require licenses but provide interesting features like built-in data analytics capabilities. Whether you end up using any of these or not, it's good to see continued innovation within the tooling market, something that doesn't seem to be slowing down decades into our SQL journey. </p><ul><li><a href="https://arctype.com/">Archetype</a></li><li><a href="https://tableplus.com/">TablePlus</a></li><li><a href="https://www.aquafold.com/">Aqua Data Studio</a></li><li><a href="https://www.beekeeperstudio.io/">Beekeeper Studio</a></li></ul><h2 id="helpful-third-party-postgresql-tools-for-application-development">Helpful Third-Party PostgreSQL Tools for Application Development</h2><p>Although the GUI/administration landscape is certainly as active as ever, one of the most impactful features of PostgreSQL is how extensible it is. If the core application doesn't provide exactly what your application needs, there's a good chance someone (or some company) is working to provide that functionality.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Blog---10-tools-that-aren-t-psql--1-.png" class="kg-image" alt loading="lazy" width="2000" height="1726" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Blog---10-tools-that-aren-t-psql--1-.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Blog---10-tools-that-aren-t-psql--1-.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Blog---10-tools-that-aren-t-psql--1-.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Blog---10-tools-that-aren-t-psql--1-.png 2000w" sizes="(min-width: 720px) 720px"></figure><p>The total distinct number of tools mentioned was similar to GUI/administration tools and generally fell into four categories: management features, cluster monitoring, query plan insights, and database DevOps tooling. For this blog post, we're going to focus on the first three areas.</p><h3 id="management-features">Management features</h3><p>It's not surprising that the most popular third-party PostgreSQL tools tend to be focused on daily management tasks of some sort. Two of the most popular tools in this area are mainstays in most self-hosted PostgreSQL circles.</p><p><strong>pgBouncer</strong></p><!--kg-card-begin: markdown--><p>PostgreSQL creates one new process (not thread) per connection. Without proper tuning and a right-sized server, a database can quickly become overwhelmed with unplanned spikes in usage. <a href="https://www.pgbouncer.org/">pgBouncer</a> is an open-source connection pooling application that helps manage connection usage for high-traffic applications.</p>
<!--kg-card-end: markdown--><p>If your database is self-hosted or your DBaaS doesn't provide some kind of connection pooling management for you, pgBouncer can be installed anywhere that makes sense with respect to your application to provide better connection management.</p><p><strong>pgBackRest</strong></p><!--kg-card-begin: markdown--><p>Database backups are essential, obviously, and PostgreSQL has always had standard tooling for backup and restore. But as databases have grown in size and application architectures have become more complex, using <code>pg_dump</code> and <code>pg_restore</code> can make it more difficult than intended to perform these tasks well.</p>
<!--kg-card-end: markdown--><p>The Crunchy Data team created <a href="https://pgbackrest.org/">pgBackRest</a> to help provide a full-fledged backups and restore system with many necessary features for enterprise workloads. Multi-threaded backup and compression, multiple repository locations, and backup resume are just a few features that make this a common and valuable tool for any PostgreSQL administrator.</p><h3 id="cluster-monitoring">Cluster monitoring</h3><p>The second area of third-party PostgreSQL tools that show up often focuses on improved database monitoring, which includes query monitoring in most cases. There are a lot of folks tackling this problem area from many different angles, which demonstrates the continued need that many developers and administrators have when managing PostgreSQL.</p><p><strong>pgBadger</strong></p><p>PostgreSQL has a lot of settings that can be tuned and details that can be logged into server logs, but there is no built-in functionality for holistically analyzing that data cohesively. This is where pgBadger steps in to help generate useful reports from all of the data your server is logging.</p><p><a href="https://github.com/darold/pgbadger">pgBadger</a> is one of a few popular PostgreSQL tools written in <a href="https://www.perl.org/">Perl</a> (which surprises me for some reason), but the developer has gone to great lengths to not require lots of Perl-specific modules for drawing charts and graphs, instead relying on common JavaScript libraries in the rendered reports.</p><p>There's a lot to look at with pgBadger, and the larger PostgreSQL community often recommends it as a helpful, long-term debugging tool for server performance issues.</p><p><strong>pganalyze</strong></p><p><a href="https://pganalyze.com/">pganalyze</a> has grown in popularity quite a lot over the last few years. <a href="https://twitter.com/LukasFittl">Lukas Fittl</a> has done a great job adding new features and capabilities while also providing a number of great PostgreSQL community resources across various platforms.</p><!--kg-card-begin: markdown--><p>pganalyze is a fee-based product that uses data provided by standard plugins (<code>pg_stat_statements</code> for example) which is then consumed through a collector that sends the data to a cloud service. If you use pganalyze to query log information as well (e.g., long-running queries), then features like example problem queries and index advisor could be really helpful for your development workflow and user experience.</p>
<!--kg-card-end: markdown--><h3 id="query-plan-analysis">Query plan analysis</h3><p>No discussion about PostgreSQL would be complete without mentioning tools that help you understand EXPLAIN output better. This is one area so many people struggle with, particularly based on what their previous experience is with another database, and a small cache of common, helpful tools have been growing in popularity to help with this essential task.</p><p><strong>Depesz EXPLAIN and Dalibo EXPLAIN</strong></p><p>Both <a href="https://explain.depesz.com/">Depesz</a> and <a href="https://explain.dalibo.com/">Dalibo</a> EXPLAIN provide a quick, free platform for taking a PostgreSQL explain plan and providing helpful insights into which operations are causing a slow query and, in some cases, providing helpful hints to help speed things up. Also, if you let them, both tools provide a permalink to the output for you to share with others if necessary.</p><p><strong>pgMustard</strong></p><p>One of my favorite EXPLAIN tools is <a href="https://www.pgmustard.com/">pgMustard</a>, created and maintained by <a href="https://uk.linkedin.com/in/michristofides">Michael Christofides</a>. This is a for-fee tool, but there are a lot of unique insights and features that pgMustard provides that others currently don't. Michael is also doing great work within the community, even recently starting a <a href="http://postgres.fm">PostgreSQL podcast</a> with Nikolay Samokhvalov, <a href="https://timescale.ghost.io/blog/what-is-sql-used-for-build-environments-where-devs-can-experiment/">with whom we recently talked about all things SQL</a>.</p><h2 id="which-visualization-tools-do-you-use">Which Visualization Tools Do You Use?</h2><p>The final tooling question on the State of PostgreSQL survey asked about visualization tools that folks used. Without a doubt, Grafana was the top vote-getter, but that's something we could have probably guessed pretty easily.</p><p>I <em>was</em> surprised that the next two top vote-getters were for pgAdmin and DBeaver, both popular database GUI tools we mentioned earlier. In both cases, visualization capabilities are somewhat limited, so it's hard to tell exactly what kind of features are being used that would categorize them as visualization tools.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Blog---10-tools-that-aren-t-psql-2.png" class="kg-image" alt loading="lazy" width="2000" height="676" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Blog---10-tools-that-aren-t-psql-2.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Blog---10-tools-that-aren-t-psql-2.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Blog---10-tools-that-aren-t-psql-2.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Blog---10-tools-that-aren-t-psql-2.png 2000w" sizes="(min-width: 720px) 720px"></figure><p>The next group of tools is more interesting to me and I wanted to highlight a few that might pique your interest to investigate further.</p><h3 id="qgis">QGIS</h3><p><a href="https://www.qgis.org/en/site/">QGIS</a> is a desktop application that's used to visualize spatial data, whether from PostGIS queries or other data sources. As I've had the pleasure of learning about GIS data and queries from <a href="https://twitter.com/RustProofLabs">Ryan Lambert</a> over the past few years, I've seen him use this tool for lots of valuable and interesting spatial queries. If you rely on PostGIS for application features and you store spatial data, take a look at how QGIS might be able to help your analysis workflow.</p><h3 id="superset">Superset</h3><p>There are a number of data visualization and dashboarding alternatives in the market, and PostgreSQL support is universally expected regardless of the tool. <a href="https://superset.apache.org/">Superset</a> is an open-source option that also has commercial support and hosting options available through Preset.io. With more than 40 chart types and a vibrant community, there's a lot to explore in the Superset ecosystem.</p><h3 id="streamlit">Streamlit</h3><p>For those developers that use Python for most of their data analysis and visualizations, <a href="https://streamlit.io/">Streamlit</a> is another popular choice that can easily fit into your existing workflow. Streamlit isn't a drag-and-drop UI for creating dashboards, but rather a programmatic interface for building and deploying data analysis applications using Python. And as of July 2022, you can deploy public data apps using Streamlit.io.</p><h2 id="what-about-you">What About You?</h2><p>There were so many interesting answers and suggestions provided by the community to these three questions. It's clear that there are a lot of people around the world working to help developers and database professionals be more productive across many common tasks.</p><p>Are there any surprises in this list or tools that you think didn't make the list? Hit us up on <a href="https://slack.timescale.com">Slack</a>, our <a href="https://www.timescale.com/forum/t/state-of-postgresql-2022-postgres-tools/735">Forum</a>, or Twitter (<a href="https://twitter.com/timescaledb">@timescaleDB</a>) to share other tools that are important to your daily PostgreSQL workflow!</p><h2 id="read-the-report">Read the Report</h2><p>Now that we’ve given you a taste of our survey results, are you curious to learn more about the PostgreSQL community? If you’d like to know more insights about the State of PostgreSQL 2022, including why respondents chose PostgreSQL, their opinion on industry events, and what information sources they would recommend to friends and colleagues, don’t miss our complete report. <a href="https://www.timescale.com/state-of-postgres/2022?utm_source=state-of-pg-2022&amp;utm_medium=blog&amp;utm_campaign=state-of-pg-2022&amp;utm_id=state-of-pg-2022&amp;utm_content=state-postgres-blog">Click here to read it and learn firsthand what the State of PostgreSQL is in 2022</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[State of PostgreSQL 2022—First Findings]]></title>
            <description><![CDATA[The results of the third State of PostgreSQL survey are almost out! While you wait for the complete report, read some of the survey’s initial findings, including where PostgreSQL users come from, their experience level, and favorite tools.]]></description>
            <link>https://www.tigerdata.com/blog/state-of-postgresql-2022-first-findings</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/state-of-postgresql-2022-first-findings</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[State of PostgreSQL]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Fri, 08 Jul 2022 14:59:54 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2022/07/BlogHero_First-Findings.png">
            </media:content>
            <content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">🚀</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">About the State of PostgreSQL</strong></b><i><em class="italic" style="white-space: pre-wrap;">Timescale’s love for PostgreSQL, one of the world’s most advanced open-source databases with 30+ years of history, runs deep. </em></i><a href="http://www.timescale.com/"><i><em class="italic" style="white-space: pre-wrap;">We built our products on PostgreSQL</em></i></a><i><em class="italic" style="white-space: pre-wrap;">, </em></i><a href="https://timescale.ghost.io/blog/the-future-of-community-in-light-of-babelfish/"><i><em class="italic" style="white-space: pre-wrap;">are proud members of the PostgreSQL community,</em></i></a><a href="https://www.youtube.com/playlist?list=PLsceB9ac9MHRnmNZrCn_TWkUrCBCPR3mc"><i><em class="italic" style="white-space: pre-wrap;">and wouldn’t exist without it and the extensibility it provides</em></i></a><i><em class="italic" style="white-space: pre-wrap;">.In 2019, Timescale launched the first State of PostgreSQL report, advancing our desire to provide greater insights into the vibrant and growing PostgreSQL user base. From the most popular programming languages and favorite features to whether respondents use PostgreSQL for work or personal projects (or both!), the State of PostgreSQL provides valuable insights into this great community. Following a one-year hiatus due to the pandemic, we resumed the annual survey in 2021. </em></i><a href="https://www.timescale.com/state-of-postgres-results"><i><em class="italic" style="white-space: pre-wrap;">Check out our previous reports </em></i></a><i><em class="italic" style="white-space: pre-wrap;">for more info, and keep reading to learn more about this year’s first findings. </em></i><a href="https://www.timescale.com/state-of-postgres/2022?utm_source=state-of-pg-2022&amp;utm_medium=blog&amp;utm_campaign=state-of-pg-2022&amp;utm_id=state-of-pg-2022&amp;utm_content=state-postgres-blog"><i><em class="italic" style="white-space: pre-wrap;">If you want to read the complete 2022 report, go ahead!</em></i></a></div></div><p>Earlier this year, we launched the third <em>State of PostgreSQL</em> survey. Participation reached unprecedented levels, with nearly one thousand PostgreSQL users answering our questions —more than double compared to the previous year! A huge thank you to you all for giving back to the community by sharing your experiences with PostgreSQL.</p><p>Gathering and making this information open to the public is our way of helping build a better and more inclusive PostgreSQL community. Here are some of our initial findings.</p><h2 id="demographics"><br>Demographics<br></h2><h3 id="what-is-your-primary-geographical-location">What is your primary geographical location?</h3><p>Mirroring the 2019 and 2021 survey results, most respondents (54.4 %) are located in the EMEA (Europe, Middle East, Africa) region, followed by North America, with 25.9 %.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/What-is-your-primary-geographical-location.png" class="kg-image" alt="" loading="lazy" width="1800" height="1117" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/What-is-your-primary-geographical-location.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/What-is-your-primary-geographical-location.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/What-is-your-primary-geographical-location.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/What-is-your-primary-geographical-location.png 1800w" sizes="(min-width: 720px) 720px"></figure><h3 id="how-long-have-you-been-using-postgresql"><br><br>How long have you been using PostgreSQL?</h3><p>While most of this year’s participants have been using PostgreSQL for 3-5 years, the number of new users experimenting with the database for less than a year has grown (6.4 %). The majority of the surveyed PostgreSQL users—54 %— have been using PostgreSQL for six or more years.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/How-long-have-you-been-using-PostgreSQL.png" class="kg-image" alt="" loading="lazy" width="1800" height="1116" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/How-long-have-you-been-using-PostgreSQL.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/How-long-have-you-been-using-PostgreSQL.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/How-long-have-you-been-using-PostgreSQL.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/How-long-have-you-been-using-PostgreSQL.png 1800w" sizes="(min-width: 720px) 720px"></figure><h3 id="what-is-your-current-profession-or-job-status"><br><br>What is your current profession or job status?</h3><p>Most PostgreSQL users (43.3 %) work as software developers/engineers, followed by software architects (13.2 %) and database administrators or DBAs (7.3 %). However, we introduced more title options in this year’s survey, which lent a bit more nuance to the answers. For example, we learned that 5.8 % of respondents are consultants, while 2.4 % are researchers.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Blog---current-profession-or-job-status_-1.png" class="kg-image" alt="" loading="lazy" width="1724" height="1116" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Blog---current-profession-or-job-status_-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Blog---current-profession-or-job-status_-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Blog---current-profession-or-job-status_-1.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Blog---current-profession-or-job-status_-1.png 1724w" sizes="(min-width: 720px) 720px"></figure><h2 id="community">Community</h2><h3 id="have-you-ever-contributed-to-postgresql"><br>Have you ever contributed to PostgreSQL?</h3><p>Among our sample of PostgreSQL users with 15 or more years of experience, 44&nbsp;% said they have contributed to PostgreSQL at least once. In fact, regardless of their experience, users across the board have contributed to the PostgreSQL community.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Blog---Have-you-ever-contributed-to-PostgreSQL_.png" class="kg-image" alt="" loading="lazy" width="1801" height="1117" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Blog---Have-you-ever-contributed-to-PostgreSQL_.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Blog---Have-you-ever-contributed-to-PostgreSQL_.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Blog---Have-you-ever-contributed-to-PostgreSQL_.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Blog---Have-you-ever-contributed-to-PostgreSQL_.png 1801w" sizes="(min-width: 720px) 720px"></figure><p><br><br><br>Over 300 respondents answered bonus questions and shed light on what they like the most about the PostgreSQL community, where they see room for improvement, and what would make the community more welcoming.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Improvement-in-PostgreSQL--1-.png" class="kg-image" alt="" loading="lazy" width="1836" height="926" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Improvement-in-PostgreSQL--1-.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Improvement-in-PostgreSQL--1-.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Improvement-in-PostgreSQL--1-.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Improvement-in-PostgreSQL--1-.png 1836w" sizes="(min-width: 720px) 720px"></figure><p></p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Blog---welcoming-to-newcomers_---v2.png" class="kg-image" alt="" loading="lazy" width="1837" height="926" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Blog---welcoming-to-newcomers_---v2.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Blog---welcoming-to-newcomers_---v2.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Blog---welcoming-to-newcomers_---v2.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Blog---welcoming-to-newcomers_---v2.png 1837w" sizes="(min-width: 720px) 720px"></figure><p></p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Best-thing-about-the-community.png" class="kg-image" alt="" loading="lazy" width="1836" height="926" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Best-thing-about-the-community.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Best-thing-about-the-community.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Best-thing-about-the-community.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Best-thing-about-the-community.png 1836w" sizes="(min-width: 720px) 720px"></figure><h2 id="tools">Tools</h2><p>Three tools stood out among the respondents who use them for queries and administration tasks: <a href="https://www.postgresql.org/docs/current/app-psql.html"><strong>psql</strong></a> (69.4 %), <a href="https://www.pgadmin.org/"><strong>pgAdmin</strong></a> (35.3 %), and <a href="https://dbeaver.io/"><strong>DBeaver</strong></a> (26.2 %).</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/07/Top_3_Adim_Tools.png" class="kg-image" alt="" loading="lazy" width="1836" height="926" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/07/Top_3_Adim_Tools.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/07/Top_3_Adim_Tools.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/07/Top_3_Adim_Tools.png 1600w, https://timescale.ghost.io/blog/content/images/2022/07/Top_3_Adim_Tools.png 1836w" sizes="(min-width: 720px) 720px"></figure><h2 id="read-the-report">Read the Report</h2><p>Now that we’ve given you a taste of our survey results, are you curious to learn more about the PostgreSQL community? If you’d like to know more insights about the <em>State of PostgreSQL 2022, </em>including why respondents chose PostgreSQL, their opinion on industry events, and what information sources they would recommend to friends and colleagues, don’t miss our complete report. <a href="https://www.timescale.com/state-of-postgres/2022?utm_source=state-of-pg-2022&amp;utm_medium=blog&amp;utm_campaign=state-of-pg-2022&amp;utm_id=state-of-pg-2022&amp;utm_content=state-postgres-blog">Click here to read the report and learn firsthand what the <em>State of PostgreSQL</em> is in 2022</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Made Data Aggregation Better and Faster on PostgreSQL With TimescaleDB 2.7]]></title>
            <description><![CDATA[They’re so fast we can’t catch up! Check out our benchmarks with two datasets to learn how we used continuous aggregates to make queries up to 44,000x faster, while requiring 60 % less storage (on average). ]]></description>
            <link>https://www.tigerdata.com/blog/how-we-made-data-aggregation-better-and-faster-on-postgresql-with-timescaledb-2-7</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/how-we-made-data-aggregation-better-and-faster-on-postgresql-with-timescaledb-2-7</guid>
            <category><![CDATA[Engineering]]></category>
            <category><![CDATA[General]]></category>
            <category><![CDATA[Announcements & Releases]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Tue, 21 Jun 2022 12:58:38 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2022/06/candlesticks-2.png">
            </media:content>
            <content:encoded><![CDATA[<p><a href="https://timescale.ghost.io/blog/what-the-heck-is-time-series-data-and-why-do-i-need-a-time-series-database-dcf3b1b18563/">Time-series data</a> is the lifeblood of the analytics revolution in nearly every industry today. One of the most difficult challenges for application developers and data scientists is aggregating data efficiently without always having to query billions (or trillions) of raw data rows. Over the years, developers and databases have created numerous ways to solve this problem, usually similar to one of the following options:</p><ul><li><strong>DIY processes to pre-aggregate data and store it in regular tables</strong>. Although this provides a lot of flexibility, particularly with indexing and data retention, it's cumbersome to develop and maintain, particularly deciding how to track and update aggregates with data that arrives late or has been updated in the past.</li><li><strong>Extract Transform and Load (ETL) process for longer-term analytics.</strong> Even today, development teams employ entire groups that specifically manage ETL processes for databases and applications because of the constant overhead of creating and maintaining the perfect process.</li><li><strong>Materialized views.</strong> While these VIEWS are flexible and easy to create, they are static snapshots of the aggregated data. Unfortunately, developers need to manage updates using TRIGGERs or CRON-like applications in all current implementations. And in all but a very few databases, all historical data is replaced each time, preventing developers from dropping older raw data to save space and computation resources every time the data is refreshed.</li></ul><p>Most developers head down one of these paths because we learn, often the hard way, that running reports and analytic queries over the same raw data, request after request, doesn't perform well under heavy load. In truth, most raw time-series data doesn't change after it's been saved, so these complex aggregate calculations return the same results each time.</p><p>In fact, as a long-term time-series database developer, I've used all of these methods too, so that I could manage historical aggregate data to make reporting, dashboards, and analytics faster and more valuable, even under heavy usage.</p><p>I loved when customers were happy, even if it meant a significant amount of work behind the scenes maintaining that data.</p><p>But, I always wished for a more straightforward solution.</p><h2 id="how-timescaledb-improves-queries-on-aggregated-data-in-postgresql">How TimescaleDB Improves Queries on Aggregated Data in PostgreSQL</h2><p><a href="https://timescale.ghost.io/blog/continuous-aggregates-faster-queries-with-automatically-maintained-materialized-views/">In 2019, TimescaleDB introduced continuous aggregates to solve this very problem</a>, making the ongoing aggregation of massive time-series data easy and flexible. This is the feature that first caught my attention as a PostgreSQL developer looking to build more scalable time-series applications—precisely because I had been doing it the hard way for so long.</p><p>Continuous aggregates look and act like <a href="https://www.tigerdata.com/learn/guide-to-postgresql-views" rel="noreferrer">materialized views in PostgreSQL</a>, but with many of the additional features I was looking for (<a href="https://www.youtube.com/watch?v=1V1ADr6CKz4">if you want to learn more about views, materialized views, and continuous aggregates, check out this lesson from our Foundations of PostgreSQL and TimescaleDB course</a>). These are just some of the things they do:</p><ul><li>Automatically track changes and additions to the underlying raw data.</li><li>Provide configurable, user-defined policies to keep the materialized data up-to-date automatically.</li><li>Automatically append new data (as <a href="https://timescale.ghost.io/blog/achieving-the-best-of-both-worlds-ensuring-up-to-date-results-with-real-time-aggregation/">real-time aggregates</a> by default) before the scheduled process has materialized to disk. This setting is configurable.</li><li>Retain historical aggregated data even if the underlying raw data is dropped.</li><li>Can be compressed to reduce storage needs and further improve the performance of analytic queries.</li><li>Keep dashboards and reports running smoothly.</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/ABL_CAGGS_Comparison-Chart_V2.0.png" class="kg-image" alt="Table comparing the functionality of PostgreSQL materialized views with continuous aggregates in TimescaleDB" loading="lazy" width="1800" height="2066" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/ABL_CAGGS_Comparison-Chart_V2.0.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/ABL_CAGGS_Comparison-Chart_V2.0.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/06/ABL_CAGGS_Comparison-Chart_V2.0.png 1600w, https://timescale.ghost.io/blog/content/images/2022/06/ABL_CAGGS_Comparison-Chart_V2.0.png 1800w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Table comparing the functionality of PostgreSQL materialized views with continuous aggregates in TimescaleDB</em></i></figcaption></figure><p>Once I tried continuous aggregates, I realized that TimescaleDB provided the solution that I (and many other PostgreSQL users) were looking for. With this feature, managing and analyzing massive volumes of time-series data in PostgreSQL finally felt fast and easy.</p><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">Want to make your queries even faster? Try <a href="https://www.timescale.com/blog/an-incremental-materialized-view-on-steroids-how-we-made-continuous-aggregates-even-better/" rel="noreferrer">hierarchical continuous aggregates</a>, a.k.a. continuous aggregates on top of continuous aggregates.</div></div><h2 id="what-about-other-databases">What About Other Databases?</h2><p>By now, some readers might be thinking something along these lines:</p><p><em>“Continuous aggregates may help with the management and analytics of time-series data in PostgreSQL, but that’s what NoSQL databases are for—they already provide the features you needed from the get-go. Why didn’t you try a NoSQL database?”</em></p><p>Well, I did.</p><p>There are numerous time-series and NoSQL databases on the market that attempt to solve this specific problem. I looked at (and used) many of them. But from my experience, nothing can quite match the advantages of a relational database with a feature like continuous aggregates for time-series data. These other options provide a lot of features for a myriad of use cases, but they weren't the right solution for this particular problem, among other things.</p><h3 id="what-about-mongodb">What about MongoDB?</h3><p><a href="https://www.mongodb.com/">MongoDB</a> has been the go-to for many data-intensive applications. Included since version 4.2 is a feature called <a href="https://www.mongodb.com/docs/manual/core/materialized-views/">On-Demand Materialized Views</a>. On the surface, it works similar to a materialized view by combining the <a href="https://www.mongodb.com/docs/manual/core/aggregation-pipeline/">Aggregation Pipeline</a> feature with a $merge operation to mimic ongoing updates to an aggregate data collection. However, there is no built-in automation for this process, and MongoDB doesn't keep track of any modifications to underlying data. The developer is still required to keep track of which time frames to materialize and how far back to look.</p><h3 id="what-about-influxdb">What about InfluxDB?</h3><p>For many years <a href="https://www.influxdata.com/">InfluxDB</a> has been the destination for time-series applications. Although <a href="https://www.tigerdata.com/blog/what-is-high-cardinality" rel="noreferrer">we've discussed in other articles how InfluxDB doesn't scale effectively, particularly with high cardinality datasets</a>, it does provide a feature called <a href="https://docs.influxdata.com/influxdb/v1.8/query_language/continuous_queries/">Continuous Queries.</a> This feature is also similar to a materialized view and goes one step further than MongoDB by automatically keeping the dataset updated. Unfortunately, it suffers from the same lack of raw data monitoring and doesn't provide nearly as much flexibility as SQL in how the datasets are created and stored.</p><h3 id="what-about-clickhouse">What about Clickhouse?</h3><p><a href="https://clickhouse.com/">Clickhouse</a>, and several recent forks like <a href="https://www.firebolt.io/">Firebolt</a>, have redefined the way some analytic workloads perform. Even with some of the<a href="https://timescale.ghost.io/blog/what-is-clickhouse-how-does-it-compare-to-postgresql-and-timescaledb-and-how-does-it-perform-for-time-series-data/"> impressive query performance</a>, it provides a mechanism similar to a materialized view as well, backed by an <a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/aggregatingmergetree/">AggregationMergeTree</a> engine. In a sense, this provides almost real-time aggregated data because all inserts are saved to both the regular table and the materialized view. The biggest downside of this approach is dealing with updates or modifying the timing of the process.</p><h2 id="recent-improvements-in-continuous-aggregates-meet-timescaledb-27">Recent Improvements in Continuous Aggregates: Meet TimescaleDB 2.7</h2><p>Continuous aggregates were first introduced in <a href="https://timescale.ghost.io/blog/continuous-aggregates-faster-queries-with-automatically-maintained-materialized-views/">TimescaleDB 1.3</a> solving the problems that many PostgreSQL users, including me, faced with time-series data and materialized views: automatic updates, real-time results, easy data management, and the option of using the view for downsampling.</p><p>But continuous aggregates have come a long way. One of the previous improvements was the introduction of <a href="https://timescale.ghost.io/blog/increase-your-storage-savings-with-timescaledb-2-6-introducing-compression-for-continuous-aggregates/">compression for continuous aggregates in TimescaleDB 2.6</a>. Now, we took it a step further with the arrival of TimescaleDB 2.7, which introduces dramatic performance improvements in continuous aggregates. <strong>They are now blazing fast—up to 44,000x faster in some queries than in previous versions. </strong></p><p>Let me give you one concrete example: <strong>in initial testing using live, real-time stock trade transaction data, typical candlestick aggregates were nearly 2,800x faster to query </strong>than in previous versions of continuous aggregates (which were already fast!)</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/06/sonic-run.gif" class="kg-image" alt="" loading="lazy" width="498" height="206"></figure><p>Later in this post, we will dig into the performance and storage improvements introduced by TimescaleDB 2.7 by presenting a complete benchmark of continuous aggregates using multiple datasets and queries. 🔥</p><p>But the improvements don’t end here.</p><p>First, the new continuous aggregates also require 60&nbsp;% less storage (on average) than before for many common aggregates, which directly translates into storage savings. <br><br>Second, in previous versions of TimescaleDB, continuous aggregates came with certain limitations: users, for example, could not use certain functions like DISTINCT, FILTER, or ORDER BY. These limitations are now gone. TimescaleDB 2.7 ships with a completely redesigned materialization process that solves many of the previous usability issues, so you can use any aggregate function to define your continuous aggregate. <a href="https://docs.timescale.com/timescaledb/latest/overview/release-notes/">Check out our release notes for all the details on what's new.</a></p>
<!--kg-card-begin: html-->
<div class="highlight">
	
    <p class="highlight__text">
        <svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
	</svg>
✨ A big thank you to the Timescale engineers that made the improvements in continuous aggregates possible, with special mentions to Fabrízio Mello, Markos Fountoulakis, and David Kohn.  
    </p>
</div>

<!--kg-card-end: html-->
<p><br>And now, the fun part.</p><h2 id="show-me-the-numbers-benchmarking-aggregate-queries">Show Me the Numbers: Benchmarking Aggregate Queries</h2><p>To test the new version of continuous aggregates, we chose two datasets that represent common time-series datasets: IoT and financial analysis.</p><ul><li><strong>IoT dataset (~1.7 billion rows): </strong>The IoT data we leveraged is the New York City Taxicab dataset that's been maintained by Todd Schneider for a number of years, and scripts are available in his <a href="https://github.com/toddwschneider/nyc-taxi-data">GitHub repository</a> to load data into PostgreSQL. Unfortunately, a week after his latest update, the transit authority that maintains the actual datasets changed their long-standing export data format from CSV to Parquet—which means the current scripts will not work. Therefore, the dataset we tested with is from data prior to that change and covers ride information from 2014 to 2021.</li><li><strong>Stock transactions dataset (~23.7 million rows): </strong>The financial dataset we used is a real-time stock trade dataset provided by <a href="https://twelvedata.com/">Twelve Data</a> and ingests ongoing transactions for the top 100 stocks by volume from February 2022 until now. Real-time transaction data is typically the source of many stock trading analysis applications requiring aggregate rollups over intervals for visualizations like <a href="https://docs.timescale.com/timescaledb/latest/tutorials/financial-candlestick-tick-data/create-candlestick-aggregates/">candlestick charts </a>and machine learning analysis. While our example dataset is smaller than a full-fledged financial application would maintain, it provides a working example of ongoing data ingestion using continuous aggregates, TimescaleDB <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/compression/about-compression/">native compression</a>, and <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/data-retention/about-data-retention/">automated raw data retention</a> (while keeping aggregate data for long-term analysis).</li></ul><p>You can use a sample of this data, generously provided by Twelve Data, to try all of the improvements in TimescaleDB 2.7 by following<a href="https://docs.timescale.com/timescaledb/latest/tutorials/ingest-real-time-websocket-data/"> this tutorial</a>, which provides stock trade data for the last 30 days. Once you have the database setup, you can take it a step further by registering for an API key and <a href="https://docs.timescale.com/timescaledb/latest/tutorials/ingest-real-time-websocket-data/">following our tutorial to ingest ongoing transactions from the Twelve Data API</a>.</p><h3 id="creating-continuous-aggregates-using-standard-postgresql-aggregate-functions">Creating Continuous Aggregates Using Standard PostgreSQL Aggregate Functions<br></h3><p>The first thing we benchmarked was to create an aggregate query that used standard PostgreSQL aggregate functions like <code>MIN()</code>, <code>MAX()</code>, and <code>AVG()</code>. In each dataset we tested, we created the same continuous aggregate in TimescaleDB 2.6.1 and 2.7, ensuring that both aggregates had computed and stored the same number of rows. </p><p><strong>IoT dataset</strong></p><p>This continuous aggregate resulted in 1,760,000 rows of aggregated data spanning seven years of data.</p><pre><code class="language-sql">CREATE MATERIALIZED VIEW hourly_trip_stats
WITH (timescaledb.continuous, timescaledb.finalized=false) 
AS
SELECT 
	time_bucket('1 hour',pickup_datetime) bucket,
	avg(fare_amount) avg_fare,
	min(fare_amount) min_fare,
	max(fare_amount) max_fare,
	avg(trip_distance) avg_distance,
	min(trip_distance) min_distance,
	max(trip_distance) max_distance,
	avg(congestion_surcharge) avg_surcharge,
	min(congestion_surcharge) min_surcharge,
	max(congestion_surcharge) max_surcharge,
	cab_type_id,
	passenger_count
FROM 
	trips
GROUP BY 
	bucket, cab_type_id, passenger_count</code></pre><p></p><p><strong>Stock transactions dataset</strong></p><p>This continuous aggregate resulted in 950,000 rows of data at the time of testing, although these are updated as new data comes in.</p><pre><code class="language-sql">CREATE MATERIALIZED VIEW five_minute_candle_delta
WITH (timescaledb.continuous) AS
    SELECT
        time_bucket('5 minute', time) AS bucket,
        symbol,
        FIRST(price, time) AS "open",
        MAX(price) AS high,
        MIN(price) AS low,
        LAST(price, time) AS "close",
        MAX(day_volume) AS day_volume,
        (LAST(price, time)-FIRST(price, time))/FIRST(price, time) AS change_pct
    FROM stocks_real_time srt
    GROUP BY bucket, symbol;
</code></pre><p>To test the performance of these two continuous aggregates, we selected the following queries, all common queries among our users for both the IoT and financial use cases:</p><ol><li>SELECT COUNT (*)</li><li>SELECT COUNT (*) with WHERE</li><li>ORDER BY</li><li>time_bucket reaggregation</li><li>FILTER</li><li>HAVING </li></ol><p>Let’s take a look at the results.</p><h3 id="query-1-%60select-count-from%E2%80%A6%60">Query #1: `SELECT COUNT(*) FROM…`</h3><p>Doing a <code>COUNT(*)</code> from PostgreSQL is a known performance bottleneck. It's one of the reasons we created the <a href="https://docs.timescale.com/api/latest/hyperfunctions/approximate_row_count/"><code>approximate_row_count()</code></a> function in TimescaleDB which uses table statistics to provide a close approximation of the overall row count. However, it's instinctual for most users (and ourselves, if we're honest) to try and get a quick row count by doing a <code>COUNT(*)</code> query:</p><pre><code class="language-sql">-- IoT dataset
SELECT count(*) FROM hourly_trip_stats;

-- Stock transactions dataset
SELECT count(*) FROM five_min_candle_delta;</code></pre><p>And most users recognized that in previous versions of TimescaleDB, the materialized data seemed slower than normal to do a COUNT over. <br></p><p>Thinking about our two example datasets, both continuous aggregates reduce the overall row count from raw data by 20x or more. So, while counting rows in PostgreSQL is slow, it always felt a little slower than it had to be. The reason was that not only did PostgreSQL have to scan and count all of the rows of data, it had to group the data a second time because of some additional data that TimescaleDB stored as part of the original design of continuous aggregates. With the new design of continuous aggregates in TimescaleDB 2.7, that second grouping is no longer required, and PostgreSQL can just query the data normally, translating into faster queries.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Query--1-2.png" class="kg-image" alt="Table comparing the performance of a query with SELECT COUNT (*) in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7" loading="lazy" width="1351" height="441" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Query--1-2.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Query--1-2.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Query--1-2.png 1351w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Performance of a query with SELECT COUNT (*) in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7</em></i></figcaption></figure><h3 id="query-2-select-count-based-on-the-value-of-a-column">Query #2: SELECT COUNT(*) Based on The Value of a Column</h3><p>Another common query that many analytic applications perform is to count the number of records where the aggregate value is within a certain range:</p><pre><code class="language-sql">-- IoT  dataset
SELECT count(*) FROM hourly_trip_stats
WHERE avg_fare &gt; 13.1
AND bucket &gt; '2018-01-01' AND bucket &lt; '2019-01-01';

-- Stock transactions dataset
SELECT count(*) FROM five_min_candle_delta
WHERE change_pct &gt; 0.02;
</code></pre><p>In previous versions of continuous aggregates, TimescaleDB had to finalize the value before it could be filtered against the predicate value, which caused queries to perform more slowly. With the new version of continuous aggregates, PostgreSQL can now search for the value directly, <em>and</em> we can add an index to meaningful columns to speed up the query even more!</p><p>In the case of the financial dataset, we see a very significant improvement: 1,336x faster. The large change in performance can be attributed to the formula query that has to be calculated over all of the rows of data in the continuous aggregate. With the IoT dataset, we're comparing against a simple average function, but for the stock data, multiple values have to be finalized (FIRST/LAST) before the formula can be calculated and used for the filter.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Query--2-2.png" class="kg-image" alt="Table comparing the performance of a query with SELECT COUNT (*) plus WHERE in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7." loading="lazy" width="1351" height="441" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Query--2-2.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Query--2-2.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Query--2-2.png 1351w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Performance of a query with SELECT COUNT (*) plus WHERE in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7</em></i></figcaption></figure><h3 id="query-3-select-top-10-rows-by-value">Query #3: Select Top 10 Rows by Value<br></h3><p>Taking the first example a step further, it's very common to query data within a range of time and get the top rows:</p><pre><code class="language-sql">-- IoT dataset
SELECT * FROM hourly_trip_stats
ORDER BY avg_fare desc
LIMIT 10;

-- Stock transactions dataset
SELECT * FROM five_min_candle_delta
ORDER BY change_pct DESC 
LIMIT 10;</code></pre><p>In this case, we tested queries with the continuous aggregate set to provide <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/real-time-aggregates/">real-time results</a> (the default for continuous aggregates) and materialized-only results. When set to real-time, TimescaleDB always queries data that's been materialized first and then appends (with a <code>UNION</code>) any newer data that exists in the raw data but that has not yet been materialized by the ongoing refresh policy. And, because it's now possible to index columns within the continuous aggregate, we added an index on the <code>ORDER BY</code> column.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Query--3-1.png" class="kg-image" alt="Table comparing the performance of a query with ORDER BY in a continuous aggregate TimescaleDB 2.6.1 and TimescaleDB 2.7." loading="lazy" width="1351" height="606" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Query--3-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Query--3-1.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Query--3-1.png 1351w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Performance of a query with ORDER BY in a continuous aggregate TimescaleDB 2.6.1 and TimescaleDB 2.7</em></i></figcaption></figure><p><strong>Yes, you read that correctly. Nearly 45,000x better performance on  <code>ORDER BY</code> </strong>when the query only searches through materialized data.</p><p>The dramatic difference between real-time and materialized-only queries is because of the <code>UNION</code> of both materialized and raw aggregate data. The PostgreSQL planner needs to union the total result before it can limit the query to 10 rows (in our example), and so all of the data from both tables need to be read and ordered first. When you only query materialized data, PostgreSQL and TimescaleDB knows that it can query just the index of the materialized data.</p><p>Again, storing the finalized form of your data and indexing column values dramatically impacts the querying performance of historical aggregate data! And all of this is updated continuously over time in a non-destructive way—something that's impossible to do with any other relational database, including vanilla PostgreSQL.</p><h3 id="query-4-timescale-hyperfunctions-to-re-aggregate-into-higher-time-buckets">Query #4: Timescale Hyperfunctions to Re-aggregate Into Higher Time Buckets</h3><p>Another example we wanted to test was the impact finalizing data values has on our suite of <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hyperfunctions/">analytical hyperfunctions</a>. Many of the hyperfunctions we provide as part of the <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hyperfunctions/install-toolkit/">TimescaleDB Toolkit</a> utilize custom aggregate values that allow many different values to be accessed later depending on the needs of an application or report. Furthermore, these aggregate values can be <a href="https://timescale.ghost.io/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-2/">re-aggregated into different size time buckets</a>. This means that if the aggregate functions fit your use case, one continuous aggregate can produce results for many different time_bucket sizes! This is a feature many users have asked for over time, and hyperfunctions make this possible.</p><p>For this example, we only examined the New York City Taxicab dataset to benchmark the impact of finalized CAGGs. Currently, there is not an aggregate hyperfunction that aligns with the OHLC values needed for the stock data set, however, <a href="https://github.com/timescale/timescaledb-toolkit/issues/445">there is a feature request</a> for it! (😉)</p><p>Although there are not currently any one-to-one hyperfunctions that provide exact replacements for our min/max/avg example, we can still observe the query improvement using a <code>tdigest</code> value for each of the columns in our original query.</p><p><strong>Original min/max/avg continuous aggregate for multiple columns:</strong></p><pre><code class="language-sql">CREATE MATERIALIZED VIEW hourly_trip_stats
WITH (timescaledb.continuous, timescaledb.finalized=false) 
AS
SELECT 
	time_bucket('1 hour',pickup_datetime) bucket,
	avg(fare_amount) avg_fare,
	min(fare_amount) min_fare,
	max(fare_amount) max_fare,
	avg(trip_distance) avg_distance,
	min(trip_distance) min_distance,
	max(trip_distance) max_distance,
	avg(congestion_surcharge) avg_surcharge,
	min(congestion_surcharge) min_surcharge,
	max(congestion_surcharge) max_surcharge,
	cab_type_id,
	passenger_count
FROM 
	trips
GROUP BY 
	bucket, cab_type_id, passenger_count
</code></pre><p><strong>Hyperfunction-based continuous aggregate for multiple columns:</strong></p><pre><code class="language-sql">CREATE MATERIALIZED VIEW hourly_trip_stats_toolkit
WITH (timescaledb.continuous, timescaledb.finalized=false) 
AS
SELECT 
	time_bucket('1 hour',pickup_datetime) bucket,
	tdigest(1,fare_amount) fare_digest,
	tdigest(1,trip_distance) distance_digest,
	tdigest(1,congestion_surcharge) surcharge_digest,
	cab_type_id,
	passenger_count
FROM 
	trips
GROUP BY 
	bucket, cab_type_id, passenger_count</code></pre><p>With the continuous aggregate created, we then queried this data in two different ways:</p><p><strong>1. Using the same `time_bucket()` size defined in the continuous aggregate, which in this example was one-hour data.</strong></p><pre><code class="language-sql">SELECT 
	bucket AS b,
	cab_type_id, 
	passenger_count,
	min_val(ROLLUP(fare_digest)),
	max_val(ROLLUP(fare_digest)),
	mean(ROLLUP(fare_digest))
FROM hourly_trip_stats_toolkit
WHERE bucket &gt; '2021-05-01' AND bucket &lt; '2021-06-01'
GROUP BY b, cab_type_id, passenger_count 
ORDER BY b DESC, cab_type_id, passenger_count;</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Query--4.png" class="kg-image" alt="Table comparing the erformance of a query with time_bucket() in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7 (the query uses the same bucket size as the definition of the continuous aggregate)" loading="lazy" width="1350" height="354" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Query--4.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Query--4.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Query--4.png 1350w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Performance of a query with time_bucket() in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7 (the query uses the same bucket size as the definition of the continuous aggregate)</em></i></figcaption></figure><p><strong>2. We re-aggregated the data from one-hour buckets into one-day buckets. </strong>This allows us to efficiently query different bucket lengths based on the original bucket size of the continuous aggregate.</p><pre><code class="language-sql">SELECT 
	time_bucket('1 day', bucket) AS b,
	cab_type_id, 
	passenger_count,
	min_val(ROLLUP(fare_digest)),
	max_val(ROLLUP(fare_digest)),
	mean(ROLLUP(fare_digest))
FROM hourly_trip_stats_toolkit
WHERE bucket &gt; '2021-05-01' AND bucket &lt; '2021-06-01'
GROUP BY b, cab_type_id, passenger_count 
ORDER BY b DESC, cab_type_id, passenger_count;</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Query--4-2.png" class="kg-image" alt="Table comparing the performance of a query with time_bucket() in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7. The query re-aggregates the data from one-hour buckets into one-day buckets." loading="lazy" width="1350" height="354" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Query--4-2.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Query--4-2.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Query--4-2.png 1350w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Performance of a query with time_bucket() in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7. The query re-aggregates the data from one-hour buckets into one-day buckets</em></i></figcaption></figure><p>In this case, the speed is almost identical because the same amount of data has to be queried. But if these aggregates satisfy your data requirements, only one continuous aggregate would be necessary in many cases, rather than a different continuous aggregate for each bucket size (one minute, five minutes, one hour, etc.)</p><h3 id="query-5-pivot-queries-with-filter">Query #5: Pivot Queries With FILTER</h3><p><a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/about-continuous-aggregates/#function-support">In previous versions of continuous aggregates, many common SQL features were not permitted</a> because of how the partial data was stored and finalized later. Using a PostgreSQL <code>FILTER</code> clause was one such restriction.</p><p>For example, we took the IoT dataset and created a simple <code>COUNT(*)</code> to calculate each company's number of taxi rides ( <code>cab_type_id</code>) for each hour. Before TimescaleDB 2.7, you would have to store this data in a narrow column format, storing a row in the continuous aggregate for each cab type.</p><pre><code class="language-sql">CREATE MATERIALIZED VIEW hourly_ride_counts_by_type 
WITH (timescaledb.continuous, timescaledb.finalized=false) 
AS
SELECT 
	time_bucket('1 hour',pickup_datetime) bucket,
	cab_type_id,
  	COUNT(*)
FROM trips
  	WHERE cab_type_id IN (1,2)
GROUP BY 
	bucket, cab_type_id;</code></pre><p>To then query this data in a pivoted fashion, we could <code>FILTER</code> the continuous aggregate data after the fact.</p><pre><code class="language-sql">SELECT bucket,
	sum(count) FILTER (WHERE cab_type_id IN (1)) yellow_cab_count,
  	sum(count) FILTER (WHERE cab_type_id IN (2)) green_cab_count
FROM hourly_ride_counts_by_type
WHERE bucket &gt; '2021-05-01' AND bucket &lt; '2021-06-01'
GROUP BY bucket
ORDER BY bucket;</code></pre><p>In TimescaleDB 2.7, you can now store the aggregated data using a <code>FILTER</code> clause to achieve the same result in one step!</p><pre><code class="language-sql">CREATE MATERIALIZED VIEW hourly_ride_counts_by_type_new 
WITH (timescaledb.continuous) 
AS
SELECT 
	time_bucket('1 hour',pickup_datetime) bucket,
  	COUNT(*) FILTER (WHERE cab_type_id IN (1)) yellow_cab_count,
  	COUNT(*) FILTER (WHERE cab_type_id IN (2)) green_cab_count
FROM trips
GROUP BY 
	bucket;</code></pre><p>Querying this data is much simpler, too, because the data is already pivoted and finalized.</p><pre><code class="language-sql">SELECT * FROM hourly_ride_counts_by_type_new 
WHERE bucket &gt; '2021-05-01' AND bucket &lt; '2021-06-01'
ORDER BY bucket;
</code></pre><p>This saves storage (50&nbsp;% fewer rows in this case) and CPU to finalize the <code>COUNT(*)</code> and then filter the results each time based on <code>cab_type_id</code>. We can see this in the query performance numbers.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Query--5.png" class="kg-image" alt="Table comparing the performance of a query with FILTER in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7." loading="lazy" width="1350" height="354" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Query--5.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Query--5.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Query--5.png 1350w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Performance of a query with FILTER in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7.</em></i></figcaption></figure><p>Being able to use <code>FILTER</code> and other SQL features improve both developer experience and flexibility long term!</p><h3 id="query-6-having-stores-significantly-less-materialized-data">Query #6: HAVING Stores Significantly Less Materialized Data</h3><p>As a final example of how the improvements to continuous aggregates will impact your day-to-day development and analytics processes, let's look at a simple query that uses a <code>HAVING</code> clause to reduce the number of rows that the aggregate stores.</p><p>In previous versions of TimescaleDB, the having clause couldn't be applied at materialization time. Instead, the <code>HAVING</code> clause was applied after the fact to all of the aggregated data as it was finalized. In many cases, this dramatically affected both the speed of queries to the continuous aggregate and the amount of data stored overall.</p><p>Using our stock data as an example, let's create a continuous aggregate that only stores a row of data if the <code>change_pct</code> value is greater than 20 %. This would indicate that a stock price changed dramatically over one hour, something we don't expect to see in most hourly stock trades.</p><pre><code class="language-sql">CREATE MATERIALIZED VIEW one_hour_outliers
WITH (timescaledb.continuous) AS
    SELECT
        time_bucket('1 hour', time) AS bucket,
        symbol,
        FIRST(price, time) AS "open",
        MAX(price) AS high,
        MIN(price) AS low,
        LAST(price, time) AS "close",
        MAX(day_volume) AS day_volume,
        (LAST(price, time)-FIRST(price, time))/LAST(price, time) AS change_pct
    FROM stocks_real_time srt
    GROUP BY bucket, symbol
   HAVING (LAST(price, time)-FIRST(price, time))/LAST(price, time) &gt; .02;</code></pre><p>Once the dataset is created, we can query each aggregate to see how many rows matched our criteria.</p><pre><code class="language-sql">SELECT count(*) FROM one_hour_outliers;</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Query--6.png" class="kg-image" alt="Table comparing the performance of a query with HAVING in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7." loading="lazy" width="1351" height="354" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Query--6.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Query--6.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Query--6.png 1351w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Performance of a query with HAVING in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7</em></i></figcaption></figure><p>The biggest difference here (and the one that will more negatively impact the performance of your application over time) is the storage size of this aggregated data. Because TimescaleDB 2.7 only stores rows that meet the criteria, the data footprint is significantly smaller!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Query--6-2.png" class="kg-image" alt="Table comparing the storage footprint of a continuous aggregate bucketing stock transactions by the hour in TimescaleDB 2.6.1 and TimescaleDB 2.7." loading="lazy" width="1351" height="354" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Query--6-2.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Query--6-2.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Query--6-2.png 1351w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Storage footprint of a continuous aggregate bucketing stock transactions by the hour in TimescaleDB 2.6.1 and TimescaleDB 2.7</em></i></figcaption></figure><h2 id="storage-savings-in-timescaledb-27"><br>Storage Savings in TimescaleDB 2.7</h2><p>One of the final pieces of this update that excites us is how much storage will be saved over time. On many occasions, users with large datasets that contained complex equations in their continuous aggregates would join our <a href="https://slack.timescale.com/">Slack community</a> to ask why more storage is required for the rolled-up aggregate than the raw data.</p><p>In every case we've tested, the new, finalized form of continuous aggregates is smaller than the same example in previous versions of TimescaleDB, with or without a <code>HAVING</code> clause that might filter additional data out.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/06/Storage-Savings.png" class="kg-image" alt="Table comparing the storage footprint of a query with HAVING in a continuous aggregate in TimescaleDB 2.6.1 and TimescaleDB 2.7." loading="lazy" width="1351" height="654" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/06/Storage-Savings.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/06/Storage-Savings.png 1000w, https://timescale.ghost.io/blog/content/images/2022/06/Storage-Savings.png 1351w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Storage savings for different continuous aggregates in TimescaleDB 2.6.1 and TimescaleDB 2.7</em></i></figcaption></figure><h2 id="the-new-continuous-aggregates-are-a-game-changer">The New Continuous Aggregates Are a Game-Changer</h2><p>For those dealing with massive amounts of time-series data, continuous aggregates are the best way to solve a problem that has long haunted PostgreSQL users. The following list details how continuous aggregates expand materialized views:</p><ul><li>They always stay up-to-date, automatically tracking changes in the source table for targeted, efficient updates of materialized data.</li><li>You can use configurable policies to conveniently manage refresh/update interval.</li><li>You can keep your materialized data even after the raw data is dropped, allowing you to downsample your large datasets.</li><li>And you can compress older data to save space and improve analytic queries.</li></ul><p>And in TimescaleDB 2.7, continuous aggregates got much better. First, they are blazing fast: as we demonstrated with our benchmark, the performance of continuous aggregates got consistently better across queries and datasets, up to thousands of times better for common queries. They also got lighter, requiring an average of 60&nbsp;% less storage.</p><p>But besides the performance improvements and storage savings, there are significantly fewer limitations on the types of aggregate queries you can use with continuous aggregates, such as:</p><ul><li>Aggregates with DISTINCT</li><li>Aggregates with FILTER</li><li>Aggregates with FILTER in HAVING clause</li><li>Aggregates without combine function</li><li>Ordered-set aggregates</li><li>Hypothetical-set aggregates</li></ul><p>This new version of continuous aggregates is available by default in <a href="https://docs.timescale.com/timescaledb/latest/overview/release-notes/">TimescaleDB 2.7</a>: now, when you create a new continuous aggregate, you will automatically benefit from all the latest changes. <a href="https://docs.timescale.com/timescaledb/latest/overview/release-notes/">Read our release notes for more information on TimescaleDB 2.7</a>, and for instructions on how to upgrade, <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/update-timescaledb/">check out our docs.</a></p><p>Looking to migrate your existing continue aggregates to the new version? Now, with TimescaleDB 2.8.1, you don’t have to worry about <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/migrate/">migrating from the old continuous aggregates to the new</a>. Say hello to our frictionless migration, an in-place upgrade that avoids disrupting queries over continuous aggregates in applications and dashboards and every time the data is not in the original <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertable</a>.</p>
<!--kg-card-begin: html-->
<div class="highlight">
	
    <p class="highlight__text">
        <svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
	</svg>
☁️🐯 Timescale avoids the manual work involved in updating your TimescaleDB version. Updates take place automatically during a maintenance window picked by you. 
        <a href="https://docs.timescale.com/cloud/latest/service-operations/maintenance/"								target="_blank">Learn more about maintenance and automatic version updates in Timescale,</a> 
and to test it yourself, 
                <a href="https://www.timescale.com/timescale-signup/"								target="_blank">start a free trial!</a>  
    </p>
</div>

<!--kg-card-end: html-->
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Point-in-Time PostgreSQL Database and Query Monitoring With pg_stat_statements]]></title>
            <description><![CDATA[The pg_stat_statements extension strikes again. Learn how to store metrics snapshots regularly for more efficient database monitoring.]]></description>
            <link>https://www.tigerdata.com/blog/point-in-time</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/point-in-time</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[PostgreSQL Performance]]></category>
            <category><![CDATA[PostgreSQL Tips]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Tue, 03 May 2022 13:08:42 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2022/05/PIT-PostgreSQL-Database-Query-Monitoring--1-.png">
            </media:content>
            <content:encoded><![CDATA[<p>Database monitoring is a crucial part of effective data management and building high-performance applications. <a href="https://timescale.ghost.io/blog/identify-postgresql-performance-bottlenecks-with-pg_stat_statements/">In our previous blog post</a>, we discussed how to enable <code>pg_stat_statements</code> (and that it comes standard on all Timescale instances), what data it provides, and demonstrated a few queries that you can run to glean useful information from the metrics to help pinpoint problem queries. </p><p>We also discussed one of the few pitfalls with <code>pg_stat_statements</code>: <em>all of the data it provides is cumulative since the last server restart (or a superuser reset the statistics).</em></p><p>While <code>pg_stat_statements</code> can work as a go-to source of information to determine where initial problems might be occurring when the server isn’t performing as expected, the cumulative data it provides can also pose a problem when said server is struggling to keep up with the load.</p><p>The data might show that a particular application query has been called frequently and read a lot of data from the disk to return results, but that only tells part of the story. With cumulative data, it's impossible to answer specific questions about the state of your cluster, such as:</p><ul><li>Does it usually struggle with resources at this time of day?</li><li>Are there particular forms of the query that are slower than others?</li><li>Is it a specific database that's consuming resources more than others right now?</li><li>Is that normal given the current load?</li></ul><p>The database monitoring information that <code>pg_stat_statements</code> provides is invaluable when you need it. However, it's most helpful when the data shows trends and patterns over time to visualize the true state of your database when problems arise.</p><p>It would be much more valuable if you could transform this static, cumulative data into time-series data, regularly storing snapshots of the metrics. Once the data is stored, we can use standard SQL to query delta values of each snapshot and metric to see how each database, user, and query performed interval by interval. That also makes it much easier to pinpoint <em>when</em> a problem started and <em>what</em> query or database appears to be contributing the most.</p><p><br><br>In this blog post (a sequel to <a href="https://timescale.ghost.io/blog/using-pg-stat-statements-to-optimize-queries/" rel="noreferrer">our previous 101 on the subject</a>), we'll discuss the basic process for storing and analyzing the <code>pg_stat_statements</code> snapshot data over time using TimescaleDB features to store, manage, and query the metric data efficiently. </p><p><br>Although you could automate the storing of snapshot data with the help of a few <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extensions</a>, only TimescaleDB provides everything you need, including automated job scheduling, data compression, data retention, and continuous aggregates to manage the entire solution efficiently.</p><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">You can complement pg_stat_statements with <a href="https://www.timescale.com/blog/database-monitoring-and-query-optimization-introducing-insights-on-timescale/" rel="noreferrer">Insights</a> for great database observability at an unprecedented level of granularity. To try Insights and unlock a new, insightful understanding of your queries and performance,&nbsp;<a href="https://console.cloud.timescale.com/signup?ref=timescale.com">sign up for Timescale</a>.</div></div><p></p><h2 id="postgresql-database-monitoring-201-preparing-to-store-data-snapshots">PostgreSQL Database Monitoring 201: Preparing to Store Data Snapshots</h2><p>Before we can query and store ongoing snapshot data from <code>pg_stat_statements</code>, we need to prepare a schema and, optionally, a separate database to keep all of the information we'll collect with each snapshot.</p><p>We're choosing to be very opinionated about how we store the snapshot metric data and, optionally, how to separate some information, like the query text itself. Use our example as a building block to store the information you find most useful in your environment. You may not want to keep some metric data (i.e., <code>exec_stddev</code>), and that's okay. Adjust based on your database monitoring needs.</p><h3 id="create-a-metrics-database">Create a metrics database</h3><p>Recall that <code>pg_stat_statements</code> tracks statistics for every database in your PostgreSQL cluster. Also, any user with the appropriate permissions can query all of the data while connected to any database. Therefore, while creating a separate database is an optional step, storing this data in a separate TimescaleDB database makes it easier to filter out the queries from the ongoing snapshot collection process. </p><p>We also show the creation of a separate schema called <code>statements_history</code> to store all of the tables and procedures used throughout the examples. This allows a clean separation of this data from anything else you may want to do within this database.</p><pre><code class="language-sql">psql=&gt; CREATE DATABASE statements_monitor;

psql=&gt; \c statements_monitor;

psql=&gt; CREATE EXTENSION IF NOT EXISTS timescaledb;

psql=&gt; CREATE SCHEMA IF NOT EXISTS statements_history;
</code></pre>
<h3 id="create-hypertables-to-store-snapshot-data">Create hypertables to store snapshot data</h3><p>Whether you create a separate database or use an existing TimescaleDB database, we need to create the tables to store the snapshot information. For our example, we'll create three tables:</p><ul><li><strong><code>snapshots</code> <code>(hypertable)</code></strong>: a cluster-wide aggregate snapshot of all metrics for easy cluster-level monitoring</li><li><code><strong>queries</strong></code>: separate storage of query text by <code>queryid</code>, <code>rolname</code>, and <code>datname</code> </li><li><strong><code>statements</code> <code>(hypertable)</code></strong>: statistics for each query every time the snapshot is taken, grouped by <code>queryid</code>, <code>rolname</code>, and <code>datname</code> </li></ul><p>Both the <code>snapshot</code> and <code>statements</code> tables are converted to a hypertable in the following SQL. Because these tables will store lots of metric data over time, making them hypertables unlocks powerful features for managing the data with compression and data retention, as well as speeding up queries that focus on specific periods of time. Finally, take note that each table is assigned a different <code>chunk_time_interval</code> based on the amount and frequency of the data that is added to it.</p><p>The <code>snapshots</code> table, for instance, will only receive one row per snapshot, which allows the chunks to be created less frequently (every four weeks) without growing too large. In contrast, the <code>statements</code> table will potentially receive thousands of rows every time a snapshot is taken and so creating chunks more frequently (every week) allows us to compress this data more frequently and provides more fine-grained control over data retention. </p><p>The size and activity of your cluster, along with how often you run the job to take a snapshot of data, will influence what the right <code>chunk_time_interval</code> is for your system. More information about chunk sizes and best practices can be <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hypertables/best-practices/">found in our documentation</a>.</p><pre><code class="language-sql">/*
 * The snapshots table holds the cluster-wide values
 * each time an overall snapshot is taken. There is
 * no database or user information stored. This allows
 * you to create cluster dashboards for very fast, high-level
 * information on the trending state of the cluster.
 */
CREATE TABLE IF NOT EXISTS statements_history.snapshots (
    created timestamp with time zone NOT NULL,
    calls bigint NOT NULL,
    total_plan_time double precision NOT NULL,
    total_exec_time double precision NOT NULL,
    rows bigint NOT NULL,
    shared_blks_hit bigint NOT NULL,
    shared_blks_read bigint NOT NULL,
    shared_blks_dirtied bigint NOT NULL,
    shared_blks_written bigint NOT NULL,
    local_blks_hit bigint NOT NULL,
    local_blks_read bigint NOT NULL,
    local_blks_dirtied bigint NOT NULL,
    local_blks_written bigint NOT NULL,
    temp_blks_read bigint NOT NULL,
    temp_blks_written bigint NOT NULL,
    blk_read_time double precision NOT NULL,
    blk_write_time double precision NOT NULL,
    wal_records bigint NOT NULL,
    wal_fpi bigint NOT NULL,
    wal_bytes numeric NOT NULL,
    wal_position bigint NOT NULL,
    stats_reset timestamp with time zone NOT NULL,
    PRIMARY KEY (created)
);

/*
 * Convert the snapshots table into a hypertable with a 4 week
 * chunk_time_interval. TimescaleDB will create a new chunk
 * every 4 weeks to store new data. By making this a hypertable we
 * can take advantage of other TimescaleDB features like native 
 * compression, data retention, and continuous aggregates.
 */
SELECT * FROM create_hypertable(
    'statements_history.snapshots',
    'created',
    chunk_time_interval =&gt; interval '4 weeks'
);

COMMENT ON TABLE statements_history.snapshots IS
$$This table contains a full aggregate of the pg_stat_statements view
at the time of the snapshot. This allows for very fast queries that require a very high level overview$$;

/*
 * To reduce the storage requirement of saving query statistics
 * at a consistent interval, we store the query text in a separate
 * table and join it as necessary. The queryid is the identifier
 * for each query across tables.
 */
CREATE TABLE IF NOT EXISTS statements_history.queries (
    queryid bigint NOT NULL,
    rolname text,
    datname text,
    query text,
    PRIMARY KEY (queryid, datname, rolname)
);

COMMENT ON TABLE statements_history.queries IS
$$This table contains all query text, this allows us to not repeatably store the query text$$;


/*
 * Finally, we store the individual statistics for each queryid
 * each time we take a snapshot. This allows you to dig into a
 * specific interval of time and see the snapshot-by-snapshot view
 * of query performance and resource usage
*/
CREATE TABLE IF NOT EXISTS statements_history.statements (
    created timestamp with time zone NOT NULL,
    queryid bigint NOT NULL,
    plans bigint NOT NULL,
    total_plan_time double precision NOT NULL,
    calls bigint NOT NULL,
    total_exec_time double precision NOT NULL,
    rows bigint NOT NULL,
    shared_blks_hit bigint NOT NULL,
    shared_blks_read bigint NOT NULL,
    shared_blks_dirtied bigint NOT NULL,
    shared_blks_written bigint NOT NULL,
    local_blks_hit bigint NOT NULL,
    local_blks_read bigint NOT NULL,
    local_blks_dirtied bigint NOT NULL,
    local_blks_written bigint NOT NULL,
    temp_blks_read bigint NOT NULL,
    temp_blks_written bigint NOT NULL,
    blk_read_time double precision NOT NULL,
    blk_write_time double precision NOT NULL,
    wal_records bigint NOT NULL,
    wal_fpi bigint NOT NULL,
    wal_bytes numeric NOT NULL,
    rolname text NOT NULL,
    datname text NOT NULL,
    PRIMARY KEY (created, queryid, rolname, datname),
    FOREIGN KEY (queryid, datname, rolname) REFERENCES statements_history.queries (queryid, datname, rolname) ON DELETE CASCADE
);

/*
 * Convert the statements table into a hypertable with a 1 week
 * chunk_time_interval. TimescaleDB will create a new chunk
 * every 1 weeks to store new data. Because this table will receive
 * more data every time we take a snapshot, a shorter interval
 * allows us to manage compression and retention to a shorter interval
 * if needed. It also provides smaller overall chunks for querying
 * when focusing on specific time ranges.
 */
SELECT * FROM create_hypertable(
    'statements_history.statements',
    'created',
    create_default_indexes =&gt; false,
    chunk_time_interval =&gt; interval '1 week'
);

</code></pre>
<h3 id="create-the-snapshot-stored-procedure">Create the snapshot stored procedure</h3><p>With the tables created to store the statistics data from <code>pg_stat_statements</code>, we need to create a stored procedure that will run on a scheduled basis to collect and store the data. This is a straightforward process with <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/user-defined-actions/create-and-register/">TimescaleDB user-defined actions</a>. </p><p>A user-defined action provides a method for scheduling a custom stored procedure using the underlying scheduling engine that TimescaleDB uses for automated policies like continuous aggregate refresh and data retention. Although there are other <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extensions</a> for managing schedules, this feature is included with TimescaleDB by default.<br><br>First, create the stored procedure to populate the data. In this example, we use a multi-part common table expression (CTE) to fill in each table, starting with the results of the <code>pg_stat_statements</code> view.</p><pre><code class="language-sql">CREATE OR REPLACE PROCEDURE statements_history.create_snapshot(
    job_id int,
    config jsonb
)
LANGUAGE plpgsql AS
$function$
DECLARE
    snapshot_time timestamp with time zone := now();
BEGIN
	/*
	 * This first CTE queries pg_stat_statements and joins
	 * to the roles and database table for more detail that
	 * we will store later.
	 */
    WITH statements AS (
        SELECT
            *
        FROM
            pg_stat_statements(true)
        JOIN
            pg_roles ON (userid=pg_roles.oid)
        JOIN
            pg_database ON (dbid=pg_database.oid)
        WHERE queryid IS NOT NULL
    ), 
    /*
     * We then get the individual queries out of the result
* and store the text and queryid separately to avoid
     * storing the same query text over and over.
     */
    queries AS (
        INSERT INTO
            statements_history.queries (queryid, query, datname, rolname)
        SELECT
            queryid, query, datname, rolname
        FROM
            statements
        ON CONFLICT
            DO NOTHING
        RETURNING
            queryid
    ), 
    /*
     * This query SUMs all data from all queries and databases
     * to get high-level cluster statistics each time the snapshot
     * is taken.
     */
    snapshot AS (
        INSERT INTO
            statements_history.snapshots
        SELECT
            now(),
            sum(calls),
            sum(total_plan_time) AS total_plan_time,
            sum(total_exec_time) AS total_exec_time,
            sum(rows) AS rows,
            sum(shared_blks_hit) AS shared_blks_hit,
            sum(shared_blks_read) AS shared_blks_read,
            sum(shared_blks_dirtied) AS shared_blks_dirtied,
            sum(shared_blks_written) AS shared_blks_written,
            sum(local_blks_hit) AS local_blks_hit,
            sum(local_blks_read) AS local_blks_read,
            sum(local_blks_dirtied) AS local_blks_dirtied,
            sum(local_blks_written) AS local_blks_written,
            sum(temp_blks_read) AS temp_blks_read,
            sum(temp_blks_written) AS temp_blks_written,
            sum(blk_read_time) AS blk_read_time,
            sum(blk_write_time) AS blk_write_time,
            sum(wal_records) AS wal_records,
            sum(wal_fpi) AS wal_fpi,
            sum(wal_bytes) AS wal_bytes,
            pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0'),
            pg_postmaster_start_time()
        FROM
            statements
    )
    /*
     * And finally, we store the individual pg_stat_statement 
     * aggregated results for each query, for each snapshot time.
     */
    INSERT INTO
        statements_history.statements
    SELECT
        snapshot_time,
        queryid,
        plans,
        total_plan_time,
        calls,
        total_exec_time,
        rows,
        shared_blks_hit,
        shared_blks_read,
        shared_blks_dirtied,
        shared_blks_written,
        local_blks_hit,
        local_blks_read,
        local_blks_dirtied,
        local_blks_written,
        temp_blks_read,
        temp_blks_written,
        blk_read_time,
        blk_write_time,
        wal_records,
        wal_fpi,
        wal_bytes,
        rolname,
  datname
    FROM
        statements;

END;
$function$;
</code></pre>
<p>Once you create the stored procedure, schedule it to run on an ongoing basis as a user-defined action. In the following example, we schedule snapshot data collection every minute, which may be too often for your needs. Adjust the collection schedule to suit your data capture and monitoring needs.</p><pre><code class="language-sql">/*
* Adjust the scheduled_interval based on how often
* a snapshot of the data should be captured
*/
SELECT add_job(
    'statements_history.create_snapshot',
    schedule_interval=&gt;'1 minutes'::interval
);
</code></pre>
<p>And finally, you can verify that the user-defined action job is running correctly by querying the jobs information views. If you set the <code>schedule_interval</code> for one minute (as shown above), wait a few minutes, and then ensure that <code>last_run_status</code>is <code>Success</code> and that there are zero <code>total_failures</code>.</p><pre><code>SELECT js.* FROM timescaledb_information.jobs j
 INNER JOIN timescaledb_information.job_stats js ON j.job_id =js.job_id 
WHERE j.proc_name='create_snapshot';


Name                  |Value                        |
----------------------+-----------------------------+
hypertable_schema     |                             |
hypertable_name       |                             |
job_id                |1008                         |
last_run_started_at   |2022-04-13 17:43:15.053 -0400|
last_successful_finish|2022-04-13 17:43:15.068 -0400|
last_run_status       |Success                      |
job_status            |Scheduled                    |
last_run_duration     |00:00:00.014755              |
next_start            |2022-04-13 17:44:15.068 -0400|
total_runs            |30186                        |
total_successes       |30167                        |
total_failures        |0                            |

</code></pre>
<p>The query metric database is set up and ready to query! Let's look at a few query examples to help you get started.</p><h2 id="querying-pgstatstatements-snapshot-data">Querying pg_stat_statements Snapshot Data</h2><p>We chose to create two statistics tables: one that aggregates the snapshot statistics for the cluster, regardless of a specific query, and another that stores statistics for each query per snapshot. The data is time-stamped using the created column in both tables. The rate of change for each snapshot is the difference in cumulative statistics values from one snapshot to the next.</p><p>This is accomplished in SQL using the LAG window function, which subtracts each row from the previous row ordered by the created column.</p><h3 id="cluster-performance-over-time">Cluster performance over time</h3><p>This<strong> </strong>first example queries the "snapshots" table, which stores the aggregate total of all statistics for the entire cluster. Running this query will return the total values for each snapshot, not the overall cumulative <code>pg_stat_statements</code> values.</p><pre><code class="language-sql">/*
 * This CTE queries the snapshot table (full cluster statistics)
 * to get a high-level view of the cluster state.
 * 
 * We query each row with a LAG of the previous row to retrieve
 * the delta of each value to make it suitable for graphing.
 */
WITH deltas AS (
    SELECT
        created,
        extract('epoch' from created - lag(d.created) OVER (w)) AS delta_seconds,
        d.ROWS - lag(d.rows) OVER (w) AS delta_rows,
        d.total_plan_time - lag(d.total_plan_time) OVER (w) AS delta_plan_time,
        d.total_exec_time - lag(d.total_exec_time) OVER (w) AS delta_exec_time,
        d.calls - lag(d.calls) OVER (w) AS delta_calls,
        d.wal_bytes - lag(d.wal_bytes) OVER (w) AS delta_wal_bytes,
        stats_reset
    FROM
        statements_history.snapshots AS d
    WHERE
        created &gt; now() - INTERVAL '2 hours'
    WINDOW
        w AS (PARTITION BY stats_reset ORDER BY created ASC)
)
SELECT
    created AS "time",
    delta_rows,
    delta_calls/delta_seconds AS calls,
    delta_plan_time/delta_seconds/1000 AS plan_time,
    delta_exec_time/delta_seconds/1000 AS exec_time,
    delta_wal_bytes/delta_seconds AS wal_bytes
FROM
    deltas
ORDER BY
    created ASC;  

time                         |delta_rows|calls               |plan_time|exec_time         |wal_bytes         |
-----------------------------+----------+--------------------+---------+------------------+------------------+
2022-04-13 15:55:12.984 -0400|          |                    |         |                  |                  |
2022-04-13 15:56:13.000 -0400|        89| 0.01666222812679749|      0.0| 0.000066054620811| 576.3131464496716|
2022-04-13 15:57:13.016 -0400|        89|0.016662253391151797|      0.0|0.0000677694667946|  591.643293413018|
2022-04-13 15:58:13.031 -0400|        89|0.016662503817796187|      0.0|0.0000666146741069| 576.3226820499345|
2022-04-13 15:59:13.047 -0400|        89|0.016662103471929153|      0.0|0.0000717084114511| 591.6379700812604|
2022-04-13 16:00:13.069 -0400|        89| 0.01666062607900462|      0.0|0.0001640335102535|3393.3363560151874|

</code></pre>
<h3 id="top-100-most-expensive-queries">Top 100 most expensive queries</h3><p>Getting an overview of the cluster instance is really helpful to understand the state of the whole system over time. Another useful set of data to analyze quickly is a list of the queries using the most resources of the cluster, query by query. There are many ways you could query the snapshot information for these details, and your definition of "resource-intensive" might be different than what we show, but this example gives the high-level cumulative statistics for each query over the specified time, ordered by the highest total sum of execution and planning time.</p><pre><code class="language-sql">/*
* individual data for each query for a specified time range, 
* which is particularly useful for zeroing in on a specific
* query in a tool like Grafana
*/
WITH snapshots AS (
    SELECT
        max,
        -- We need at least 2 snapshots to calculate a delta. If the dashboard is currently showing
        -- a period &lt; 5 minutes, we only have 1 snapshot, and therefore no delta. In that CASE
        -- we take the snapshot just before this window to still come up with useful deltas
        CASE
            WHEN max = min
THEN (SELECT max(created) FROM statements_history.snapshots WHERE created &lt; min)
            ELSE min
        END AS min
    FROM (
        SELECT
            max(created),
            min(created)
        FROM
            statements_history.snapshots WHERE created &gt; now() - '1 hour'::interval
            -- Grafana-based filter
            --statements_history.snapshots WHERE $__timeFilter(created)
        GROUP by
            stats_reset
        ORDER by
            max(created) DESC
        LIMIT 1
    ) AS max(max, min)
), deltas AS (
    SELECT
        rolname,
        datname,
        queryid,
        extract('epoch' from max(created) - min(created)) AS delta_seconds,
        max(total_exec_time) - min(total_exec_time) AS delta_exec_time,
        max(total_plan_time) - min(total_plan_time) AS delta_plan_time,
        max(calls) - min(calls) AS delta_calls,
        max(shared_blks_hit) - min(shared_blks_hit) AS delta_shared_blks_hit,
        max(shared_blks_read) - min(shared_blks_read) AS delta_shared_blks_read
    FROM
        statements_history.statements
    WHERE
 -- granted, this looks odd, however it helps the DecompressChunk Node tremendously,
        -- as without these distinct filters, it would aggregate first and then filter.
        -- Now it filters while scanning, which has a huge knock-on effect on the upper
        -- Nodes
        (created &gt;= (SELECT min FROM snapshots) AND created &lt;= (SELECT max FROM snapshots))
    GROUP BY
        rolname,
        datname,
        queryid
)
SELECT
    rolname,
    datname,
    queryid::text,
    delta_exec_time/delta_seconds/1000 AS exec,
    delta_plan_time/delta_seconds/1000 AS plan,
    delta_calls/delta_seconds AS calls,
    delta_shared_blks_hit/delta_seconds*8192 AS cache_hit,
    delta_shared_blks_read/delta_seconds*8192 AS cache_miss,
    query
FROM
    deltas
JOIN
    statements_history.queries USING (rolname,datname,queryid)
WHERE
    delta_calls &gt; 1
    AND delta_exec_time &gt; 1
    AND query ~* $$.*$$
ORDER BY
    delta_exec_time+delta_plan_time DESC
LIMIT 100;


rolname  |datname|queryid             |exec              |plan|calls               |cache_hit         |cache_miss|query      
---------+-------+--------------------+------------------+----+--------------------+------------------+----------+-----------
tsdbadmin|tsdb   |731301775676660043  |0.0000934033977289| 0.0|0.016660922907623773| 228797.2725854585|       0.0|WITH statem...
tsdbadmin|tsdb   |-686339673194700075 |0.0000570625206738| 0.0|  0.0005647770477161|116635.62329618855|       0.0|WITH snapsh...
tsdbadmin|tsdb   |-5804362417446225640|0.0000008223159463| 0.0|  0.0005647770477161| 786.5311077312939|       0.0|-- NOTE Thi...

</code></pre>
<p>However you decide to order this data, you now have a quick result set with the text of the query and the <code>queryid</code>. With just a bit more effort, we can dig even deeper into the performance of a specific query over time.</p><p>For example, in the output from the previous query, we can see that <code>queryid=731301775676660043</code> has the longest overall execution and planning time of all queries for this period. We can use that <code>queryid</code> to dig a little deeper into the snapshot-by-snapshot performance of this specific query.</p><pre><code class="language-sql">/*
 * When you want to dig into an individual query, this takes
 * a similar approach to the "snapshot" query above, but for 
 * an individual query ID.
 */
WITH deltas AS (
    SELECT
        created,
        st.calls - lag(st.calls) OVER (query_w) AS delta_calls,
        st.plans - lag(st.plans) OVER (query_w) AS delta_plans,
        st.rows - lag(st.rows) OVER (query_w) AS delta_rows,
        st.shared_blks_hit - lag(st.shared_blks_hit) OVER (query_w) AS delta_shared_blks_hit,
        st.shared_blks_read - lag(st.shared_blks_read) OVER (query_w) AS delta_shared_blks_read,
        st.temp_blks_written - lag(st.temp_blks_written) OVER (query_w) AS delta_temp_blks_written,
        st.total_exec_time - lag(st.total_exec_time) OVER (query_w) AS delta_total_exec_time,
        st.total_plan_time - lag(st.total_plan_time) OVER (query_w) AS delta_total_plan_time,
st.wal_bytes - lag(st.wal_bytes) OVER (query_w) AS delta_wal_bytes,
        extract('epoch' from st.created - lag(st.created) OVER (query_w)) AS delta_seconds
    FROM
        statements_history.statements AS st
    join
        statements_history.snapshots USING (created)
    WHERE
        -- Adjust filters to match your queryid and time range
        created &gt; now() - interval '25 minutes'
        AND created &lt; now() + interval '25 minutes'
        AND queryid=731301775676660043
    WINDOW
        query_w AS (PARTITION BY datname, rolname, queryid, stats_reset ORDER BY created)
)
SELECT
    created AS "time",
    delta_calls/delta_seconds AS calls,
    delta_plans/delta_seconds AS plans,
    delta_total_exec_time/delta_seconds/1000 AS exec_time,
    delta_total_plan_time/delta_seconds/1000 AS plan_time,
    delta_rows/nullif(delta_calls, 0) AS rows_per_query,
    delta_shared_blks_hit/delta_seconds*8192 AS cache_hit,
    delta_shared_blks_read/delta_seconds*8192 AS cache_miss,
    delta_temp_blks_written/delta_seconds*8192 AS temp_bytes,
    delta_wal_bytes/delta_seconds AS wal_bytes,
    delta_total_exec_time/nullif(delta_calls, 0) exec_time_per_query,
    delta_total_plan_time/nullif(delta_plans, 0) AS plan_time_per_plan,
    delta_shared_blks_hit/nullif(delta_calls, 0)*8192 AS cache_hit_per_query,
    delta_shared_blks_read/nullif(delta_calls, 0)*8192 AS cache_miss_per_query,
    delta_temp_blks_written/nullif(delta_calls, 0)*8192 AS temp_bytes_written_per_query,
    delta_wal_bytes/nullif(delta_calls, 0) AS wal_bytes_per_query
FROM
    deltas
WHERE
    delta_calls &gt; 0
ORDER BY
    created ASC;


time                         |calls               |plans|exec_time         |plan_time|ro...
-----------------------------+--------------------+-----+------------------+---------+--
2022-04-14 14:33:39.831 -0400|0.016662115132216382|  0.0|0.0000735602224659|      0.0|  
2022-04-14 14:34:39.847 -0400|0.016662248949061972|  0.0|0.0000731468396678|      0.0|  
2022-04-14 14:35:39.863 -0400|  0.0166622286820572|  0.0|0.0000712116494436|      0.0|  
2022-04-14 14:36:39.880 -0400|0.016662015187426844|  0.0|0.0000702374920336|      0.0|  

</code></pre>
<h2 id="compression-continuous-aggregates-and-data-retention">Compression, Continuous Aggregates, and Data Retention</h2><p>This self-serve query monitoring setup doesn't require TimescaleDB. You could schedule the snapshot job with other extensions or tools, and regular PostgreSQL tables could likely store the data you retain for some time without much of an issue. Still, all of this is classic time-series data, tracking the state of your PostgreSQL cluster(s) over time.</p><p>Keeping as much historical data as possible provides significant value to this database monitoring solution's effectiveness. TimescaleDB offers several features that are not available with vanilla PostgreSQL to help you manage growing time-series data and improve the efficiency of your queries and process.</p><p><a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/compression/"><strong>Compression</strong></a> on this data is highly effective and efficient for two reasons:</p><ol><li>Most of the data is stored as integers which compresses very well (96 % plus) using our type-specific algorithms.</li><li>The data can be compressed more frequently because we're never updating or deleting compressed data. This means that it's possible to store months or years of data with very little disk utilization, and queries on specific columns of data will often be significantly faster from compressed data.</li></ol><p><a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/"><strong>Continuous aggregates</strong></a> allow you to maintain higher-level rollups over time for aggregate queries that you run often. Suppose you have dashboards that show 10-minute averages for all of this data. In that case, you could write a continuous aggregate to pre-aggregate that data for you over time without modifying the snapshot process. This allows you to create new aggregations after the raw data has been stored and new query opportunities come to light.</p><p>And finally, <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/data-retention/"><strong>data retention</strong></a> allows you to drop older raw data automatically once it has reached a defined age. If continuous aggregates are defined on the raw data, it will continue to show the aggregated data which provides a complete solution for maintaining the level of data fidelity you need as data ages.</p><p>These additional features provide a complete solution for storing lots of monitoring data about your cluster(s) over the long haul. See the links provided for each feature for more information.</p><h2 id="better-self-serve-query-monitoring-with-pgstatstatements">Better Self-Serve Query Monitoring With pg_stat_statements<br></h2><p>Everything we've discussed and shown in this post is just the beginning. With a few <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertables</a> and queries, the cumulative data from <code>pg_stat_statements</code>can quickly come to life. Once the process is in place and you get more comfortable querying it, visualizing it will be very helpful. </p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/PIT-Query-pg_stat-img-1.gif" class="kg-image" alt="" loading="lazy" width="512" height="330"></figure><p><br><br><code>pg_stat_statements</code> is automatically enabled in all Timescale services. If you’re not a user yet, <a href="https://console.cloud.timescale.com/signup">you can try out Timescale for free</a> (no credit card required) to get access to a modern cloud-native database platform with <a href="https://timescale.ghost.io/blog/postgresql-timescaledb-1000x-faster-queries-90-data-compression-and-much-more/" rel="noreferrer">TimescaleDB's top performance</a>.<br><br>To complement <code>pg_stat_statements</code> for better query monitoring, check out <a href="https://timescale.ghost.io/blog/database-monitoring-and-query-optimization-introducing-insights-on-timescale/" rel="noreferrer">Insights</a>, also available for trial services.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Teaching Elephants to Fish]]></title>
            <description><![CDATA[Timescale's developer advocate Ryan Booz reflects on the PostgreSQL community and shares five ideas on how to improve it.]]></description>
            <link>https://www.tigerdata.com/blog/the-future-of-community-in-light-of-babelfish</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/the-future-of-community-in-light-of-babelfish</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[AWS]]></category>
            <category><![CDATA[Events & Recaps]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Wed, 27 Apr 2022 16:35:09 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2022/04/PostgreSQL-community-elephants.jpg">
            </media:content>
            <content:encoded><![CDATA[<h3 id="the-future-of-community-in-light-of-babelfish">The future of community in light of Babelfish</h3><p></p><p><em>This blog post was adapted from the PGConf NYC 2021 keynote. Originally published at </em><a href="https://postgresconf.org/blog/posts/teaching-elephants-to-fish">https://postgresconf.org</a>.</p><p>On December 1, 2020, at its annual re:Invent conference, Amazon AWS announced Babelfish—an open-source PostgreSQL translation layer that allows SQL Server applications to work natively, and transparently, with PostgreSQL. To be honest, as someone that's spent a significant part of my career using both SQL Server and PostgreSQL, this wasn't actually a very “exciting” development. </p><p>I'm not sure that most people in either community really gave it that much notice a year ago. In fact, my first thought was that Babelfish is just an oversized object-relational mapping (ORM) framework that wasn't tied to any specific development language. While these tools have proven to be hugely useful to many developers, nearly every DBA has first-hand war stories that demonstrate the challenge that automated query generation can impose on a complex system. Frankly, the thought terrifies me a bit.</p><p>Until recently, however, all we knew about Babelfish was based on Amazon published content. But in October 2021, Babelfish was finally released for public access and preview at <a href="https://babelfishpg.org/">https://babelfishpg.org/</a>.</p><p>Not long after the release was announced, I had the opportunity to participate in a video call with some members of the European PostgreSQL community for a first look at Babelfish in real life. It was interesting, and kind of exciting to see what worked and what didn't. However, I didn't leave that call any less concerned about the struggles my SQL Server friends will have as their management teams mandate switching to PostgreSQL using Babelfish. It also got me thinking: <em>why SQL Server?</em></p><p>I decided to look at <a href="https://db-engines.com/">DB-engines.com</a> to see if the engagement metrics that they track would shed any light on it. Although the website doesn't disclose the specific method for determining database engine rank, we know that social engagement and search engine trends play a role in the rankings.</p><p>When I zoomed in on the four "major" relational database engines, utilizing the last nine years of data, two things jumped out at me:</p><ol><li>Only PostgreSQL has seen consistent increases in popularity and engagement over the nine years of tracking.</li><li>While the three other engines have had some steady decline over the same period, SQL Server seems to have the biggest drop-off compared to Oracle and MySQL.</li></ol><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/Babelfish-Img-1.png" class="kg-image" alt="A database engines ranking showing how SQL Server seems to have the biggest drop off compared to Oracle and MySQL in the last few years." loading="lazy" width="1370" height="836" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/04/Babelfish-Img-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/04/Babelfish-Img-1.png 1000w, https://timescale.ghost.io/blog/content/images/2022/04/Babelfish-Img-1.png 1370w" sizes="(min-width: 720px) 720px"></figure><p>While I can posit many reasons why Amazon AWS chose to go directly after SQL Server for this transparent compatibility tool, the reasons to use PostgreSQL as the solution to the problem are obvious to the PostgreSQL community. </p><p>A few facts on PostgreSQL:</p><ul><li>It is the fastest growing, relational database engine on the planet.</li><li>It has a proven foundation.</li><li>It is easy to enhance through extensibility.</li></ul><p>Regardless of how we feel about this new turn of events or the potential onslaught of new support needs by users of SQL Server, there's not much we can do about it. The proverbial cat is already out of the bag.</p><p>Whether the necessary patches are included in the core PostgreSQL code or not, AWS Aurora, at least, will still offer this functionality as a service. I believe this means that over the next 2-4 years, the community will grow from a base of users that have a lot of database experience but have little footing for how to approach a similar, but different, database. And regardless of how we feel about the new demands this will put on this community, it's a group of people that still want to do their job well and contribute back to the community.</p><h2 id="why-do-i-care">Why Do I Care?</h2><p>So, why do I care? Well, if you were to put my 20+ years of database experience into a word cloud of sorts, SQL Server would occupy the largest portion of space. For nearly 15 of the last 21 years, I was primarily a Microsoft data platform user. And while PostgreSQL has occupied the second largest (and longest) portion of my database landscape, I really came of age as a database professional within the SQL Server community. In fact, since coming back into the PostgreSQL community almost four years ago, I've continued to look for ways to foster a community and learning modeled after what I knew and experienced through SQL Server and the Professional Association of SQL Server (PASS) community. And I can say, without a doubt, that I'm not the only one looking for the same thing.</p><p>Let me ask you something then.</p><p><strong><em>When was the last time you had to join a new community?</em></strong></p><p>Is the first thing that comes to mind a technical community (data, programming language, visualization tools, etc.) or something else? Whatever community that was, how did you feel after first stepping through the proverbial door?</p><p>Did you have a crowd of people ready to cheer you on, eager to see you succeed, and ready to support you?</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/Picture1.gif" class="kg-image" alt="People cheering in the audience at a sports game" loading="lazy" width="480" height="270"></figure><p>Or, did you feel like an outsider looking in, trying to figure out how to find the right help, from the right people? Did you feel like everyone else always knew how to connect with the people around you but you struggled to find comradery and resources?</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/Picture2.gif" class="kg-image" alt="Comedian Conan O'Brien looking inside a house through its window" loading="lazy" width="440" height="237"></figure><p>Depending on where you fall, what could have made it better for others or for yourself?</p><p>Let's bring this same thought experiment a little closer to home. What about the PostgreSQL community? What was your "onboarding" experience like and how does that compare to some of the newest members you've met recently?</p><p>It just so happens that we have some feedback from the larger community based on <a href="https://www.timescale.com/state-of-postgres-results/"><em>The State of PostgreSQL</em></a> survey Timescale orchestrated this past April. There were <strong>500 respondents</strong> that ranged in experience levels from novice, newly joined developers, to users with 20+ years of experience. Of those 500 respondents, <strong>49.5&nbsp;%</strong> have used PostgreSQL for <strong>five years or less</strong>, and <strong>50.5&nbsp;%</strong> had PostgreSQL for <strong>six years or more</strong>.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-4.png" class="kg-image" alt="A bar graph on how long users have been using Postgres: 49.5&nbsp;% have used PostgreSQL for five years or less" loading="lazy" width="1150" height="520" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/04/bablefish-img-4.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/04/bablefish-img-4.png 1000w, https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-4.png 1150w" sizes="(min-width: 720px) 720px"></figure><p>It would make sense, then, that about 50&nbsp;% of the survey participants felt like it was a bit difficult to use PostgreSQL and get involved with the community.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-5.png" class="kg-image" alt="Text boxes with users' opinions on the PostgreSQL community over a blue blackground" loading="lazy" width="1600" height="915" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/04/bablefish-img-5.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/04/bablefish-img-5.png 1000w, https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-5.png 1600w" sizes="(min-width: 720px) 720px"></figure><p>And yet, the other 50&nbsp;% of participants seem to have a very different experience with PostgreSQL the longer they stay connected.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-6.png" class="kg-image" alt="Text boxes with users' opinions on the PostgreSQL community over a blue blackground" loading="lazy" width="1600" height="839" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/04/bablefish-img-6.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/04/bablefish-img-6.png 1000w, https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-6.png 1600w" sizes="(min-width: 720px) 720px"></figure><p>The real goal, then, is to determine ways to improve the user experience within the PostgreSQL community earlier in the cycle, rather than hoping folks stick around for more than five years so that they can begin to have a more positive outlook on the community at large.</p><p>As a developer advocate at Timescale, one of my primary responsibilities is to engage with the PostgreSQL community so that we can figure out how to tackle these issues head-on. I'm excited to contribute to the efforts, learning better methods to teach people how to use this technology well and help them be successful.</p><p>And, as a former SQL Server professional and community member, I want to prepare for what I believe will be a growing number of database professionals joining the Slack channel, Twitter conversations, and conferences trying to improve their craft and give back to the community.</p><p>To do this, we have at least two options in the months and years ahead.</p><p>The first option is to take sides. Unfortunately, this happens all too often in technical communities. Whether it's a database engine or the newest development language, this approach is an option. "We're better! You're not!"</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-7.png" class="kg-image" alt="A meme from the Captain America Marvel movie showing two groups of superheroes opposing each other (PostgreSQL vs. SQL server)" loading="lazy" width="450" height="449"></figure><p>Or… we could choose to just sit around the table, share a meal, and learn from one another about how we can build a better, shared community based on the best parts of what each community offers.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-8.png" class="kg-image" alt="A scene from the same movie showing a group of people hanging out at a restaurant table" loading="lazy" width="450" height="233"></figure><p>Quite honestly, we could ensure that we're treating the community more like our beloved namesake—Slonik, the elephant.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-9.png" class="kg-image" alt="A group of elephants protecting a baby elephant" loading="lazy" width="450" height="282"></figure><p>It turns out that elephants are close-knit communities. They care for each other, they actively accept orphans and elephants that are in need, and they're generational, passing down knowledge and community expectations from one generation to the next. Not a bad example to follow, right?</p><h2 id="five-initial-options-for-building-a-better-postgresql-community">Five Initial Options for Building a Better PostgreSQL Community</h2><p>Let me present you with five quick, high-level thoughts on ideas we could reuse from the SQL Server community that might begin the process of improving participation and engagement.</p><h3 id="1-lead-with-empathy-and-curiosity"><strong>1. Lead with empathy and curiosity</strong></h3><p>What does it mean to lead with empathy and curiosity? When the posture of the community starts with empathy, it means that we remember what it was like to be new to a community ourselves. When we start conversations from a place of curiosity, we avoid choosing sides and instead join new users where they are. <br><br>Here are a few things to keep in mind in regards to the SQL Server community that might start to show up in greater numbers soon, although I think these are good things to remember regardless of the user.</p><p><strong>Expect confusion from users that already know SQL</strong>. Oftentimes these users honestly don't know that the dialect of SQL they've been using (T-SQL in this case) isn't a standard. They know concepts but not direct comparisons to the SQL standard or pl/pgsql.</p><p><strong>Remember that you were a newbie once</strong>. Remember when I asked you to think of the last new community you joined? ;-)</p><p><strong>Assume positive intent and that they have tried to search out a solution first.</strong> Again, many users coming from the SQL Server community have a good network of people and resources they've grown accustomed to (we'll touch on that next!). But that community has also fostered good habits for finding solutions and asking better questions. Assume the best!</p><p><strong>Prepare resources to meet their specific needs</strong>. This doesn't mean that we craft all documentation, blogs, and help forums for a specific user base. But, if we know many people will be joining this community with the same understanding of a feature or topic, we can provide them a better foundation for transferring their knowledge into PostgreSQL. It would reduce support overall and foster better community involvement.</p><p>Let me give you one really simple example from my own experience (and that of <em>many</em> SQL Server users that try PostgreSQL for the first time).</p><p>It is a common practice in nearly every T-SQL script I've written, seen, or used to create and use variables and control logic directly in the flow of a script. Because all SQL is executed by default as T-SQL, there is no need for code blocks to do data-specific logic. This really simple T-SQL example is incredibly common in the day-to-day workflow of a SQL Server DBA or developer.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-10.png" class="kg-image" alt="" loading="lazy" width="450" height="287"></figure><p>In PostgreSQL, however, I can't do this in the midst of my migration scripts or maintenance tasks directly. This was a constant source of frustration for me during the release of my first feature at a new company that was using PostgreSQL (but had very few developers that understood databases). <em>I knew</em> what I wanted to do, but I couldn't get any IDE or migration script to do what I wanted.</p><p>Eventually, I found a post that talked about anonymous functions within a SQL script for ad hoc processing. Although I didn't like the added complexity, I could finally write my migrations correctly.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-11.png" class="kg-image" alt="" loading="lazy" width="450" height="400"></figure><p>Having proactive examples in the documentation that acknowledge this difference could be a game-changer for developer success.</p><h3 id="2-lower-the-bar-for-entry-level-pghelp">2. Lower the bar for entry-level #pghelp</h3><p>More than 10 years ago, someone in the SQL Server community had the idea of using #sqlhelp Twitter hashtag to provide help. At the time, the size limit for a tweet was 140 characters so they understood it could only provide short, really succinct triage-like help. In some ways, I think this limitation has actually helped the community learn how to ask better questions that will draw valuable answers. This is evidenced by two things in my opinion.</p><p>First, not every question gets an answer. It's a community-led initiative and so the quality of the question and respect for the "free" nature of the help influence overall engagement. And second, the SQL Server community actively protects the use of this hashtag. That's not always taken kindly by outsiders, and I'm not even sure how much I agree that a community can "own" a hashtag, but it produces a valuable community around a specific technology that has proven to be helpful for thousands of users over the years.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-12.png" class="kg-image" alt=" Twitter users chipping in on a thread with the #sqlhelp hashtag" loading="lazy" width="847" height="1172" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/04/bablefish-img-12.png 600w, https://timescale.ghost.io/blog/content/images/2022/04/bablefish-img-12.png 847w" sizes="(min-width: 720px) 720px"></figure><p>Like the PostgreSQL community, SQL Server users also have a community Slack channel that is active and more long-form. But today, more than 10 years after the community started using it, #sqlhelp is an active channel of connecting with the larger community to give and receive timely help.</p><p>Could we do something similar with a #pghelp hashtag?</p><h3 id="3-support-new-members-by-cultivating-more-leaders">3. Support new members by cultivating more leaders</h3><p>As the PostgreSQL community grows, we can only support users if we build a growing group of leaders. A few of the leaders in the SQL Server community realized this same need over a decade ago and proactively sought ways to build new leaders, content creators, and community advocates. One successful example of this is an initiative called "T-SQL Tuesday," a worldwide monthly "blog fest."</p><p>The idea is simple. Each month, someone volunteers to be the host, they announce the topic at least a week in advance, and then anyone from around the world can contribute to the conversation by publishing a blog on that topic. Some of the topics are technical (replication, high availability, query tuning success), while some are more soft-skill-focused (best/worst SQL interview experience, how to avoid burnout in the SQL field).</p><p>As I said, it was specifically started as an initiative to get more people in the community to contribute to the conversation. Of almost any initiative that this community has undertaken in the last 10-15 years, T-SQL Tuesday has done more than anything else to cultivate new community leaders, alter careers, and bring collaboration across the globe. The most intriguing part of this for me is that it's free to run and participate in, and Microsoft has had nothing to do with it. It is completely community-led.</p><p>Starting in April 2022, a few PostgreSQL community members are going to start "PSQL Phriday," a monthly community blogging initiative. To learn more about it, the monthly topics, and how you can participate, watch the blogging feeds at <a href="http://planet.postgresql.org/">planet.postgresql.org</a> and monitor the #psqlphriday on Twitter. I'm excited to see this get started and can't wait to learn from many others in the community!</p><h3 id="4-seek-leaders-proactively"><strong>4. Seek leaders proactively</strong></h3><p>As new members join the community and it grows, many of them will come with a desire to contribute in some way. Some of that has happened in meaningful ways around tooling that have dramatically improved the day-to-day tasks of every SQL Server DBA.</p><p>One of the best examples of this in recent years has been the <a href="https://dbatools.io/">DBATools project</a>. This is a PowerShell toolkit of hundreds of commands that can help back up a database or migrate an entire cluster of servers, no UI necessary. It is heavily supported by the community and they're always looking for opportunities to grow, learn, and contribute. Finding these developer-focused initiatives could be a great way to enlist help and add additional support resources as the community grows.</p><h3 id="5-develop-consistent-messaging-around-community"><strong>5. Develop consistent messaging around community</strong></h3><p>Lastly, I think it would be helpful to consider ways to consistently articulate the best methods and practices for accessing help within the PostgreSQL community. Although the <a href="https://www.postgresql.org/community/">Community page</a> on the PostgreSQL.org website does list many avenues for getting help, it still requires a fair amount of cognitive load to figure out which avenue is best suited for a given need.</p><ul><li>When do I use the email lists? What if I don't want to subscribe long-term?</li><li>If I join the Slack channel, how do I best ask for help? Can I mention specific people to try and get help? Create new rooms?</li><li>Why would I use IRC or Discord over Slack?</li><li>Is there a #pghelp Twitter hashtag, and if so, what kind of questions are best asked there?</li></ul><p>As a new user in the PostgreSQL community, I wanted this kind of guidance because I didn't want to overuse a resource or direct questions to the wrong group of people. If we had more consistent guidance on how to interface with the community across the plethora of channels, then other leaders within the PostgreSQL community could give the same consistent message. </p><p>I appreciate how leaders like Brent Ozar, a leader in the SQL Server community, don't feel obligated to answer every question thrown their way. They have clear instructions on their blog that describe the best ways for someone to actually get effective help and they often direct them there. "Hey, thanks for reaching out. That sounds like a great question for #sqlhelp. Check out this link for instructions on how to use it!"</p><p>When people feel heard, they're more likely to stay connected and involved. Even if the answer often points them to good documentation of how to get help, they're still acknowledged and included.</p><h2 id="wrap-up">Wrap up</h2><p>I'd like to leave you with a small twist on a common adage.</p><p>"You can't pick your family… but you <em>can</em> influence who becomes your friends."</p><p>As this community grows, are we prepared to provide some new ways of engaging with them? These specific ideas might not all fit within the PostgreSQL community, but I'd be interested to hear your thoughts about ways we can better incorporate the skills and talents of the community we already have to prepare for the future. Feel free to reach out to me on Twitter (<a href="https://twitter.com/ryanbooz?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor">@ryanbooz</a>), through email (<a href="mailto:ryan@timescale.com">ryan@timescale.com</a>), or on our <a href="https://slack.timescale.com">Timescale Slack channel</a>. </p><p>One last thing! The 2022 State of PostgreSQL survey will open later this spring. Take some time to <a href="https://www.timescale.com/state-of-postgres-results/">review the results from last year</a>, and then sign up at the bottom of the report to be notified when the new survey is ready. The more feedback we receive, the better we can understand our community, what's working well, and what can be improved in the years to come.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using Pg_Stat_Statements to Optimize Queries]]></title>
            <description><![CDATA[Discover how the pg_stat_statements PostgreSQL extension can help you identify problematic queries and optimize your query performance.

]]></description>
            <link>https://www.tigerdata.com/blog/using-pg-stat-statements-to-optimize-queries</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/using-pg-stat-statements-to-optimize-queries</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[PostgreSQL Performance]]></category>
            <category><![CDATA[PostgreSQL Tips]]></category>
            <category><![CDATA[Cloud]]></category>
            <category><![CDATA[Announcements & Releases]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Wed, 30 Mar 2022 13:15:09 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2022/03/pg-stat-statements-timescale-2.png">
            </media:content>
            <content:encoded><![CDATA[<p><em><code>pg_stat_statements</code> allows you to quickly identify problematic or slow Postgres queries, providing instant visibility into your database performance. Today, we're announcing that we've enabled <code>pg_stat_statements</code> by default in all Timescale services. This is part of our #AlwaysBeLaunching Cloud Week with MOAR features! </em>🐯☁️</p><p>PostgreSQL is one of the fastest-growing databases in terms of usage and community size, being backed by many dedicated developers and supported by a broad ecosystem of tooling, connectors, libraries, and visualization applications. PostgreSQL is also extensible: using <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extensions</a>, users can add extra functionality to PostgreSQL’s core.  Indeed, TimescaleDB itself is <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">packaged as a PostgreSQL extension</a>, which also plays nicely with the broad set of other PostgreSQL extensions, as we’ll see today.</p><p>Today, we’re excited to share that <code>pg_stat_statements</code>, one of the most popular and widely used PostgreSQL extensions, is now enabled by default in all Timescale services. If you’re new to Timescale, <a href="https://console.cloud.timescale.com/signup">start a free trial</a> (100&nbsp;% free for 30 days, no credit card required).</p><h3 id="what-is-pgstatstatements">What is pg_stat_statements?</h3><p><a href="https://www.postgresql.org/docs/9.4/pgstatstatements.html"><code>pg_stat_statements</code></a> is a PostgreSQL extension that records information about your running queries. Identifying performance bottlenecks in your database can often feel like a cat-and-mouse game. Quickly written queries, index changes, or complicated ORM query generators can (and often do) negatively impact your database and application performance. </p><h3 id="how-to-use-pgstatstatements">How to use pg_stat_statements</h3><p>As we will show you in this post, <code>pg_stat_statements</code> is an invaluable tool to help you identify which queries are performing slowly and poorly and why. For example, you can query  <code>pg_stat_statements</code> to know how many times a query has been called, the query execution time, the hit cache ratio for a query (how much data was available in memory vs. on disk to satisfy your query),  and other helpful statistics such as the standard deviation of a query execution time.</p><p>Keep reading to learn how to query <code>pg_stat_statements</code> to identify PostgreSQL slow queries and other performance bottlenecks in your Timescale database.</p><p><em>A huge thank you to Lukas Bernert, Monae Payne, and Charis Lam for taking care of all things pg_stat_statements in Timescale. </em></p><h2 id="how-to-query-pgstatstatements-in-timescale">How to Query <code>pg_stat_statements</code> in Timescale </h2><p>Querying statistics data for your Timescale database from the <code>pg_stat_statements</code> view is straightforward once you're connected to the database.</p><figure class="kg-card kg-code-card"><pre><code class="language-SQL">SELECT * FROM pg_stat_statements;

userid|dbid |queryid             |query                         
------+-----+--------------------+------------------------------
 16422|16434| 8157083652167883764|SELECT pg_size_pretty(total_by
    10|13445|                    |&lt;insufficient privilege&gt;      
 16422|16434|-5803236267637064108|SELECT game, author_handle, gu
 16422|16434|-8694415320949103613|SELECT c.oid,c.*,d.description
    10|16434|                    |&lt;insufficient privilege&gt;      
    10|13445|                    |&lt;insufficient privilege&gt;   
 ...  |...  |...                 |...  </code></pre><figcaption><p><i><em class="italic" style="white-space: pre-wrap;">Queries that the </em></i><i><code spellcheck="false" style="white-space: pre-wrap;"><em class="italic">tsdbadmin</em></code></i><i><em class="italic" style="white-space: pre-wrap;"> user does not have access to will hide query text and identifier</em></i></p></figcaption></figure><p>The view returns many columns of data (more than 30!), but if you look at the results above, one value immediately sticks out: <code>&lt;insufficient privilege&gt;</code>. </p><p><code>pg_stat_statements</code> collects data on all databases and users, which presents a security challenge if any user is allowed to query performance data. Therefore, although any user can query data from the views, only superusers and those specifically granted the <code>pg_read_all_stats</code> permission can see all user-level details, including the <code>queryid</code> and <code>query</code> text. </p><p>This includes the <code>tsdbadmin</code> user, which is created by default for all Timescale services. Although this user owns the database and has the most privileges, it is not a superuser account and cannot see the details of all other queries within the service cluster.</p><p>Therefore, it's best to filter <code>pg_stat_statements</code> data by <code>userid</code> for any queries you want to perform.</p><figure class="kg-card kg-code-card"><pre><code class="language-SQL">-- current_user will provide the rolname of the authenticated user
SELECT * FROM pg_stat_statements pss
	JOIN pg_roles pr ON (userid=oid)
WHERE rolname = current_user;


userid|dbid |queryid             |query                         
------+-----+--------------------+------------------------------
 16422|16434| 8157083652167883764|SELECT pg_size_pretty(total_by
 16422|16434|-5803236267637064108|SELECT game, author_handle, gu
 16422|16434|-8694415320949103613|SELECT c.oid,c.*,d.description
 ...  |...  |...                 |...  		 </code></pre><figcaption><p><i><em class="italic" style="white-space: pre-wrap;">Queries for only the </em></i><i><code spellcheck="false" style="white-space: pre-wrap;"><em class="italic">tsdbadmin</em></code></i><i><em class="italic" style="white-space: pre-wrap;"> user, showing all details and statistics</em></i></p></figcaption></figure><p>When you add the filter, only data that you have access to is displayed. If you have created additional accounts in your service for specific applications, you could also filter to those accounts.</p><p>To make the rest of our example queries easier to work with, we recommend that you use this base query with a common table expression (CTE). This query form will return the same data but make the rest of the query a little easier to write.</p><figure class="kg-card kg-code-card"><pre><code class="language-SQL">-- current_user will provide the rolname of the authenticated user
WITH statements AS (
SELECT * FROM pg_stat_statements pss
		JOIN pg_roles pr ON (userid=oid)
WHERE rolname = current_user
)
SELECT * FROM statements;

userid|dbid |queryid             |query                         
------+-----+--------------------+------------------------------
 16422|16434| 8157083652167883764|SELECT pg_size_pretty(total_by
 16422|16434|-5803236267637064108|SELECT game, author_handle, gu
 16422|16434|-8694415320949103613|SELECT c.oid,c.*,d.description
 ...  |...  |...                 |...   		 </code></pre><figcaption><p><i><em class="italic" style="white-space: pre-wrap;">Query that shows the same results as before, but this time with the base query in a CTE for more concise queries later</em></i></p></figcaption></figure><p>Now that we know how to query only the data we have access to, let's review a few of the columns that will be the most useful for spotting potential problems with your queries. </p><ul><li><strong><code>calls</code></strong>: the number of times this query has been called.</li><li><strong><code>total_exec_time</code></strong>: the total time spent executing the query, in milliseconds.</li><li><strong><code>rows</code></strong>: the total number of rows retrieved by this query.</li><li><strong><code>shared_blks_hit</code></strong>: the number of blocks already cached when read for the query.</li><li><strong><code>shared_blks_read</code></strong>: the number of blocks that had to be read from the disk to satisfy all calls for this query form.</li></ul><p>Two quick reminders about the data columns above:</p><ol><li>All values are cumulative since the last time the service was started, or a superuser manually resets the values.</li><li>All values are for the same query form after parameterizing the query and based on the resulting hashed <code>queryid</code>.</li></ol><p>Using these columns of data, let's look at a few common queries that can help you narrow in on the problematic queries.</p><h2 id="long-running-postgresql-queries">Long-Running PostgreSQL Queries</h2><p>One of the quickest ways to find slow Postgres queries that merit your attention is to look at each query’s average total time. This is not a time-weighted average since the data is cumulative, but it still helps frame a relevant context for where to start.</p><p>Adjust the <code>calls</code> value to fit your specific application needs. Querying for higher (or lower) total number of calls can help you identify queries that aren't run often but are very expensive or queries that are run much more often than you expect and take longer to run than they should.</p><figure class="kg-card kg-code-card"><pre><code class="language-SQL">-- query the 10 longest running queries with more than 500 calls
WITH statements AS (
SELECT * FROM pg_stat_statements pss
		JOIN pg_roles pr ON (userid=oid)
WHERE rolname = current_user
)
SELECT calls, 
	mean_exec_time, 
	query
FROM statements
WHERE calls &gt; 500
AND shared_blks_hit &gt; 0
ORDER BY mean_exec_time DESC
LIMIT 10;


calls|mean_exec_time |total_exec_time | query
-----+---------------+----------------+-----------
 2094|        346.93 |      726479.51 | SELECT time FROM nft_sales ORDER BY time ASC LIMIT $1 |
 3993|         5.728 |       22873.52 | CREATE TEMPORARY TABLE temp_table ... |
 3141|          4.79 |       15051.06 | SELECT name, setting FROM pg_settings WHERE ... |
60725|          3.64 |      221240.88 | CREATE TEMPORARY TABLE temp_table ... |   
  801|          1.33 |        1070.61 | SELECT pp.oid, pp.* FROM pg_catalog.pg_proc p  ...|
 ... |...            |...                 |  		 </code></pre><figcaption><p><i><em class="italic" style="white-space: pre-wrap;">Queries that take the most time, on average, to execute</em></i></p></figcaption></figure><p>This sample database we're using for these queries is based on the <a href="https://github.com/timescale/nft-starter-kit">NFT starter kit</a>, which allows you to ingest data on a schedule from the OpenSea API and query NFT sales data. As part of the normal process, you can see that a <code>TEMPORARY TABLE</code> is created to ingest new data and update existing records as part of a lightweight extract-transform-load process.</p><p>That query has been called 60,725 times since this service started and has taken around 4.5 minutes of total execution time to create the table. By contrast, the first query shown takes the longest, on average, to execute—around 350 milliseconds each time. It retrieves the oldest timestamp in the <code>nft_sales</code> table and has used more than 12 minutes of execution time since the server was started.</p><p>From a work perspective, finding a way to improve the performance of the first query will have a more significant impact on the overall server workload.</p><h2 id="hit-cache-ratio">Hit Cache Ratio</h2><p>Like nearly everything in computing, databases tend to perform best when data can be queried in memory rather than going to external disk storage. If PostgreSQL has to retrieve data from storage to satisfy a query, it will typically be slower than if all of the needed data was already loaded into the reserved memory space of PostgreSQL. We can measure how often a query has to do this through a value known as Hit Cache Ratio.</p><p>Hit Cache Ratio is a measurement of how often the data needed to satisfy a query was available in memory. A higher percentage means that the data was already available and it didn't have to be read from disk, while a lower value can be an indication that there is memory pressure on the server and isn't able to keep up with the current workload.</p><p>If PostgreSQL has to constantly read data from disk to satisfy the same query, it means that other operations and data are taking precedence and "pushing" the data your query needs back out to disk each time. </p><p>This is a common scenario for time-series workloads because newer data is written to memory first, and if there isn't enough free buffer space, data that is used less will be evicted. If your application queries a lot of historical data, older <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertable</a> chunks might not be loaded into memory and ready to quickly serve the query.</p><p>A good place to start is with queries that run often and have a Hit Cache Ratio of less than 98&nbsp;%. Do these queries tend to pull data from long periods of time? If so, that could be an indication that there's not enough RAM to efficiently store this data long enough before it is evicted for newer data. </p><p>Depending on the application query pattern, you could improve Hit Cache Ratio by increasing server resources, consider index tuning to reduce table storage, or use <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/compression/">TimescaleDB compression</a> on older chunks that are queried regularly.</p><figure class="kg-card kg-code-card"><pre><code class="language-SQL">-- query the 10 longest running queries
WITH statements AS (
SELECT * FROM pg_stat_statements pss
		JOIN pg_roles pr ON (userid=oid)
WHERE rolname = current_user
)
SELECT calls, 
	shared_blks_hit,
	shared_blks_read,
	shared_blks_hit/(shared_blks_hit+shared_blks_read)::NUMERIC*100 hit_cache_ratio,
	query
FROM statements
WHERE calls &gt; 500
AND shared_blks_hit &gt; 0
ORDER BY calls DESC, hit_cache_ratio ASC
LIMIT 10;


calls | shared_blks_hit | shared_blks_read | hit_cache_ratio |query
------+-----------------+------------------+-----------------+--------------
  118|            441126|                 0|           100.00| SELECT bucket, slug, volume AS "volume (count)", volume_eth...
  261|          62006272|             22678|            99.96| SELECT slug FROM streamlit_collections_daily cagg...¶        I
 2094|         107188031|           7148105|            93.75| SELECT time FROM nft_sales ORDER BY time ASC LIMIT $1...      
  152|          41733229|                 1|            99.99| SELECT slug FROM streamlit_collections_daily cagg...¶        I
  154|          36846841|             32338|            99.91| SELECT a.img_url, a.name, MAX(s.total_price) AS price, time...

 ... |...               |...               | ...             | ...</code></pre><figcaption><p><i><em class="italic" style="white-space: pre-wrap;">The query that shows the Hit Cache Ratio of each query, including the number of buffers that were ready from disk or memory to satisfy the query</em></i></p></figcaption></figure><p>This sample database isn't very active, so the overall query counts are not very high compared to what a traditional application would probably show. In our example data above, a query called more than 500 times is a "frequently used query." </p><p>We can see above that one of the most expensive queries also happens to have the lowest Hit Cache Ratio of 93.75&nbsp;%. This means that roughly 6&nbsp;% of the time, PostgreSQL has to retrieve data from disk to satisfy the query. While that might not seem like a lot, your most frequently called queries should have a ratio of 99&nbsp;% or more in most cases.</p><p>If you look closely, notice that this is the same query that stood out in our first example that showed how to find long-running queries. It's quickly becoming apparent that we can probably tune this query in some way to perform better. As it stands now, it's the slowest query per call, and it consistently has to read some data from disk rather than from memory.</p><h2 id="queries-with-high-standard-deviation">Queries With High Standard Deviation</h2><p>For a final example, let's consider another way to judge which queries often have the greatest opportunity for improvement: using the standard deviation of a query execution time.</p><p>Finding the slowest queries is a good place to start. However, as discussed in the blog post <a href="https://timescale.ghost.io/blog/what-time-weighted-averages-are-and-why-you-should-care/">What Time-Weighted Averages Are and Why You Should Care</a>, averages are only part of the story. Although <code>pg_stat_statements</code> doesn't provide a method for tracking time-weighted averages, it does track the standard deviation of all calls and execution time.</p><h3 id="how-can-this-be-helpful">How can this be helpful?</h3><p>Standard deviation is a method of assessing how widely the time each query execution takes compared to the overall mean. If the standard deviation value is small, then queries all take a similar amount of time to execute. If the standard deviation value is large, this indicates that the execution time of the query varies significantly from request to request.</p><p>Determining how good or bad the standard deviation is for a particular query requires more data than just the mean and standard deviation values. To make the most sense of these numbers, we at least need to add the minimum and maximum execution times to the query. By doing this, we can start to form a mental model for the overall span execution times that the query takes.</p><p>In the example result below, we're only showing the data for one query to make it easier to read, the same <code>ORDER BY time LIMIT 1</code> query we've seen in our previous example output.</p><figure class="kg-card kg-code-card"><pre><code class="language-SQL">-- query the 10 longest running queries
WITH statements AS (
SELECT * FROM pg_stat_statements pss
		JOIN pg_roles pr ON (userid=oid)
WHERE rolname = current_user
)
SELECT calls, 
	min_exec_time,
	max_exec_time, 
	mean_exec_time,
	stddev_exec_time,
	(stddev_exec_time/mean_exec_time) AS coeff_of_variance,
	query
FROM statements
WHERE calls &gt; 500
AND shared_blks_hit &gt; 0
ORDER BY mean_exec_time DESC
LIMIT 10;


Name              |Value                                                |
------------------+-----------------------------------------------------+
calls             |2094                                                 |
min_exec_time     |0.060303                                             |
max_exec_time     |1468.401726                                          |
mean_exec_time    |346.9338636657108                                    |
stddev_exec_time  |212.3896857655582                                    |
coeff_of_variance |0.612190702635494                                    |
query             |SELECT time FROM nft_sales ORDER BY time ASC LIMIT $1|	 </code></pre><figcaption><p><i><em class="italic" style="white-space: pre-wrap;">Queries showing the min, max, mean, and standard deviation of each query</em></i></p></figcaption></figure><p>In this case, we can extrapolate a few things from these statistics:</p><ul><li>For our application, this query is called frequently (remember, more than 500 calls is a lot for this sample database).</li><li>If we look at the full range of execution time in conjunction with the mean, we see that the mean is not centered. This could imply that there are execution time outliers or that the data is skewed. Both are good reasons to investigate this query’s execution times further.</li><li>Additionally, if we look at the coefficient of variation column, which is the ratio between the standard deviation and the mean (also called the coefficient of variation), we get 0.612 which is fairly high. In general, if this ratio is above 0.3, then the variation of your data is quite large. Since we find the data is quite varied, it seems to imply that instead of a few outliers skewing the mean, there are a number of execution times taking longer than they should. This provides further confirmation that the execution time for this query should be investigated further.</li></ul><p>When I examine the output of these three queries together, this specific <code>ORDER BY time LIMIT 1</code> query seems to stick out. It's slower per call than most other queries, it often requires the database to retrieve data from disk, and the execution times seem to vary dramatically over time.<br><br>As long as I understood where this query was used and how the application could be impacted, I would certainly put this "first point" query on my list of things to improve.</p><h2 id="speed-up-your-postgresql-queries">Speed Up Your PostgreSQL Queries</h2><p>The <code>pg_stat_statements</code> extension is an invaluable monitoring tool, especially when you understand how statistical data can be used in the database and application context. </p><p>For example, an expensive query called a few times a day or month might not be worth the effort to tune right now. Instead, a moderately slow query called hundreds of times an hour (or more) will probably better use your query tuning effort.</p><p>If you want to learn how to store metrics snapshots regularly and move from static, cumulative information to time-series data for more efficient database monitoring, check out the blog post <a href="https://timescale.ghost.io/blog/point-in-time/"><strong>Point-in-Time PostgreSQL Database and Query Monitoring With pg_stat_statements</strong></a><strong>.</strong></p><p><code>pg_stat_statements</code> is automatically enabled in all Timescale services. If you’re not a user yet, <a href="https://console.cloud.timescale.com/signup">you can try out Timescale for free</a> (no credit card required) to get access to a modern cloud-native database platform with <a href="https://timescale.ghost.io/blog/postgresql-timescaledb-1000x-faster-queries-90-data-compression-and-much-more/">TimescaleDB's top performance</a>, one-click <a href="https://timescale.ghost.io/blog/high-availability-for-your-production-environments-introducing-database-replication-in-timescale-cloud/">database replication</a>, <a href="https://timescale.ghost.io/blog/introducing-one-click-database-forking-in-timescale-cloud/">forking</a>, and <a href="https://timescale.ghost.io/blog/vpc-peering-from-zero-to-hero/">VPC peering</a>.<br></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Select the Most Recent Record (of Many Items) With PostgreSQL]]></title>
            <description><![CDATA[Get five methods for retrieving the most recent data for each item in your PostgreSQL database quickly and efficiently.]]></description>
            <link>https://www.tigerdata.com/blog/select-the-most-recent-record-of-many-items-with-postgresql</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/select-the-most-recent-record-of-many-items-with-postgresql</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Tutorials]]></category>
            <category><![CDATA[Engineering]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Fri, 04 Feb 2022 14:50:31 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/10/Screenshot-2023-10-11-at-6.56.02-PM.png">
            </media:content>
            <content:encoded><![CDATA[<p><a href="https://timescale.ghost.io/blog/time-series-data/" rel="noreferrer">Time-series data is ubiquitous in almost every application today</a>. One of the most frequent queries applications make on time-series data is to find the most recent value for a given device or item.</p><p>In this blog post, we'll explore five methods for accessing the most recent value in PostgreSQL. Each option has its advantages and disadvantages, which we'll discuss as we go.</p><p><em>Note: Throughout this post, references to a "device" or "truck" are simply placeholders to whatever your application is storing time-series data for, whether it be an air quality sensor, airplane, car, website visits, or something else. As you read, focus on the concept of each option, rather than the specific data we're using as an example.</em></p><h2 id="the-problem">The Problem</h2><p>Knowing how to query the most recent timestamp and data for a device in large time-series datasets is often a challenge for many application developers. We study the data, determine the appropriate schema, and create the indexes that <em>should</em> make the queries return quickly. </p><p><a href="https://www.timescale.com/learn/postgresql-performance-tuning-how-to-size-your-database" rel="noreferrer">When the queries aren't as fast as we expect</a>, it's easy to be confused because indexes in PostgreSQL are supposed to help your queries return quickly - correct?</p><p>In most cases, the answer to that is emphatically "true". With the appropriate index, PostgreSQL is normally <em>very</em> efficient at retrieving data for your query. There are always nuances that we don't have time to get into in this post (<a href="https://www.timescale.com/learn/postgresql-performance-tuning-optimizing-database-indexes" rel="noreferrer">don't create too many indexes, make sure statistics are kept up-to-date, etc.</a>), but generally speaking, the right index will dramatically improve the query performance of a SQL database, PostgreSQL included.</p><p>Quick aside:</p><p><em>Before we dive into how to efficiently find specific records in a large time-series database using indexes, I want to make sure we're talking about the same thing. For the duration of this post, all references to indexes specifically mean a <a href="https://use-the-index-luke.com/sql/anatomy/the-tree"><em>B-tree index</em></a>. These are the most common index supported by all major OLTP databases and they are very good at locating specific rows of data across tables large and small. PostgreSQL actually supports <strong>many</strong> different index types that can help for various types of queries and data (including timestamp-centric data), but from here on out, we're only talking about B-tree indexes.</em></p><h2 id="the-impact-of-indexes">The Impact of Indexes</h2><p>In our TimescaleDB <a href="https://slack.timescale.com">Slack community channel</a> and in other developer forums such as StackOverflow (<a href="https://dba.stackexchange.com/questions/177162/how-to-make-distinct-on-faster-in-postgresql">example</a>), developers often wonder why a query for the latest value is slow in PostgreSQL even when it seems like the correct index exists to make the query "fast"?</p><p>The answer to that lies in how the PostgreSQL query planner works. It doesn't always use the index exactly how you might expect as we'll discuss below. In order to demonstrate how PostgreSQL might use an index on a large time-series table, let's set the stage with a set of fictitious data. </p><p>For these example queries, let's pretend that our application is tracking a trucking fleet, with sensors that report data a few times a minute as long as the truck has a cell connection. Sometimes the truck loses signal which causes data to be sent a few hours or days later. Although the app would certainly be more involved and have a more complex schema for tracking both time-series and business-related data, let's focus on two of the tables.</p><p><strong>Truck</strong></p><p><br>This table tracks every truck that is part of the fleet. Even for a very large company, this table will typically contain only a few tens of thousands of rows.</p><table>
<thead>
<tr>
<th>truck_id</th>
<th>make</th>
<th>model</th>
<th>weight_class</th>
<th>date_acquired</th>
<th>active_status</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Ford</td>
<td>Single Sleeper</td>
<td>S</td>
<td>2018-03-14</td>
<td>true</td>
</tr>
<tr>
<td>2</td>
<td>Tesla</td>
<td>Double Sleeper</td>
<td>XL</td>
<td>2019-02-18</td>
<td>false</td>
</tr>
<tr>
<td>…</td>
<td>…</td>
<td>…</td>
<td>…</td>
<td>…</td>
<td></td>
</tr>
</tbody>
</table>
<p>For the queries below, we'll pretend that this table has ~10,000 trucks, most of which are currently active and recording data a few times a minute.</p><p><strong>Truck Reading</strong></p><p><br>The reading hypertable stores all data that is delivered from every truck over time. Data typically comes in a few times a minute in time order, although older data can arrive when trucks lose their connection to cell service or transmitters break down. For this example, we'll show a wide-format table schema, and only a few columns of data to keep things simple. Many IoT applications store many types of data points for each set of readings.</p><table>
<thead>
<tr>
<th>ts</th>
<th>truck_id</th>
<th>milage</th>
<th>fuel</th>
<th>latitude</th>
<th>longitude</th>
</tr>
</thead>
<tbody>
<tr>
<td>2021-11-30 16:39:46</td>
<td>1</td>
<td>49.8</td>
<td>29</td>
<td>40.626</td>
<td>83.139</td>
</tr>
<tr>
<td>2021-11-30 16:39:46</td>
<td>2</td>
<td>33.0</td>
<td>371</td>
<td>40.056</td>
<td>78.978</td>
</tr>
<tr>
<td>2021-11-30 16:39:46</td>
<td>3</td>
<td>54.5</td>
<td>403</td>
<td>42.732</td>
<td>83.756</td>
</tr>
<tr>
<td>…</td>
<td>…</td>
<td>…</td>
<td>…</td>
<td>…</td>
<td>…</td>
</tr>
</tbody>
</table>
<p>When you create a TimescaleDB hypertable, an index on the timestamp column will be created automatically unless you specifically tell the <code>create_hypertable()</code> function not to. For the <code>truck_reading</code> table, the default index should look similar to:</p><p><code>CREATE INDEX ix_ts ON truck_reading (ts DESC);</code></p><p>This index (or at least a composite index that uses the time column first) is necessary for even the most basic queries where time is involved and is strongly recommended for hypertable chunk management. Queries involving time alone like <code>MIN(ts)</code> or <code>MAX(ts)</code> can easily be satisfied from this index.</p><p>However, if we wanted to know the minimum or maximum readings for a specific truck, PostgreSQL would have no path to quickly find that information. Consider the following query that searches for the most recent readings of a specific truck:</p><pre><code class="language-sql">SELECT * FROM truck_reading WHERE truck_id = 1234 ORDER BY ts DESC LIMIT 1;
</code></pre>
<p>If the <code>truck_reading</code> table only had the default timestamp index ( <code>ix_ts</code> above), PostgreSQL has no efficient method to get the most recent row of data for this specific truck. Instead, it has to start reading the index from the beginning (the most recent timestamp is first based on the index order) and check each row to see if it contains <code>1234</code> as the <code>truck_id</code>. </p><p>If this truck had reported recently, PostgreSQL would only have to read a few thousand rows <em>at most</em> and the query would still be "fast". If the truck hadn't recorded data in a few hours or days, PostgreSQL might have to read hundreds of thousands, or millions of rows of data, before it finds a row where <code>truck_id = 1234</code>.</p><p>To demonstrate this, we created a sample dataset with ~20 million rows of data (1 week for 10,000 trucks) and then deleted the most recent 12 hours for <code>truck_id = 1234</code>.</p><p>In the EXPLAIN output below, we can see that PostgreSQL had to scan the entire index and FILTER out more than 1.53 million rows that did not match the `truck_id` we were searching for. Even more alarming is the amount of data PostgreSQL had to process to correctly retrieve the one row of data we were asking for - <strong><em>~184MB of data!</em></strong> (23168 buffers x 8kb per buffer)</p><pre><code class="language-sql">QUERY PLAN                                                                  
------------------------------------------------------------------
Limit  (cost=0.44..289.59 rows=1 width=52) (actual time=189.343..189.344 rows=1 loops=1)                                                       
  Buffers: shared hit=23168                                                 
  -&gt;  Index Scan using ix_ts on truck_reading  (cost=0.44..627742.58 rows=2171 width=52) (actual time=189.341..189.341 rows=1 loops=1)|
        Filter: (truck_id = 1234)                                           
        Rows Removed by Filter: 1532532                                     
        Buffers: shared hit=23168                                           
Planning:                                                                   
  Buffers: shared hit=5                                                     
Planning Time: 0.116 ms                                                     
Execution Time: 189.364 ms 
</code></pre>
<p>If your application has to do that much work for each query, it will quickly become bottlenecked on the simplest of queries as the data grows.</p><p><strong><em>Therefore, it's essential that we have the correct index(es) for the typical query patterns of our application. </em></strong></p><p>In this example (and in many real-life applications), we should <strong><em>at least</em></strong> create one other index that includes both <code>truck_id</code> and <code>ts</code>. This will allow queries about a specific truck based on time to be searched much more efficiently. An example index would look like this:</p><pre><code class="language-sql">CREATE INDEX ix_truck_id_ts ON truck_reading (truck_id, ts DESC);
</code></pre>
<p>With this index created, PostgreSQL can find the most recent record for a specific truck very quickly, whether it reported data a few seconds or a few weeks ago. </p><p>With the same dataset as above, the same query which returns a datapoint for <code>truck_id = 1234</code> from 12 hours ago reads only<strong><em> 40kb of data</em></strong>! That's ~<strong>4600x less data</strong> that had to be read because we created the appropriate index, not to mention the sub-millisecond execution time! <em>That's bananas!</em></p><pre><code class="language-sql">QUERY PLAN                                                                                                                                               
-----------------------------------------------------
Limit  (cost=0.56..1.68 rows=1 width=52) (actual time=0.015..0.015 rows=1 loops=1)                                                                
  Buffers: shared hit=5                                                    
  -&gt;  Index Scan using ix_truck_id_ts on truck_reading  (cost=0.56..2425.55 rows=2171 width=52) (actual time=0.014..0.014 rows=1 loops=1)
        Index Cond: (truck_id = 1234)                                       
        Buffers: shared hit=5                                               
Planning:                                                                   
  Buffers: shared hit=5                                                     
Planning Time: 0.117 ms                                                     
Execution Time: 0.028 ms
                                                            
</code></pre>
<p>To be clear, both queries did use an index to search for the row. The difference is in how the indexes were used to find the data we wanted.</p><p>The first query had to FILTER the tuple because only the timestamp was part of the index. Filtering takes place <strong><em>after</em></strong> the tuple is read from disk, which means a lot more work takes place just trying to find the correct data.</p><p>In contrast, the second query used both parts of the index ( <code>truck_id</code> and <code>ts</code>) as part of the Index Condition. This means that only the rows that match the constraint are read from disk. In this case, that's a very small number and the query is much faster!</p><p>Unfortunately, even with <strong><em>both</em></strong> of these targeted indexes, there are a few common time-series SQL queries that won't perform as well as most developers expect them to. </p><p>Let's talk about why that is.</p><h3 id="open-ended-queries">Open-ended queries:</h3><p>Open-ended queries look for unique data points (first, last, most recent) without specifying a specific time-range or device constraint ( <code>truck_id</code> in our example). These types of queries leave the planner with few options, so it assumes that it will have to scan through the entire index <em>at planning time</em>. That might not be true, but PostgreSQL can't really know before it executes the query and starts looking for the data.</p><p>This is especially difficult when tables are partitioned because the actual indexes are stored independently with each table partition. Therefore, there is no global index for the entire table that identifies if a specific <code>truck_id</code> (in our case) exists in a partition. Once again, when the PostgreSQL planner doesn't have enough information during the planning phase, it assumes that each partition will need to be queried, typically causing increased planning time.</p><p>Consider a query like the following, which asks for the earliest reading for a specific <code>truck_id</code>:</p><pre><code class="language-sql">SELECT * FROM truck_reading WHERE truck_id=1234 ORDER BY ts LIMIT 1;
</code></pre>
<p>With the two indexes we have in place ( <code>(ts DESC)</code> and <code>(truck_id, ts DESC)</code>), it <em>feels</em> like this should be a fast query. But because the hypertable is partitioned on time, the planner initially assumes that it will have to scan each chunk. If you have a lot of partitions, the planning time will take longer. </p><p>If the <code>truck_reading</code> table is actively receiving new data, the execution of the query will still be "fast" because the answer will probably be found in the first chunk and returned quickly. But if <code>truck_id=1234</code> has <strong><em>never</em></strong> reported any data or has been offline for weeks, PostgreSQL will have to both <em>plan</em> and then <em>scan the index of</em> every chunk. The query will use the composite index on each partition to quickly determine there are no records for the truck, but it still has to take the time to plan and execute the query.</p><p>Instead, we want to avoid doing unnecessary work whenever possible and avoid the potential for this query anti-pattern.</p><h3 id="high-cardinality-queries">High-cardinality queries:</h3><p>Many queries can also be negatively impacted by increasing cardinality, becoming slower as data volumes grow and more individual items are tracked. Options 1-4 below are good examples of queries that perform well on small to medium-size datasets, but <em>often</em> become slower as volumes and cardinality increase.</p><p>These queries attempt to "step" through the time-series table by <code>truck_id</code>, taking advantage of the indexes on the hypertable. However, as more items need to be queried, the iteration often becomes slower because the index is too big to efficiently fit in memory, causing PostgreSQL to frequently swap data to and from disk.</p><p>Understanding that these two types of queries <em>may</em> not perform as well under every circumstance, let's examine five different methods for getting the most recent record for each item in your time-series table. In most circumstances, at least one of these options will work well for your data.</p><h2 id="development-production">Development != Production</h2><p>One quick word of warning as we jump into the SQL examples below.</p><p>It's always good to remember that your development database is unlikely to have the same volume, cardinality, and transaction throughput as your production database. Any one of the example queries we show below might perform really well on a smaller, less active database, only to perform more poorly than expected in production.</p><p>It's always best to test in an environment that is as similar to production as possible. How to do that is beyond the scope of this post, but a few options could be:</p><ul><li>Use <a href="https://timescale.ghost.io/blog/blog/introducing-one-click-database-forking-in-timescale-cloud/">one-click database forking</a> with your <a href="https://console.cloud.timescale.com/signup">Timescale</a> instance to easily make a copy of production for testing and learning. Using data as close to production as possible is usually preferred!</li><li>Back up and restore your production database to an approved location and anonymize the data, keeping similar cardinality and row statistics. Always <code>ANALYZE</code> the table after any data changes.</li><li>Consider reusing your schema and generating lots of high-volume, high-cardinality sample data with <code>generate_series()</code> (possibly using some of the ideas from <a href="https://timescale.ghost.io/blog/blog/how-to-create-lots-of-sample-time-series-data-with-postgresql-generate_series/">our series about generating more realistic sample data</a> inside of PostgreSQL).</li></ul><p>Whatever method you choose, always remember that a database with 1 million rows of time-series data for 100 items will act much differently from a database with 10 billion rows of time-series data for 10,000 items reporting every few seconds.</p><p>Now that we've discussed how indexes help us find the data and reviewed some of the query patterns that can be slower than usual, it's time to write some SQL and talk about when it might be appropriate to use each option.</p><h2 id="option-1-naive-group-by">Option 1: Naive GROUP BY</h2><p>SQL is a powerful language. Unfortunately, every database that allows queries to be written in SQL often has slightly different functions for doing similar work, or simply doesn't support SQL standards that would otherwise allow for efficient "last point" queries like we've been discussing.</p><p>However, in nearly every database where SQL is a supported query language, you can run this query to get the most recent time that a truck recorded data. In <em>most</em> cases, this will not perform well on large datasets because the <code>GROUP BY</code> clause prevents the indexes from being used.</p><pre><code class="language-sql">SELECT max(time) FROM truck_reading GROUP BY truck_id;
</code></pre>
<p>Because the indexes won't be used in PostgreSQL, this approach is not recommended for high-volume/high-cardinality datasets. But, it will get the result you expect, even if it's not efficient.</p><p>If you have a query like this, consider how one of the other options listed below might better fit your query pattern.</p><h2 id="option-2-lateral-join">Option 2: LATERAL JOIN</h2><p>One of the easiest pieces of advice to give for any PostgreSQL database developer is <a href="https://www.timescale.com/learn/sql-joins-summary">to learn how to use LATERAL JOINs</a>. In some other database engines (like SQL Server), these are called APPLY commands, but they do essentially the same thing—run the inner query for every row produced by the outer query. Because it is a JOIN, the inner query can utilize values from the outer query. (While this is similar to a correlated subquery, it's not the same thing.)</p><p>LATERAL JOINs are a great option when you, as the developer or administrator, know approximately how many rows the outer query will return. For a few hundred or a few thousand rows, this pattern is likely to return your "recent" record very quickly as long as the correct index is in place.</p><pre><code class="language-sql">SELECT * FROM trucks t 
INNER JOIN LATERAL (
	SELECT * FROM truck_reading 
	WHERE truck_id = t.truck_id
	ORDER BY ts DESC 
	LIMIT 1
) l ON TRUE
ORDER BY t.truck_id DESC;
</code></pre>
<p>The convenient thing about a LATERAL JOIN query is that additional filtering can be applied to the outer query to identify specific items to retrieve data for. In most cases, the relational business data ( <code>trucks</code>) will be a smaller table with faster query times. Paging can also be applied to the smaller table more efficiently (ie. <code>OFFSET 500 LIMIT 100</code>), which further reduces the total work that the inner query needs to perform.</p><p>Unfortunately, one downside of a LATERAL JOIN query is that it can be susceptible to the high cardinality issue we discussed above in at least two ways.</p><p>First, if the outer query returns many more items than the inner table has data for, this query will loop over the inner table doing more work than necessary. For example, if the <code>truck</code> table had 10,000 entries for trucks, but only 1,000 of them had ever reported readings, the query would loop over the inner query 10x more than it needed to. </p><p>Second, even if the cardinality of the inner and outer query generally match, if that cardinality is high or the table on the inner query is very large, a LATERAL JOIN query will slow down over time as memory or I/O become a limiting factor. At some point, you may need to consider <strong>Option 5</strong> below as a final solution.</p><h2 id="option-3-timescaledb-skipscan">Option 3: TimescaleDB SkipScan</h2><p><em>Disclaimer: this method only works when the TimescaleDB extension is installed. If you aren’t using it yet, you can find more information on our <a href="https://docs.timescale.com/install/latest/"><em>documentation page</em></a>.</em></p><p>LATERAL JOINs are a great tool to have on hand when working with iterative queries. However, as we just discussed, they're not always the best choice when iterating the items of the outer query would cause the inner query to be executed often, looking for data that doesn't exist.</p><p>This is when it can be advantageous to use the reading table itself to get the distinct items and related data. In particular, this is helpful when we want to query trucks that have reported data within a period of time, for example, the last 24 hours. While we could add a filter to the inner query above ( <code>WHERE ts &gt; now() - INTERVAL '24 hours'</code>), we'd still have to iterate over every <code>truck_id</code>, some of which might not have reported data in the last 24 hours.</p><p>Because we already created the <code>ix_truck_id_ts</code> index above that is ordered by <code>truck_id</code> and <code>ts DESC</code>, a common approach that many PostgreSQL developers try is to use a <code>DISTINCT ON</code> query with PostgreSQL.</p><pre><code class="language-sql">SELECT DISTINCT ON (truck_id) * FROM truck_reading WHERE ts &gt; now() - INTERVAL '24 hours' ORDER BY truck_id, ts DESC;
</code></pre>
<p>If you try this <strong><em>without</em></strong> <a href="https://docs.timescale.com/install/latest/">TimescaleDB</a> installed, <em>it won't perform well</em> - <strong>even though we have an index that <em>appears</em> to have the data ordered correctly and easy to "jump" through! </strong>This is because, as of PostgreSQL 14, there is no feature within the query execution phase that can "walk" the index to find each unique instance of a particular key. Instead, PostgreSQL essentially reads all of the data, groups it by the <code>ON</code> columns, and then filters out all but the first row (based on order).</p><p>However, <em>with the TimescaleDB extension installed</em> (version 2.3 or greater), <strong><em>the <code>DISTINCT ON</code> query will work much more efficiently</em><em> </em></strong>as long as the correct index exists and is ordered the same as the query. This is because the TimescaleDB extension <a href="https://timescale.ghost.io/blog/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql/">adds a new query node called "SkipScan</a>" which will start scanning the index with the next key value as soon as another one is found, in order. One of the best parts of (SkipScan) is that it works on <strong>any</strong> PostgreSQL table with a B-tree index. It doesn't <em>have</em> to be a TimescaleDB hypertable! </p><p>There are a few nuances to how the index is used, all of which are outlined in the blog post linked above.</p><h2 id="option-4-loose-index-scan">Option 4: Loose Index Scan</h2><p>If you don't (or can't) install the TimescaleDB extension, there is still a way to query the <code>truck_reading</code> table to efficiently return the timestamp of the most recent reading for each <code>truck_id</code>. </p><p>On the PostgreSQL Wiki there is a page dedicated to the <a href="https://wiki.postgresql.org/wiki/Loose_indexscan">Loose Index Scan</a>. It demonstrates a way to use recursive CTE queries to essentially do what the TimescaleDB (SkipScan) node does. It's not nearly as straightforward to write and is more difficult to return multiple rows (it's not the same as a DISTINCT query), but it does provide a way to more efficiently use the index to retrieve one row for each item.</p><p>The biggest drawback with this approach is that it's much more difficult to return multiple columns of data with the recursive CTE (and in most cases, it's simply impossible to return multiple rows). So while some developers refer to this as a Skip Scan query, it doesn't easily allow you to retrieve all of the row data for a high-volume table like the (SkipScan) query node that TimescaleDB provides.</p><pre><code class="language-sql">/*
 * Loose index scan via https://wiki.postgresql.org/wiki/Loose_indexscan
 */
WITH RECURSIVE t AS (
   SELECT min(ts) AS time FROM truck_reading
   UNION ALL
   SELECT (SELECT min(ts) FROM truck_reading WHERE ts &gt; t.ts)
   FROM t WHERE t.ts IS NOT NULL
   )
SELECT ts FROM t WHERE ts IS NOT NULL
UNION ALL
SELECT null WHERE EXISTS(SELECT 1 FROM truck_reading WHERE ts IS NULL);
</code></pre>
<h2 id="option-5-logging-table-and-trigger">Option 5: Logging Table and Trigger</h2><p>Sometimes, particularly with large, high-cardinality datasets, the above options aren't efficient enough for day-to-day operations. Querying for the last reading of all items, or the devices that haven't reported a value in the last 24 hours, will not meet your expectations as data volume and cardinality grows. </p><p>In this case, a better option might be to maintain a table that stores the last readings for each device as it's inserted into the raw time-series table so that your application can query a much smaller dataset for the most recent values. To track and update the logging table, we'll create a database trigger on the raw data (hyper)table.</p><p><em>"Wait a minute! Did you just say we're going to create a database trigger? Doesn't everyone say you should never use them?" </em></p><p>It's true. Triggers often get a bad rap in the SQL world, and honestly, that can often be justified. Used properly and with the correct implementation, database triggers can be tremendously useful and have minimal impact on SELECT performance. Insert and Update performance will be impacted because each transaction has to do more work. The performance hit may or may not impact your application, so testing is essential.</p><p>The SQL below provides a minimal example of how you could implement this kind of logging. There are a <strong><em>lot</em><em> </em></strong>of considerations on how to best implement this option for your specific application. Thoroughly test any new processes you ad to the data processing in your database.</p><p>In short, the example script below:</p><ul><li>creates a table to store the most recent data. If you only want to store the most recent timestamp of each truck's readings, this could easily just insert values into a new field on the <code>truck</code> table</li><li>ALTER's the FILLFACTOR of the table to 90% because it will be UPDATE heavy</li><li>creates a trigger function that will insert a row if it doesn't exist for a truck or update the values if a row for that truck already has an entry in the table ( <code>ON CONFLICT</code>)</li><li>enables the trigger on the data hypertable</li></ul><p>The key to this approach is to only track what is necessary, reducing the amount of work PostgreSQL has to do as part of the overall transaction that is ingesting raw data.  If your application updates values for 100,000 devices every second (and you're tracking 50 columns of data), a different trigger approach might be necessary. If this is the kind of data volume you see regularly, we assume that you have an experienced PostgreSQL DBA on your team to help manage and maintain your application database—and help you decide if the logging table approach will work with the available server resources.</p><pre><code class="language-sql">/*
 * The logging table alternative. The PRIMARY KEY will create an
*  index on the truck_id column to make querying for specific trucks more efficient
 */
CREATE TABLE truck_log ( 
	truck_id int PRIMARY KEY REFERENCES trucks (truck_id),
	last_time timestamp,
	milage int,
	fuel int,
	latitude float8,
	longitude float8
);

/*
* Because the table will mostly be UPDATE heavy, a slightly reduced
* FILLFACTOR can alleviate maintenance contention and reduce
* page bloat on the table.
*/
ALTER TABLE truck_log SET (fillfactor=90);

/*
 * This is the trigger function which will be executed for each row
*  of an INSERT or UPDATE. Again, YMMV, so test and adjust appropriately
 */
CREATE OR REPLACE FUNCTION create_truck_trigger_fn()
  RETURNS TRIGGER LANGUAGE PLPGSQL AS
$BODY$
BEGIN
  INSERT INTO truck_log VALUES (NEW.truck_id, NEW.time, NEW.milage, NEW.fuel, NEW.latitude, NEW.longitude) ON CONFLICT (truck_id) DO UPDATE SET    last_time=NEW.time,
   milage=NEW.milage,
   fuel=NEW.fuel,
   latitude=NEW.latitude,
   longitude=NEW.longitude;
  RETURN NEW;
END
$BODY$;

/*
*  With the trigger function created, actually assign it to the truck_reading
*  table so that it will execute for each row
*/ 
CREATE TRIGGER create_truck_trigger
  BEFORE INSERT OR UPDATE ON truck_reading
  FOR EACH ROW EXECUTE PROCEDURE create_truck_trigger_fn();
</code></pre>
<p>With these pieces in place, the new table will start receiving new rows of data and updating the last values as data is ingested. Querying this table will be much more efficient than searching through hundreds of millions of rows.</p><h2 id="review-the-options">Review The Options</h2><table>
<thead>
<tr>
<th></th>
<th>Requires matching index</th>
<th>Impacted by higher cardinality</th>
<th>Insert performance may be impacted</th>
</tr>
</thead>
<tbody>
<tr>
<td>Option 1: GROUP BY</td>
<td></td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>Option 2: LATERAL JOIN</td>
<td>X</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>Option 3: TimescaleDB SkipScan</td>
<td>X</td>
<td>X</td>
<td>X (if index needs to be added)</td>
</tr>
<tr>
<td>Option 4: Recursive CTE</td>
<td>X</td>
<td>X</td>
<td>X (if index needs to be added)</td>
</tr>
<tr>
<td>Option 5: Logging table</td>
<td></td>
<td></td>
<td>X</td>
</tr>
</tbody>
</table>
<h2 id="wrap-up">Wrap Up </h2><p>Whatever approach you take, hopefully one of these options will help you take the next step to improve the performance of your application.</p><p>If you are not using TimescaleDB yet,&nbsp;<a href="https://www.timescale.com/?ref=timescale.com" rel="noreferrer">take a look</a>. It's a PostgreSQL extension that&nbsp;<a href="https://timescale.ghost.io/blog/postgresql-timescaledb-1000x-faster-queries-90-data-compression-and-much-more/" rel="noreferrer">will make your queries faster</a>via&nbsp;<a href="https://www.timescale.com/?ref=timescale.com" rel="noreferrer">automatic partitioning</a>, query planner enhancements, improved materialized views,&nbsp;<a href="https://timescale.ghost.io/blog/building-columnar-compression-in-a-row-oriented-database/" rel="noreferrer">columnar compression</a>, and much more.&nbsp;</p><p>If you're running your PostgreSQL database in your own hardware,&nbsp;<a href="https://docs.timescale.com/self-hosted/latest/install/?ref=timescale.com" rel="noreferrer">you can simply add the TimescaleDB extension</a>. If you prefer to try Timescale in AWS,&nbsp;<a href="https://console.cloud.timescale.com/signup?ref=timescale.com" rel="noreferrer">create a free account on our platform</a>. It only takes a couple seconds, no credit card required!</p><p><a href="https://www.timescale.com/learn/postgresql-performance-tuning-how-to-size-your-database" rel="noreferrer"><em>PS: For more tips that will help you enhance performance in PostgreSQL, check out our collection of articles on PostgreSQL Performance Tuning</em></a><em>. </em></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to shape sample data with PostgreSQL generate_series() and SQL]]></title>
            <description><![CDATA[Learn how to quickly create recurring time-series data for charting and testing PostgreSQL and TimescaleDB functions.]]></description>
            <link>https://www.tigerdata.com/blog/how-to-shape-sample-data-with-postgresql-generate_series-and-sql</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/how-to-shape-sample-data-with-postgresql-generate_series-and-sql</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[General]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Thu, 20 Jan 2022 14:34:18 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2022/01/pexels-burak-kebapci-187041.jpg">
            </media:content>
            <content:encoded><![CDATA[<p><strong><em>In the lifecycle of any application, developers are often asked to create proof-of-concept features, test newly released functionality, and visualize data analysis. In many cases, the available test data is stale, not representative of normal usage, or simply doesn't exist for the feature being implemented. In situations like this, knowing how to quickly create sample time-series data with native PostgreSQL and SQL functions is a valuable skill to draw upon!</em></strong></p><p>In this three-part series on generating sample time-series data, we demonstrate how to use the built-in PostgreSQL function, <code><a href="https://www.postgresql.org/docs/current/functions-srf.html">generate_series()</a></code>, to more easily create large sets of data to help test various workloads, database features, or just to create fun samples.</p><p>In <a href="https://timescale.ghost.io/blog/blog/how-to-create-lots-of-sample-time-series-data-with-postgresql-generate_series/">part 1</a> and <a href="https://timescale.ghost.io/blog/blog/generating-more-realistic-sample-time-series-data-with-postgresql-generate_series/">part 2</a> of the series, we reviewed how <code>generate_series()</code> works, how to join multiple series using a CROSS JOIN to create large datasets quickly, and finally how to create and use custom PostgreSQL functions as part of the query to generate more realistic values for your dataset. If you haven't used <code>generate_series()</code> much before, we recommend first reading the other two posts. <em>The first one is an </em><a href="https://timescale.ghost.io/blog/blog/how-to-create-lots-of-sample-time-series-data-with-postgresql-generate_series/"><em>intro to the generate_series() function</em></a><em>, and the second one shows </em><a href="https://timescale.ghost.io/blog/blog/generating-more-realistic-sample-time-series-data-with-postgresql-generate_series/"><em>how to generate more realistic data</em></a><em>.</em></p><p>With those skills in hand, you can quickly and easily generate tens of millions of rows of realistic-looking data. Even still, there's one more problem that we hinted at in part 2 - all of our data, regardless of how it's formatted or constrained, is still based on the <code>random()</code> function. This means that over thousands or millions of samples, every device we create data for will likely have the same MAX() and MIN() value, and the distribution of random values over millions of rows for each device generally means that all devices will have similar average values.</p><p>This third post demonstrates a few methods for influencing how to create data that mimics a desired shape or trend. Do you need to simulate time-series values that cycle over time? What about demonstrating a counter value that resets every so often to test the <a href="https://docs.timescale.com/api/latest/hyperfunctions/counter_aggs/">counter_agg</a> hyperfunction? Are you trying to create new dashboards that display sales data over time, influenced for different months of the year when sales would ebb and flow?</p><p>Below we'll cover all of these examples to provide you with the final building blocks to create awesome sample data for all of your testing and exploration needs. Remember, however, that these examples are just the beginning. Keep playing. Tweak the formulas or add different relational data to influence the values that get generated so that it meets your use case.</p><h2 id="data-inception">Data inception</h2><p>Time-series data often has patterns. Weather temperatures and rainfall measurements change in a (mostly) predictable way throughout the year. Vibration measurements from an IoT device connected to an air conditioning system usually increase in the summer and decrease in the winter. Manufacturing data that measures the total units produced per hour (and the percentage of defective units) usually follow a pattern based on shift schedules and seasonal demand.</p><p>If you want to demonstrate this kind of data without having access to the production dataset, how would you go about it using <code>generate_series()</code>? SQL functions ended up being pretty handy when we discussed different methods for creating realistic-looking data in <a href="https://timescale.ghost.io/blog/blog/generating-more-realistic-sample-time-series-data-with-postgresql-generate_series/">part 2</a>. Do you think they might help here? 😉</p><h3 id="two-options-to-easily-return-the-row-number">Two options to easily return the row number</h3><p>Remember, for our purposes we're specifically talking about creating sample <em><strong>time-series data</strong></em>. Every row increases along the time axis, and if we use the multiplication formula from part 1, we can determine how many rows our sample data query will generate. Using built-in SQL functions, we can quickly start manipulating data values that change with the cycle of time. 💥</p><p>There are many reasons why it can be helpful to know the ordinal position of each row number in a query result. That's why standard SQL dialects have some variation of the <code>row_number() over()</code> window function. This simple, yet powerful, window function allows us to return the row number of a result set, and can utilize the ORDER BY and PARTITION keywords to further determine the row values.</p><!--kg-card-begin: markdown--><pre><code class="language-SQL">SELECT ts, row_number() over(order by time) AS rownum
FROM generate_series('2022-01-01','2022-01-05',INTERVAL '1 day') ts;

ts                          |rownum|
-----------------------------+------+
2022-01-01 00:00:00.000 -0500|     1|
2022-01-02 00:00:00.000 -0500|     2|
2022-01-03 00:00:00.000 -0500|     3|
2022-01-04 00:00:00.000 -0500|     4|
2022-01-05 00:00:00.000 -0500|     5|
</code></pre>
<!--kg-card-end: markdown--><p>In a normal query, this can be useful for tasks like paging data in a web API when there is a need to consistently return values based on a common partition.</p><p>There's one problem though. <code>row_number() over()</code> requires PostgreSQL (and any other SQL database) to process the query results twice to add the values correctly. Therefore, it's very useful, but also very expensive as datasets grow. </p><p>Fortunately, <strong>PostgreSQL helps us once again</strong> for our specific use case of generating sample time-series data. </p><p><br>Through this series of blog posts on generating sample time-series data, we've discussed that <code>generate_series()</code> is a <a href="https://www.postgresql.org/docs/current/functions-srf.html">Set Returning Function (SRF)</a>. Like the results from a table, set data can be JOINed and queried. Additionally, PostgreSQL provides the <code>WITH ORDINALITY</code> clause that can be applied to any SRF to generate an additional, incrementing BIGINT column. The best part? It doesn't require a second pass through the data in order to generate this value!</p><!--kg-card-begin: markdown--><pre><code class="language-sql">SELECT ts AS time, rownum
FROM generate_series('2022-01-01','2022-01-05',INTERVAL '1 day') WITH ORDINALITY AS t(ts,rownum);

time                         |rownum|
-----------------------------+------+
2022-01-01 00:00:00.000 -0500|     1|
2022-01-02 00:00:00.000 -0500|     2|
2022-01-03 00:00:00.000 -0500|     3|
2022-01-04 00:00:00.000 -0500|     4|
2022-01-05 00:00:00.000 -0500|     5|
</code></pre>
<!--kg-card-end: markdown--><p>Because it serves our purpose and is more efficient, the remainder of this post will use <code>WITH ORDINALITY</code>. However, remember that you <em>can</em> accomplish the same results using <code>row_number() over()</code> if that's more comfortable for you.</p><h3 id="harnessing-the-row-value">Harnessing the row value</h3><p>With increasing timestamps and an increasing integer on every row, we can begin to use other functions to create interesting data.</p><p>Remember from the previous blog posts that calling a function as part of your query executes the function for each row and returns the value. Just like a regular column, however, we don't have to actually emit that column in the final query results. Instead, the function value for that row can be used in calculating values in other columns.</p><p>As an example, let's modify the previous query. Instead of displaying the row number, let's multiply the value by 2. That is, the function value is treated as an input to a multiplication formula.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">SELECT ts AS time, 2 * rownum AS rownum_by_two
FROM generate_series('2022-01-01','2022-01-05',INTERVAL '1 day') WITH ORDINALITY AS t(ts,rownum);

time                         |rownum_by_two|
-----------------------------+------+
2022-01-01 00:00:00.000 -0500|     2|
2022-01-02 00:00:00.000 -0500|     4|
2022-01-03 00:00:00.000 -0500|     6|
2022-01-04 00:00:00.000 -0500|     8|
2022-01-05 00:00:00.000 -0500|   10|
</code></pre>
<!--kg-card-end: markdown--><p>Easy enough, right? What else can we do with the row number value?</p><h3 id="counters-with-reset">Counters with reset</h3><p>Many time-series datasets record values that reset over time, often referred to as counters. The odometer on a car is an example. If you drive far enough, it will "roll over" to zero again and start counting upward. The same is true for many utilities, like water and electric meters, that track consumption. Eventually, the total digits will increment to the point where the counter resets and starts from zero again.</p><p>To simulate this with time-series data, we can use the incrementing row number and after some period of time, reset the count and start over using the modulus operator (%).</p><!--kg-card-begin: markdown--><pre><code class="language-sql">– This example resets the counter every 10 rows 
WITH counter_rows AS (
	SELECT ts, 
		CASE WHEN rownum % 10 = 0 THEN 10
		     ELSE rownum % 10 END AS row_counter
	FROM generate_series(now() - INTERVAL '5 minutes', now(), INTERVAL '1 second') WITH ORDINALITY AS t(ts, rownum)
)
SELECT ts, row_counter
FROM counter_rows;


ts                         |row_counter|
-----------------------------+-----------+
2022-01-07 13:17:46.427 -0500|          1|
2022-01-07 13:17:47.427 -0500|          2|
2022-01-07 13:17:48.427 -0500|          3|
2022-01-07 13:17:49.427 -0500|          4|
2022-01-07 13:17:50.427 -0500|          5|
2022-01-07 13:17:51.427 -0500|          6|
2022-01-07 13:17:52.427 -0500|          7|
2022-01-07 13:17:53.427 -0500|          8|
2022-01-07 13:17:54.427 -0500|          9|
2022-01-07 13:17:55.427 -0500|         10|
2022-01-07 13:17:56.427 -0500|          1|
… | …
</code></pre>
<!--kg-card-end: markdown--><p>By putting the CASE statement inside of the CTE, the counter data can be selected more easily to test other functions. For instance, to see how the <code><a href="https://docs.timescale.com/api/latest/hyperfunctions/counter_aggs/rate/">rate()</a></code> and <code><a href="https://docs.timescale.com/api/latest/hyperfunctions/counter_aggs/delta/">delta()</a></code> hyperfunctions work, we can use <code><a href="https://docs.timescale.com/api/latest/hyperfunctions/time_bucket/">time_bucket()</a></code> to group our 1-second readings into 1-minute buckets.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">WITH counter_rows AS (
	SELECT ts, 
		CASE WHEN rownum % 10 = 0 THEN 10
		     ELSE rownum % 10 END AS row_counter
	FROM generate_series(now() - INTERVAL '5 minutes', now(), INTERVAL '1 second') WITH ORDINALITY AS t(ts, rownum)
)
SELECT time_bucket('1 minute', ts) bucket, 
  delta(counter_agg(ts,row_counter)),
  rate(counter_agg(ts, row_counter))
FROM counter_rows
GROUP BY bucket
ORDER BY bucket;

bucket                       |delta|rate|
-----------------------------+-----+----+
2022-01-07 13:25:00.000 -0500| 33.0| 1.0|
2022-01-07 13:26:00.000 -0500| 59.0| 1.0|
2022-01-07 13:27:00.000 -0500| 59.0| 1.0|
2022-01-07 13:28:00.000 -0500| 59.0| 1.0|
2022-01-07 13:29:00.000 -0500| 59.0| 1.0|
2022-01-07 13:30:00.000 -0500| 26.0| 1.0|
</code></pre>
<!--kg-card-end: markdown--><p><code>time_bucket()</code> outputs the starting time of the bucket, which based on our date math for <code>generate_series()</code> produces four complete buckets of 1-minute aggregated data, and two partial buckets - one for the minute we are currently in, and a second bucket for the partial 5 minutes ago. We can see that the delta correctly calculates the difference between the last and first readings of each bucket, and the rate of change (the increment between each reading) correctly displays a unit of one.</p><p>What are some other ways we can use these PostgreSQL functions to generate different shapes of data to help you explore other features of SQL and TimescaleDB quickly?</p><h3 id="increasing-trend-over-time">Increasing trend over time</h3><p>With the knowledge of how to create an ordinal value for each row of data produced by  <code>generate_series()</code>, we can explore other ways of generating useful time-series data. Because the row number value will always increase, we can easily produce a random dataset that always increases over time but has some variability to it. Consider this a very rough representation of daily website traffic over the span of two years.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">SELECT ts, (10 + 10 * random()) * rownum as value FROM generate_series
       ( '2020-01-01'::date
       , '2021-12-31'::date
       , INTERVAL '1 day') WITH ORDINALITY AS t(ts, rownum);
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/fig1.svg" class="kg-image" alt="Line chart showing fake daily website visits per day over two years of time, rising up and to the right" loading="lazy" width="900" height="500"><figcaption>Sample daily website traffic growing over time with random daily values</figcaption></figure><p>In reality this chart isn't very realistic or representative. Any website that gains and loses viewers upwards of 50% per day probably isn't going to have great long-term success. Don't worry, we can do better with this example after we learn about another method for creating shaped data using sine waves.</p><h3 id="simple-cycles-sine-wave">Simple cycles (sine wave)</h3><p>Using the built-in <code>sin()</code> and <code>cos()</code> PostgreSQL functions, we can generate data useful for graphing and testing functions that need a predictable data trend. This is particularly useful for testing TimescaleDB downsampling hyperfunctions like <a href="https://docs.timescale.com/api/latest/hyperfunctions/downsample/lttb/">lttb</a> or <a href="https://docs.timescale.com/api/latest/hyperfunctions/downsample/asap/">asap</a>. These functions can take tens of thousands (or millions) of data points and return a smaller, but still accurately representative dataset for graphing.</p><p>We'll start with a basic example that produces one row per day, for 30 days. For each row number value, we'll get the sine value that can be used to graph a wave.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">–- subtract 1 from the row number for wave to start
-- at zero radians and produce a more representative chart
SELECT  ts,
 cos(rownum-1) as value
FROM generate_series('2021-01-01','2021-01-30',INTERVAL '1 day') WITH ORDINALITY AS t(ts, rownum);

ts                         |value                 |
-----------------------------+--------------------+
2021-01-01 00:00:00.000 -0500|                 1.0|
2021-01-02 00:00:00.000 -0500|  0.5403023058681398|
2021-01-03 00:00:00.000 -0500| -0.4161468365471424|
2021-01-04 00:00:00.000 -0500| -0.9899924966004454|
2021-01-05 00:00:00.000 -0500| -0.6536436208636119|
2021-01-06 00:00:00.000 -0500| 0.28366218546322625|
… | …
</code></pre>
<!--kg-card-end: markdown--><p>Unfortunately, the graph of this SINE wave doesn't look all that appealing. For one month of daily data points, we only have ~6 distinct data points from peak to peak of each wave.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/basic_30day_cosine-1.svg" class="kg-image" alt="Graph showing a cosine wave using daily values for one month" loading="lazy" width="900" height="500"><figcaption>Cosine wave graph using daily values for one month</figcaption></figure><p>The reason our sine wave is so jagged is that sine and cosine values are measured in radians (based on 𝞹), not degrees. A complete cycle (peak-to-peak) on a sine wave happens from zero to 2*𝞹 (~6.28…). Therefore, every ~6 rows of data will produce a complete period in the wave - unless we find a way to modify that value. </p><p>To take control over the sine/cosine values, we need to think about how to modify the data based on the date range and interval (how many rows) and what we want the wave to look like.</p><p>This means we need to take a quick trip back to math class to talk about radians.</p><h2 id="math-class-flashback">Math class flashback!</h2><p>Step back with me for a minute to primary school and your favorite math subject - Algebra 2 (or Trigonometry as the case may be). How many hours did you spend working with graph paper (or graphing calculators) determining the amplitude, period, and shift of a sine or cosine graph?</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/20220114_Creating-sample-data-Image1--v2.0.svg" class="kg-image" alt="Sine wave graph showing period and amplitude, modified from https://www.mathsisfun.com/algebra/amplitude-period-frequency-phase-shift.html" loading="lazy"><figcaption>Sine wave period and amplitude</figcaption></figure><p>If you reach even further into your memory, you might remember this formula which allows you to modify the various aspects of a wave.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/20220114_Creating-sample-data-Image2--v2.0b.svg" class="kg-image" alt="Mathematical formula showing how to modify a sine wave period, amplitude, and shift. Formula: y = A sin(B(x+C))+D" loading="lazy" width="900" height="400"><figcaption>Mathematical formula for modifying the shape and values of a sine wave</figcaption></figure><p>There's a lot here, I know. Let's primarily focus on the two numbers that matter most for our current use case:</p><p>X = the "number of radians", which is the row number in our dataset</p><p>B = a value to multiply the row number by, to decrease the "radian" value for each row</p><p><em>(A, C, and D change the height and placement of the wave, but to start, we want to elongate each period and provide more "points" on the line to graph.)</em></p><p>Let's start with a small dataset example, generating cosine data for three months of daily timestamps with no modifications.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">SELECT  ts, 
cos(rownum) as value
FROM generate_series('2021-01-01','2021-03-31',INTERVAL '1 day') WITH ORDINALITY AS t(ts, rownum);


ts                         |value                  |
-----------------------------+---------------------+
2021-01-01 00:00:00.000 -0500|   0.5403023058681398|
2021-01-02 00:00:00.000 -0500|  -0.4161468365471424|
2021-01-03 00:00:00.000 -0500|  -0.9899924966004454|
2021-01-04 00:00:00.000 -0500|  -0.6536436208636119|
2021-01-05 00:00:00.000 -0500|  0.28366218546322625|
2021-01-06 00:00:00.000 -0500|    0.960170286650366|
2021-01-07 00:00:00.000 -0500|   0.7539022543433046|
2021-01-08 00:00:00.000 -0500| -0.14550003380861354|
2021-01-09 00:00:00.000 -0500|  -0.9111302618846769|
… | …
2021-03-29 00:00:00.000 -0400|   0.9993732836951247|
2021-03-30 00:00:00.000 -0400|   0.5101770449416689|
2021-03-31 00:00:00.000 -0400|  -0.4480736161291701|
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/basic_90day_cosine.svg" class="kg-image" alt="Graph showing a cosine wave using daily values for three months" loading="lazy" width="900" height="500"><figcaption>Cosine wave with daily data for three months</figcaption></figure><p>In this example, we see ~14 peaks in our wave because there are 90 points of data and without modification, the wave will have a period (peak-to-peak) every ~6.28 points. To lengthen the cycle, we need to perform some simple division.</p><!--kg-card-begin: markdown--><pre><code class="language-bash">[cycle modifying value] = 6.28/[total interval (rows) per cycle]
</code></pre>
<!--kg-card-end: markdown--><p>Using the same 3 months of generated daily values, let's see how to modify the data to lengthen the period of the wave.</p><h3 id="one-cycle-per-month-30-days">One cycle per month (30 days)</h3><p>If we want our daily data to cycle every 30 days, multiply our row number value by 6.28/30.</p><p>6.28/30 = .209 (the row number radians modifier)</p><!--kg-card-begin: markdown--><pre><code class="language-sql">SELECT  ts, cos(rownum * 6.28/30) as value
FROM generate_series('2021-01-01','2021-03-31',INTERVAL '1 day') WITH ORDINALITY AS t(ts, rownum);
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/basic_90day_cosine_3_phases.svg" class="kg-image" alt="Graph showing a cosine wave, adjusted to produce one wave period per month, three total periods" loading="lazy" width="900" height="500"><figcaption>Cosine wave using daily data for three months, adjusted to have monthly periods</figcaption></figure><h3 id="one-cycle-per-quarter-90-days">One cycle per quarter (90 days)</h3><p>6.28/90 = .07 (this is our radians modifier)</p><pre><code>SELECT  ts, cos(rownum * 6.28/90) as value
FROM generate_series('2021-01-01','2021-03-31',INTERVAL '1 day') WITH ORDINALITY AS t(ts, rownum);</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/basic_90day_cosine_1_phase1.svg" class="kg-image" alt="Graph showing a cosine wave, adjusted to produce one wave over three months of data" loading="lazy" width="900" height="500"><figcaption>Cosine wave using daily data for three months, adjusted to have one 3-month period</figcaption></figure><p>To modify the overall length of the period, you need to modify the row number value based on the total number of rows in the result and the granularity of the timestamp.</p><p>Here are some example values that you can use to modify the wave period based on the interval used with <code>generate_series()</code>.</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>generate_series() interval</th>
<th>Desired period length</th>
<th>Divide 6.28 by…</th>
</tr>
</thead>
<tbody>
<tr>
<td>daily</td>
<td>1 month</td>
<td>30</td>
</tr>
<tr>
<td>daily</td>
<td>3 months</td>
<td>90</td>
</tr>
<tr>
<td>hourly</td>
<td>1 day</td>
<td>24</td>
</tr>
<tr>
<td>hourly</td>
<td>1 week</td>
<td>168</td>
</tr>
<tr>
<td>hourly</td>
<td>1 month</td>
<td>720</td>
</tr>
<tr>
<td>minute</td>
<td>1 hour</td>
<td>60</td>
</tr>
<tr>
<td>minute</td>
<td>1 day</td>
<td>1440</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h3 id="modifying-the-wave-amplitude-and-shift">Modifying the wave amplitude and shift</h3><p>Another tweak we can make to our wave data is to change the amplitude (difference between the min and max peaks) and, as necessary, shift the wave up or down on the Y-axis. </p><p>To do this, multiply the cosine value by the value that maximum value you want the wave to have. For example, we can multiply the monthly cycle data by 10, which changes the overall minimum and maximum values of the data.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">SELECT  ts, 10 * cos(rownum * 6.28/30) as value
FROM generate_series('2021-01-01','2021-03-31',INTERVAL '1 day') WITH ORDINALITY AS t(ts, rownum);
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/basic_90day_cosine_adjusted_amplitude.svg" class="kg-image" alt="Graph showing a cosine wave with adjusted amplitude from -10 to 10 on the Y-axis" loading="lazy" width="900" height="500"><figcaption>Cosine wave using daily data for three months with increased amplitude</figcaption></figure><p>Notice that the min/max values are now from -10 to 10.</p><p>We can take it one step further by adding a value to the output which will shift the final values up or down on the Y-axis. In this example, we modified the previous query by adding 10 to the value of each row which results in values from 0 to 20.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">SELECT  ts, 10 + 10 * cos(rownum * 6.28/30) as value
FROM generate_series('2021-01-01','2021-03-31',INTERVAL '1 day') WITH ORDINALITY AS t(ts, rownum);
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/basic_90day_cosine_y_sift.svg" class="kg-image" alt="Graph showing a cosine wave with shifted min/max values on the Y-axis of zero to twenty" loading="lazy" width="900" height="500"><figcaption>Cosine wave using daily data for three months with shifted min/max values on the Y-axis</figcaption></figure><p>Why spend so much time showing you how to generate and manipulate sine or cosine wave data, especially when we rarely see repeatable data this smooth in real life? </p><p>One of the main advantages of using consistent, predictable data like this in testing is that you can easily tell if your application, charting tools, and query are working as expected. Once you begin adding in unpredictable, real-life data, it can be difficult to determine if the data, query, or application are producing unexpected results. Quickly generating known data with a specific pattern can help rule out errors with the query, at least.</p><p>The second advantage of using a known dataset is that it can be used to shape and influence the results of other queries. Earlier in this post, we demonstrated a very simplistic example of increasing website traffic by multiplying the row number and a random value. Let's look at how we can join both datasets to create a better shape for the sample website traffic data.</p><h3 id="better-website-traffic-samples">Better website traffic samples</h3><p>One of the key takeaways from this series of posts is that <code>generate_series()</code> returns a set of data that can be JOINed and manipulated like data from a regular table. Therefore, we can join together our rough "website traffic" data and our sine wave to produce a smoother, more realistic set of data to experiment with. SQL for the win!</p><p>Overall this is one of the more complex examples we've presented, utilizing multiple common table expressions (CTE) to break the various sets into separate tables that we can query and join. However, this also means that you can independently modify the time range and other values to change the data that is generated from this query for your own experimentation.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">-- This is the generate series data
-- with a &quot;short&quot; date to join with later
WITH daily_series AS ( 
	SELECT ts, date(ts) AS day, rownum FROM generate_series
       ( '2020-01-01'
       , '2021-12-31'
       , '1 day'::interval) WITH ORDINALITY AS t(ts, rownum)
),
-- This selects the time, &quot;day&quot;, and a 
-- random value that represents our daily website visits
daily_value AS ( 
	SELECT ts, day, rownum, random() AS val
    FROM daily_series
    ORDER BY day
),
-- This cosine wave dataset has the same &quot;day&quot; values which allow 
-- it to be joined to the daily_value easily. The wave value is used to modify
-- the &quot;website&quot; value by some percentage to smooth it out 
-- in the shape of the wave.
daily_wave AS ( 
	SELECT
       day,
       -- 6.28 radians divided by 180 days (rows) to get 
       -- one peak every 6 months (twice a year)
       1 + .2 * cos(rownum * 6.28/180) as p_mod
       FROM daily_series
       day
)
-- (500 + 20 * val) = 500-520 visits per day before modification
-- p_mod = an adjusted cosine value that raises or lowers our data each day
-- row_number = a big incremental value for each row to quickly increase &quot;visits&quot; each day
SELECT dv.ts, (500 + 20 * val) * p_mod * rownum as value
FROM daily_value dv
	INNER JOIN daily_wave dw ON dv.DAY=dw.DAY
    order by ts;
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/sample_wave-based_website_traffic-2years-1.svg" class="kg-image" alt="Graph showing increasing, fake website visits, adjusted with cosine wave data to provide shape with elevated visits twice a year" loading="lazy" width="900" height="500"><figcaption>Combining wave data and random increasing data to shape the data pattern</figcaption></figure><p>Without much effort, we are able to generate a time-series dataset, use two different SQL functions, and join multiple sets together to create fun, graphical data. In this example, our traffic peaks twice a year (every ~180 days) during July and late December.</p><p>But we don't have to stop there. We can carry our website traffic example one step further by applying just a little more control over how much the data increases or decreases during certain periods. </p><p>Once again, relational data to the rescue!</p><h3 id="influence-the-pattern-with-relational-data">Influence the pattern with relational data</h3><p>As a final example, let's consider one other type of data that we can include in our queries that influence the final generated values - relational data. Although we've been using data that was created using <code>generate_series()</code> to produce some fun and interesting sample datasets, we can just as easily JOIN to other data in our database to further manipulate the final result.</p><p>There are many ways you could JOIN to and use additional data depending on your use case and the type of time-series data you're trying to mimic. For example:</p><ul><li><strong>IoT data from weather sensors:</strong> store the typical weekly temperature highs/lows in a database table and use those values as input to the <code>random_between()</code> function we created in post 2</li><li><strong>Stock data analysis:</strong> store the dates for quarterly disclosures and a hypothetical factor that will influence the impact on stock price moving forward</li><li><strong>Sales or website traffic:</strong> store the monthly or weekly change observed in a typical sales cycle. Does traffic or sales increase a quarter-end? What about during the end-of-year holiday season? </li></ul><p>To demonstrate this, we'll use the fictitious website traffic data from earlier in this post. Specifically, we've decided that we want to see a spike in traffic during June and December. </p><p>First, we create a regular PostgreSQL table to store the numerical month (1-12) and a float value which will be used to modify our generated data (up or down). This will allow us to tweak the overall shape for a given month.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">CREATE TABLE overrides (
	m_val INT NOT NULL,
	p_inc FLOAT4 NOT null
);

INSERT INTO overrides(m_val, p_inc) VALUES 
	(1,.1.04), – 4% residual increase from December
	(2,1),
	(3,1),
	(4,1),
	(5,1),
	(6,1.10),-- June increase of 10%
	(7,1),
	(8,1),
	(9,1),
	(10,1),
	(11,1.08), -- 8% early shoppers sales/traffic growth
	(12,1.18); -- 18% holiday increase
</code></pre>
<!--kg-card-end: markdown--><p>Using this simple dataset, let's first join it to the "simplistic" query that had randomly growing data over time.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">WITH daily_series AS (
-- a random value that increases over time based on the row number
SELECT ts, date_part('month',ts) AS m_val, (10 + 10*random()) * rownum as value FROM generate_series
       ( '2020-01-01'::date
       , '2021-12-31'::date
       , INTERVAL '1 day') WITH ORDINALITY AS t(ts, rownum)
)
-- join to the `overrides` table to get the 'p_inc' value 
-- for the month of the current row
SELECT ts, value * p_inc AS value FROM daily_series ds
INNER JOIN overrides o ON ds.m_val=o.m_val
ORDER BY ts;
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/enhanced_sample_daily_website_traffic-2years.svg" class="kg-image" alt="Graph showing increasing, fake website visits, adjusted during some months using values from a relational table" loading="lazy" width="900" height="920"><figcaption>Sample website traffic for two years, modified during some months with relational data</figcaption></figure><p>Joining to the <code>overrides</code> table based on the month of each data point, we are able to multiply the percentage increase (<code>p_inc</code>) value and the fake website traffic value to influence the trend of our data during specific time periods.</p><p>Combining everything we've learned and taking this example one step further, we can enhance the cosine data query with the same monthly override values to tweak our fake, cyclical time-series data that represents growing website traffic with a more realistic shape.</p><!--kg-card-begin: markdown--><pre><code class="language-sql">​​-- This is the generate series data
-- with a &quot;short&quot; date to join with later
WITH daily_series AS ( 
	SELECT ts, date(ts) AS day, rownum FROM generate_series
       ( '2020-01-01'
       , '2021-12-31'
       , '1 day'::interval) WITH ORDINALITY AS t(ts, rownum)
),
-- This selects the time, &quot;day&quot;, and a 
-- random value that represents our daily website visits
-- 'm_val' will be used to join with the 'overrides' table
daily_value AS ( 
	SELECT ts, day, date_part('month',ts) as m_val, rownum, random() AS val
    FROM daily_series
    ORDER BY day
),
-- This cosine wave dataset has the same &quot;day&quot; values which allow 
-- it to be joined to the daily_value easily. The wave value is used to modify
-- the &quot;website&quot; value by some percentage to smooth it out 
-- in the shape of the wave.
daily_wave AS ( 
	SELECT
       day,
       -- 6.28 radians divided by 180 days (rows) to get 
       -- one peak every 6 months (twice a year)
       1 + .2 * cos(rownum * 6.28/180) as p_mod
       FROM daily_series
       day
)
-- (500 + 20 * val) = 500-520 visits per day before modification
-- p_mod = an adjusted cosine value that raises or lowers our data each day
-- row_number = a big incremental value for each row to quickly increase &quot;visits&quot; each day
-- p_inc = a monthly adjustment value taken from the 'overrides' table
SELECT dv.ts, (500 + 20 * val) * p_mod * rownum * p_inc as value
FROM daily_value dv
	INNER JOIN daily_wave dw ON dv.DAY=dw.DAY
    inner join overrides o on dv.m_val=o.m_val
    order by ts; 
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/sample_wave-based_website_traffic_enhanced-2years.svg" class="kg-image" alt="Graph showing increasing, fake website visits, combined with sine wave data and adjusted during some months using values from a relational table" loading="lazy" width="900" height="500"><figcaption>Sample website traffic for two years combined with sine wave data and modified during some months with relational data</figcaption></figure><h2 id="wrapping-it-up">Wrapping it up</h2><p>In this 3rd and final blog post of our series about generating sample time-series datasets, we demonstrated how to add shape and trend into your sample time-series data (e.g., increasing web traffic over time and quarterly sales cycles) using built-in SQL functions and relational data. With a little bit of math mixed in, we learned how to manipulate the pattern of generated data, which is particularly useful for visualizing time-series data and learning analytical PostgreSQL or TimescaleDB functions.</p><p>To see some of these examples in action, watch my video on creating realistic sample data:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/Ff2ltGrPGIg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p>If you have questions about using <a href="https://www.postgresql.org/docs/current/functions-srf.html">generate_series()</a> or have any questions about TimescaleDB, please <a href="https://slack.timescale.com/">join our community Slack channel</a>, where you'll find an active community and a handful of the Timescale team most days.</p><p>If you want to try creating larger sets of sample time-series data using generate_series() and see how the exciting features of TimescaleDB work, <a href="https://www.timescale.com/timescale-signup">sign up for a free 30-day trial</a> or <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/install-timescaledb/self-hosted/">install and manage it on your instances</a>. (You can also learn more by <a href="https://docs.timescale.com/timescaledb/latest/tutorials/">following one of our many tutorials</a>.)</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Generating More Realistic Sample Time-Series Data With PostgreSQL generate_series()]]></title>
            <description><![CDATA[In the second post about generate_series(), learn ways to create more realistic-looking data for testing and evaluating new features in PostgreSQL and TimescaleDB.]]></description>
            <link>https://www.tigerdata.com/blog/generating-more-realistic-sample-time-series-data-with-postgresql-generate_series</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/generating-more-realistic-sample-time-series-data-with-postgresql-generate_series</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[General]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Thu, 11 Nov 2021 14:51:33 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2021/11/pierre-chatel-innocenti-F4VHOj76D0o-unsplash.jpg">
            </media:content>
            <content:encoded><![CDATA[<p>In this three-part series on generating sample time-series data, we demonstrate how to use the built-in <a href="https://www.tigerdata.com/learn/understanding-postgresql-user-defined-functions" rel="noreferrer">PostgreSQL function</a>, <a href="https://www.postgresql.org/docs/current/functions-srf.html"><code>generate_series()</code></a>, to more easily create large sets of data to help test various workloads, database features, or just to create fun samples.</p><p><a href="https://timescale.ghost.io/blog/blog/how-to-create-lots-of-sample-time-series-data-with-postgresql-generate_series/">In part 1 of the series</a>, we reviewed how <code>generate_series()</code> works, including the ability to join multiple series into a larger table of time-series data - through a feature known as a CROSS (or Cartesian) JOIN. We ended the first post by showing you how to quickly calculate the number of rows a query will produce and modify the parameters for <code>generate_series()</code> to fine-tune the size and shape of the data.</p><p>However, there was one problem with the data we could produce at the end of the first post. The data that we were able to generate was very basic and not very realistic. Without more effort, using functions like <code>random()</code> to generate values doesn't provide much control over precisely what numbers are produced, so the data still feels more fake than we might want.</p>
<p>This second post will demonstrate a few ways to create more realistic-looking data beyond a column or two of random decimal values. Read on for more.</p><p>In <a href="https://timescale.ghost.io/blog/blog/how-to-shape-sample-data-with-postgresql-generate_series-and-sql/">part 3</a> of this blog series adds one final tool to the mix - combining the data formatting techniques below with additional equations and relational data to shape your sample time-series output into something that more closely resembles real-life applications.</p><p>By the end of this series, you'll be ready to test almost any feature that TimescaleDB offers and create quick datasets for your testing and demos!</p><h2 id="a-brief-review-of-generateseries">A brief review of generate_series()</h2><p>In the first post, we demonstrated how <a href="https://www.postgresql.org/docs/current/functions-srf.html"><code>generate_series()</code></a> (a Set Returning Function) could quickly create a data set based on a range of numeric values or dates. The generated data is essentially an in-memory table that can quickly create large sets of sample data.</p><pre><code class="language-sql">-- create a series of values, 1 through 5, incrementing by 1
SELECT * FROM generate_series(1,5);

generate_series|
---------------|
              1|
              2|
              3|
              4|
              5|


-- generate a series of timestamps, incrementing by 1 hour
SELECT * from generate_series('2021-01-01','2021-01-02', INTERVAL '1 hour');

    generate_series     
------------------------
 2021-01-01 00:00:00+00
 2021-01-01 01:00:00+00
 2021-01-01 02:00:00+00
 2021-01-01 03:00:00+00
 2021-01-01 04:00:00+00
...
</code></pre><p>We then discussed how the data quickly becomes more complex as we join the various sets together (along with some value returning functions) to create a multiple of both sets together.</p><p>This example from the first post joined a timestamp set, a numeric set, and the <code>random()</code> function to create fake CPU data for four fake devices over time.</p><pre><code class="language-sql">-- there is an implicit CROSS JOIN between the two generate_series() sets
SELECT time, device_id, random()*100 as cpu_usage 
FROM generate_series('2021-01-01 00:00:00','2021-01-01 04:00:00',INTERVAL '1 hour') as time, 
generate_series(1,4) device_id;


time               |device_id|cpu_usage          |
-------------------+---------+-------------------+
2021-01-01 00:00:00|        1|0.35415126479989567|
2021-01-01 01:00:00|        1| 14.013393572770028|
2021-01-01 02:00:00|        1|   88.5015939122006|
2021-01-01 03:00:00|        1|  97.49037810105996|
2021-01-01 04:00:00|        1|  50.22781125586846|
2021-01-01 00:00:00|        2|  46.41196423062297|
2021-01-01 01:00:00|        2|  74.39903569177027|
2021-01-01 02:00:00|        2|  85.44087332221935|
2021-01-01 03:00:00|        2|  4.329394730750735|
2021-01-01 04:00:00|        2| 54.645873866589056|
2021-01-01 00:00:00|        3|  63.01888063314749|
2021-01-01 01:00:00|        3|  21.70606884856987|
2021-01-01 02:00:00|        3|  32.47610779097485|
2021-01-01 03:00:00|        3| 47.565982341726354|
2021-01-01 04:00:00|        3|  64.34867263419619|
2021-01-01 00:00:00|        4|   78.1768041898232|
2021-01-01 01:00:00|        4|  84.51505102850199|
2021-01-01 02:00:00|        4| 24.029611792753514|
2021-01-01 03:00:00|        4|  17.08996115345549|
2021-01-01 04:00:00|        4| 29.642690955760997|
</code></pre><p>And finally, we talked about how to calculate the total number of rows your query would generate based on the time range, the interval between timestamps, and the number of "things" for which you are creating fake data.</p><table>
<thead>
<tr>
<th>Range of readings</th>
<th>Length of interval</th>
<th>Number of "devices"</th>
<th>Total rows</th>
</tr>
</thead>
<tbody>
<tr>
<td>1 year</td>
<td>1 hour</td>
<td>4</td>
<td>35,040</td>
</tr>
<tr>
<td>1 year</td>
<td>10 minutes</td>
<td>100</td>
<td>5,256,000</td>
</tr>
<tr>
<td>6 months</td>
<td>5 minutes</td>
<td>1,000</td>
<td>52,560,000</td>
</tr>
</tbody>
</table>
<p>Still, <strong><em>the main problem remains.</em></strong> Even if we can generate 50 million rows of data with a few lines of SQL, the data we generate isn't very realistic. It's all random numbers, with lots of decimals and minimal variation.</p><p>As we saw in the query above (generating fake CPU data), any columns of data that we add to the SELECT query are added to each row of the resulting set. If we add static text (like 'Hello, Timescale!'), that text is repeated for every row. Likewise, adding a function as a column value will be called one time for each row of the final set. </p><p>That's what happened with the <code>random()</code> function in the CPU data example. Every row has a different value because the function is called separately for each row of generated data. We can use this to our advantage to begin making the data look more realistic.</p><p>With a little more thought and custom <a href="https://www.tigerdata.com/blog/function-pipelines-building-functional-programming-into-postgresql-using-custom-operators" rel="noreferrer">PostgreSQL functions</a>, we can start to bring our sample data "to life."</p><h3 id="what-is-realistic-data">What is realistic data?</h3><p>This feels like a good time to make sure we're on the same page. What do I mean by "realistic" data?</p><p>Using the basic techniques we've already discussed allows you to create a lot of data quickly. In most cases, however, you often know what the data you're trying to explore looks like. It's probably not a bunch of decimal or integer values. Even if the data you're trying to mimic <em>are</em> just numeric values, they likely have valid ranges and maybe a predictable frequency.</p><p>Take our simple example of CPU and temperature data from above. With just two fields, we have a few choices to make if we want the generated data to <em>feel</em> more realistic.</p><ul><li>Is CPU a percentage? Out of 100% or are we representing multi-core CPUs that can present as 200%, 400%, or 800%?</li><li>Is temperature measured in Fahrenheit or Celsius? What are reasonable values for CPU temperature in each unit? Do we store temperature with decimals or as an integer in the schema?</li><li>What if we added a "note" field to the schema for messages that our monitoring software might add to the readings from time to time? Would every reading have a note or just when a threshold was reached? Is there a special diagnostic message at the top of each hour that we need to replicate in some way?</li></ul><p>Using <code>random()</code> and static text by themselves allows us to generate <em>lots</em> of data with many columns, but it's not going to be very interesting or as useful in testing features in the database.</p><p>That's the goal of the second and third posts in this series, helping you to produce sample data that looks more like the real thing without much extra work. Yes, it <em>will</em> still be random, but it will be random within constraints that help you feel more connected to the data as you explore various aspects of time-series data.</p><p>And, by using functions, all of the work is easily reusable from table to table.</p><h3 id="walk-before-you-run">Walk before you run</h3><p>In each of the examples below, we'll approach our solutions much as we learned in elementary math class: <em>show your work</em>! It's often difficult to create a function or procedure in PostgreSQL without playing with a plain SQL statement first. This abstracts away the need to think about function inputs and outputs at the outset so that we can focus on how the SQL works to produce the value we want. </p><p>Therefore, the examples below show you how to get a value (random numbers, text, JSON, etc.) in a SELECT statement first before converting the SQL into a function that can be reused. This kind of iterative process is a great way to learn features of PostgreSQL, particularly when it's combined with <a href="https://www.postgresql.org/docs/current/functions-srf.html"><code>generate_series()</code></a>.</p><p>So, take one foot and put it in front of the other, and let's start creating better sample data.</p><h2 id="creating-more-realistic-numbers">Creating more realistic numbers</h2><p>In time-series data, numeric values are often the most common data type. Using a function like <code>random()</code> without any other formatting creates very… well... random (and precise) numbers with lots of decimal points. While it <em>works,</em> the values aren't <em>realistic</em>. Most users and devices aren't tracking CPU usage to 12+ decimals. We need a way to manipulate and constrain the final value that's returned in the query.</p><p>For numeric values, PostgreSQL provides many built-in functions to modify the output. In many cases, using <code>round()</code> and <code>floor()</code> with basic arithmetic can quickly start shaping the data in a way that better fits your schema and use case.</p>
<p>Let's modify the example query for getting device metrics, returning values for CPU and temperature. We want to update the query to ensure that the data values are "customized" for each column, returning values within a specific range and precision. Therefore, we need to apply a standard formula to each numeric value in our SELECT query.</p><pre><code class="language-bash">Final value = random() * (max allowed value - min allowed value) + min allowed value</code></pre><p>This equation will always generate a decimal value between (and inclusive of) the min and max value. If <code>random()</code> returns a value of 1, the final output will equal the maximum value. If <code>random()</code> returns a value of 0, then the result will equal the minimum value. Any other number that <code>random()</code> returns will produce some output between the min and max values.</p>
<p>Depending on whether we want a decimal or integer value, we can further format the "final value" of our formula with <code>round()</code> and <code>floor()</code>.</p><p>This example produces a reading every minute for one hour for 10 devices. The cpu value will always fall between 3 and 100 (with four decimals of precision), and the temperature will always be an integer between 28 and 83.</p><pre><code class="language-sql">SELECT
  time,
  device_id,
  round((random()* (100-3) + 3)::NUMERIC, 4) AS cpu,
  floor(random()* (83-28) + 28)::INTEGER AS tempc
FROM 
	generate_series(now() - interval '1 hour', now(), interval '1 minute') AS time, 
	generate_series(1,10,1) AS device_id;


time                         |device_id|cpu    |tempc        |
-----------------------------+---------+-------+-------------+
2021-11-03 12:47:01.181 -0400|        1|53.7301|           61|
2021-11-03 12:48:01.181 -0400|        1|34.7655|           46|
2021-11-03 12:49:01.181 -0400|        1|78.6849|           44|
2021-11-03 12:50:01.181 -0400|        1|95.5484|           64|
2021-11-03 12:51:01.181 -0400|        1|86.3073|           82|
…|...|...|...
</code></pre><p>By using our simple formula and formatting the result correctly, the query produced the "curated" output (random as it is) we wanted.</p><h3 id="the-power-of-functions">The power of functions</h3><p>But there's also a bit of a letdown here, isn't there? Typing that formula repeatedly for each value - trying to remember the order of parameters and when I need to cast a value - will become tedious quickly. After all, you only have so many <a href="https://keysleft.com/">keystrokes left</a>. </p><p>The solution is to create and use PostgreSQL functions that can take the inputs we need, do the correct calculations, and return the formatted value that we want. There are <em>many</em> ways we could accomplish a calculation like this in a function. Use this example as a starting place for your learning and exploration.</p><p><em><strong>Note:</strong> </em>In this example, I chose to return the value from this function as a <code>numeric</code> data type because it can return values that <em>look</em> like integers (no decimals) or floats (decimals). As long as the return values are inserted into a table with the intended schema, this is a "trick" to visually see what we expect - an integer or a float. In general, the <code>numeric</code> data type will often perform worse in queries and features like compression because of how <code>numeric</code>  values are represented internally. We recommend avoiding <code>numeric</code> types in schema design whenever possible, preferring the float or integer types instead.</p><pre><code class="language-sql">/*
 * Function to create a random numeric value between two numbers
 * 
 * NOTICE: We are using the type of 'numeric' in this function in order
 * to visually return values that look like integers (no decimals) and 
 * floats (with decimals). However, if inserted into a table, the assumption
 * is that the appropriate column type is used. The `numeric` type is often
 * not the correct or most efficient type for storing numbers in a table.
 */
CREATE OR REPLACE FUNCTION random_between(min_val numeric, max_val numeric, round_to int=0) 
   RETURNS numeric AS
$$
 DECLARE
 	value NUMERIC = random()* (min_val - max_val) + max_val;
BEGIN
   IF round_to = 0 THEN 
	 RETURN floor(value);
   ELSE 
   	 RETURN round(value,round_to);
   END IF;
END
$$ language 'plpgsql';
</code></pre><p>This example function uses the minimum and maximum values provided, applies the "range" formula we discussed earlier, and finally returns a <code>numeric</code> value that either has decimals (to the specified number of digits) or not. Using this function in our query, we can simplify creating formatted values for sample data, and it cleans up the SQL, making it easier to read and use.</p><pre><code class="language-sql">SELECT
  time,
  device_id,
  random_between(3,100, 4) AS cpu,
  random_between(28,83) AS temperature_c
FROM 
	generate_series(now() - interval '1 hour', now(), interval '1 minute') AS time, 
	generate_series(1,10,1) AS device_id;
</code></pre><p>This query provides the same formatted output, but now it's much easier to repeat the process.</p><h2 id="creating-more-realistic-text">Creating more realistic text</h2><p>What about text? So far, in both articles, we've only discussed how to generate numeric data. We all know, however, that time-series data often contain more than just numeric values. Let's turn to another common data type: text. </p><p>Time-series data often contains text values. When your schema contains log messages, item names, or other identifying information stored as text, we want to generate sample text that feels more realistic, even if it's random.</p><p>Let's consider the query used earlier that creates CPU and temperature data for a set of devices. If the devices were real, the data they create might contain an intermittent status message of varying length.</p><p>To figure out how to generate this random text, we will follow the same process as before, working directly in a stand-alone SQL query before moving our solution into a reusable function. After some initial attempts (and ample Googling), I came up with this example for producing random text of variable length using a defined character set. As with the <code>random_between()</code> function above, this can be modified to suit your needs. For instance, it would be fairly easy to get unique, random hexadecimal values by limiting the set of characters and lengths. </p><p><em>Let your creativity guide you.</em></p><pre><code class="language-sql">WITH symbols(characters) as (VALUES ('ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 {}')),
w1 AS (
	SELECT string_agg(substr(characters, (random() * length(characters) + 1) :: INTEGER, 1), '') r_text, 'g1' AS idx
	FROM symbols,
generate_series(1,10) as word(chr_idx) -- word length
	GROUP BY idx)
SELECT
  time,
  device_id,
  random_between(3,100, 4) AS cpu,
  random_between(28,83) AS temperature_c,
  w1.r_text AS note
FROM w1, generate_series(now() - interval '1 hour', now(), interval '1 minute') AS time, 
	generate_series(1,10,1) AS device_id
ORDER BY 1,2;

time                         |device_id|cpu     |temperature_c|note      |
-----------------------------+---------+--------+-------------+----------+
2021-11-03 16:49:24.218 -0400|        1| 88.3525|           50|I}3U}FIsX9|
2021-11-03 16:49:24.218 -0400|        2| 29.5313|           53|I}3U}FIsX9|
2021-11-03 16:49:24.218 -0400|        3| 97.6065|           70|I}3U}FIsX9|
2021-11-03 16:49:24.218 -0400|        4| 96.2170|           40|I}3U}FIsX9|
2021-11-03 16:49:24.218 -0400|        5| 53.2318|           82|I}3U}FIsX9|
2021-11-03 16:49:24.218 -0400|        6| 73.7244|           56|I}3U}FIsX9|
</code></pre><p>In this case, it was easier to generate a random value inside of a CTE that we could reference later in the query. However, this approach has one problem that's pretty easy to spot in the first few rows of returned data. </p><p>While the CTE does create random text of 10 characters (go ahead and run it a few times to verify), the value of the CTE is generated once each time and then cached, repeating the same result over and over for every row. Once we transfer the query into a function, we expect to see a different value for each row.</p><p>For this second example function to generate "words" of random lengths (or no text at all in some cases), the user will need to provide an integer for the minimum and maximum length of the generated text. After some testing, we also added a simple randomizing feature. </p><p>Notice the IF...THEN condition that we added. Any time the generated number is divided by five and has a remainder of zero or one, the function will not return a text value. There is nothing special about this approach to providing randomness to the frequency of the output, so feel free to adjust this part of the function to suit your needs.</p><pre><code class="language-sql">/*
 * Function to create random text, of varying length
 */
CREATE OR REPLACE FUNCTION random_text(min_val INT=0, max_val INT=50) 
   RETURNS text AS
$$
DECLARE 
	word_length NUMERIC  = floor(random() * (max_val-min_val) + min_val)::INTEGER;
	random_word TEXT = '';
BEGIN
	-- only if the word length we get has a remainder after being divided by 5. This gives
	-- some randomness to when words are produced or not. Adjust for your tastes.
	IF(word_length % 5) &gt; 1 THEN
	SELECT * INTO random_word FROM (
		WITH symbols(characters) AS (VALUES ('ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 '))
		SELECT string_agg(substr(characters, (random() * length(characters) + 1) :: INTEGER, 1), ''), 'g1' AS idx
		FROM symbols
		JOIN generate_series(1,word_length) AS word(chr_idx) on 1 = 1 -- word length
		group by idx) a;
	END IF;
	RETURN random_word;
END
$$ LANGUAGE 'plpgsql';
</code></pre><p>When we use this function to add random text to our sample time-series query, notice that the text is random in length (between 2 and 10 characters) and frequency.</p><pre><code class="language-sql">SELECT
  time,
  device_id,
  random_between(3,100, 4) AS cpu,
  random_between(28,83) AS temperature_c,
  random_text(2,10) AS note
FROM generate_series(now() - interval '1 hour', now(), interval '1 minute') AS time, 
	generate_series(1,10,1) AS device_id
ORDER BY 1,2;

time                         |device_id|cpu     |temperature_c|note     |
-----------------------------+---------+--------+-------------+---------+
2021-11-04 14:17:03.410 -0400|        1| 86.5780|           67|         |
2021-11-04 14:17:03.410 -0400|        2|  3.5370|           76|pCVBp AZ |
2021-11-04 14:17:03.410 -0400|        3| 59.7085|           28|kMrr     |
2021-11-04 14:17:03.410 -0400|        4| 69.6153|           46|3UdA     |
2021-11-04 14:17:03.410 -0400|        5| 33.0906|           56|d0sSUilx |
2021-11-04 14:17:03.410 -0400|        6| 44.2837|           74|         |
2021-11-04 14:17:03.410 -0400|        7| 14.2550|           81|TOgbHOU  |
</code></pre><p>Hopefully, you're starting to see a pattern. Using <code>generate_series()</code> and some custom functions can help you create time-series data of many shapes and sizes.</p><p>We've demonstrated ways to create more realistic numbers and text data because they are the primary data types used in time-series data. Are there any other data types included with time-series data that you might need to generate with your sample data? </p><p>What about JSON values?</p><h2 id="creating-sample-json">Creating sample JSON</h2><p><strong>Note:</strong> The sample queries below create JSON strings as the output with the intention that it would be inserted into a table for further testing and learning. In PostgreSQL, JSON string data can be stored in a JSON or JSONB column, each providing different features for querying and displaying the JSON data. In most circumstances, JSONB is the preferred column type because it provides more efficient storage and the ability to create indexes over the contents. The main downside is that the actual formatting of the JSON string, including the order of the keys and values, is not retained and may be difficult to reproduce exactly. To better understand the differences of when you would store JSON string data with one column type over the other, please <a href="https://www.postgresql.org/docs/current/datatype-json.html">refer to the PostgreSQL documentation</a>.</p><p>PostgreSQL has supported JSON and JSONB data types for many years. With each major release, the feature set for working with JSON and overall query performance improves. In a growing number of data models, particularly when REST or Graph APIs are involved, storing extra meta information as a JSON document can be beneficial. The data is available if needed while facilitating efficient queries on serialized data stored in regular columns.</p><p>We used a design pattern similar to this in our <a href="https://docs.timescale.com/timescaledb/latest/tutorials/analyze-nft-data/">NFT Starter Kit</a>. The OpenSea JSON API used as the data source for the starter kit includes many properties and values for each asset and collection. A lot of the values weren't helpful for the specific analysis in that tutorial. However, we knew that some of the values in the JSON properties could be useful in future analysis, tutorials, or demonstrations. Therefore, we stored additional metadata about assets and collections in a JSONB field to query it if needed. Still, it didn't complicate the schema design for otherwise common data like <code>name</code> and <code>asset_id</code>.</p><p>Storing data in a JSON field is also a common practice in areas like IIoT device data. Engineers usually have an agreed-upon schema to store and query metrics produced by the device, followed by a "free form" JSON column that allows engineers to send error or diagnostic data that changes over time as hardware is modified or updated.</p><p>There are several approaches to add JSON data to our sample query. One added challenge is that JSON data includes both a key and a value, along with the possibility of numerous levels of child object nesting. The approach you take will depend on how complex you want the PostgreSQL function to be and the end goal of the sample data. In this example, we'll create a function that takes an array of keys for the JSON and generates random numerical values for each key without nesting. Generating the JSON string in SQL from our values is straightforward, thanks to built-in PostgreSQL functions for reading and writing JSON strings. 🎉</p><p>As with the other examples in this post, we'll start by using a CTE to generate a random JSON document in a stand-alone SELECT query to verify that the result is what we want. Remember, we'll observe the same issue we had earlier when generating random text in the stand-alone query because we are using a CTE. The JSON is random every time the query runs, but the string is reused for all rows in the result set. CTE's are materialized once for each reference in a query, whereas functions are called again for every row. Because of this, we won't observe random values in each row until we move the SQL into a function to reuse later.</p><pre><code class="language-sql">WITH random_json AS (
SELECT json_object_agg(key, random_between(1,10)) as json_data
    FROM unnest(array['a', 'b']) as u(key))
  SELECT json_data, generate_series(1,5) FROM random_json;

json_data       |generate_series|
----------------+---------------+
{"a": 6, "b": 2}|              1|
{"a": 6, "b": 2}|              2|
{"a": 6, "b": 2}|              3|
{"a": 6, "b": 2}|              4|
{"a": 6, "b": 2}|              5|
</code></pre><p>We can see that the JSON data is created using our keys (['a','b']) with numbers between 1 and 10. We just have to create a function that will create random JSON data each time it is called. This function will always return a JSON document with numeric integer values for each key we provide for demonstration purposes. Feel free to enhance this function to return more complex documents with various data types if that's a requirement for you.</p><pre><code class="language-sql">CREATE OR REPLACE FUNCTION random_json(keys TEXT[]='{"a","b","c"}',min_val NUMERIC = 0, max_val NUMERIC = 10) 
   RETURNS JSON AS
$$
DECLARE 
	random_val NUMERIC  = floor(random() * (max_val-min_val) + min_val)::INTEGER;
	random_json JSON = NULL;
BEGIN
	-- again, this adds some randomness into the results. Remove or modify if this
	-- isn't useful for your situation
	if(random_val % 5) &gt; 1 then
		SELECT * INTO random_json FROM (
			SELECT json_object_agg(key, random_between(min_val,max_val)) as json_data
	    		FROM unnest(keys) as u(key)
		) json_val;
	END IF;
	RETURN random_json;
END
$$ LANGUAGE 'plpgsql';
</code></pre><p>With the <code>random_json()</code> function in place, we can test it in a few ways. First, we'll simply call the function directly without any parameters, which will return a JSON document with the default keys provided in the function definition ("a", "b", "c") and values from 0 to 10 (the default minimum and maximum value).</p><pre><code class="language-sql">SELECT random_json();

random_json             |
------------------------+
{"a": 7, "b": 3, "c": 8}|
</code></pre><p>Next, we'll join this to a small numeric set from <code>generate_series()</code>.</p><pre><code class="language-sql">SELECT device_id, random_json() FROM generate_series(1,5) device_id;

device_id|random_json              |
---------+-------------------------+
        1|{"a": 2, "b": 2, "c": 2} |
        2|                         |
        3|{"a": 10, "b": 7, "c": 1}|
        4|                         |
        5|{"a": 7, "b": 1, "c": 0} |
</code></pre><p>Notice two things with this example.</p><p>First, the data is different for each row, showing that the function gets called for each row and produces different numeric values each time. Second, because we kept the same random output mechanism from the <code>random_text()</code> example, not every row includes JSON.</p><p>Finally, let's add this into the sample query for generating device data that we've used throughout this article to see how to provide an array of keys ("building" and "rack") for the generated JSON data.</p><pre><code class="language-sql">SELECT
  time,
  device_id,
  random_between(3,100, 4) AS cpu,
  random_between(28,83) AS temperature_c,
  random_text(2,10) AS note,
  random_json(ARRAY['building','rack'],1,20) device_location
FROM generate_series(now() - interval '1 hour', now(), interval '1 minute') AS time, 
	generate_series(1,10,1) AS device_id
ORDER BY 1,2;


time                         |device_id|cpu     |temperature_c|note     |device_location             |
-----------------------------+---------+--------+-------------+---------+----------------------------+
2021-11-04 16:19:22.991 -0400|        1| 14.7614|           70|CTcX8 2s4|                            |
2021-11-04 16:19:22.991 -0400|        2| 62.2618|           81|x1V      |{"rack": 4, "building": 5}  |
2021-11-04 16:19:22.991 -0400|        3| 10.1214|           50|1PNb     |                            |
2021-11-04 16:19:22.991 -0400|        4| 96.3742|           29|aZpikXGe |{"rack": 12, "building": 4} |
2021-11-04 16:19:22.991 -0400|        5| 22.5327|           30|lM       |{"rack": 2, "building": 3}  |
2021-11-04 16:19:22.991 -0400|        6| 57.9773|           44|         |{"rack": 16, "building": 5} |
...
</code></pre><p>There are just so many possibilities for creating sample data with <code>generate_series()</code>, PostgreSQL functions, and some custom logic.</p><h2 id="putting-it-all-together">Putting it all together</h2><p>Let's put what we've learned into practice, using these three functions to create and insert ~1 million rows of data and then query it with the hyperfunctions <a href="https://docs.timescale.com/api/latest/hyperfunctions/time_bucket/"><code>time_bucket()</code></a>, <a href="https://docs.timescale.com/api/latest/hyperfunctions/time_bucket_ng/"><code>time_bucket_ng()</code></a>, <a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/approx_percentile/"><code>approx_percentile()</code></a> and <a href="https://docs.timescale.com/api/latest/hyperfunctions/time-weighted-averages/time_weight/"><code>time_weight()</code></a>. To do this, we'll create two tables: one will be a list of computer hosts and the second will be a <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertable</a> that stores fake time-series data about the computers.</p><h3 id="step-1-create-the-schema-and-hypertable">Step 1: Create the schema and <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertable</a><br></h3><pre><code class="language-sql">CREATE TABLE host (
	id int PRIMARY KEY,
	host_name TEXT,
	LOCATION jsonb
);

CREATE TABLE host_data (
	date timestamptz NOT NULL,
	host_id int NOT NULL,
	cpu double PRECISION,
	tempc int,
	status TEXT	
);

SELECT create_hypertable('host_data','date');
</code></pre><h3 id="step-2-generate-and-insert-data">Step 2: Generate and insert data</h3><pre><code class="language-sql">-- Insert data to create fake hosts
INSERT INTO host
SELECT id, 'host_' || id::TEXT AS name, 
	random_json(ARRAY['building','rack'],1,20) AS LOCATION
FROM generate_series(1,100) AS id;


-- insert ~1.3 million records for the last 3 months
INSERT INTO host_data
SELECT date, host_id,
	random_between(5,100,3) AS cpu,
	random_between(28,90) AS tempc,
	random_text(20,75) AS status
FROM generate_series(now() - INTERVAL '3 months',now(), INTERVAL '10 minutes') AS date,
generate_series(1,100) AS host_id;
</code></pre><h3 id="step-3-query-data-using-timebucket-and-timebucketng">Step 3: Query data using <code>time_bucket()</code> and <code>time_bucket_ng()</code></h3><pre><code class="language-sql">-- Using time_bucket(), query the average CPU and max tempc
SELECT time_bucket('7 days', date) AS bucket, host_name,
	avg(cpu),
	max(tempc)
FROM host_data
JOIN host ON host_data.host_id = host.id
WHERE date &gt; now() - INTERVAL '1 month'
GROUP BY 1,2
ORDER BY 1 DESC, 2;


-- try the experimental time_bucket_ng() to query data in month buckets
SELECT timescaledb_experimental.time_bucket_ng('1 month', date) AS bucket, host_name,
	avg(cpu) avg_cpu,
	max(tempc) max_temp
FROM host_data
JOIN host ON host_data.host_id = host.id
WHERE date &gt; now() - INTERVAL '3 month'
GROUP BY 1,2
ORDER BY 1 DESC, 2;
</code></pre><h3 id="step-4-query-data-using-toolkit-hyperfunctions">Step 4: Query data using toolkit hyperfunctions</h3><pre><code class="language-sql">-- query all host in building 10 for 7 day buckets
-- also try the new percentile approximation function to 
-- get the p75 of data for each 7 day period
SELECT time_bucket('7 days', date) AS bucket, host_name,
	avg(cpu),
	approx_percentile(0.75,percentile_agg(cpu)) p75,
	max(tempc)
FROM host_data
JOIN host ON host_data.host_id = host.id
WHERE date &gt; now() - INTERVAL '1 month'
	AND LOCATION -&gt; 'building' = '10'
GROUP BY 1, 2
ORDER BY 1 DESC, 2;



-- To test time-weighted averages, we need to simulate missing
-- some data points in our host_data table. To do this, we'll
-- randomly select ~10% of the rows, and then delete them from the
-- host_data table.
WITH random_delete AS (SELECT date, host_id FROM host_data
	 JOIN host ON host_id = id WHERE 
	date &gt; now() - INTERVAL '2 weeks'
	ORDER BY random() LIMIT 20000
)
DELETE FROM host_data hd
USING random_delete rd
WHERE hd.date = rd.date
AND hd.host_id = rd.host_id;


-- Select the daily time-weighted average and regular average
-- of each host for building 10 for the last two weeks.
-- Notice the variation in the two numbers because of the missing data.
SELECT time_bucket('1 day',date) AS bucket,
	host_name,
	average(time_weight('LOCF',date,cpu)) weighted_avg,
	avg(cpu) 
FROM host_data
	JOIN host ON host_data.host_id = host.id
WHERE LOCATION -&gt; 'building' = '10'
AND date &gt; now() - INTERVAL '2 weeks'
GROUP BY 1,2
ORDER BY 1 DESC, 2;
</code></pre><p>In a few lines of SQL, we created 1.3 million rows of data and were able to test four different functions in TimescaleDB, all without relying on any external source. 💪</p><p>Still, you may notice one last issue with the values in our <code>host_data</code> table (even though the values are not more realistic in nature). By using <code>random()</code> as the basis for our queries, the calculated numeric values all tend to have an equal distribution within the specified range which causes the average of the values to always be near the median. This makes sense statistically, but it highlights one other area of improvement to the data we generate. In the third post of this series, we'll demonstrate a few ways to influence the generated values to provide shape to the data (and even some outliers if we need them).</p>
<h2 id="reviewing-our-progress">Reviewing our progress</h2><p>When using a database like TimescaleDB or testing features in PostgreSQL, generating a representative dataset is a beneficial tool to have in your SQL toolbelt. </p><p><a href="https://timescale.ghost.io/blog/blog/how-to-create-lots-of-sample-time-series-data-with-postgresql-generate_series/">In the first post</a>, we learned how to generate lots of data by combining the result sets of multiple <code>generate_series()</code> functions. Using the implicit <code>CROSS JOIN</code>, the total number of rows in the final output is a product of each set together. When one of the data sets contains timestamps, the output can be used to create time-series data for testing and querying.</p>
<p>The problem with our initial examples was that the actual values we generated were random and lacked control over their precision - and all of the data was numeric. So in this second post, we demonstrated how to format the numeric data for a given column and generate random data of other types, like text and JSON documents. We also added an example in the text and JSON functions that created randomness in how often the values were emitted for each of those columns.</p><p>Again, all of these are building block examples for you to use, creating functions that generate the kind of data you need to test.</p><p>To see some of these examples in action, watch my video on creating realistic sample data:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/iKBH_p327vw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></figure><p>In <a href="https://timescale.ghost.io/blog/blog/how-to-shape-sample-data-with-postgresql-generate_series-and-sql/">part 3</a> of this series, we will demonstrate how to add shape and trends into your sample time-series data (e.g., increasing web traffic over time and quarterly sales cycles) using the formatting functions in this post in conjunction with relational lookup tables and additional mathematical functions. Knowing how to manipulate the pattern of generated data is particularly useful for visualizing time-series data and learning analytical PostgreSQL or TimescaleDB functions.</p><p>If you have questions about using <a href="https://www.postgresql.org/docs/current/functions-srf.html"><code>generate_series()</code></a> or have any questions about TimescaleDB, please <a href="https://slack.timescale.com">join our community Slack channel</a>, where you'll find an active community and a handful of the Timescale team most days.</p><p>If you want to try creating larger sets of sample time-series data using <code>generate_series()</code> and see how the exciting features of TimescaleDB work, <a href="https://www.timescale.com/timescale-signup">sign up for a free 30-day trial</a> or <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/install-timescaledb/self-hosted/">install and manage it on your instances</a>. (You can also learn more by <a href="https://docs.timescale.com/timescaledb/latest/tutorials/">following one of our many tutorials</a>.)</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What Is ClickHouse and How Does It Compare to PostgreSQL and TimescaleDB for Time Series?]]></title>
            <description><![CDATA[A detailed benchmark comparing the TimescaleDB and ClickHouse ingest speeds, disk space, and query response times.]]></description>
            <link>https://www.tigerdata.com/blog/what-is-clickhouse-how-does-it-compare-to-postgresql-and-timescaledb-and-how-does-it-perform-for-time-series-data</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/what-is-clickhouse-how-does-it-compare-to-postgresql-and-timescaledb-and-how-does-it-perform-for-time-series-data</guid>
            <category><![CDATA[Engineering]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Benchmarks & Comparisons]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Thu, 21 Oct 2021 13:43:23 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/09/Timescale-vs-clickhouse-hero.png">
            </media:content>
            <content:encoded><![CDATA[<p>Over the past year, one database we keep hearing about is ClickHouse, a column-oriented OLAP database initially built and open-sourced by Yandex. </p><p>In this detailed post, which is the culmination of three months of research and analysis, we answer the most common questions we hear, including:</p><ul><li>What is ClickHouse (including a deep dive into its architecture)</li><li>How does ClickHouse compare to PostgreSQL</li><li>How does ClickHouse compare to TimescaleDB</li><li>How does ClickHouse perform for time-series data vs. TimescaleDB</li></ul><p>At Timescale, we take our benchmarks very seriously. We find that, in our industry, there is far too much vendor-biased “benchmarketing” and not enough honest “benchmarking.” We believe developers deserve better. So we take great pains to really understand the technologies we are comparing against—and also to point out places where the other technology shines (and where TimescaleDB may fall short). </p><p>You can see this in our other detailed benchmarks vs. <a href="https://www.timescale.com/blog/timescaledb-vs-amazon-timestream-6000x-higher-inserts-175x-faster-queries-220x-cheaper/" rel="noreferrer">AWS Timestream</a> (29-minute read), <a href="https://www.timescale.com/blog/how-to-store-time-series-data-mongodb-vs-timescaledb-postgresql-a73939734016/" rel="noreferrer">MongoDB</a> (19-minute read), and <a href="https://www.timescale.com/blog/timescaledb-vs-influxdb-for-time-series-data-timescale-influx-sql-nosql-36489299877" rel="noreferrer">InfluxDB</a> (26-minute read).</p><p>We’re also database nerds at heart who really enjoy learning about and digging into other systems. (Which are a few reasons why these posts—including this one—are so long!)</p><p>So, to better understand the strengths and weaknesses of ClickHouse, we spent the last three months and hundreds of hours benchmarking, testing, reading documentation, and working with contributors.</p><p><em>Shout-out to Timescale engineers Alexander Kuzmenkov, who was most recently a core developer on ClickHouse, and Aleksander Alekseev, who is also a PostgreSQL contributor, who helped check our work and keep us honest with this post.</em></p><h2 id="how-clickhouse-fared-in-our-tests">How ClickHouse Fared in Our Tests</h2><p>ClickHouse is a very impressive piece of technology. In some tests, ClickHouse proved to be a blazing-fast database, able to ingest data faster than anything else we’ve tested so far (including TimescaleDB). In some complex queries, particularly those that do complex grouping aggregations, ClickHouse is hard to beat.</p><p>But nothing in databases comes for free. ClickHouse achieves these results because its developers have made specific architectural decisions. These architectural decisions also introduce limitations, especially when compared to PostgreSQL and TimescaleDB.</p><h3 id="clickhouse%E2%80%99s-limitationsweaknesses-include">ClickHouse’s limitations/weaknesses include:</h3><ul><li><strong>Worse query performance than TimescaleDB at nearly all queries</strong> in the <a href="https://github.com/timescale/tsbs#:~:text=The%20Time%20Series%20Benchmark%20Suite,write%20performance%20of%20various%20databases.">Time-Series Benchmark Suite</a>, except for complex aggregations.</li><li><strong>Poor inserts and much higher disk usage</strong> (e.g., 2.7x higher disk usage than TimescaleDB) at small batch sizes (e.g., 100-300 rows/batch).</li><li><strong>Non-standard SQL-like query language</strong> with several limitations (e.g., joins are discouraged, syntax is at times non-standard).</li><li><strong>Lack of other features</strong> one would expect in a robust SQL database (e.g., PostgreSQL or TimescaleDB): no transactions, no correlated sub-queries, no stored procedures, no user-defined functions, no index management beyond primary and secondary indexes, no triggers.</li><li><strong>Inability to modify or delete data at a high rate and low latency—</strong>instead have to batch deletes and updates.</li><li><strong>Batch deletes and updates happen asynchronously.</strong></li><li>Because data modification is asynchronous, <strong>ensuring consistent backups is difficult</strong>: the only way to ensure a consistent backup is to stop all writes to the database.</li><li><strong>Lack of transactions and lack of data consistency also affect other features like materialized views</strong> because the server can't atomically update multiple tables at once. If something breaks during a multi-part insert to a table with materialized views, the end result is an inconsistent state of your data.</li></ul><p>We list these shortcomings not because we think ClickHouse is a bad database. We actually think it’s a great database—well, to be more precise, a great database <em>for certain workloads</em>. And as a developer, you have to choose the right tool for <em>your workload</em>.</p><h3 id="why-does-clickhouse-fare-well-in-certain-cases-but-worse-in-others">Why does ClickHouse fare well in certain cases but worse in others?</h3><p>The answer is the <em>underlying architecture</em>.</p><p>Generally, in databases, there are two types of fundamental architectures, each with strengths and weaknesses: OnLine Transactional Processing (OLTP) and OnLine Analytical Processing (OLAP).</p><table>
<thead>
<tr>
<th style="text-align:center">OLTP</th>
<th style="text-align:center">OLAP</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">Large and small datasets</td>
<td style="text-align:center">Large datasets focused on reporting/analysis</td>
</tr>
<tr>
<td style="text-align:center">Transactional data (the raw, individual records matter)</td>
<td style="text-align:center">Pre-aggregated or transformed data to foster better reporting</td>
</tr>
<tr>
<td style="text-align:center">Many users performing varied queries and updates on data across the system</td>
<td style="text-align:center">Fewer users performing deep data analysis with few updates</td>
</tr>
<tr>
<td style="text-align:center">SQL is the primary language for interaction</td>
<td style="text-align:center">Often, but not always, utilizes a particular query language other than SQL</td>
</tr>
</tbody>
</table>
<h2 id="clickhouse-postgresql-and-timescaledb-architectures">ClickHouse, PostgreSQL, and TimescaleDB architectures</h2><p>At a high level, ClickHouse is an excellent OLAP database designed for systems of analysis. </p><p>PostgreSQL, by comparison, is a general-purpose database designed to be a versatile and reliable OLTP database for systems of record with high user engagement. </p><p>TimescaleDB is a relational database for time-series: purpose-built on PostgreSQL for time-series workloads. It combines the best of PostgreSQL plus new capabilities that increase performance, reduce cost, and provide an overall better developer experience for time series.</p><p>So, if you find yourself needing to perform fast analytical queries on mostly immutable large datasets with few users, i.e., OLAP, ClickHouse may be the better choice.</p><p>Instead, if you find yourself needing something more versatile that works well for powering applications with many users and likely frequent updates/deletes, i.e., OLTP, PostgreSQL may be the better choice.</p><p>And if your applications have time-series data—and especially if you also want the versatility of PostgreSQL—TimescaleDB is likely the best choice.</p><h2 id="time-series-benchmark-suite-results-summary-timescaledb-vs-clickhouse">Time-Series Benchmark Suite results summary (TimescaleDB vs. ClickHouse)</h2><p>We can see the impact of these architectural decisions on how TimescaleDB and ClickHouse fare with time-series workloads. </p><p>We spent hundreds of hours working with ClickHouse and TimescaleDB during this benchmark research. We tested insert loads from 100 million rows (1 billion metrics) to 1 billion rows (10 billion metrics), cardinalities from 100 to 10 million, and numerous combinations in between. We really wanted to understand how each database works across various datasets.</p><p>Overall, for inserts we find that ClickHouse outperforms on inserts with large batch sizes—but underperforms with smaller batch sizes. For queries, we find that ClickHouse underperforms on most queries in the benchmark suite, except for complex aggregates. </p><h3 id="insert-performance">Insert performance</h3><p>When rows are batched between 5,000 and 15,000 rows per insert, speeds are fast for both databases, with ClickHouse performing noticeably better:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/09/01-timescale-vs-clickhouse.png" class="kg-image" alt="Insert comparison between ClickHouse and TimescaleDB at cardinalities between 100 and 1 million hosts" loading="lazy" width="1842" height="1288" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/09/01-timescale-vs-clickhouse.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/09/01-timescale-vs-clickhouse.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/09/01-timescale-vs-clickhouse.png 1600w, https://timescale.ghost.io/blog/content/images/2023/09/01-timescale-vs-clickhouse.png 1842w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Performance comparison: ClickHouse outperforms TimescaleDB at all cardinalities when batch sizes are 5,000 rows or greater</span></figcaption></figure><p>However, when the batch size is smaller, the results are reversed in two ways: insert speed and disk consumption. With larger batches of 5,000 rows/batch, ClickHouse consumed ~16GB of disk during the test, while TimescaleDB consumed ~19GB (both before compression).</p><p>With smaller batch sizes, not only does TimescaleDB maintain steady insert speeds<strong> </strong>that are faster than ClickHouse between 100-300 rows/batch, but disk usage is 2.7x higher with ClickHouse. This difference should be expected because of the architectural design choices of each database, but it's still interesting to see.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/09/02-small-batch-insert-performance.png" class="kg-image" alt="Insert comparison of TimescaleDB and ClickHouse with small batch sizes. TimescaleDB outperforms and uses 2.7x less disk space." loading="lazy" width="1842" height="1358" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/09/02-small-batch-insert-performance.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/09/02-small-batch-insert-performance.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/09/02-small-batch-insert-performance.png 1600w, https://timescale.ghost.io/blog/content/images/2023/09/02-small-batch-insert-performance.png 1842w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Performance comparison: Timescale outperforms ClickHouse with smaller batch sizes and uses 2.7x less disk space</span></figcaption></figure><h3 id="query-performance">Query performance</h3><p>For testing query performance, we used a "standard" dataset that queries data for 4,000 hosts over a three-day period, with a total of 100 million rows. In our experience running benchmarks in the past, we found that this cardinality and row count works well as a representative dataset for benchmarking because it allows us to run many ingest and query cycles across each database in a few hours. </p><p>Based on ClickHouse’s reputation as a fast OLAP database, we expected ClickHouse to outperform TimescaleDB for nearly all queries in the benchmark.</p><p>When we ran TimescaleDB without compression, ClickHouse did outperform. </p><p>However, when we enabled TimescaleDB compression—which is the recommended approach—we found the opposite, with TimescaleDB outperforming nearly across the board:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/09/03-query-latency.png" class="kg-image" alt="Bar chart displaying results of query response between TimescaleDB and ClickHouse. TimescaleDB outperforms in almost every query category." loading="lazy" width="2000" height="1584" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/09/03-query-latency.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/09/03-query-latency.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/09/03-query-latency.png 1600w, https://timescale.ghost.io/blog/content/images/2023/09/03-query-latency.png 2354w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Results of query benchmarking between TimescaleDB and ClickHouse. TimescaleDB outperforms in almost every query category</span></figcaption></figure><p>(For those that want to replicate our findings or better understand why ClickHouse and TimescaleDB perform the way they do under different circumstances, please read the entire article for the full details.)</p><h3 id="cars-vs-bulldozers">Cars vs. bulldozers</h3><p>Today, we live in the golden age of databases: there are so many databases that all these lines (OLTP/OLAP/time-series/etc.) are blurring. Yet every database is architected differently and, as a result, has different advantages and disadvantages. As a developer, you should choose the right tool for the job. </p><p>After spending lots of time with ClickHouse, reading their docs, and working through weeks of benchmarks, we found ourselves repeating this simple analogy:</p><p><em>ClickHouse is like a bulldozer - very efficient and performant for a specific use case. PostgreSQL (and TimescaleDB) is like a car: versatile, reliable, and useful in most situations you will face in your life. </em></p><p>Most of the time, a “car” will satisfy your needs. But if you find yourself doing a lot of “construction”, by all means, get a “bulldozer.”</p><p>We aren’t the only ones who feel this way. Here is a similar opinion shared on <a href="https://news.ycombinator.com/item?id=28596718">HackerNews</a> by <em>stingraycharles </em>(whom we don’t know, but <em>stingraycharles</em> if you are reading this—we love your username):</p>
<!--kg-card-begin: html-->
<div class="quote-block">
    <p class="quote-block__text">
        "TimescaleDB has a great time-series story and an average data warehousing story; Clickhouse has a great data warehousing story, an average time-series story, and a bit meh clustering story (YMMV)."
    </p>   
</div>
<!--kg-card-end: html-->
<p>In the rest of this article, we do a deep dive into the ClickHouse architecture and then highlight some of the advantages and disadvantages of ClickHouse, PostgreSQL, and TimescaleDB that result from the architectural decisions that each of its developers (including us) have made. </p><p>We conclude with a more detailed time-series benchmark analysis. We also have a detailed description of our testing environment to replicate these tests yourself and verify our results.</p><p>Yes, we’re the makers of TimescaleDB, so you may not trust our analysis. If so, we ask you to hold your skepticism for the next few minutes and give the rest of this article a read. </p><p>As you (hopefully) will see, we spent a lot of time understanding ClickHouse for this comparison: first, to make sure we were conducting the benchmark the right way so that we were fair to Clickhouse, but also because we are database nerds at heart and were genuinely curious to learn how ClickHouse was built. </p><h3 id="next-steps">Next steps</h3><p>Are you curious about TimescaleDB? The easiest way to get started is by <a href="https://console.cloud.timescale.com/signup" rel="noreferrer">creating a free Timescale Cloud account</a>, which will give you access to a fully managed TimescaleDB instance (100&nbsp;% free for 30 days).</p><p>If you want to host TimescaleDB yourself, you can do it completely for free: <a href="https://github.com/timescale/timescaledb">visit our GitHub</a> to learn more about options, get installation instructions, and more (⭐️  are very much appreciated! 🙏)</p><p>One last thing:<a href="http://slack.timescale.com"> Join our Community Slack</a> to ask questions, get advice, and connect with other developers (we are +7,000 and counting!). We, the authors of this post, are very active on all channels—as well as all our engineers, members of Team Timescale, and many passionate users.</p><h2 id="what-is-clickhouse">What Is ClickHouse?</h2><p>ClickHouse, short for “Clickstream Data Warehouse”, is a columnar OLAP database that was initially built for web analytics in Yandex Metrica. Generally, ClickHouse is known for its high insert rates, fast analytical queries, and SQL-like dialect.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2021/10/04-timeline--1-.jpg" class="kg-image" alt="Timeline of ClickHouse development from 2008 to 2020" loading="lazy" width="2000" height="903" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2021/10/04-timeline--1-.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2021/10/04-timeline--1-.jpg 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2021/10/04-timeline--1-.jpg 1600w, https://timescale.ghost.io/blog/content/images/2021/10/04-timeline--1-.jpg 2000w" sizes="(min-width: 720px) 720px"></figure><p><em>Timeline of ClickHouse development (</em><a href="https://clickhouse.com/blog/en/2020/the-clickhouse-community/"><em>Full history here.</em></a><em>)</em></p><p>We are fans of ClickHouse. It is a very good database built around certain architectural decisions that make it a good option for OLAP-style analytical queries. In particular, in our benchmarking with the Time Series Benchmark Suite (TSBS),<strong> ClickHouse performed better for data ingestion than any time-series database we've tested so far</strong> (TimescaleDB included) at an average of more than 600k rows/second on a single instance <em>when rows are batched appropriately</em>. </p><p>But nothing in databases comes for free - and as we’ll show below, this architecture also creates significant limitations for ClickHouse, making it slower for many types of time-series queries and some insert workloads. </p><p>If your application doesn't fit within the architectural boundaries of ClickHouse (or TimescaleDB, for that matter), you'll probably end up with a frustrating development experience, redoing a lot of work down the road.</p><h2 id="the-clickhouse-architecture">The ClickHouse Architecture</h2><p>ClickHouse was designed for OLAP workloads, which have specific characteristics. From the <a href="https://clickhouse.com/docs/en/">ClickHouse documentation</a>, here are some of the requirements for this type of workload: </p><ul><li>The vast majority of requests are for read access.</li><li>Data is inserted in fairly large batches (&gt; 1000 rows), not by single rows, or it is not updated at all.</li><li>Data is added to the DB but is not modified.</li><li>For reads, quite a large number of rows are processed from the DB, but only a small subset of columns.</li><li>Tables are “wide,” meaning they contain a large number of columns.</li><li>Queries are relatively rare (usually hundreds of queries per server or less per second).</li><li>For simple queries, latencies of around 50 ms are allowed.</li><li>Column values are fairly small: numbers and short strings (for example, 60 bytes per URL).</li><li>Requires high throughput when processing a single query (up to billions of rows per second per server).</li><li>Transactions are not necessary.</li><li>Low requirements for data consistency.</li><li>There is one large table per query. All tables are small, except for one.</li><li>A query result is significantly smaller than the source data. In other words, data is filtered or aggregated so the result fits in a single server’s RAM.</li></ul><p>How is ClickHouse designed for these workloads? Here are some of the key aspects of their architecture: </p><ul><li>Compressed, column-oriented storage</li><li>Table Engines</li><li>Indexes</li><li>Vector Computation Engine</li></ul><h3 id="compressed-column-oriented-storage"><strong>Compressed, column-oriented storage</strong></h3><p>First, ClickHouse (like nearly all OLAP databases) is column-oriented (or columnar), meaning that data for the same table column is stored together. (In contrast, in row-oriented storage, used by nearly all OLTP databases, data for the same table row is stored together.)</p><p>Column-oriented storage has a few advantages:</p><ul><li>If your query only needs to read a few columns, then reading that data is much faster (you don’t need to read entire rows, just the columns)</li><li>Storing columns of the same data type together leads to greater compressibility (although, as we have shown, it is possible to build <a href="https://www.timescale.com/blog/building-columnar-compression-in-a-row-oriented-database" rel="noreferrer">columnar compression into row-oriented storage</a>).</li></ul><h3 id="table-engines">Table engines</h3><p>To improve the storage and processing of data in ClickHouse, columnar data storage is implemented using a collection of table "engines". The table engine determines the type of table and the features that will be available for processing the data stored inside. </p><p>ClickHouse primarily uses the <strong>MergeTree table engine</strong> as the basis for how data is written and combined. Nearly all other table engines derive from MergeTree and allow additional functionality to be performed automatically as the data is (later) processed for long-term storage.</p><p>(Quick clarification: From this point forward, whenever we mention MergeTree, we're referring to the overall MergeTree architecture design and all table types that derive from it unless we specify a specific MergeTree type.)</p><p>At a high level, MergeTree allows data to be written and stored very quickly to multiple immutable files (called "parts" by ClickHouse). These files are later processed in the background at some point in the future and merged into a larger <strong>part</strong> with the goal of reducing the total number of <strong>parts</strong> on disk (fewer files = more efficient data reads later). This is one of the key reasons behind ClickHouse’s astonishingly high insert performance on large batches.</p><p>All columns in a table are stored in separate <strong>parts</strong> (files), and all values in each column are stored in the order of the primary key. This column separation and sorting implementation makes future data retrieval more efficient, particularly when computing aggregates on large ranges of contiguous data.</p><h3 id="indexes">Indexes</h3><p>Once the data is stored and merged into the most efficient set of <strong>parts</strong> for each column, queries need to know how to efficiently find the data. For this, Clickhouse relies on two types of indexes: the primary index and, additionally, a secondary (data skipping) index.</p><p>Unlike a traditional OLTP, BTree index, which knows how to locate any row in a table, the ClickHouse primary index is sparse in nature, meaning that it does not have a pointer to the location of every value for the primary index. Instead, because all data is stored in primary key order, <strong>the primary index stores the value of the primary key in every N-th row</strong> (called index_granularity, 8192 by default). This is done with the specific design goal of<strong> fitting the primary index into memory </strong>for extremely fast processing.</p><p>When your query patterns fit with this index style, the sparse nature can help improve query speed significantly. The one limitation is that you cannot create other indexes on specific columns to help improve a different query pattern. We'll discuss this more later.</p><h3 id="vector-computation-engine">Vector computation engine</h3><p>ClickHouse was designed with the desire to have "online" query processing in a way that other OLAP databases hadn't been able to achieve. Even with compression and columnar data storage, most other OLAP databases still rely on incremental processing to pre-compute aggregated data. It has generally been the pre-aggregated data that's provided the speed and reporting capabilities.</p><p>To overcome these limitations, ClickHouse implemented a series of vector algorithms for working with large arrays of data on a column-by-column basis. With vectorized computation, ClickHouse can specifically work with data in blocks of tens of thousands of rows (per column) for many computations. Vectorized computing also provides an opportunity to write more efficient code that utilizes modern SIMD processors and keeps code and data closer together for better memory access patterns, too.</p><p>In total, this is a great feature for working with large data sets and writing complex queries on a limited set of columns and something TimescaleDB could benefit from as we explore more opportunities to utilize columnar data.</p><p>That said, as you'll see from the benchmark results, enabling compression in TimescaleDB (which converts data into compressed columnar storage) improves the query performance of many aggregate queries in ways that are even better than ClickHouse.</p><h3 id="clickhouse-architectures-disadvantages-aka-nothing-comes-for-free">ClickHouse architecture's disadvantages (a.k.a. nothing comes for free)</h3><p>Nothing comes for free in database architectures. Clearly, ClickHouse is designed with a very specific workload in mind. Similarly, it is <em>not designed</em> for other types of workloads. </p><p>We can see an initial set of disadvantages from the <a href="https://clickhouse.com/docs/en/introduction/distinctive-features/">ClickHouse docs</a>:</p><ul><li>No full-fledged transactions.</li><li>Lack of ability to modify or delete already inserted data with a high rate and low latency. There are batch deletes and updates available to clean up or modify data, for example, to comply with GDPR, but not for regular workloads.</li><li>The sparse index makes ClickHouse not so efficient for point queries retrieving single rows by their keys.</li></ul><p>There are a few disadvantages that are worth going into detail:</p><ul><li>Data can’t be directly modified in a table</li><li>Some “synchronous” actions aren’t really synchronous</li><li>SQL-like, but not quite SQL</li><li>No data consistency in backups</li></ul><h3 id="mergetree-limitation-data-can%E2%80%99t-be-directly-modified-in-a-table">MergeTree limitation: Data can’t be directly modified in a table</h3><p>All tables in ClickHouse are immutable. There is no way to directly update or delete a value that's already been stored. Instead, any operations that <code>UPDATE</code> or <code>DELETE</code> data can only be accomplished through an <code>ALTER TABLE</code> statement that applies a filter and actually re-writes the entire table (<strong>part</strong> by <strong>part</strong>) in the background to update or delete the data in question. Essentially, it's just another merge operation with some filters applied. </p><p>As a result, several MergeTree table engines exist to solve this deficiency—to solve common scenarios where frequent data modifications would otherwise be necessary. Yet, this can lead to unexpected behavior and non-standard queries.</p><p>For example, if you need to store only the most recent reading of a value, creating a <a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/collapsingmergetree/">CollapsingMergeTree table type</a> is your best option. With this table type, an additional column (called <code>Sign</code>) is added to the table, which indicates which row is the current state of an item when all other field values match. ClickHouse will then asynchronously delete rows with a <code>Sign</code> that cancel each other out (a value of 1 vs -1), leaving the most recent state in the database.</p><p>For example, consider a common database design pattern where the most recent values of a sensor are stored alongside the long-term time-series table for fast lookup. We'll call this table <strong>SensorLastReading</strong>. In ClickHouse, this table would require the following pattern to store the most recent value every time new information is stored in the database.</p><p><strong>SensorLastReading</strong></p>
<table>
<thead>
<tr>
<th>SensorID</th>
<th>Temp</th>
<th>Cpu</th>
<th>Sign</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>55</td>
<td>78</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>When new data is received, you need to add <strong>2 more rows</strong> to the table, one to negate the old value and one to replace it.</p><table>
<thead>
<tr>
<th>SensorID</th>
<th>Temp</th>
<th>Cpu</th>
<th>Sign</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>55</td>
<td>78</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>55</td>
<td>78</td>
<td>-1</td>
</tr>
<tr>
<td>1</td>
<td>40</td>
<td>35</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>At some point after this insert, ClickHouse will merge the changes, removing the two rows that cancel each other out on Sign, leaving the table with just this row:</p><table>
<thead>
<tr>
<th>SensorID</th>
<th>Temp</th>
<th>Cpu</th>
<th>Sign</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>40</td>
<td>35</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>But remember, <strong>MergeTree operations are asynchronous,</strong> so queries can occur on data before something like the collapse operation has been performed. Therefore, the queries to get data out of a CollapsingMergeTree table require additional work, like multiplying rows by their `Sign`, to make sure you get the correct value any time the table is in a state that still contains duplicate data. </p><p>Here is one solution that the ClickHouse documentation provides, modified for our sample data. Notice that with numerical numbers, you can get the "correct" answer by multiplying all values by the Sign column and adding a HAVING clause.</p><pre><code class="language-sql">SELECT
    SensorID,
    sum(Temp * Sign) AS Temp,
    sum(Cpu * Sign) AS Cpu
FROM SensorLastReading
GROUP BY SensorId
HAVING sum(Sign) &gt; 0
</code></pre>
<p>Again, the value here is that MergeTree tables provide really fast ingestion of data at the expense of transactions and simple concepts like UPDATE and DELETE in the way traditional applications would try to use a table like this. With ClickHouse, it's just more work to manage this kind of data workflow. </p><p>Because ClickHouse isn't an ACID database, these background modifications (or really any data manipulations) have no guarantees of ever being completed. Because there is no such thing as transaction isolation, any SELECT query that touches data in the middle of an UPDATE or DELETE modification (or a Collapse modification, as we noted above) will get whatever data is <strong><em>currently</em></strong> in each part. If the delete process, for instance, has only modified 50% of the parts for a column, queries would return outdated data from the remaining parts that have not yet been processed.</p><p>More importantly, <strong>this holds true for all data that is stored in ClickHouse, </strong>not just the large, analytical-focused tables that store something like time-series data, <em>but also the related metadata</em>. </p><p>While it's understandable that time-series data, for example, is often insert-only (and rarely updated), business-centric metadata tables almost always have modifications and updates as time passes. </p><p>Regardless, the related business data that you may store in ClickHouse to do complex joins and deeper analysis is still in a MergeTree table (or variation of a MergeTree), and therefore, updates or deletes would still require an entire rewrite (through the use of <code>ALTER TABLE</code>,) any time there are modifications.</p><h3 id="distributed-mergetree-tables">Distributed MergeTree tables</h3><p>Distributed tables are another example of where asynchronous modifications might cause you to change how you query data. If your application writes data directly to the distributed table (rather than to different cluster nodes, which is possible for advanced users), the data is first written to the "initiator" node, which in turn copies the data to the shards in the background as quickly as possible. Because there are no transactions to verify that the data was moved as part of something like two-phase commits (available in PostgreSQL), your data might not actually be where you think it is.</p><p>There is at least one other problem with how distributed data is handled. Because ClickHouse does not support transactions and data is in a constant state of being moved, there is no guarantee of consistency in the state of the cluster nodes. Saving 100,000 rows of data to a distributed table doesn't guarantee that backups of all nodes will be consistent with one another (we'll discuss reliability in a bit). Some of that data might have been moved, and some of it might still be in transit.</p><p>Again, this is by design, so there's nothing specifically wrong with what's happening in ClickHouse! It's just something to be aware of when comparing ClickHouse to something like PostgreSQL and TimescaleDB.</p><h3 id="some-%E2%80%9Csynchronous%E2%80%9D-actions-aren%E2%80%99t-really-synchronous"><strong>Some “synchronous” actions aren’t really synchronous</strong></h3><p>Most actions in ClickHouse are not synchronous. But we found that even some of the ones labeled “synchronous” weren’t really synchronous either.</p><p>One particular example that caught us by surprise during our benchmarking was how <code>TRUNCATE</code> worked. We ran many test cycles against ClickHouse and TimescaleDB to identify how changes in row batch size, workers, and even cardinality impacted the performance of each database. At the end of each cycle, we would <code>TRUNCATE</code> the database in each server, expecting the disk space to be released quickly so that we could start the next test. In PostgreSQL (and other OLTP databases), this is an atomic action. As soon as the truncate is complete, the space is freed up on disk.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/pasted-image-0--1--5.png" class="kg-image" alt="Dashboard graph showing disk usage and immedate release of space after using TRUNCATE" loading="lazy" width="1524" height="540" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/01/pasted-image-0--1--5.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/01/pasted-image-0--1--5.png 1000w, https://timescale.ghost.io/blog/content/images/2022/01/pasted-image-0--1--5.png 1524w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">TRUNCATE is an atomic action in TimescaleDB/PostgreSQL and frees disk almost immediately</span></figcaption></figure><p>We expected the same thing with ClickHouse because the documentation mentions that this is a <strong>synchronous</strong> action (and most things are <strong>not</strong> synchronous in ClickHouse). It turns out, however, that the files only get marked for deletion and the disk space is freed up at a later, unspecified time in the background. There's no specific guarantee for when that might happen.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/pasted-image-0-10.png" class="kg-image" alt="Dashboard graph showing disk usage of ClickHouse and the time needed to free disk space after TRUNCATE" loading="lazy" width="1526" height="550" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/01/pasted-image-0-10.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/01/pasted-image-0-10.png 1000w, https://timescale.ghost.io/blog/content/images/2022/01/pasted-image-0-10.png 1526w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">TRUNCATE is an asynchronous action in ClickHouse, freeing disk at some future time</span></figcaption></figure><p>For our tests, it was a minor inconvenience. <strong>We had to add a 10-minute sleep into the testing cycle</strong> to ensure that ClickHouse had released the disk space fully. In real-world situations, like ETL processing that utilizes staging tables, a <code>TRUNCATE</code> wouldn't actually free the staging table data immediately, which could cause you to modify your current processes.</p><p>We point a few of these scenarios to simply highlight the point that ClickHouse isn't a drop-in replacement for many things that a system of record (OLTP database) is generally used for in modern applications. Asynchronous data modification can take a lot more effort to effectively work with data.</p><h3 id="sql-like-but-not-quite-sql">SQL-like, but not quite SQL</h3><p>In many ways, ClickHouse was ahead of its time by choosing SQL as the language of choice.</p><p>ClickHouse chose early in its development to utilize SQL as the primary language for managing and querying data. Given the focus on data analytics, this was a smart and obvious choice, given that SQL was already widely adopted and understood for querying data. </p><p>In ClickHouse, the SQL isn't something that was added after the fact to satisfy a portion of the user community. That said, what ClickHouse provides is a SQL-like language that doesn't comply with any actual standard.</p><p>The challenges of a SQL-like query language are many. For example, retraining users who will be accessing the database (or writing applications that access the database). Another challenge is a lack of ecosystem: connectors and tools that speak SQL won’t just work out of the box—i.e., they will require some modification (and again, knowledge by the user) to work. </p><p>Overall, ClickHouse handles basic SQL queries well. </p><p>However, because the data is stored and processed in a different way from most SQL databases, there are a number of commands and functions you may expect to use from a SQL database (e.g., PostgreSQL, TimescaleDB), but which ClickHouse doesn't support or has limited support for:</p><ul><li>Not optimized for JOINs</li><li>No index management beyond the primary and secondary indexes</li><li>No recursive CTEs</li><li>No correlated subqueries or LATERAL joins</li><li>No stored procedures</li><li>No user-defined functions</li><li>No triggers<br></li></ul><p>One example that <strong>stands out about ClickHouse is that </strong><a href="https://www.timescale.com/learn/sql-joins-summary"><strong>JOINs</strong></a><strong>, by nature, are generally discouraged because the query engine lacks any ability to optimize the join of two or more tables</strong>. </p><p>Instead, users are encouraged to either query table data with separate sub-select statements and then and then use something like a <a href="https://www.timescale.com/learn/what-is-a-sql-inner-join"><code>ANY INNER JOIN</code></a>, which strictly looks for <a href="https://clickhouse.tech/docs/en/sql-reference/statements/select/join/#select-join-types">unique pairs on both sides of the join </a>(avoiding a cartesian product that can occur with standard JOIN types). There's also no caching support for the product of a JOIN, so if a table is joined multiple times, <strong>the query on that table is executed multiple times</strong>, further slowing down the query.</p><p>For example, all of the "double-groupby" queries in the TSBS group by multiple columns and then join to the tag table to get the `hostname` for the final output. Here is how that query is written for each database.</p><p><strong>TimescaleDB:</strong></p><pre><code class="language-sql">WITH cpu_avg AS (
     SELECT time_bucket('1 hour', time) as hour,
       hostname, 
	   AVG(cpu_user) AS mean_cpu_user
     FROM cpu
     WHERE time &gt;= '2021-01-01T12:30:00Z' 
       AND time &lt; '2021-01-02T12:30:00Z'
     GROUP BY 1, 2
)
SELECT hour, hostname, mean_cpu_user
FROM cpu_avg
JOIN tags ON cpu_avg.tags_id = tags.id
ORDER BY hour, hostname;
</code></pre>
<p><strong>ClickHouse:</strong></p><pre><code class="language-sql">SELECT
    hour,
    id,
    mean_cpu_user
FROM
(
    SELECT
        toStartOfHour(created_at) AS hour,
        tags_id AS id,
        AVG(cpu_user) as mean_cpu_user
    FROM cpu
    WHERE (created_at &gt;= '2021-01-01T12:30:00Z') 
        AND (created_at &lt; '2021-01-02T12:30:00Z')
    GROUP BY
        hour,
        id
) AS cpu_avg
ANY INNER JOIN tags USING (id)
ORDER BY
    hour ASC,
    id;
</code></pre>
<p><strong>Reliability: no data consistency in backups</strong></p><p>One last aspect to consider as part of the ClickHouse architecture and its lack of support for transactions is that there is no data consistency in backups. As we've already shown, all data modification (even sharding across a cluster) is asynchronous. Therefore, the only way to ensure a consistent backup would be to stop all writes to the database and then make a backup. Data recovery struggles with the same limitation.</p><p>The lack of transactions and data consistency also affects other features like materialized views because the server can't atomically update multiple tables at once. If something breaks during a multi-part insert to a table with materialized views, the end result is an inconsistent state of your data.</p><p>ClickHouse is aware of these shortcomings and is certainly working on or planning updates for future releases. Some form of transaction support <a href="https://github.com/ClickHouse/ClickHouse/issues/22086">has been in discussion for some time</a>, and <a href="https://github.com/ClickHouse/ClickHouse/pull/21945">backups are in process</a> and merged into the main branch of code, although it's <a href="https://github.com/ClickHouse/ClickHouse/pull/21945#issuecomment-933598875">not yet recommended for production use</a>. But even then, it only provides limited support for transactions.</p><h2 id="clickhouse-vs-postgresql">ClickHouse vs. PostgreSQL</h2><p><em>(A proper ClickHouse vs. PostgreSQL comparison would probably take another 8,000 words. To avoid making this post even longer, we opted to provide a short comparison of the two databases—but if anyone wants to provide a more detailed comparison, we would love to read it.) </em></p><p>As we can see above, ClickHouse is a well-architected database for OLAP workloads. Conversely, PostgreSQL is a well-architected database for OLTP workloads. </p><p>Also, PostgreSQL isn’t just an OLTP database: it’s the fastest-growing and most loved OLTP database (<a href="https://db-engines.com/en/ranking">DB-Engines</a>, <a href="https://survey.stackoverflow.co/2024/technology#1-databases" rel="noreferrer">Stack Overflow 2024</a><a href="https://insights.stackoverflow.com/survey/2021#section-most-popular-technologies-databases"> Developer Survey</a>).</p><p>As a result, we won’t compare the performance of ClickHouse vs. PostgreSQL because—to continue our analogy from before—it would be like comparing the performance of a bulldozer vs. a car. These are two different things designed for two different purposes. </p><p>We’ve already established why ClickHouse is excellent for analytical workloads. Let’s now understand why PostgreSQL is so loved for transactional workloads: versatility, extensibility, and reliability.</p><h3 id="postgresql-versatility-and-extensibility">PostgreSQL versatility and extensibility</h3><p>Versatility is one of the distinguishing strengths of PostgreSQL. It's one of the main reasons for the recent resurgence of PostgreSQL in the wider technical community.</p><p><a href="https://www.timescale.com/blog/best-practices-for-picking-postgresql-data-types/" rel="noreferrer">PostgreSQL supports a variety of data types</a>, including arrays, JSON, and more. It supports <a href="https://www.timescale.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered/" rel="noreferrer">various index types</a>—not just the common B-tree but also GIST, GIN, and more. Full-text search? Check. Role-based access control? Check. And, of course, full SQL.</p><p>Also, through the use of extensions, PostgreSQL can retain the things it's good at while adding specific functionality to enhance the ROI of your development efforts.</p><p>Does your application need geospatial data? Add the PostGIS extension. What about features that benefit time-series data workloads? Add TimescaleDB. <a href="https://www.timescale.com/learn/postgresql-extensions-pg-trgm">Could your application benefit from the ability to search using trigrams? Add pg_trgm</a>.</p><p>With all these capabilities, PostgreSQL is quite flexible, meaning it is essentially future-proof. As your application or workload changes, you will know that you can still adapt PostgreSQL to your needs.</p><p>(For one specific example of the powerful extensibility of PostgreSQL, please read how our engineering team built <a href="https://www.timescale.com/blog/function-pipelines-building-functional-programming-into-postgresql-using-custom-operators/" rel="noreferrer">functional programming into PostgreSQL using customer operators</a>.)</p><h3 id="postgresql-reliability">PostgreSQL reliability</h3><p>As developers, we’re resolved to the fact that programs crash, servers encounter hardware or power failures, disks fail, or experience corruption. You can mitigate this risk (e.g., robust software engineering practices, uninterrupted power supplies, disk RAID, etc.) but not eliminate it completely; it’s a fact of life for systems. </p><p>In response, databases are built with various mechanisms to further reduce such risk, including streaming replication to replicas, full-snapshot backup and recovery, streaming backups, robust data export tools, etc.</p><p>PostgreSQL has the benefit of 20+ years of development and usage, which has resulted in not just a reliable database but also a broad spectrum of rigorously tested tools: streaming replication for high availability and read-only replicas, pg_dump and pg_recovery for full database snapshots, pg_basebackup and log shipping/streaming for incremental backups and arbitrary point-in-time recovery, pgBackrest or WAL-E for continuous archiving to cloud storage, and robust COPY FROM and COPY TO tools for quickly importing/exporting data with a variety of formats. </p><p>This enables PostgreSQL to offer a greater “peace of mind”—because all of the skeletons in the closet have already been found (and addressed).</p><h2 id="clickhouse-vs-timescaledb">ClickHouse vs. TimescaleDB</h2><p>TimescaleDB is the leading relational database for time series, built on PostgreSQL. It offers everything PostgreSQL has to offer, plus a full time-series database. </p><p>As a result, all of the advantages of PostgreSQL also apply to TimescaleDB, including versatility and reliability.</p><p>But TimescaleDB adds some critical capabilities that allow it to outperform Postgres for time-series data:</p><ul><li><strong>Hypertables: </strong>The foundation for many TimescaleDB features (listed below), hypertables provide <a href="https://www.timescale.com/learn/data-partitioning-what-it-is-and-why-it-matters" rel="noreferrer">automatically partitioned data</a> across time and space for more performant inserts and queries</li><li><strong>Continuous aggregates: </strong><a href="https://www.timescale.com/learn/postgresql-materialized-views-and-where-to-find-them">Intelligently updated materialized views for time-series data</a>. Rather than recreating the materialized view every time, TimescleDB updates data based only on underlying changes to raw data.</li><li><strong>Columnar compression: </strong><a href="https://www.timescale.com/learn/what-is-data-compression-and-how-does-it-work">Efficient data compression of 90%+ on most time-series data</a> with dramatically improved query performance for historical, long+narrow queries.</li><li><strong>Hyperfunctions: </strong><a href="https://docs.timescale.com/api/latest/hyperfunctions/">Analytic-focused functions added to PostgreSQL to enhance time-series queries</a> with features like approximate percentiles, efficient downsampling, and two-step aggregation.</li><li><strong>Function pipelines (</strong><a href="https://www.timescale.com/blog/function-pipelines-building-functional-programming-into-postgresql-using-custom-operators/" rel="noreferrer"><strong>released this week!</strong></a><strong>): </strong>Radically improve the developer ergonomics of analyzing data in PostgreSQL and SQL by applying principles from functional programming and popular tools like Python’s Pandas and PromQL.</li></ul><h2 id="clickhouse-vs-timescaledb-performance-for-time-series-data">ClickHouse vs. TimescaleDB Performance for Time-Series Data</h2><p>Time-series data has exploded in popularity because the value of tracking and analyzing how things change over time has become evident in every industry: DevOps and IT monitoring, industrial manufacturing, financial trading and risk management, sensor data, ad tech, application eventing, smart home systems, autonomous vehicles, professional sports, and more. </p><p>It's unique from more traditional business-type (OLTP) data in at least two primary ways: it is primarily insert heavy, and the scale of the data grows at an unceasing rate. This impacts both data collection and storage, as well as how we analyze the values themselves. Traditional OLTP databases often can't handle millions of transactions per second or provide effective means of storing and maintaining the data.</p><p>Time-series data is also more unique than general analytical (OLAP) data in that queries generally have a time component, and queries rarely touch every row in the database.</p><p>Over the last few years, however, the lines between the capabilities of OLTP and OLAP databases have started to blur. For the last decade, the storage challenge was mitigated by numerous NoSQL architectures while still failing to effectively deal with the query and analytics required of time-series data.</p><p>As a result, many applications try to find the right balance between the transactional capabilities of OLTP databases and the large-scale analytics provided by OLAP databases. It makes sense, therefore, that many applications would try to use ClickHouse, which offers fast ingest and analytical query capabilities for time-series data.</p><p>So, let's see how both ClickHouse and TimescaleDB compare for time-series workloads using our standard <a href="https://github.com/timescale/tsbs">TSBS</a> benchmarks.</p><p><strong>Performance Benchmarks</strong></p><p>Let me start by saying that this wasn't a test we completed in a few hours and then moved on from. In fact, just yesterday, while finalizing this blog post, <strong>we installed the latest version of ClickHouse</strong> (released three days ago) and ran all of the tests again to ensure we had the best numbers possible! (benchmarking, not benchmarketing)</p><p>In preparation for the final set of tests, we ran benchmarks on both TimescaleDB and ClickHouse dozens of times each—<em>at least</em>. We tried different cardinalities, different lengths of time for the generated data, and various settings for things that we had easy control over—like "chunk_time_interval" with TimescaleDB. We wanted to really understand how each database would perform with typical cloud hardware and the specs that we often see in the wild.</p><p>We also acknowledge that most real-world applications don't work like the benchmark does: ingesting data first and querying it second. Separating each operation allows us to understand which settings impacted each database during different phases, allowing us to tweak benchmark settings for each database along the way to get the best performance.</p><p>Finally, we always view these benchmarking tests as an academic and self-reflective experience. That is, spending a few hundred hours working with both databases often causes us to consider ways we might improve TimescaleDB (in particular), and thoughtfully consider when we can—and should—say that another database solution is a good option for specific workloads.</p><h3 id="%E2%80%8B%E2%80%8Bmachine-configuration">​​Machine Configuration</h3><p>For this benchmark, we consciously decided to use cloud-based hardware configurations that were reasonable for a medium-sized workload typical of startups and growing businesses. In previous benchmarks, we've used bigger machines with specialized RAID storage, <strong>a very typical</strong> <strong>setup</strong> for a production database environment.</p><p>But, as time has marched on and we see more developers use Kubernetes and modular infrastructure setups without lots of specialized storage and memory optimizations, it felt more genuine to benchmark each database on instances that more closely matched what we tend to see in the wild. Sure, we can always throw more hardware and resources to help spike numbers, but that often doesn't help convey what most real-world applications can expect.</p><p>To that end, for comparing both insert and read latency performance, we used the following setup in AWS:</p><ul><li><strong>Versions</strong>: TimescaleDB <a href="https://github.com/timescale/timescaledb/releases/tag/2.4.0">version 2.4.0</a>, community edition, with PostgreSQL 13; ClickHouse <a href="https://github.com/ClickHouse/ClickHouse/releases/tag/v21.6.5.37-stable">version 21.6.5</a> (the latest non-beta releases for both databases at the time of testing).</li><li>1 remote client machine running TSBS, 1 database server, both in the same cloud datacenter</li><li><strong>Instance size</strong>: Both client and database server ran on Amazon EC2 virtual machines (m5.4xlarge) with 16 vCPU and 64GB Memory each.</li><li><strong>OS</strong>: Both server and client machines ran Ubuntu 20.04.3</li><li><strong>Disk Size</strong>: 1TB of EBS GP2 storage</li><li><strong>Deployment method</strong>: Installed via apt-get using official sources</li></ul><h3 id="database-configuration">Database configuration</h3><p><strong>ClickHouse:</strong> No configuration modification was done with the ClickHouse. We simply installed it per their documentation. There is not currently a tool like <code>timescaledb-tune</code> for ClickHouse.</p>
<p><strong>TimescaleDB:</strong> For TimescaleDB, we followed the recommendations in the timescale documentation. Specifically, we ran <a href="https://github.com/timescale/timescaledb-tune"><code>timescaledb-tune</code></a> and accepted the configuration suggestions which are based on the specifications of the EC2 instance. We also set <code>synchronous_commit=off</code> in <code>postgresql.conf</code>. This is a common performance configuration for write-heavy workloads while still maintaining transactional, logged integrity.</p>
<h3 id="insert-performance-1">Insert performance</h3><p>For insert performance, we used the following datasets and configurations. The datasets were created using <a href="https://github.com/timescale/tsbs">Time-Series Benchmarking Suite</a> with the <code>cpu-only</code> use case.</p>
<ul><li><strong>Dataset</strong>: 100-1,000,000 simulated devices generated 10 CPU metrics every 10 seconds for ~100 million reading intervals.</li><li><strong>Intervals</strong> used for each configuration are as follows: 31 days for 100 devices; 3 days for 4,000 devices; 3 hours for 100,000 devices; 30 minutes for 1,000,000</li><li><strong>Batch size</strong>: Inserts were made using a batch size of 5,000 which was used for both ClickHouse and TimescaleDB. We tried multiple batch sizes and found that in most cases there was little difference in overall insert efficiency between 5,000 and 15,000 rows per batch with each database.</li><li><strong>TimescaleDB chunk size</strong>: We set the chunk time depending on the data volume, aiming for 7-16 chunks in total for each configuration (<a href="https://docs.timescale.com/latest/using-timescaledb/hypertables?utm_source=timescale-influx-benchmark&amp;utm_medium=blog&amp;utm_campaign=july-2020-advocacy&amp;utm_content=chunks-docs#best-practices">more on chunks here</a>).<br></li></ul><p>In the end, these were the performance numbers for ingesting pre-generated time-series data from the TSBS client machine into each database using a batch size of 5,000 rows.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/09/07-clickhouse-improvement.png" class="kg-image" alt="Table showing the final insert results between ClickHouse and TimescaleDB when using larger 5,000 rows/batch" loading="lazy" width="2000" height="682" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/09/07-clickhouse-improvement.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/09/07-clickhouse-improvement.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/09/07-clickhouse-improvement.png 1600w, https://timescale.ghost.io/blog/content/images/2023/09/07-clickhouse-improvement.png 2064w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Insert performance comparison between ClickHouse and TimescaleDB with 5,000 row/batches</span></figcaption></figure><p>To be honest, <strong>this didn't surprise us</strong>. We've seen numerous recent blog posts about ClickHouse ingest performance, and since ClickHouse uses a different storage architecture and mechanism that doesn't include transaction support or ACID compliance, we generally expected it to be faster.</p><p>The story does change a bit, however, when you consider that ClickHouse is designed to save every "transaction" of ingested rows as separate files (to be merged later using the MergeTree architecture). It turns out that when you have much lower batches of data to ingest, ClickHouse is significantly slower and consumes much more disk space than TimescaleDB.</p><p><em>(Ingesting 100 million rows, 4,000 hosts, 3 days of data - 22GB of raw data)</em></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/09/08-chunk-time-interval.png" class="kg-image" alt="Table showing the impact of using smaller batch sizes has on TimescaleDB and ClickHouse. TimescaleDB insert performance and disk usage stays steady, while ClickHouse performance is negatively impacted" loading="lazy" width="2000" height="802" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/09/08-chunk-time-interval.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/09/08-chunk-time-interval.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/09/08-chunk-time-interval.png 1600w, https://timescale.ghost.io/blog/content/images/2023/09/08-chunk-time-interval.png 2064w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Insert performance comparison between ClickHouse and TimescaleDB using smaller batch sizes, which significantly impacts ClickHouse's performance and disk usage</span></figcaption></figure><p>Do you notice something in the numbers above?</p><p>Regardless of batch size, TimescaleDB consistently consumed ~19GB of disk space with each data ingest benchmark before compression. This is a result of the <code>chunk_time_interval</code> which determines how many chunks will get created for a given range of time-series data. Although ingest speeds may decrease with smaller batches, the same chunks are created for the same data, resulting in consistent disk usage patterns. Before compression, it's easy to see that TimescaleDB continually consumes the same amount of disk space regardless of the batch size.</p>
<p>By comparison, ClickHouse storage needs are correlated to how many files need to be written (which is partially dictated by the size of the row batches being saved). It can actually take significantly more storage to save data to ClickHouse before it can be merged into larger files. Even at 500-row batches, ClickHouse consumed 1.75x more disk space than TimescaleDB for a source data file that was 22GB in size.</p><h3 id="read-latency">Read latency</h3><p>For benchmarking read latency, we used the following setup for each database (the machine configuration is the same as the one used in the Insert comparison):</p><ul>
<li><strong>Dataset</strong>: 4,000/10,000 simulated devices generated 10 CPU metrics every 10 seconds for 3 full days (100M+ reading intervals, 1B+ metrics)</li>
<li>We also enabled <a href="https://www.timescale.com/blog/building-columnar-compression-in-a-row-oriented-database/?utm_source=timescale-influx-benchmark&amp;utm_medium=blog&amp;utm_campaign=july-2020-advocacy&amp;utm_content=compression-blog">native compression</a> on TimescaleDB. We compressed everything but the most recent chunk of data, leaving it uncompressed. This configuration is a commonly recommended one where raw, uncompressed data is kept for recent time periods and older data is compressed, enabling greater query efficiency (see our <a href="https://docs.timescale.com/latest/using-timescaledb/compression?utm_source=timescale-influx-benchmark&amp;utm_medium=blog&amp;utm_campaign=july-2020-advocacy&amp;utm_content=compression-docs#quick-start">compression docs</a> for more). The parameters we used to enable compression are as follows: We segmented by the <code>tags_id</code> columns and ordered by <code>time</code> descending and <code>usage_user</code> columns.</li>
</ul>
<p>On read (i.e., query) latency, the results are more complex. Unlike inserts, which primarily vary on cardinality size (and perhaps batch size), the universe of possible queries is essentially infinite, especially with a language as powerful as SQL. Often, the best way to benchmark read latency is to do it with the actual queries you plan to execute. For this case, we use a broad set of queries to mimic the most common query patterns.</p><p>The results shown below are the median from 1000 queries for each query type. Latencies in this chart are all shown as milliseconds, with an additional column showing the relative performance of TimescaleDB compared to ClickHouse (highlighted in green when TimescaleDB is faster, in blue when ClickHouse is faster).</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/09/09-clickhouse-benchmark-read-latency-performance.png" class="kg-image" alt="Table showing query response results when querying 4,000 hosts and 100 million rows of data. TimescaleDB outperforms in almost all query categories." loading="lazy" width="2000" height="1849" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/09/09-clickhouse-benchmark-read-latency-performance.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/09/09-clickhouse-benchmark-read-latency-performance.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/09/09-clickhouse-benchmark-read-latency-performance.png 1600w, https://timescale.ghost.io/blog/content/images/size/w2400/2023/09/09-clickhouse-benchmark-read-latency-performance.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Results of benchmarking query performance of 4,000 hosts with 100 million rows of data</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/09/10-clickhouse-benchmark-read-latency-performance.png" class="kg-image" alt="Table showing query response results when querying 10,000 hosts and 100 million rows of data. TimescaleDB outperforms in almost all query categories." loading="lazy" width="2000" height="1815" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/09/10-clickhouse-benchmark-read-latency-performance.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/09/10-clickhouse-benchmark-read-latency-performance.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/09/10-clickhouse-benchmark-read-latency-performance.png 1600w, https://timescale.ghost.io/blog/content/images/size/w2400/2023/09/10-clickhouse-benchmark-read-latency-performance.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Results of benchmarking query performance of 10,000 hosts with 100 million rows of data</span></figcaption></figure><h3 id="simple-rollups">Simple rollups</h3><p>For simple rollups (i.e., single-groupby), when aggregating one metric across a single host for 1 or 12 hours, or multiple metrics across one or multiple hosts (either for 1 hour or 12 hours), TimescaleDB generally outperforms ClickHouse at both low and high cardinality. In particular, TimescaleDB exhibited up to 1058 % of the performance of ClickHouse on configurations with 4,000 and 10,000 devices, with 10 unique metrics being generated every read interval.</p><h3 id="aggregates">Aggregates</h3><p>When calculating a simple aggregate for 1 device, TimescaleDB consistently outperforms ClickHouse across any number of devices. In our benchmark, TimescaleDB demonstrates 156 % of the performance of ClickHouse when aggregating 8 metrics across 4,000 devices and 164 % when aggregating 8 metrics across 10,000 devices. Once again, TimescaleDB outperforms ClickHouse for high-end scenarios.</p><h3 id="double-rollups">Double rollups</h3><p>The one set of queries that ClickHouse consistently bested TimescaleDB in query latency was in the double rollup queries that aggregate metrics by time and another dimension (e.g., GROUPBY time, deviceId). We'll go into a bit more detail below on why this might be, but this also wasn't completely unexpected.</p><h3 id="thresholds">Thresholds</h3><p>When selecting rows based on a threshold, TimescaleDB demonstrates between 249-357&nbsp;% the performance of ClickHouse when computing thresholds for a single device but only 130-58% the performance of ClickHouse when computing thresholds for all devices for a random time window.</p><h3 id="complex-queries">Complex queries</h3><p>For complex queries that go beyond rollups or thresholds, the comparison is a bit more nuanced, particularly when looking at TimescaleDB. The difference is that TimescaleDB gives you control over which chunks are compressed. In most time-series applications, especially things like IoT, there's a constant need to find the most recent value of an item or a list of the top X things by some aggregation. This is what the <code>lastpoint</code> and <code>groupby-orderby-limit</code> queries benchmark.</p>
<p>As we've shown previously with other databases (<a href="https://www.timescale.com/blog/timescaledb-vs-influxdb-for-time-series-data-timescale-influx-sql-nosql-36489299877/">InfluxDB</a> and <a href="https://www.timescale.com/blog/how-to-store-time-series-data-mongodb-vs-timescaledb-postgresql-a73939734016/">MongoDB</a>), and as ClickHouse documents themselves, getting individual ordered values for items is not a use case for a MergeTree-like/OLAP database, generally because there is no ordered index that you can define for a time, key, and value. This means asking for the most recent value of an item still causes a more intense scan of data in OLAP databases.</p>
<p>We see that expressed in our results. TimescaleDB was around 3486% faster than ClickHouse when searching for the most recent values (<code>lastpoint</code>) for each item in the database. This is because the most recent uncompressed chunk will often hold the majority of those values as data is ingested and a great example of why this flexibility with compression can have a significant impact on the performance of your application.</p>
<p>We fully admit, however, that compression doesn't always return favorable results for every query form. In the last complex query, <code>groupby-orderby-limit</code>, ClickHouse bests TimescaleDB by a significant amount, almost 15x faster. What our results didn't show is that queries that read from an uncompressed chunk (the most recent chunk) are 17x faster than ClickHouse, averaging 64ms per query. The query looks like this in TimescaleDB:</p>
<pre><code class="language-sql">SELECT time_bucket('60 seconds', time) AS minute, max(usage_user)
        FROM cpu
        WHERE time &lt; '2021-01-03 15:17:45.311177 +0000'
        GROUP BY minute
        ORDER BY minute DESC
        LIMIT 5
</code></pre>
<p>As you might guess, when the chunk is uncompressed, PostgreSQL indexes can be used to order the data by time quickly. When the chunk is compressed, the data matching the predicate (`WHERE time &lt; '2021-01-03 15:17:45.311177 +0000'` in the example above) must first be decompressed before it is ordered and searched.</p><p>When the data for a <code>lastpoint</code> query falls within an uncompressed chunk (which is often the case with near-term queries that have a predicate like <code>WHERE time &lt; now() - INTERVAL '6 hours'</code>), the results are startling.</p><p><em>(uncompressed chunk query, 4k hosts)</em></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2021/10/11-clickhouse-benchmark-uncompressed-chunk--1-.jpg" class="kg-image" alt="Table showing the positive impact querying uncompressed data in TimescaleDB can have, specifically the lastpoint and groupby-orderby-limit queries." loading="lazy" width="2000" height="307" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2021/10/11-clickhouse-benchmark-uncompressed-chunk--1-.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2021/10/11-clickhouse-benchmark-uncompressed-chunk--1-.jpg 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2021/10/11-clickhouse-benchmark-uncompressed-chunk--1-.jpg 1600w, https://timescale.ghost.io/blog/content/images/2021/10/11-clickhouse-benchmark-uncompressed-chunk--1-.jpg 2000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Query latency performance when </span><code spellcheck="false" style="white-space: pre-wrap;"><span>lastpoint</span></code><span style="white-space: pre-wrap;"> and </span><code spellcheck="false" style="white-space: pre-wrap;"><span>groupby-orderby-limit</span></code><span style="white-space: pre-wrap;"> queries use an uncompressed chunk in TimescaleDB</span></figcaption></figure><p>One of the key takeaways from this last set of queries is that the features provided by a database can have a material impact on the performance of your application. Sometimes, it just works, while other times, having the ability to fine-tune how data is stored can be a game-changer.</p><h3 id="read-latency-performance-summary">Read latency performance summary</h3><ul><li>For simple queries, TimescaleDB outperforms ClickHouse, regardless of whether native compression is used.</li><li>For typical aggregates, even across many values and items, TimescaleDB outperforms ClickHouse.</li><li>Doing more complex double rollups, ClickHouse outperforms TimescaleDB every time. To some extent, we were surprised by the gap and will continue to understand how we can better accommodate queries like this on raw time-series data. One solution to this disparity in a real application would be to use a continuous aggregate to pre-aggregate the data.</li><li>When selecting rows based on a threshold, TimescaleDB outperforms ClickHouse and is up to 250% faster.</li><li>For some complex queries, particularly a standard query like "lastpoint", TimescaleDB vastly outperforms ClickHouse</li><li>Finally, depending on the time range being queried, TimescaleDB can be significantly faster (up to 1760%) than ClickHouse for grouped and ordered queries. When these kinds of queries reach further back into compressed chunks, ClickHouse outperforms TimescaleDB because more data must be decompressed to find the appropriate max() values to order by.</li></ul><h2 id="conclusion">Conclusion</h2><p>You made it to the end! Thank you for taking the time to read our detailed report.</p><p>Understanding ClickHouse and then comparing it with PostgreSQL and TimescaleDB made us appreciate that there is a lot of choice in today’s database market—but often, there is still only one <em>right tool for the job</em>. </p><p>Before deciding which to use for your application, we recommend taking a step back and analyzing your stack, your team's skills, and your needs, now and in the future. Choosing the best technology for your situation now can make all the difference down the road. Instead, you want to pick an architecture that evolves and grows with you, not one that forces you to start all over when the data starts flowing from production applications.</p><p>We’re always interested in feedback, and we’ll continue to share our insights with the greater community.</p><h3 id="want-to-learn-more-about-timescaledb">Want to learn more about TimescaleDB?<br></h3><p><a href="https://console.cloud.timescale.com/signup"><strong>Create a free account to get started</strong></a> with a fully managed TimescaleDB instance (100&nbsp;% free for 30 days).</p><p>Want to host TimescaleDB yourself? <a href="https://github.com/timescale/timescaledb">Visit our GitHub</a> to learn more about options, get installation instructions, and more (and, as always, ⭐️  are  appreciated!)</p><p><a href="https://slack.timescale.com/">Join our Slack community</a> to ask questions, get advice, and connect with other developers (the authors of this post, as well as our co-founders, engineers, and passionate community members, are active on all channels).</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Create (Lots of) Sample Time-Series Data With PostgreSQL generate_series()]]></title>
            <description><![CDATA[Use generate_series to see how TimescaleDB uses PostgreSQL's rock-solid foundation to build a scalable, fully extensible, powerful time-series database.]]></description>
            <link>https://www.tigerdata.com/blog/how-to-create-lots-of-sample-time-series-data-with-postgresql-generate_series</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/how-to-create-lots-of-sample-time-series-data-with-postgresql-generate_series</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Time Series Data]]></category>
            <category><![CDATA[General]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Thu, 26 Aug 2021 18:05:53 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/10/Screenshot-2023-10-12-at-5.55.04-PM.png">
            </media:content>
            <content:encoded><![CDATA[<p>As the makers of <a href="https://www.timescale.com" rel="noreferrer">TimescaleDB</a>, we often need to quickly create lots of sample time-series data to demonstrate a new database feature, run a benchmark, or talk about use cases internally. We also see users in our <a href="https://slack.timescale.com/">community Slack</a> asking how they can test a feature or decide if their data is a good fit for TimescaleDB. </p><p><a href="https://www.timescale.com/learn/is-postgres-partitioning-really-that-hard-introducing-hypertables" rel="noreferrer"><em>📝 If you want to know why people want to use Timescale, check this out.</em></a></p><p>Although using real data from your current application would be great (and ideal),  knowing <strong>how to quickly create a representative time-series dataset using varying cardinalities and different lengths of time</strong> is a helpful - and advantageous - skill to have.</p><p>Fortunately PostgreSQL provides a built-in function to help us create this sample data using the SQL that we already know and love - no external tools required.</p><p>In this three-part blog series, we'll go through a few ways to use the <code>generate_series()</code> function to create large datasets in PostgreSQL, including:</p><ul><li>What PostgreSQL <code>generate_series()</code> is and how to use it for basic data generation </li><li>How to create more realistic-looking time-series data with custom <a href="https://www.tigerdata.com/learn/understanding-postgresql-user-defined-functions" rel="noreferrer">PostgreSQL functions</a></li><li>Ways to create complex time-series data using additional PostgreSQL math functions and JOINs.</li></ul><p>By the end of this series, you'll be ready to test almost any PostgreSQL or TimescaleDB feature, create quick datasets for general testing, meetup presentations, demos, and more!</p><h2 id="intro-to-postgresql-generateseries">Intro to PostgreSQL generate_series()</h2><p><code>generate_series()</code> is a built-in <a href="https://www.tigerdata.com/blog/function-pipelines-building-functional-programming-into-postgresql-using-custom-operators" rel="noreferrer">PostgreSQL function</a> that makes it easy to create ordered tables of numbers or dates. The PostgreSQL documentation calls it a <a href="https://www.postgresql.org/docs/13/functions-srf.html">Set Returning Function</a> because it can return more than one row. </p><p>The function is simple to start, taking at least two required arguments to specify the start and stop parameters for the generated data.</p><pre><code class="language-sql">SELECT * FROM generate_series(1,5);</code></pre><p>This will produce output that looks like this:</p><pre><code class="language-sql">generate_series|
---------------|
              1|
              2|
              3|
              4|
              5|</code></pre><p>This first example shows that <code>generate_series()</code> returns sequential numbers between a start parameter and a stop parameter. When used to generate numeric data, <code>generate_series()</code>a  will increment the values by 1. However, an optional third parameter can be used to specify the increment length, known as the step parameter.</p><p>For example, if we wanted to generate rows that counted by two from 0 to 10, we could use this SQL instead:</p><pre><code class="language-sql">SELECT * from generate_series(0,10,2);

 generate_series 
-----------------
               0
               2
               4
               6
               8
              10</code></pre><h2 id="postgresql-generateseries-with-dates">PostgreSQL generate_series() With Dates</h2><p>Using <code>generate_series()</code> to produce a range of dates is equally straightforward. The only difference is that the third parameter, the step <code>INTERVAL</code> used to increment the date, is <strong><em>required </em></strong>for date generation, as shown here:</p><pre><code class="language-sql">SELECT * from generate_series(
	'2021-01-01',
    '2021-01-02', INTERVAL '1 hour'
  );

    generate_series     
------------------------
 2021-01-01 00:00:00+00
 2021-01-01 01:00:00+00
 2021-01-01 02:00:00+00
 2021-01-01 03:00:00+00
 2021-01-01 04:00:00+00
 2021-01-01 05:00:00+00
 2021-01-01 06:00:00+00
 2021-01-01 07:00:00+00
 2021-01-01 08:00:00+00
 2021-01-01 09:00:00+00
 2021-01-01 10:00:00+00
 2021-01-01 11:00:00+00
 2021-01-01 12:00:00+00
 2021-01-01 13:00:00+00
 2021-01-01 14:00:00+00
 2021-01-01 15:00:00+00
 2021-01-01 16:00:00+00
 2021-01-01 17:00:00+00
 2021-01-01 18:00:00+00
 2021-01-01 19:00:00+00
 2021-01-01 20:00:00+00
 2021-01-01 21:00:00+00
 2021-01-01 22:00:00+00
 2021-01-01 23:00:00+00
 2021-01-02 00:00:00+00
(25 rows)</code></pre><p>Notice that the returned dates are <em>inclusive</em> of the start and stop values, just as we saw with the numeric example before. The reason we got 25 rows (representing 25 hours rather than 24 as you might expect) is that the stop value can be reached using the equal one-hour <code>INTERVAL</code> (the step parameter). As long as the <code>INTERVAL</code> can increment evenly up to the stop date, it will be included.</p><p>However, if the step interval resulted in the stop value being skipped over, it will not be included in your output. For example, if we modify the step <code>INTERVAL</code> above to '1 hour 25 minutes', the result only returns 17 rows, the last of which is before the stop value.</p><pre><code class="language-sql">SELECT * from generate_series(
	'2021-01-01','2021-01-02', 
    INTERVAL '1 hour 25 minutes'
   );

    generate_series     
------------------------
 2021-01-01 00:00:00+00
 2021-01-01 01:25:00+00
 2021-01-01 02:50:00+00
 2021-01-01 04:15:00+00
 2021-01-01 05:40:00+00
 2021-01-01 07:05:00+00
 2021-01-01 08:30:00+00
 2021-01-01 09:55:00+00
 2021-01-01 11:20:00+00
 2021-01-01 12:45:00+00
 2021-01-01 14:10:00+00
 2021-01-01 15:35:00+00
 2021-01-01 17:00:00+00
 2021-01-01 18:25:00+00
 2021-01-01 19:50:00+00
 2021-01-01 21:15:00+00
 2021-01-01 22:40:00+00
(17 rows)</code></pre><h2 id="how-to-generate-time-series-data">How to Generate Time-Series Data</h2><p>Now that we understand how to use <code>generate_series()</code>, how do we create some <a href="https://timescale.ghost.io/blog/time-series-data/">time-series data</a> to insert into TimescaleDB for testing and visualization? </p><p>To do this, we utilize a standard feature of relational databases and SQL. Recall that <code>generate_series()</code> is a Set Returning Function that returns a "table" of data (a set) just as if we had selected it from a table. Therefore, just like when we select data from a regular table with SQL, we can add more columns of data using other functions or static values. </p><p>You may have done this at some point with a string of text that needed to be repeated for each returned row.</p><pre><code class="language-sql">SELECT 'Hello Timescale!' as myStr, * FROM generate_series(1,5);

     myStr     | generate_series 
------------------+-----------------
 Hello Timescale! |               1
 Hello Timescale! |               2
 Hello Timescale! |               3
 Hello Timescale! |               4
 Hello Timescale! |               5
(5 rows)</code></pre><p>In this example, we simply added data to the rows being returned from <code>generate_series()</code>. For every row it returned, we added a column with some static text.</p>
<p>But, these added columns don't have to be static data! Using the <a href="https://www.postgresql.org/docs/13/functions-math.html">built-in <code>random()</code></a> function (for example), we can generate data that starts to look a little more like data we'd see when monitoring computer CPU values (i.e., realistic data to use for our demos and tests!).</p><pre><code class="language-sql">SELECT random()*100 as CPU, * FROM generate_series(1,5);

        cpu         | generate_series 
--------------------+-----------------
 48.905450626783775 |               1
  71.94031820213382 |               2
 25.210553719011486 |               3
  19.24163308357194 |               4
  8.434915599133674 |               5
(5 rows)</code></pre><p>In this example, <code>generate_series()</code> produced five rows of data; for every row, PostgreSQL also executed the <code>random()</code> function to produce a value. With a little creativity, this can become a pretty efficient method for generating lots of data quickly.</p><h2 id="performance-considerations-when-generating-sample-data">Performance Considerations When Generating Sample Data</h2><h3 id="functions">Functions</h3><p>Before getting too deep into the many ways we can use various functions with <code>generate_series()</code> to create more realistic data, let's tackle one potential downside with functions before your mind starts thinking about a myriad of ways to produce interesting, sample data. </p><p>Functions are a powerful database tool, allowing complex code to be hidden behind a standard interface. In the example above, I don't have to know how <code>random()</code> produces a value. I just have to call it in my SQL statement (without arguments) and a random value is returned for every row.</p><p>Unfortunately, functions can slow down your query and use lots of resources if you're not careful. This is true any time you call functions in SQL, not just when creating sample data.</p><p>The reason for this inefficiency is that scalar functions are executed once for each column and row in which they are used. So, if you produce a set of 1,000 rows with <code>generate_series()</code> and then add on 5 additional columns with data generated by functions, PostgreSQL has to effectively process 5,001 function calls, once to generate the initial series of data which is returned as a set, and 5 times that for each row because of the additional 5 columns.</p><p>The main issue is that PostgreSQL has no idea how to determine how much "work" it will take to generate the result from each function in the query. Something like <code>random()</code> required very minimal effort, so calling it 5,000 or even 1 million times won't break the bank for most machines. However, if the function you're calling generates lots of temporary data internally before producing a result, be aware that it could perform more slowly and consume more resources on your database server.</p><p><em>The takeaway here is that nothing comes for free, especially when functions are called once for every column, for every row</em>. Most of the time the data you generate will easily complete in a handful of seconds for tens of millions of rows. But, if you notice a query that generates data taking a long time, consider finding alternative ways to generate data for the columns that require more resources.</p><h3 id="transactions">Transactions</h3><p>There is a second caveat to be aware of when using a tool like <code>generate_series()</code>. All data is created and inserted as a single transaction. You <em>can</em> put in more time and effort to create functions and methods for breaking up the work, but understand if you create (SELECT) one large set of data with 100 million rows, it's likely to consume a lot of memory and CPU on the server while the process occurs.</p><p>This doesn't mean it's broken or any less valid of a method for generating data. However, as we demonstrate additional ways to create more realistic data in parts 2 and 3 of this series, recognize that the more data you create, the more resources you'll need from the server without managing batches and transactions with a more advanced setup.</p><p>Alright, let's get back to the fun!</p><h2 id="tips-for-quickly-increasing-sample-dataset-scale">Tips for Quickly Increasing Sample Dataset Scale</h2><p>So far we've looked at how <code>generate_series()</code> works and how you can use functions to add additional dynamic content to the output. But how do we quickly get to the scale of tens of millions of rows (or more)?</p><p>This is where the concept of a Cartesian product comes into play with databases. A Cartesian product (otherwise known as a <a href="https://www.tigerdata.com/learn/what-is-a-sql-cross-join" rel="noreferrer">CROSS JOIN</a>) takes two or more result sets (rows from multiple tables or functions) and produces a new result set that contains rows equal to the count of the first set multiplied by the count of the second set. </p><p>In doing so, the database outputs every row from the first table with the value of the first row from the second table. It does this over and over until all rows in both tables have been iterated. (and yes, if you join more than two tables this way, the process just keeps multiplying, tables processed left to right)</p><p>Let's look at a small example using two <code>generate_series()</code> in the same select statement.</p><pre><code class="language-sql">SELECT * from generate_series(1,10) a, generate_series(1,2) b;

a |b|
--+-+
 1|1|
 2|1|
 3|1|
 4|1|
 5|1|
 6|1|
 7|1|
 8|1|
 9|1|
10|1|
 1|2|
 2|2|
 3|2|
 4|2|
 5|2|
 6|2|
 7|2|
 8|2|
 9|2|
10|2|</code></pre><p>As you can see, PostgreSQL generated 10 rows for the first series and 2 rows for the second series. After processing and iterating over each table consecutively, the query produced a total of 20 rows (10 x 2). For generating lots of data quickly, this is about as easy as it gets!</p><p>The same process applies when using <code>generate_series()</code> to return a set of dates. If you (effectively) CROSS JOIN multiple sets (numbers or dates), the total number of rows in the final set will be a product of all sets. Again, for generating sample time-series data, you'd be hard-pressed to find an easier method!</p><p>In this example, we generate 12 timestamps an hour apart, a random value representing CPU usage, and then a second series of four values that represent IDs for fake devices. This should produce 48 rows (eg. 12 timestamps x 4 device IDs = 48 rows).</p><pre><code class="language-sql">SELECT time, device_id, random()*100 as cpu_usage 
FROM generate_series(
	'2021-01-01 00:00:00',
    '2021-01-01 11:00:00',
    INTERVAL '1 hour'
  ) as time, 
generate_series(1,4) device_id;


time               |device_id|cpu_usage          |
-------------------+---------+-------------------+
2021-01-01 00:00:00|        1|0.35415126479989567|
2021-01-01 01:00:00|        1| 14.013393572770028|
2021-01-01 02:00:00|        1|   88.5015939122006|
2021-01-01 03:00:00|        1|  97.49037810105996|
2021-01-01 04:00:00|        1|  50.22781125586846|
2021-01-01 05:00:00|        1|  77.93431470586931|
2021-01-01 06:00:00|        1|  45.73481750582076|
2021-01-01 07:00:00|        1|   70.7999843735724|
2021-01-01 08:00:00|        1|   4.72949831884506|
2021-01-01 09:00:00|        1|  85.29122113229981|
2021-01-01 10:00:00|        1| 14.539664281598874|
2021-01-01 11:00:00|        1|  45.95244258556228|
2021-01-01 00:00:00|        2|  46.41196423062297|
2021-01-01 01:00:00|        2|  74.39903569177027|
2021-01-01 02:00:00|        2|  85.44087332221935|
2021-01-01 03:00:00|        2|  4.329394730750735|
2021-01-01 04:00:00|        2| 54.645873866589056|
2021-01-01 05:00:00|        2|  6.544334492894777|
2021-01-01 06:00:00|        2|  39.05071228953645|
2021-01-01 07:00:00|        2|  71.07264365438404|
2021-01-01 08:00:00|        2|   72.4732704336219|
2021-01-01 09:00:00|        2| 34.533280927542975|
2021-01-01 10:00:00|        2| 26.764760864598003|
2021-01-01 11:00:00|        2|  62.32048879645227|
2021-01-01 00:00:00|        3|  63.01888063314749|
2021-01-01 01:00:00|        3|  21.70606884856987|
2021-01-01 02:00:00|        3|  32.47610779097485|
2021-01-01 03:00:00|        3| 47.565982341726354|
2021-01-01 04:00:00|        3|  64.34867263419619|
2021-01-01 05:00:00|        3|  57.74424991855476|
2021-01-01 06:00:00|        3| 55.593286571750156|
2021-01-01 07:00:00|        3|  36.92650110894995|
2021-01-01 08:00:00|        3| 53.166926049881624|
2021-01-01 09:00:00|        3| 10.009505806123897|
2021-01-01 10:00:00|        3| 58.067700285561585|
2021-01-01 11:00:00|        3|  81.58883725078034|
2021-01-01 00:00:00|        4|   78.1768041898232|
2021-01-01 01:00:00|        4|  84.51505102850199|
2021-01-01 02:00:00|        4| 24.029611792753514|
2021-01-01 03:00:00|        4|  17.08996115345549|
2021-01-01 04:00:00|        4| 29.642690955760997|
2021-01-01 05:00:00|        4|  90.83844806413275|
2021-01-01 06:00:00|        4| 6.5019080489854275|
2021-01-01 07:00:00|        4|    32.336484070672|
2021-01-01 08:00:00|        4|    55.595524107963|
2021-01-01 09:00:00|        4|   97.5442141375293|
2021-01-01 10:00:00|        4|   37.0741925805568|
2021-01-01 11:00:00|        4| 19.093927249791776|</code></pre><h2 id="choosing-a-date-range">Choosing a Date Range</h2><p>Now it's time to put all of the features and concepts together. We've seen how to use <code>generate_series()</code> to create a sample table of data (both numbers and dates), add static and dynamic content to each row, and finally how to join multiple sets of data together to create a deterministic number of rows to create test data.</p><p>The final piece of the puzzle is to figure out how to create date ranges that are more dynamic using date math. Once again, PostgreSQL makes this straightforward because date math is handled automatically.</p><p>When generating sample data, you usually have an idea of the duration of time you want to generate—one month, six months, or a year—for instance. But calculating your use case's exact start and end timestamps can get pretty tedious. </p><p>Instead, it's often easier to use <code>now()</code> or a static ending timestamp (ie. '2021-06-01 00:00:00'), and then using date math to get the starting timestamp based on your chosen interval. This makes it <em>very</em> easy to generate data for different durations simply by changing the interval. And to be clear, you can go the other direction (picking a start timestamp and adding time), but that can often lead to data in the future.</p><p>Let's look at three examples to demonstrate ways of creating dynamic date ranges.</p><h3 id="create-six-months-of-data-with-one-hour-intervals-ending-now">Create six months of data with one-hour intervals, ending now()</h3><pre><code class="language-sql">SELECT time, device_id, random()*100 as cpu_usage 
FROM generate_series(
	now() - INTERVAL '6 months',
    now(),
    INTERVAL '1 hour'
   ) as time, 
generate_series(1,4) device_id;</code></pre><p>Notice that we use the PostgreSQL function <code>now()</code> to automatically choose the ending timestamp, and then use date math (- INTERVAL '6 months') to let PostgreSQL find the starting timestamp for us. With almost no effort, we can easily generate weeks, months, or years of time-series data by changing the <code>INTERVAL</code> we subtract from <code>now()</code>.</p><h3 id="create-one-year-of-data-with-one-hour-intervals-ending-on-a-timestamp">Create one year of data with one-hour intervals, ending on a timestamp</h3><pre><code class="language-sql">SELECT time, device_id, random()*100 as cpu_usage 
FROM generate_series(
	'2021-08-01 00:00:00' - INTERVAL '6 months',
    '2021-08-01 00:00:00',
    INTERVAL '1 hour'
  ) as time, 
generate_series(1,4) device_id;
</code></pre><p>This example is the same as the first, but here we specify the timestamp that we want to end on. PostgreSQL can still do the date math for us (subtracting 6 months in this example), but we get control over the exact ending timestamp to use. This is particularly useful when you want the actual time portion of the timestamp to begin and end on even, rounded hours, minutes, or seconds. When we use <code>now()</code>, the timestamp can produce (what feels like) random timestamp causing all of the time-series data to have timestamps that are increments of <code>now()</code>.</p><h3 id="create-1-year-of-data-with-one-hour-intervals-beginning-on-a-timestamp">Create 1 year of data with one-hour intervals, beginning on a timestamp</h3><p>For this last example, we're going to specify the start timestamp instead. This can be useful when you need to test a specific fiscal period or maybe some logic that deals with the turning of each year. In this case, maybe you just need a month of data but it needs to cross over from one year to the next. In that case, trying to figure out the math of how far back to start and how far to go forward might be more complicated than you want to worry about.</p><pre><code class="language-sql">SELECT time, device_id, random()*100 as cpu_usage 
FROM generate_series(
	'2020-12-15 00:00:00',
    '2020-12-15 00:00:00' + INTERVAL '2 months',
    INTERVAL '1 hour'
  ) as time, 
generate_series(1,4) device_id;
</code></pre><p>Each of these examples uses date math to help you find the appropriate start and end timestamps so that you can easily adjust to create more or less data while still fitting the range profile that you need. </p><p>Speaking of calculating how many rows you want to generate… 😉</p><h2 id="calculating-total-rows">Calculating Total Rows</h2><p>We now have all the tools we need to create datasets of almost any size. For time-series data specifically, it's a straightforward calculation to figure out how many rows your query will generate.</p><p><em>Total Rows = Readings per hour * Total hours * Number of "things" being tracked</em></p><p>Using this formula, we can quickly determine how many rows will be created by changing any combination of the total range (start/end timestamps), how many readings per hour for each item, or the total number of items. Let's look at a couple of examples to get an idea of how quickly you could create many rows of time-series data for your testing scenario.</p><table>
<thead>
<tr>
<th>Range of readings</th>
<th>Length of interval</th>
<th>Number of "devices"</th>
<th>Total rows</th>
</tr>
</thead>
<tbody>
<tr>
<td>1 year</td>
<td>1hour</td>
<td>4</td>
<td>35,040</td>
</tr>
<tr>
<td>1 year</td>
<td>10 minutes</td>
<td>100</td>
<td>5,256,000</td>
</tr>
<tr>
<td>6 months</td>
<td>5 minutes</td>
<td>1,000</td>
<td>52,560,000</td>
</tr>
</tbody>
</table>
<p>As you can see, the numbers start to add up <em>very</em> quickly.</p><h2 id="lets-review">Let's Review</h2><p>In this first post, we've demonstrated that it's pretty easy to generate lots of data using the PostgreSQL <code>generate_series()</code> function. We also learned that when you select multiple sets (using <code>generate_series()</code> or selecting from tables and functions), PostgreSQL will produce what's known as a Cartesian product, the selection of all rows from all tables - the product of all rows. With this knowledge, we can quickly create large and diverse datasets to test various features of PostgreSQL and TimescaleDB.</p><h2 id="keep-learning-about-generating-sample-data">Keep Learning About Generating Sample Data</h2><p>This was part 1 of the three-part series: </p><ul><li><strong>Read part 2: </strong><a href="https://timescale.ghost.io/blog/blog/generating-more-realistic-sample-time-series-data-with-postgresql-generate_series/"><strong>Generating more realistic sample time-series data with PostgreSQL <code>generate_series()</code></strong></a>. Learn how to use custom user-defined functions to create more realistic-looking data to use for testing, including generated text, numbers constrained by a range, and even fake JSON data.</li><li><strong>Read part 3: </strong><a href="https://timescale.ghost.io/blog/blog/how-to-shape-sample-data-with-postgresql-generate_series-and-sql/"><strong>How to shape sample data with PostgreSQL <code>generate_series()</code> and SQL</strong></a><strong>. </strong>Learn how to add shape and trends into your sample time-series data (e.g., increasing web traffic over time and quarterly sales cycles) using the formatting functions in this post in conjunction with relational lookup tables and additional mathematical functions. Knowing how to manipulate the pattern of generated data is particularly useful for visualizing time-series data and learning analytical PostgreSQL or TimescaleDB functions.</li><li><strong>Watch the video: </strong></li></ul><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/t5ULlC1MYWU?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></figure><p></p><h2 id="try-it-yourself-with-timescaledb">Try it Yourself With TimescaleDB </h2><p>If you are not using TimescaleDB yet,&nbsp;<a href="https://www.timescale.com/?ref=timescale.com" rel="noreferrer">take a look</a>. It's a <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extension</a> that&nbsp;<a href="https://timescale.ghost.io/blog/postgresql-timescaledb-1000x-faster-queries-90-data-compression-and-much-more/" rel="noreferrer">will make your queries faster</a> via&nbsp;<a href="https://www.timescale.com/?ref=timescale.com" rel="noreferrer">automatic partitioning</a>, query planner enhancements, improved materialized views,&nbsp;<a href="https://timescale.ghost.io/blog/building-columnar-compression-in-a-row-oriented-database/" rel="noreferrer">columnar compression</a>, and much more.&nbsp;<a href="https://www.tigerdata.com/blog/building-columnar-compression-in-a-row-oriented-database" rel="noreferrer">It also comes with awesome functionality for time-series data analysis. </a></p><p>If you're running your PostgreSQL database in your own hardware,&nbsp;<a href="https://docs.timescale.com/self-hosted/latest/install/?ref=timescale.com" rel="noreferrer">you can simply add the TimescaleDB extension</a>. If you prefer to try Timescale in AWS,&nbsp;<a href="https://console.cloud.timescale.com/signup?ref=timescale.com" rel="noreferrer">create a free account on our platform</a>. </p><p>See what's possible when you <a href="https://www.tigerdata.com/learn/what-is-a-sql-inner-join" rel="noreferrer">join PostgreSQL</a> with time-series superpowers!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Hacking NFL Data With PostgreSQL, TimescaleDB, and SQL]]></title>
            <description><![CDATA[Learn how to use time-series data provided by the NFL to uncover insights.]]></description>
            <link>https://www.tigerdata.com/blog/hacking-nfl-data-with-postgresql-timescaledb-and-sql</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/hacking-nfl-data-with-postgresql-timescaledb-and-sql</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Time Series Data]]></category>
            <category><![CDATA[Analytics]]></category>
            <dc:creator><![CDATA[Attila Toth]]></dc:creator>
            <pubDate>Tue, 27 Jul 2021 13:32:25 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2021/07/ameer-basheer-Yzef5dRpwWg-unsplash--1-.jpg">
            </media:content>
            <content:encoded><![CDATA[<p><em>Learn how to use time-series data provided by the NFL to uncover valuable insights into many player performance metrics—and ways to apply the same methods to improve your fantasy league team, your knowledge of the game, or your viewing experience—all with PostgreSQL, standard SQL, and freely available extensions.</em></p><p>Time-series data is everywhere, including, much to our surprise, the world of professional sports. At Timescale, we're always looking for fun ways to showcase the expanding reach of time-series data. <a href="https://docs.timescale.com/timescaledb/latest/tutorials/analyze-intraday-stocks/">Stock</a>, <a href="https://docs.timescale.com/timescaledb/latest/tutorials/analyze-cryptocurrency-data/">cryptocurrency</a>, <a href="https://docs.timescale.com/timescaledb/latest/tutorials/nyc-taxi-cab/">IoT</a>, and <a href="https://docs.timescale.com/timescaledb/latest/tutorials/promscale/">infrastructure metrics</a> data are relatively common and widely understood time-series data scenarios. Head to Twitter on any given day, search for <a href="https://twitter.com/hashtag/TimeSeries">#timeseries</a> or <a href="https://twitter.com/hashtag/TimescaleDB">#TimescaleDB</a>, and you're sure to find questions about high-frequency trading or massive-scale observability data with tools like Prometheus.</p><p>You can imagine our excitement, then, when we happened upon the <a href="https://operations.nfl.com/gameday/analytics/big-data-bowl/">NFL Big Data Bowl</a>, an annual competition that encourages the data science community to use historical player position and play data to create machine learning models. </p><p>Did the NFL <strong><em>really</em></strong> give access to 18+ million rows of detailed play data from every regular season NFL game?</p>
<!--kg-card-begin: html-->
<div style="width:100%;height:0;padding-bottom:100%;position:relative;"><iframe src="https://giphy.com/embed/2w3uNmvsIZ09hNzgzo" width="100px" height="100px" style="position:absolute; min-width: 100%; min-height: 100%" frameBorder="0" class="giphy-embed" allowFullScreen></iframe></div><p><a href="https://giphy.com/gifs/nfl-colts-indianapolis-2w3uNmvsIZ09hNzgzo">via GIPHY</a></p>
<!--kg-card-end: html-->
<p>For background, the National Football League (NFL) is the US professional sports league for American football, and the NFL season is followed by tens of millions of people, culminating in the annual Super Bowl (which attracts 100M+ global viewers, whether for the game or for the commercials). </p><p>Each NFL game takes place as a series of “plays,” in which the two teams try to score and prevent the other team from scoring. There are approximately 200 plays per game, with up to 15 games a week during the regular season. A healthy amount of data, but nothing unmanageable.  </p><p>So, at first glance, football game metrics might not immediately jump out as anything special. </p><p>But then the NFL did something pretty ambitious and amazing. </p><p>All <a href="https://operations.nfl.com/gameday/technology/nfl-next-gen-stats/">NFL players are equipped with RFID chips</a> that track players’ position, speed, and various other metrics, which teams use to identify trends, mitigate risks, and continuously optimize.  The NFL started tracking and storing data for every player on the field, for every play, for every game. </p><p>As a result, we now have access to a very detailed analysis of exactly how a play unfolded, how quickly various players accelerated during each play, and the play’s outcome. A traditional view of play-by-play metrics is “down and distance” and the result of the play (yards gained, whether or not there was a score, and so on). With the NFL’s dataset, we're able to mine approximately 100 data points at 100-millisecond intervals throughout the play to see speed, distance, involved players, and much more.</p><p>This isn’t ordinary data. <a href="https://timescale.ghost.io/blog/blog/what-the-heck-is-time-series-data-and-why-do-i-need-a-time-series-database-dcf3b1b18563/">This is time-series data</a>. Time-series data is a sequence of data points collected over time intervals, giving us the ability to track changes over time. In the case of the NFL’s dataset, we have time-series data that represents how a play changes, including the locations of the players on the field, the location of the ball, the relative acceleration of players in the field of play, and so much more.</p><p>Time-series data comes at you fast, sometimes generating millions of data points per second (<a href="https://timescale.ghost.io/blog/blog/what-the-heck-is-time-series-data-and-why-do-i-need-a-time-series-database-dcf3b1b18563/">read more about time-series data</a>). Because of the sheer volume and rate of information, time-series data can already be complex to query and analyze, which is why we built TimescaleDB, a petabyte-scale, relational database for <a href="https://www.tigerdata.com/blog/time-series-introduction" rel="noreferrer">time series</a>. </p><p>We couldn't pass up the opportunity to look at the NFL dataset with TimescaleDB, exploring ways we could peer deeper into player performance in hopes of providing insights about overall player performance in the coming season. </p><p>Read on for more information about the <a href="https://www.kaggle.com/c/nfl-big-data-bowl-2021/overview">NFL’s dataset</a> and how you can start using it, plus some sample queries to jumpstart your analysis. They may help you get more enjoyment out of the game.</p><p><strong>If you’d like to get started with NFL data, you can spin up a fully managed TimescaleDB service</strong>: create an account to <a href="https://console.cloud.timescale.com/signup">try it for free</a> for 30 days. The instructions later in this post will take you through how to ingest the data and start using it for analysis.</p><p>If you’re new to time-series data or just have some questions you’d like to ask about the dataset, <a href="https://slack.timescale.com">join our public Slack community</a>, where you’ll find Timescale team members and thousands of time-series enthusiasts, and we’ll be happy to help you.</p><h2 id="the-nfl-time-series-dataset">The NFL Time Series Dataset</h2><p><br>Over the last few years, the NFL and Kaggle have collaborated on the <a href="https://www.kaggle.com/c/nfl-big-data-bowl-2021/overview">NFL Big Data Bowl</a>. The goal is to use historical data to answer a predetermined genre of questions, typically producing a machine learning model that can help predict the outcome of certain plays during regular season games.</p><p>Although the 2020/2021 contest is over, the sample dataset they provided from a prior season is still available for download and analysis. The 2020/2021 competition focused on pass-play defense efficiency; therefore, only the tracking data for offensive and defensive "playmakers" is available in the dataset. No offensive or defensive linemen data is included. (You can read more about <a href="https://www.kaggle.com/c/nfl-big-data-bowl-2021/discussion/217170">last year’s winners</a>.)</p><p>(Keep watching the <a href="https://operations.nfl.com/gameday/analytics/big-data-bowl/">NFL website</a> for more information on the next Big Data Bowl.)</p><h2 id="accessing-the-data">Accessing the Data</h2><p><br>For the purposes of this blog post and accompanying tutorial, we will use <a href="https://www.kaggle.com/c/nfl-big-data-bowl-2021/overview">the sample data provided by the NFL</a>. This data is from the 2018 NFL season and is available as CSV files, including game-specific data and week-by-week tracking data for each player involved in the "offensive" part of the pass play. Contest participants in the next season of the contest will have access to new weekly game data.</p><p>This data is also very relational in nature, which means that SQL is a great medium to start gleaning value – without the need for Jupyter notebooks, other data science specific languages (like Python or R), or additional toolsets. </p><p>If you want to follow along - or recreate! - the queries we go through below, <a href="https://docs.timescale.com/timescaledb/latest/tutorials/nfl-analytics/">follow our tutorial</a> to set up the tables, ingest data, and start analyzing data in TimescaleDB. For those unfamiliar with TimescaleDB, it’s built on PostgreSQL, so you’ll find that all of our queries are standard SQL. If you know SQL, you’ll know how to do everything here. (Some of the more advanced query examples we provide require our new, advanced hyperfunctions, which come pre-installed with any <a href="https://console.cloud.timescale.com" rel="noreferrer">Timescale instance</a>.)</p><h2 id="lets-start-exploring-time-series-insights">Let's Start Exploring Time-Series Insights!</h2><p><br>We've provided the steps needed to ingest the dataset into TimescaleDB in the <a href="https://docs.timescale.com/timescaledb/latest/tutorials/nfl-analytics/">accompanying tutorial</a>, so we won’t go into that here. </p><p>The NFL dataset includes the following data:</p>
<ul>
<li><strong>Games</strong>: all relevant data about each game of the regular season, including date, teams, time, and location</li>
<li><strong>Players</strong>: information on each player, including what team they play for and their originating college</li>
<li><strong>Plays</strong>: a wealth of data about each pass play in the game. Helpful fields include the down, description of the play that happened, line of scrimmage, and total offensive yardage, among other details.</li>
<li><strong>Week [1-17]</strong>: for each week of the season, the NFL provides a new CSV file with the tracking data of every player, for every play (pass plays for this data). Interesting fields include X/Y position data (relative to the football field) every few hundred milliseconds throughout each play, player acceleration, and the "type" of a route that was taken. (In our tutorial, this data is imported into the <code>tracking</code> table and totals almost 20 million rows of time-series data.)</li>
</ul>
<p>In addition to the NFL dataset, we also provide some extra data from Wikipedia that includes game scores and stadium conditions for each game, which you can load as part of the tutorial. With other time-series databases, it can be difficult to combine your time-series data with any other data you may have on hand (see <a href="https://timescale.ghost.io/blog/blog/timescaledb-vs-influxdb-for-time-series-data-timescale-influx-sql-nosql-36489299877/">our TimescaleDB vs. InfluxDB comparison</a> for reference). </p><p>Because TimescaleDB is PostgreSQL with time-series superpowers, it supports JOINS, so any extra relational data you want to add for deeper analysis is just a SQL query away. In our case, we’re able to combine the NFL’s play-by-play data along with weather data for each stadium.</p><p>Once you have the data ready, the world of NFL playmakers is at your fingertips, so let’s get started!</p><h2 id="the-power-of-sql">The Power of SQL</h2><p>Year after year, we see SQL listed as one of the most popular languages among developers on the <a href="https://insights.stackoverflow.com/survey/2020#technology-programming-scripting-and-markup-languages-all-respondents">Stack Overflow survey</a>. Sometimes, however, we can be lured into thinking that the only way to gain insights from relational data is to query it with powerful data analytics tools and languages, create data frames, and use specialized regression algorithms before we can do anything productive.</p><p>SQL, it often feels, is only useful for getting and storing data in applications and that we need to leave the "heavy lifting" of analysis to more mature tools.</p><p>Not so! SQL can data munge with the best of them! Let's look at a first, quick example.</p><h3 id="average-yards-per-position-per-game">Average yards per position, per game</h3><p>For this first example, we'll query the <code>tracking</code> table (the player movement data from all 17 weeks of games) and join to the <code>game</code> table to determine the number of yards per player position, per game.</p>
<p>The results give you a quick overview of how many yards different positions ran throughout each game. You could use this later to compare specific players to see how they compared, more or less yards, to that total.</p><pre><code class="language-SQL">WITH total_position_yards AS (
	SELECT sum(dis) position_yards, POSITION, gameid FROM tracking t 
	GROUP BY POSITION, gameid)
SELECT avg(position_yards), position, game_date
FROM game g
INNER JOIN total_position_yards tpy ON g.game_id = tpy.gameid
WHERE POSITION IN ('QB','RB','WR','TE')
GROUP BY game_date, POSITION;
</code></pre>
<h3 id="number-of-plays-by-offensive-player"><br>Number of plays by offensive player</h3><p>As a season progresses and players get injured (or traded), it's helpful to know which of the available players have more playing experience, rather than those that have been sitting on the sideline for most of the season. Players with more playing time are often able to contribute to the outcome of the game.</p><p>This query finds all players that were on the offense for any play and counts how many total passing plays they have been a part of, ordered by total passing plays descending.</p><pre><code class="language-SQL">WITH snap_events AS (
-- Create a table that filters the play events to show only snap plays
-- and display the players team information
 SELECT DISTINCT player_id, t.event, t.gameid, t.playid,
   CASE
     WHEN t.team = 'away' THEN g.visitor_team
     WHEN t.team = 'home' THEN g.home_team
     ELSE NULL
     END AS team_name
 FROM tracking t
 LEFT JOIN game g ON t.gameid = g.game_id
 WHERE t.event IN ('snap_direct','ball_snap')
)
-- Count these events &amp; filter results to only display data when the player was
-- on the offensive
SELECT a.player_id, pl.display_name, COUNT(a.event) AS play_count, a.team_name
FROM snap_events a
LEFT JOIN play p ON a.gameid = p.gameid AND a.playid = p.playid
LEFT JOIN player pl ON a.player_id = pl.player_id
WHERE a.team_name = p.possessionteam
GROUP BY a.player_id, pl.display_name, a.team_name
ORDER BY play_count DESC;
</code></pre>
<table>
<thead>
<tr>
<th>player_id</th>
<th>display_name</th>
<th>play_count</th>
<th>team_name</th>
</tr>
</thead>
<tbody>
<tr>
<td>2506109</td>
<td>Ben Roethlisberger</td>
<td>725</td>
<td>PIT</td>
</tr>
<tr>
<td>2558149</td>
<td>JuJu Smith-Schuster</td>
<td>691</td>
<td>PIT</td>
</tr>
<tr>
<td>2533031</td>
<td>Andrew Luck</td>
<td>683</td>
<td>IND</td>
</tr>
<tr>
<td>2508061</td>
<td>Antonio Brown</td>
<td>679</td>
<td>PIT</td>
</tr>
<tr>
<td>310</td>
<td>Matt Ryan</td>
<td>659</td>
<td>ATL</td>
</tr>
<tr>
<td>2506363</td>
<td>Aaron Rodgers</td>
<td>656</td>
<td>GB</td>
</tr>
<tr>
<td>2505996</td>
<td>Eli Manning</td>
<td>639</td>
<td>NYG</td>
</tr>
<tr>
<td>2543495</td>
<td>Davante Adams</td>
<td>630</td>
<td>GB</td>
</tr>
<tr>
<td>2540158</td>
<td>Zach Ertz</td>
<td>629</td>
<td>PHI</td>
</tr>
<tr>
<td>2532820</td>
<td>Kirk Cousins</td>
<td>621</td>
<td>MIN</td>
</tr>
<tr>
<td>79860</td>
<td>Matthew Stafford</td>
<td>619</td>
<td>DET</td>
</tr>
<tr>
<td>2504211</td>
<td>Tom Brady</td>
<td>613</td>
<td>NE</td>
</tr>
</tbody>
</table>
<p>If you’re familiar with American football, you might know that players are substituted in and out of the game based on game conditions. Stronger, larger players may play in some situations, while faster, more agile players may play in others. </p><p>Quarterbacks, however, are the most “important” players on the field, and tend to play more than others. However, by omitting quarterbacks, we can get a deeper insight into players across all other positions.</p><pre><code class="language-SQL">WITH snap_events AS (
-- Create a table that filters the play events to show only snap plays
-- and display the players team information
 SELECT DISTINCT player_id, t.event, t.gameid, t.playid,
   CASE
     WHEN t.team = 'away' THEN g.visitor_team
     WHEN t.team = 'home' THEN g.home_team
     ELSE NULL
     END AS team_name
 FROM tracking t
 LEFT JOIN game g ON t.gameid = g.game_id
 WHERE t.event IN ('snap_direct','ball_snap')
)
-- Count these events &amp; filter results to only display data when the player was
-- on the offensive
SELECT a.player_id, pl.display_name, COUNT(a.event) AS play_count, a.team_name, pl."position"
FROM snap_events a
LEFT JOIN play p ON a.gameid = p.gameid AND a.playid = p.playid
LEFT JOIN player pl ON a.player_id = pl.player_id
WHERE a.team_name = p.possessionteam AND pl."position" != 'QB'
GROUP BY a.player_id, pl.display_name, a.team_name, pl."position"
ORDER BY play_count DESC;
</code></pre>
<p>So, now we can see the non-quarterbacks who are on offense the most in a season:</p><table>
<thead>
<tr>
<th>player_id</th>
<th>display_name</th>
<th>play_count</th>
<th>team_name</th>
<th>position</th>
</tr>
</thead>
<tbody>
<tr>
<td>2558149</td>
<td>JuJu Smith-Schuster</td>
<td>691</td>
<td>PIT</td>
<td>WR</td>
</tr>
<tr>
<td>2508061</td>
<td>Antonio Brown</td>
<td>679</td>
<td>PIT</td>
<td>WR</td>
</tr>
<tr>
<td>2543495</td>
<td>Davante Adams</td>
<td>630</td>
<td>GB</td>
<td>WR</td>
</tr>
<tr>
<td>2540158</td>
<td>Zach Ertz</td>
<td>629</td>
<td>PHI</td>
<td>TE</td>
</tr>
<tr>
<td>2541785</td>
<td>Adam Thielen</td>
<td>612</td>
<td>MIN</td>
<td>WR</td>
</tr>
<tr>
<td>2543468</td>
<td>Mike Evans</td>
<td>610</td>
<td>TB</td>
<td>WR</td>
</tr>
<tr>
<td>2555295</td>
<td>Sterling Shepard</td>
<td>610</td>
<td>NYG</td>
<td>WR</td>
</tr>
<tr>
<td>2540169</td>
<td>Robert Woods</td>
<td>604</td>
<td>LA</td>
<td>WR</td>
</tr>
<tr>
<td>2552600</td>
<td>Nelson Agholor</td>
<td>604</td>
<td>PHI</td>
<td>WR</td>
</tr>
<tr>
<td>2543488</td>
<td>Jarvis Landry</td>
<td>592</td>
<td>CLE</td>
<td>WR</td>
</tr>
<tr>
<td>2540165</td>
<td>DeAndre Hopkins</td>
<td>587</td>
<td>HOU</td>
<td>WR</td>
</tr>
<tr>
<td>2543498</td>
<td>Brandin Cooks</td>
<td>581</td>
<td>LA</td>
<td>WR</td>
</tr>
</tbody>
</table>
<h3 id="sack-percentage-by-quarterback-on-passing-plays">Sack percentage by quarterback on passing plays</h3><p>We can start to go a little deeper by extracting specific data from the <code>tracking</code> table and layering queries on top of it to make correlations. One piece of information that might be helpful in your analysis is knowing which quarterbacks are sacked most often during passing plays. In football, a “sack” is a negative play for the offense, and quarterbacks who get sacked more often tend to be lower performers overall.</p>
<p>Once you know those players, you could expand your analysis to see if they are sacked more on specific types of plays (shotgun formation) or maybe if sacks occur more often in a specific quarter of the game (maybe the fourth quarter because the offensive line is more tired, or the team tends to be behind late in games and must pass more often).</p><p>Queries like this can quickly show you quarterbacks that are more likely to get sacked, particularly when they play a strong defensive team.<br><br>To get started, we wanted to find the sack percentage of each quarterback based on the total number of pass plays they were involved in during the regular season. To do that we approached the tracking data by layering on Common Table Expressions so that each query could build upon previous results.</p><p>First, we select the distinct list of all plays, for each quarterback (<code>qb_plays</code>). The reason we do a <code>SELECT DISTINCT…</code> is because the tracking table holds multiple entries for each player, for each play. We just need one row for each play, for each quarterback.</p>
<p>With this result, we can then count the number of total plays per quarterback (<code>total_qb_plays</code>), the total number of games each quarterback played (<code>qb_games</code>) and then finally the number of pass plays the quarterback was a part of that resulted in a sack (<code>sacks</code>).</p>
<p>With that data in hand, we can finally query all of the values, do a percentage calculation, and order it by the total sack count.</p><pre><code class="language-SQL">WITH qb_plays AS (
	SELECT DISTINCT ON (POSITION, playid, gameid) POSITION, playid, player_id, gameid 
	FROM tracking t 
	WHERE POSITION = 'QB'
),
total_qb_plays AS (
	SELECT count(*) play_count, player_id FROM qb_plays
	GROUP BY player_id
),
qb_games AS (
	SELECT count(DISTINCT gameid) game_count, player_id FROM qb_plays 
	GROUP BY player_id
),
sacks AS (
	SELECT count(*) sack_count, player_id 
	FROM play p
	INNER JOIN qb_plays ON p.gameid = qb_plays.gameid AND p.playid = qb_plays.playid
	WHERE p.passresult = 'S'
	GROUP BY player_id
)
SELECT play_count, game_count, sack_count, (sack_count/play_count::float)*100 sack_percentage, display_name FROM total_qb_plays tqp
INNER JOIN qb_games qg ON tqp.player_id = qg.player_id
LEFT JOIN sacks s ON s.player_id = qg.player_id
INNER JOIN player ON tqp.player_id = player.player_id
ORDER BY sack_count DESC NULLS last;
</code></pre>
<p>If you're an ardent football fan, the results from 2018 probably don't surprise you.</p><table>
<thead>
<tr>
<th>play_count</th>
<th>game_count</th>
<th>sack_count</th>
<th>sack_percentage</th>
<th>display_name</th>
</tr>
</thead>
<tbody>
<tr>
<td>579</td>
<td>16</td>
<td>65</td>
<td>11.23</td>
<td>Deshaun Watson</td>
</tr>
<tr>
<td>602</td>
<td>16</td>
<td>55</td>
<td>9.14</td>
<td>Dak Prescott</td>
</tr>
<tr>
<td>611</td>
<td>16</td>
<td>53</td>
<td>8.67</td>
<td>Derek Carr</td>
</tr>
<tr>
<td>656</td>
<td>16</td>
<td>49</td>
<td>7.47</td>
<td>Aaron Rodgers</td>
</tr>
<tr>
<td>462</td>
<td>15</td>
<td>48</td>
<td>10.39</td>
<td>Russell Wilson</td>
</tr>
<tr>
<td>639</td>
<td>16</td>
<td>47</td>
<td>7.36</td>
<td>Eli Manning</td>
</tr>
<tr>
<td>448</td>
<td>14</td>
<td>45</td>
<td>10.04</td>
<td>Josh Rosen</td>
</tr>
<tr>
<td>659</td>
<td>16</td>
<td>43</td>
<td>6.53</td>
<td>Matt Ryan</td>
</tr>
<tr>
<td>386</td>
<td>14</td>
<td>43</td>
<td>11.14</td>
<td>Marcus Mariota</td>
</tr>
<tr>
<td>619</td>
<td>16</td>
<td>41</td>
<td>6.62</td>
<td>Matthew Stafford</td>
</tr>
<tr>
<td>621</td>
<td>15</td>
<td>38</td>
<td>6.12</td>
<td>Kirk Cousins</td>
</tr>
<tr>
<td>324</td>
<td>11</td>
<td>37</td>
<td>11.42</td>
<td>Ryan Tannehill</td>
</tr>
<tr>
<td>447</td>
<td>11</td>
<td>36</td>
<td>8.05</td>
<td>Carson Wentz</td>
</tr>
</tbody>
</table>
<p>Of course, there are a few quarterbacks that always seem to have a way of avoiding a sack.</p><table>
<thead>
<tr>
<th>play_count</th>
<th>game_count</th>
<th>sack_count</th>
<th>sack_percentage</th>
<th>display_name</th>
</tr>
</thead>
<tbody>
<tr>
<td>725</td>
<td>16</td>
<td>25</td>
<td>3.45</td>
<td>Ben Roethlisberger</td>
</tr>
<tr>
<td>682</td>
<td>16</td>
<td>22</td>
<td>3.23</td>
<td>Andrew Luck</td>
</tr>
<tr>
<td>613</td>
<td>16</td>
<td>21</td>
<td>3.43</td>
<td>Tom Brady</td>
</tr>
</tbody>
</table>
<p>Now, let’s try some more “advanced” queries and analyses.</p><h2 id="faster-insights-with-postgresql-and-timescaledb">Faster Insights With PostgreSQL and TimescaleDB</h2><p>So far, the queries we've shown are interesting and help provide insights to various players throughout the season – but if you were looking closely, they're all regular SQL statements. </p><p>Examining a season of NFL tracking data isn't like typical time-series data, however. Most of the queries we want to perform need to examine all 20 million rows in some way.</p><p>This is where a tool that's been built for time-series analysis, even when the data isn't typical time-series data, can significantly improve your ability to examine the data and save money at the same time.</p><h2 id="faster-queries-with-timescaledb-continuous-aggregates">Faster Queries With TimescaleDB Continuous Aggregates</h2><p>We noticed that we often needed to build queries that started with the <code>tracking</code> table, filtering data by specific players, positions, and games. Part of the reason is that the <code>play</code> table doesn't list all of the players who were involved in a particular play. As a result, we need to cross-reference the <code>tracking</code> table to identify the players who were involved in any given play.</p>
<p>The first example query we demonstrated - “average yards per position, per game” - is a good example of this. The query begins by summing all yards, by position, for each game.</p><p>This means that every row in <code>tracking</code> has to be read and aggregated <em>before</em> we can do any other analysis. Scanning those 20 million rows is pretty boring, repetitive, and slow work – especially compared to the analysis we want to do!</p>
<p>On our small test instance, the "average yards" query takes about 8 seconds to run. We could increase the size of the instance (which will cost us more money), or we could be smarter about how we query the data (which will cost us more time).</p><p>Instead, we can use continuous aggregates to pre-aggregate the data we're querying over and over again, which reduces the amount of work TimescaleDB needs to do every time we run the query. (Continuous aggregates are like PostgreSQL materialized views. For more info, check out our <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/">continuous aggregates docs</a>.)</p><pre><code class="language-SQL">CREATE MATERIALIZED VIEW player_yards_by_game_
WITH (timescaledb.continuous) AS
SELECT player_id, position, gameid,
 time_bucket(INTERVAL '1 day', "time") AS bucket,
 SUM(dis) AS yards
FROM tracking t
GROUP BY player_id, position, gameid, bucket;
</code></pre>
<p>After running this query and creating a continuous aggregate, we can modify that first query just slightly, using this as our basis table.</p><pre><code class="language-SQL">WITH total_position_yards AS (
	SELECT sum(yards) position_yards, POSITION, gameid 
FROM player_yards_by_game t 
	GROUP BY POSITION, gameid)
SELECT avg(position_yards), position, game_date
FROM game g
INNER JOIN total_position_yards tpy ON g.game_id = tpy.gameid
WHERE POSITION IN ('QB','RB','WR','TE')
GROUP BY game_date, POSITION
ORDER BY game_date, position;
</code></pre>
<p>We get the same result, but now the query runs in 100ms - <strong>800x faster</strong>!</p><h2 id="advanced-sql-data-analysis-with-timescaledb-hyperfunctions">Advanced SQL Data Analysis With TimescaleDB Hyperfunctions</h2><p>Finally, the more we dug into the data, the more and more we found we needed (or wanted) functions specifically tuned for time-series data analysis to answer the types of questions we wanted to ask.</p><p>It is for this kind of analysis that we built <a href="https://timescale.ghost.io/blog/blog/introducing-hyperfunctions-new-sql-functions-to-simplify-working-with-time-series-data-in-postgresql/">TimescaleDB hyperfunctions</a>, a series of SQL functions within TimescaleDB that make it easier to manipulate and analyze time-series data in PostgreSQL with fewer lines of code.</p><h3 id="grouping-data-into-percentiles">Grouping data into percentiles</h3><p>The NFL dataset is a great use case for <a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/">percentiles</a>. Being able to quickly find players that perform better or worse than some cohort is really powerful.</p><p>As an example, we'll use the same continuous aggregate we created earlier (total yards, per game, per player) to find the median total yards traveled by position for each game.</p><pre><code class="language-SQL">WITH sum_yards AS (
--Add position to the table to allow for grouping by it later
 SELECT a.player_id, display_name, SUM(yards) AS yards, p.position, gameid
 FROM player_yards_by_game a
 LEFT JOIN player p ON a.player_id = p.player_id
 GROUP BY a.player_id, display_name, p.position, gameid
)
--Find the mean and median for each position type
SELECT position, mean(percentile_agg(yards)) AS mean_yards, approx_percentile(0.5, percentile_agg(yards)) AS median_yards
FROM sum_yards
WHERE POSITION IS NOT null
GROUP BY position
ORDER BY mean_yards DESC;
</code></pre>
<table>
<thead>
<tr>
<th>position</th>
<th>mean_yards</th>
<th>median_yards</th>
</tr>
</thead>
<tbody>
<tr>
<td>FS</td>
<td>595.583433048431</td>
<td>626.388099960848</td>
</tr>
<tr>
<td>CB</td>
<td>572.3336749867212</td>
<td>592.2175990890378</td>
</tr>
<tr>
<td>WR</td>
<td>552.6508570179277</td>
<td>555.5030569048633</td>
</tr>
<tr>
<td>S</td>
<td>530.6436781609186</td>
<td>550.5961518474892</td>
</tr>
<tr>
<td>SS</td>
<td>522.5604103343453</td>
<td>551.1296628916651</td>
</tr>
<tr>
<td>MLB</td>
<td>462.70229007633407</td>
<td>490.77906906009343</td>
</tr>
<tr>
<td>ILB</td>
<td>402.7882871125599</td>
<td>403.3779668359464</td>
</tr>
<tr>
<td>OLB</td>
<td>393.40014271151847</td>
<td>390.6742117791442</td>
</tr>
<tr>
<td>QB</td>
<td>334.7025466893028</td>
<td>352.1192705472368</td>
</tr>
<tr>
<td>LB</td>
<td>328.9812527472519</td>
<td>257.72003396053884</td>
</tr>
<tr>
<td>TE</td>
<td>327.9515596330271</td>
<td>257.72003396053884</td>
</tr>
</tbody>
</table>
<h3 id="finding-extreme-outliers">Finding extreme outliers</h3><p>Finally, we can build upon this percentile query to find players at each position that run more than 95% of all other players at that position. For some positions, like wide receiver or free safety, this could help us find the “outlier” players that are able to travel the field consistently throughout a game – and make plays!</p><pre><code class="language-SQL">WITH sum_yards AS (
--Add position to the table to allow for grouping by it later
 SELECT a.player_id, display_name, SUM(yards) AS yards, p.position
 FROM player_yards_by_game a
 LEFT JOIN player p ON a.player_id = p.player_id
 GROUP BY a.player_id, display_name, p.position
),
position_percentile AS (
	SELECT POSITION, approx_percentile(0.95, percentile_agg(yards)) AS p95
	FROM sum_yards 
	GROUP BY position
)
SELECT a.POSITION, a.display_name, yards, p95
	FROM sum_yards a
	LEFT JOIN position_percentile pp ON a.POSITION = pp.position
	WHERE yards &gt;= p95
AND a.POSITION IN ('WR','FS','QB','TE')
ORDER BY position;
</code></pre>
<table>
<thead>
<tr>
<th>position</th>
<th>display_name</th>
<th>yards</th>
<th>p95</th>
</tr>
</thead>
<tbody>
<tr>
<td>FS</td>
<td>Eric Weddle</td>
<td>13869.759999999997</td>
<td>12320.288323166456</td>
</tr>
<tr>
<td>FS</td>
<td>Adrian Amos</td>
<td>12989.439999999966</td>
<td>12320.288323166456</td>
</tr>
<tr>
<td>FS</td>
<td>Tyrann Mathieu</td>
<td>12565.219999999956</td>
<td>12320.288323166456</td>
</tr>
<tr>
<td>QB</td>
<td>Aaron Rodgers</td>
<td>7422.35999999995</td>
<td>6667.51452813257</td>
</tr>
<tr>
<td>QB</td>
<td>Patrick Mahomes</td>
<td>6985.989999999952</td>
<td>6667.51452813257</td>
</tr>
<tr>
<td>QB</td>
<td>Matt Ryan</td>
<td>6759.959999999969</td>
<td>6667.51452813257</td>
</tr>
<tr>
<td>TE</td>
<td>Zach Ertz</td>
<td>13124.58999999995</td>
<td>10667.986199523099</td>
</tr>
<tr>
<td>TE</td>
<td>Jimmy Graham</td>
<td>12693.679999999982</td>
<td>10667.986199523099</td>
</tr>
<tr>
<td>TE</td>
<td>Travis Kelce</td>
<td>12218.129999999957</td>
<td>10667.986199523099</td>
</tr>
<tr>
<td>TE</td>
<td>David Njoku</td>
<td>11502.159999999965</td>
<td>10667.986199523099</td>
</tr>
<tr>
<td>TE</td>
<td>George Kittle</td>
<td>11058.099999999975</td>
<td>10667.986199523099</td>
</tr>
<tr>
<td>TE</td>
<td>Kyle Rudolph</td>
<td>10761.949999999968</td>
<td>10667.986199523099</td>
</tr>
<tr>
<td>TE</td>
<td>Jared Cook</td>
<td>10678.22999999998</td>
<td>10667.986199523099</td>
</tr>
<tr>
<td>WR</td>
<td>Antonio Brown</td>
<td>16877.559999999965</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Brandin Cooks</td>
<td>15510.01999999995</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>JuJu Smith-Schuster</td>
<td>15492.76999999996</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Robert Woods</td>
<td>15253.179999999958</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Nelson Agholor</td>
<td>15180.32999999997</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Tyreek Hill</td>
<td>15106.609999999973</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Zay Jones</td>
<td>14790.589999999967</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Sterling Shepard</td>
<td>14673.79999999996</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Mike Evans</td>
<td>14620.129999999983</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Davante Adams</td>
<td>14574.509999999951</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Kenny Golladay</td>
<td>14354.499999999973</td>
<td>14271.23409723974</td>
</tr>
<tr>
<td>WR</td>
<td>Jarvis Landry</td>
<td>14281.509999999971</td>
<td>14271.23409723974</td>
</tr>
</tbody>
</table>
<h2 id="where-can-the-data-take-you">Where Can the Data Take You?</h2><p>As you’ve seen in this example, <strong>time-series data is everywhere</strong>. Being able to harness it gives you a huge advantage, whether you’re working on a professional solution or a personal project.</p><p>We’ve shown you a few ways that time-series queries can unlock interesting insights, give you a greater appreciation for the game and its players, and (hopefully) inspired you to dig into the data yourself.</p><p><strong>To get started with the </strong><a href="https://www.kaggle.com/c/nfl-big-data-bowl-2021/overview"><strong>NFL data</strong></a><strong>:</strong></p><ul><li><strong>Spin up a fully managed TimescaleDB service</strong>: create an account to <a href="https://console.cloud.timescale.com/signup">try it for free</a> for 30 days.</li><li><a href="https://docs.timescale.com/timescaledb/latest/tutorials/nfl-analytics/">Follow our complete tutorial</a> for step-by-step instructions for preparing and ingesting the dataset, along with several more queries to help you glean insights from the dataset.</li></ul><p>If you’re new to time-series data or just have some questions about how to use TimescaleDB to analyze the NFL’s dataset, <a href="https://slack.timescale.com">join our public Slack community</a>. You’ll find Timescale engineers and thousands of time-series enthusiasts from around the world – and we’ll be happy to help you.</p><p>🙏 We’d like to thank the NFL for making this data available and the millions of passionate fans around the world who make the NFL such an exciting game to watch.</p><p>And, Geaux Saints 🏈!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Improving DISTINCT Query Performance Up to 8,000x on PostgreSQL]]></title>
            <description><![CDATA[Learn common performance pitfalls and discover techniques to optimize your DISTINCT PostgreSQL queries.]]></description>
            <link>https://www.tigerdata.com/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[PostgreSQL Performance]]></category>
            <dc:creator><![CDATA[Sven Klemm]]></dc:creator>
            <pubDate>Thu, 06 May 2021 11:01:13 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2021/05/pexels-pixabay-373543.jpg">
            </media:content>
            <content:encoded><![CDATA[<p>PostgreSQL is an amazing database, but it can struggle with certain types of queries, especially as tables approach tens and hundreds of millions of rows (or more). <a href="https://www.timescale.com/learn/understanding-distinct-in-postgresql-with-examples" rel="noreferrer"><code>DISTINCT</code> queries</a> are an example of this.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/baby-yoda-tea.gif" class="kg-image" alt="Baby Yoda drinking tea" loading="lazy" width="320" height="320"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Waiting for our DISTINCT queries to return</em></i></figcaption></figure><p>Why are <code>DISTINCT</code> queries slow on PostgreSQL when they seem to ask an "easy" question? It turns out that PostgreSQL currently lacks the ability to efficiently pull a list of unique values from an ordered index. </p><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">🔖</div><div class="kg-callout-text">Learning PostgreSQL? <a href="https://www.timescale.com/learn/understanding-distinct-in-postgresql-with-examples" rel="noreferrer">Read the basics on DISTINCT</a>.</div></div><p></p><p><br></p><p>Even when you have an index that matches the exact order and columns for these "last-point" queries, PostgreSQL is still forced to scan the entire index to find all unique values. As a table grows (and <a href="https://timescale.ghost.io/blog/blog/what-the-heck-is-time-series-data-and-why-do-i-need-a-time-series-database-dcf3b1b18563/">they grow quickly with time-series data</a>), this operation keeps getting slower.</p><p>Other databases, such as MySQL, Oracle, and DB2, implement a feature called "Loose index scan," "Index Skip Scan," or “Skip Scan,” to speed up the performance of queries like this. </p><p>When a database has a feature like "Skip Scan," it can incrementally jump from one ordered value to the next without reading all of the rows in between. <em>Without</em> support for this feature, the database engine has to scan the entire ordered index and then deduplicate it at the end—which is a much slower process.</p><p>Since 2018, there have been <a href="https://commitfest.postgresql.org/19/1741/">plans to support something similar</a> in PostgreSQL. <em>(<strong>Note</strong>: We couldn’t use this implementation directly due to some limitations of what is possible within the </em><a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer"><em>Postgres extension</em></a><em> framework.)</em></p><p>Unfortunately, this patch wasn't included in the <a href="https://commitfest.postgresql.org/32/">CommitFest</a> for PostgreSQL 14, so it won't be included until PostgreSQL 15 at the earliest (i.e., no sooner than Fall 2022, at least 1.5 years from now). </p><p>We don’t want our users to have to wait that long.</p><h2 id="what-is-timescales-skipscan">What is Timescale's SkipScan?</h2><p>Today, via TimescaleDB 2.2.1, we are releasing <strong>TimescaleDB SkipScan</strong>, a custom query planner node that makes ordered <code>DISTINCT</code> queries blazing fast in PostgreSQL 🔥. </p><p>As you'll see in the benchmarks below, <strong>some queries performed more than</strong> <strong>8,000x better than before</strong>—and many of the SQL queries your applications and analytics tools use could also see dramatic improvements with this new feature.</p><p>This feature works in both Timescale <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertables</a> and normal PostgreSQL tables. </p><p>This means that with Timescale, not only will your time-series <code>DISTINCT</code> queries be faster, but <strong>any other related queries you may have on normal PostgreSQL tables will also be faster. </strong></p><p>This is because Timescale is not just a time-series database. It’s a relational database, specifically, a relational database for <a href="https://www.tigerdata.com/blog/time-series-introduction" rel="noreferrer">time series</a>. Developers who use Timescale benefit from a purpose-built time-series database plus a classic relational (Postgres) database, all in one, with full SQL support.</p><p>And to be clear, we love PostgreSQL. We employ engineers who contribute to PostgreSQL. We contribute to the ecosystem around PostgreSQL. PostgreSQL is the world’s fastest-growing database, and we are excited to support it alongside thousands of other users and contributors.</p><p>We constantly seek to advance the state of the art with databases, and features like SkipScan are only our latest contribution to the industry. SkipScan makes Timescale and PostgreSQL better, more competitive databases overall, especially compared to MySQL, Oracle, DB2, and others. </p><h3 id="how-to-check-and-optimize-your-query-performance-in-postgresql">How to check (and optimize) your query performance in PostgreSQL</h3><p>If you're new to PostgreSQL and are wondering how to check your query performance in the first place (and optimize it!), we're going to leave two helpful resources here:</p><ul><li><a href="https://www.timescale.com/forum/t/a-beginners-guide-to-explain-analyze/77">This beginner's guide to <code>EXPLAIN ANALYZE</code> </a>by Michael Christofides in one of our Timescale Community Days. And here's a blog post on <a href="https://www.timescale.com/learn/explaining-postgresql-explain" rel="noreferrer">Explaining EXPLAIN</a> in case you're more of a reader.</li></ul><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/31EmOKBP1PY?start=1&amp;feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="A beginners guide to EXPLAIN ANALYZE – Michael Christofides"></iframe></figure><ul><li>And our <a href="https://timescale.ghost.io/blog/identify-postgresql-performance-bottlenecks-with-pg_stat_statements/">blog post on using pg_stat_statements to optimize queries</a>.</li></ul><h3 id="optimizing-distinct-query-performance-what-about-recursive-ctes">Optimizing DISTINCT query performance: What about RECURSIVE CTEs?</h3><p>However, if you're an experienced PostgreSQL user, you might point out that it is already possible to get reasonably fast <code>DISTINCT</code>queries via <code>RECURSIVE CTEs</code>.</p><p>From the <a href="https://wiki.postgresql.org/wiki/Loose_indexscan">PostgreSQL Wiki</a>, using a <code>RECURSIVE CTE</code> can get you good results, but writing these kinds of queries can often feel cumbersome and unintuitive, especially for developers new to PostgreSQL:</p><pre><code class="language-sql">WITH RECURSIVE cte AS (
   (SELECT tags_id FROM cpu ORDER BY tags_id, time DESC LIMIT 1)
   UNION ALL
   SELECT (
      SELECT tags_id FROM cpu
      WHERE tags_id &gt; t.tags_id 
      ORDER BY tags_id, time DESC LIMIT 1
   )
   FROM cte t
   WHERE t.tags_id IS NOT NULL
)
SELECT * FROM cte LIMIT 50;
</code></pre><p>But even if writing a <code>RECURSIVE CTE</code> like this in day-to-day querying felt natural to you, there's a bigger problem. Most application developers, ORMs, and charting tools like Grafana or Tableau will still use the simpler, straight-forward form:</p><pre><code class="language-sql">SELECT DISTINCT ON (tags_id) * FROM cpu
WHERE tags_id &gt;=1 
ORDER BY tags_id, time DESC
LIMIT 50;</code></pre><p>In PostgreSQL, without a ", such as MySQL, Oracle, and DB2, implement a feature called "Loose index scan," "Index Skip Scan," or “Skip Scan" node, this query will perform the much slower Index Only Scan, causing your applications and graphing tools to feel clunky and slow.</p><p>Surely there's a better way, right?</p><h2 id="skipscan-is-the-way">SkipScan Is the Way</h2><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/01/Screen-Shot-2021-04-16-at-1.45.56-PM.png" class="kg-image" alt="" loading="lazy" width="1472" height="608" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/01/Screen-Shot-2021-04-16-at-1.45.56-PM.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/01/Screen-Shot-2021-04-16-at-1.45.56-PM.png 1000w, https://timescale.ghost.io/blog/content/images/2022/01/Screen-Shot-2021-04-16-at-1.45.56-PM.png 1472w" sizes="(min-width: 720px) 720px"></figure><p>SkipScan is an optimization for queries in the form of <code>SELECT DISTINCT ON</code> (column). Conceptually, a SkipScan is a regular IndexScan that “skips” across an index looking for the next value that is greater than the current value:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2021/05/skip-scan-illustration.png" class="kg-image" alt="Illustration of how a Skip Scan search works on a Btree index" loading="lazy" width="519" height="253"><figcaption><i><em class="italic" style="white-space: pre-wrap;">SkipScan: An index scan that “skips” across an index looking for the next greater value</em></i></figcaption></figure><p>With SkipScan in Timescale/PostgreSQL, query planning and execution can now utilize a new node (displayed as <code>(SkipScan)</code> in the <code>EXPLAIN</code> output) to quickly return distinct items from a properly ordered index. </p><p>Rather than scanning the entire index with an Index Only Scan, SkipScan incrementally searches for each successive item in the ordered index. As it locates one item, the <code>(SkipScan)</code> node quickly restarts the search for the next item. This is a <em>much</em> more efficient way of finding distinct items in an ordered index. (<a href="https://github.com/timescale/timescaledb/blob/master/tsl/src/nodes/skip_scan/exec.c">See GitHub for more details.</a>)</p><h2 id="benchmarking-timescaledb-skipscan-vs-a-normal-postgresql-index-scan">Benchmarking TimescaleDB SkipScan vs. a Normal PostgreSQL Index Scan</h2><p>In every example query, <strong>Timescale with SkipScan improved query response times by at least 26x</strong>. </p><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">✨</div><div class="kg-callout-text">If you don't want to go through the entire benchmark, here's a short and sweet piece on <a href="https://www.timescale.com/blog/skip-scan-under-load/" rel="noreferrer">SkipScan's performance under load</a>.</div></div><p><br></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p>But the real surprise is <strong>how much of a difference it makes at lower cardinalities with lots of data—</strong>it is <strong>almost 8,500x faster to retrieve <em>all columns</em> for the most recent reading of each device</strong>. That's fast!</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/01/mandalorian-ships.gif" class="kg-image" alt="Mandolorian Razor Crest being chased by X-wing fighters" loading="lazy" width="1280" height="720" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/01/mandalorian-ships.gif 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/01/mandalorian-ships.gif 1000w, https://timescale.ghost.io/blog/content/images/2022/01/mandalorian-ships.gif 1280w" sizes="(min-width: 720px) 720px"></figure><p>In our tests, <strong>SkipScan is also consistently faster—by 80x or more—in our 4,000 device benchmarks</strong>. (This level of cardinality is typical for many users of Timescale.)</p><p>Before we share the full results, here is how our benchmark was set up.</p><h3 id="benchmark-setup">Benchmark setup</h3><p>To perform our benchmarks, we installed Timescale on a DigitalOcean Droplet using the following specifications. PostgreSQL and Timescale were installed from packages, and we applied the recommended tuning from <a href="https://github.com/timescale/timescaledb-tune"><code>timescaledb-tune</code></a>.</p><ul><li>8 Intel vCPUs</li><li>16&nbsp;GB of RAM</li><li>320&nbsp;GB NVMe SSD</li><li>Ubuntu 20.04 LTS</li><li>Postgres 12.6</li><li>TimescaleDB 2.2 <em>(The first release with SkipScan. TimescaleDB 2.2.1 primarily adds distributed hypertable support and some bug fixes.)</em></li></ul><p>To demonstrate the performance impact of SkipScan on varying degrees of cardinality, we benchmarked three separate datasets of varying sizes. To generate our datasets, we used the 'cpu-only' use case in the <a href="https://github.com/timescale/tsbs">Time Series Benchmark Suite (TSBS)</a>, which creates 10 metrics every 10 seconds for each device (identified by the <code>tag_id</code> in our benchmark queries).</p><table>
<thead>
<tr>
<th>Dataset 1</th>
<th>Dataset 2</th>
<th>Dataset 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>100 devices</td>
<td>4000 devices</td>
<td>10,000 devices</td>
</tr>
<tr>
<td>4 months of data</td>
<td>4 days of data</td>
<td>36 hours of data</td>
</tr>
<tr>
<td>~103,000,000 rows</td>
<td>~103,000,000 rows</td>
<td>~144,000,000 rows</td>
</tr>
</tbody>
</table>
<h3 id="additional-data-preparation">Additional data preparation</h3><p>Not all device data is up-to-date in real life because devices go offline and internet connections get interrupted. Therefore, to simulate a more realistic scenario (i.e., that some devices had stopped reporting for a period of time), we deleted rows for random devices over each of the following periods.</p><table>
<thead>
<tr>
<th>Dataset 1</th>
<th>Dataset 2</th>
<th>Dataset 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>5 random devices over:</td>
<td>100 random devices over:</td>
<td>250 random devices over:</td>
</tr>
<tr>
<td>30 minutes</td>
<td>1 hour</td>
<td>10 minutes</td>
</tr>
<tr>
<td>36 hours</td>
<td>12 hours</td>
<td>1 hour</td>
</tr>
<tr>
<td>7 days</td>
<td>36 hours</td>
<td>12 hours</td>
</tr>
<tr>
<td>1 month</td>
<td>3 days</td>
<td>24 hours</td>
</tr>
</tbody>
</table>
<p>To delete the data, we utilized the <code>tablesample</code> function of Postgres. This <code>SELECT</code> feature allows you to return a random sample of rows from a table based on a percentage of the total rows. In the example below, we randomly sample 10% of the rows ( <code>bernoulli(10)</code> ) and then take the first 10 ( <code>limit 10</code> ).</p><pre><code class="language-sql">DELETE FROM cpu
WHERE tags_id IN 
  (SELECT id FROM tags tablesample bernoulli(10) LIMIT 10)
  AND time &gt;= now() - INTERVAL '30 minutes';</code></pre><p>From there, we ran each benchmarking query multiple times to accommodate for caching, with and without SkipScan enabled.</p><p>As mentioned earlier, the following two indexes were present on the hypertable for all queries.</p><pre><code class="language-sql">"cpu_tags_id_time_idx" btree (tags_id, "time" DESC)
"cpu_time_idx" btree ("time" DESC)</code></pre><h3 id="benchmark-results">Benchmark results</h3><p>Here are the results:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2021/05/Skip-Scan-vs-Normal.jpg" class="kg-image" alt="" loading="lazy" width="2000" height="2183" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2021/05/Skip-Scan-vs-Normal.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2021/05/Skip-Scan-vs-Normal.jpg 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2021/05/Skip-Scan-vs-Normal.jpg 1600w, https://timescale.ghost.io/blog/content/images/2021/05/Skip-Scan-vs-Normal.jpg 2000w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">TimescaleDB with SkipScan improved the query response by at least 26x, up to 8500x in some cases.</em></i></figcaption></figure><h2 id="about-the-queries-benchmarked">About the Queries Benchmarked</h2><p>For this test, we benchmarked five types of common queries:</p><h3 id="scenario-1-what-was-the-last-reported-time-of-each-device-in-a-paged-list">Scenario #1: What was the last reported time of each device in a paged list?</h3><pre><code class="language-sql">SELECT DISTINCT ON (tags_id) tags_id, time FROM cpu
ORDER BY tags_id, time DESC
LIMIT 10 OFFSET 50;</code></pre><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/01/SkipScan---Scenario-1.jpg" class="kg-image" alt="" loading="lazy" width="1095" height="236" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/01/SkipScan---Scenario-1.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/01/SkipScan---Scenario-1.jpg 1000w, https://timescale.ghost.io/blog/content/images/2022/01/SkipScan---Scenario-1.jpg 1095w" sizes="(min-width: 720px) 720px"></figure><h3 id="scenario-2-what-was-the-time-and-most-recently-reported-set-of-values-for-each-device-in-a-paged-list">Scenario #2: What was the time and most recently reported set of values for each device in a paged list?</h3><pre><code class="language-sql">SELECT DISTINCT ON (tags_id) * FROM cpu
ORDER BY tags_id, time DESC
LIMIT 10 OFFSET 50;</code></pre><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/01/SkipScan---Scenario-2.jpg" class="kg-image" alt="" loading="lazy" width="1095" height="236" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/01/SkipScan---Scenario-2.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/01/SkipScan---Scenario-2.jpg 1000w, https://timescale.ghost.io/blog/content/images/2022/01/SkipScan---Scenario-2.jpg 1095w" sizes="(min-width: 720px) 720px"></figure><h3 id="scenario-3-what-is-the-most-recent-point-for-all-reporting-devices-in-the-last-5-minutes">Scenario #3: What is the most recent point for all reporting devices in the last 5 minutes?</h3><pre><code class="language-sql">SELECT DISTINCT ON (tags_id) * FROM cpu 
WHERE time &gt;= now() - INTERVAL '5 minutes' 
ORDER BY tags_id, time DESC;</code></pre><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/01/SkipScan---Scenario-3.jpg" class="kg-image" alt="" loading="lazy" width="1095" height="236" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/01/SkipScan---Scenario-3.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/01/SkipScan---Scenario-3.jpg 1000w, https://timescale.ghost.io/blog/content/images/2022/01/SkipScan---Scenario-3.jpg 1095w" sizes="(min-width: 720px) 720px"></figure><h3 id="scenario-4-which-devices-reported-at-some-time-today-but-not-within-the-last-hour">Scenario #4: Which devices reported at some time today but not within the last hour?</h3><pre><code class="language-sql">WITH older AS (
  SELECT DISTINCT ON (tags_id) tags_id FROM cpu 
  WHERE time &gt; now() - INTERVAL '24 hours'
)                                          
SELECT * FROM older o 
WHERE NOT EXISTS (
  SELECT 1 FROM cpu 
  WHERE cpu.tags_id = o.tags_id 
  AND time &gt; now() - INTERVAL '1 hour'
);</code></pre><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2021/05/SkipScan---Scenario-4.jpg" class="kg-image" alt="" loading="lazy" width="2000" height="431" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2021/05/SkipScan---Scenario-4.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2021/05/SkipScan---Scenario-4.jpg 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2021/05/SkipScan---Scenario-4.jpg 1600w, https://timescale.ghost.io/blog/content/images/2021/05/SkipScan---Scenario-4.jpg 2000w" sizes="(min-width: 720px) 720px"></figure><h3 id="scenario-5-which-devices-reported-yesterday-but-not-in-the-last-24-hours">Scenario #5: Which devices reported yesterday but not in the last 24 hours?</h3><pre><code class="language-sql">WITH older AS (
  SELECT DISTINCT ON (tags_id) tags_id FROM cpu 
  WHERE time &gt; now() - INTERVAL '48 hours'
  AND time &lt; now() - INTERVAL '24 hours'
)                                          
SELECT * FROM older o 
WHERE NOT EXISTS (
  SELECT 1 FROM cpu 
  WHERE cpu.tags_id = o.tags_id 
  AND time &gt; now() - INTERVAL '24 hour'
);</code></pre><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2021/05/SkipScan---Scenario-5.jpg" class="kg-image" alt="" loading="lazy" width="2000" height="431" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2021/05/SkipScan---Scenario-5.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2021/05/SkipScan---Scenario-5.jpg 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2021/05/SkipScan---Scenario-5.jpg 1600w, https://timescale.ghost.io/blog/content/images/2021/05/SkipScan---Scenario-5.jpg 2000w" sizes="(min-width: 720px) 720px"></figure><h2 id="how-will-your-application-improve">How Will Your Application Improve?</h2><p>But SkipScan isn’t a theoretical improvement reserved for benchmarking blog posts 😉—it has real-world implications, and many applications we use rely on getting this data as fast as possible.</p><p>Think about the applications you use (or develop) every day. Do they retrieve paged lists of unique items from database tables to fill dropdown options (or grids of data)?</p><p>At a few thousand items, the query latency might not be very noticeable. But, as your data grows and you have millions of rows of data and tens of thousands of distinct items, that dropdown menu might take seconds—or minutes—to populate. </p><p>SkipScan can reduce that to tens of <em>milliseconds</em>!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/tenor.gif" class="kg-image" alt="" loading="lazy" width="498" height="287"><figcaption><span style="white-space: pre-wrap;">Baby Yoda</span></figcaption></figure><p>Even better, SkipScan also provides a fast, efficient way of answering the question that so many people with time-series data ask every day:</p><p><em>"What was the last time and value recorded for each of my [devices / users / services / crypto and stock investments / etc]?"</em></p><p>As long as there is an index on "device_id" and "time" descending, SkipScan will retrieve the data using a query like this much more efficiently.</p><pre><code class="language-sql">SELECT DISTINCT ON (device_id) * FROM cpu 
ORDER BY device_id, time DESC;</code></pre><p>With SkipScan, your application and dashboards that rely on these types of queries will now load a whole lot faster 🚀  (see below).</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2021/05/4k_with_skipscan.gif" class="kg-image" alt="" loading="lazy" width="658" height="527" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2021/05/4k_with_skipscan.gif 600w, https://timescale.ghost.io/blog/content/images/2021/05/4k_with_skipscan.gif 658w"><figcaption><i><em class="italic" style="white-space: pre-wrap;">TimescaleDB 2.2 </em></i><i><b><strong class="italic" style="white-space: pre-wrap;">with</strong></b></i><i><em class="italic" style="white-space: pre-wrap;"> SkipScan enabled runs in less than 400&nbsp;ms</em></i></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2021/05/4k_without_skipscan.gif" class="kg-image" alt="" loading="lazy" width="658" height="527" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2021/05/4k_without_skipscan.gif 600w, https://timescale.ghost.io/blog/content/images/2021/05/4k_without_skipscan.gif 658w"><figcaption><i><em class="italic" style="white-space: pre-wrap;">TimescaleDB 2.2</em></i><i><b><strong class="italic" style="white-space: pre-wrap;"> without</strong></b></i><i><em class="italic" style="white-space: pre-wrap;"> SkipScan enabled runs in 23 seconds</em></i></figcaption></figure><h2 id="how-to-use-skipscan-on-timescale">How to Use SkipScan on Timescale</h2><p>How do you get started? Upgrade to TimescaleDB 2.2.1 and set up your schema and indexing as described below. You should start to see immediate speed improvements in many of your <code>DISTINCT</code> queries.</p><p><strong>To ensure that a (SkipScan) node can be chosen for your query plan:</strong></p><p><strong>First, the query must use the <code>DISTINCT</code> keyword on a single column</strong>. The benchmarking queries above will give you some examples to draw from.</p><p><strong>Second, there must be an index that contains the <code>DISTINCT</code> column first, and any other <code>ORDER BY</code> columns.</strong> Specifically:</p><ul><li>The index needs to be a <code>BTREE</code> index.</li><li>The index needs to match the <code>ORDER BY</code> in your query.</li><li>The <code>DISTINCT</code> column must either be the first column of the index, or any leading column(s) must be used as constraints in your query.</li></ul><p>In practice, this means that if we use the questions from the beginning of this blog post ("retrieve a list of unique IDs in order" and "retrieve the last reading of each ID"), we would need at least one index like this (but if you're using a TimescaleDB hypertable, this likely already exists):</p><pre><code class="language-sql"> "cpu_tags_id_time_idx" btree (tags_id, "time" DESC)</code></pre><p>With that index in place, you should start to see immediate benefit if your queries look similar to the benchmarking examples below. When SkipScan is chosen for your query, the <code>EXPLAIN ANALYZE</code> output will show one or more <code>Custom Scan (SkipScan)</code> nodes similar to this:</p><pre><code>-&gt;  Unique
  -&gt;  Merge Append
    Sort Key: _hyper_8_79_chunk.tags_id, _hyper_8_79_chunk."time" DESC
     -&gt;  Custom Scan (SkipScan) on _hyper_8_79_chunk
      -&gt;  Index Only Scan using _hyper_8_79_chunk_cpu_tags_id_time_idx on _hyper_8_79_chunk
          Index Cond: (tags_id &gt; NULL::integer)
     -&gt;  Custom Scan (SkipScan) on _hyper_8_80_chunk
      -&gt;  Index Only Scan using _hyper_8_80_chunk_cpu_tags_id_time_idx on _hyper_8_80_chunk
         Index Cond: (tags_id &gt; NULL::integer)
...</code></pre><h2 id="learn-more-and-get-started">Learn More and Get Started</h2><p>If you’re new to Timescale, <a href="https://console.cloud.timescale.com/signup">create a free account</a> to get started with a fully managed TimescaleDB instance (100&nbsp;% free for 30 days).</p><p>If you are an existing user:</p><ul><li><strong>Timescale: </strong>TimescaleDB 2.2.1 is now the default for all new services on Timescale, and any of your existing services will be automatically upgraded during your next maintenance window.</li><li><strong>Self-managed TimescaleDB</strong>: <a href="https://docs.timescale.com/latest/update-timescaledb">Here are the upgrade instructions</a>. </li></ul><p>Join our <a href="https://slack.timescale.com">Slack Community</a> to share your results, ask questions, get advice, and connect with other developers (I, as well as our co-founders, engineers, and passionate community members, are active on all channels).</p><p>You can also <a href="https://github.com/timescale/timescaledb">visit our GitHub</a> to learn more (and, as always, ⭐️ are appreciated!)<br></p><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TimescaleDB vs. Amazon Timestream: 6,000x Higher Inserts, 5-175x Faster Queries, 150-220x Cheaper]]></title>
            <description><![CDATA[Our TimescaleDB vs Amazon Timestream results surprised us, but even after testing several configurations, we found Timestream slow, expensive, and missing key database capabilities like backups, restores, updates, and deletes. ]]></description>
            <link>https://www.tigerdata.com/blog/timescaledb-vs-amazon-timestream-6000x-higher-inserts-175x-faster-queries-220x-cheaper</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/timescaledb-vs-amazon-timestream-6000x-higher-inserts-175x-faster-queries-220x-cheaper</guid>
            <category><![CDATA[Announcements & Releases]]></category>
            <category><![CDATA[Benchmarks & Comparisons]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <dc:creator><![CDATA[Ryan Booz]]></dc:creator>
            <pubDate>Wed, 02 Dec 2020 19:41:47 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-2023-08-02---Timestream---Compare---Hero.png">
            </media:content>
            <content:encoded><![CDATA[<p>This post compares TimescaleDB and Amazon Timestream across quantitative and qualitative dimensions. </p><p>Yes, we are the developers of TimescaleDB, so you might quickly disregard our comparison as biased. But if you let the analysis speak for itself, you’ll find that we stay as objective as possible and aim to be fair to <a href="https://www.tigerdata.com/blog/so-long-timestream-how-and-why-to-migrate-before-its-too-late" rel="noreferrer">Amazon Timestream</a> in our testing and results reporting. </p><p>Also, if you want to check our work or run your own analysis, we provide all our testing via the <a href="https://github.com/timescale/tsbs">Time Series Benchmark Suite</a>, an open-source project that anyone can use and contribute to.</p><h2 id="about-timescaledb-and-amazon-timestream">About TimescaleDB and Amazon Timestream</h2><p><strong>TimescaleDB, </strong>first launched in <a href="https://timescale.ghost.io/blog/blog/when-boring-is-awesome-building-a-scalable-time-series-database-on-postgresql-2900ea453ee2/">April 2017</a>, is today the industry-leading relational database for <a href="https://www.tigerdata.com/blog/time-series-introduction" rel="noreferrer">time series</a>, open-source, engineered on top of PostgreSQL, and offered via download or as a fully managed service on AWS. </p><p>The TimescaleDB community has become the largest developer community for time-series data: tens of millions of downloads; over 500,000 active databases; organizations like AppDynamics, Bosch, Cisco, Comcast, Credit Suisse, DigitalOcean, Dow Chemical, Electronic Arts, Fujitsu, IBM, Microsoft, Rackspace, Schneider Electric, Samsung, Siemens, Uber, Walmart, Warner Music, WebEx, and thousands of others (all in addition to the PostgreSQL community and ecosystem).</p><p><strong>Amazon Timestream </strong>was first announced at AWS re:Invent <a href="https://aws.amazon.com/blogs/aws/aws-previews-and-pre-announcements-at-reinvent-2018-andy-jassy-keynote/">November 2018</a>, but its launch was delayed until <a href="https://aws.amazon.com/blogs/aws/store-and-access-time-series-data-at-any-scale-with-amazon-timestream-now-generally-available/">September 2020</a>. This is Amazon’s time-series database-as-a-service. Amazon Timestream not only shares a similar name to TimescaleDB, but also embraces SQL as its query language. Amazon Timestream customers include Autodesk, PubNub, and Trimble.</p><p>We compare TimescaleDB and Amazon Timestream across several dimensions:</p><ul><li>Insert and query performance</li><li>Cost for equivalent workloads</li><li>Backups, reliability, and tooling</li><li>Query language, ecosystem, ease-of-use</li><li>Clouds and regions supported</li></ul><p>Below is a summary of our results. For those interested, we go into much more detail later in this post.</p><h2 id="insert-performance-query-performance">Insert Performance, Query Performance</h2><p>Our results are striking. TimescaleDB outperformed Amazon Timestream 6,000x on inserts and 5-175x on queries, depending on the query type. In particular, there were workloads and query types easily supported by TimescaleDB that Amazon Timestream was unable to handle.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Datasets-for-benchmarking-table.png" class="kg-image" alt="TimescaleDB achieves 6,000 times higher inserts than Amazon Timestream" loading="lazy" width="1355" height="891" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-Datasets-for-benchmarking-table.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-Datasets-for-benchmarking-table.png 1000w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Datasets-for-benchmarking-table.png 1355w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-175x-faster-queries.png" class="kg-image" alt="TimescaleDB achieved 5 to 175 times better query performance than Amazon Timestream" loading="lazy" width="1562" height="1160" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-175x-faster-queries.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-175x-faster-queries.png 1000w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-175x-faster-queries.png 1562w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Note: Several queries’ ratios (high-cpu-all, lastpoint, groupby-orderby-limit) are “undefined” because Amazon Timestream did not finish executing them within the default 60-second timeout period that Timestream imposes, while TimescaleDB completed them in less than a single second</em></i></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Query-Performance.png" class="kg-image" alt="Table showing latency ratios for various queries - run on 100 devices x 10 metrics - in milliseconds" loading="lazy" width="1710" height="1496" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-Query-Performance.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-Query-Performance.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/08/timescale-vs-amazon-timestream-Query-Performance.png 1600w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Query-Performance.png 1710w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Results of benchmarking query performance between TimescaleDB and Amazon Timestream</em></i></figcaption></figure><p>These results were so dramatic that we did not believe them at first, and we tried a variety of workloads and settings to ensure we weren’t missing anything. </p><p><a href="https://www.reddit.com/r/aws/comments/jsgn9x/realworld_aws_timestream_ingest_performance/">We even posted on Reddit</a> to see if others had been able to get better performance with Amazon Timestream. Although feedback was hard to find, we weren’t the only ones seeing these performance results, as evidenced by a <a href="https://crate.io/a/amazon-timestream-first-impressions/">similar benchmark by Crate.io</a>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2022/01/Screen-Shot-2020-11-17-at-12.00.37-PM--1-.png" class="kg-image" alt="Screenshot of Amazon Timestream UI showing list of databases created during benchmarking process" loading="lazy" width="1584" height="1262" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/01/Screen-Shot-2020-11-17-at-12.00.37-PM--1-.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/01/Screen-Shot-2020-11-17-at-12.00.37-PM--1-.png 1000w, https://timescale.ghost.io/blog/content/images/2022/01/Screen-Shot-2020-11-17-at-12.00.37-PM--1-.png 1584w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">We REALLY tried to get Amazon Timestream to perform better. Just look at all of the databases we created through the process!</em></i></figcaption></figure><p>After all of our attempts to achieve better Amazon Timestream performance, we were even more confused when we read a <a href="https://aws.amazon.com/blogs/database/deriving-real-time-insights-over-petabytes-of-time-series-data-with-amazon-timestream/">recent post on the AWS Database Blog</a> that discusses achieving ingest speeds of three billion metrics/hour. </p><p>Although the details of how they ingested this scale of data aren’t completely clear, it appears that each “monitored host” sent individual metrics at various intervals directly to Amazon Timestream.</p><p>To achieve three billion metrics/hour in their test, four million hosts sent 26 metrics every two minutes, an average of 33,000 hosts reporting 866,667 metrics every second. </p><p>It’s certainly impressive to support 33,000 connections per second without issue, and this demonstrates one of the key advantages that Amazon presents with a serverless architecture like Timestream. </p><p>If you have an edge-based IoT system that pre-computes metrics on thousands of edge nodes before sending them, Amazon Timestream could simplify your data collection architecture. </p><p>However, as you’ll see, if you have a more traditional client-server data-collection architecture, or <a href="https://timescale.ghost.io/blog/blog/create-a-data-pipeline-with-timescaledb-and-kafka/">one using a more common streaming pipeline with database consumers, like Apache Kafka</a>, TimescaleDB can import more than three million metrics per second from one client—and doesn’t need 33,000 clients.</p><p>Because performance benchmarking is complex, we share the details of our setup, configurations, and workload patterns later in this post, as well as instructions on how to reproduce them.</p><h2 id="cost-for-equivalent-workloads">Cost for Equivalent Workloads</h2><p>The stark difference in performance translates into a large cost differential as well. </p><p>To compare costs, we calculated the cost for our above insert and query workloads, which store one billion metrics in TimescaleDB and ~410 million metrics in Amazon Timestream (because we were unable to load the full one billion—more later in this post), and ran our suite of queries on top.</p><p>For the same workloads, we found that <a href="https://www.timescale.com/products">fully managed TimescaleDB</a> is 154x cheaper than Amazon Timestream (224x cheaper if you’re self-managing TimescaleDB on a virtual machine) and inserted twice as many metrics.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-154x-cheaper-than-Amazon-Timestream.png" class="kg-image" alt="To run this test TimescaleDB took less than an hour at a cost of $2.18. The same test took a week and cost $336 in Amazon Timestream" loading="lazy" width="1382" height="746" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-154x-cheaper-than-Amazon-Timestream.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-154x-cheaper-than-Amazon-Timestream.png 1000w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-154x-cheaper-than-Amazon-Timestream.png 1382w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The results of the costs for equivalent workloads</span></figcaption></figure><p>We go into further details about the cost comparison later in this post.</p><h2 id="backups-reliability-and-tooling">Backups, Reliability, and Tooling</h2><p>For reliability, the differences are also striking. In particular, backups, reliability, and tooling feel like an afterthought with Amazon Timestream.</p><p>In the <a href="https://docs.aws.amazon.com/timestream/latest/developerguide/timestream.pdf">240-page development guide</a> for Amazon Timestream, the words “recovery” and “restore” don’t appear at all, and the word “backup” appears only once to tell the developer that there is no backup mechanism. </p><p>Instead, you can “[...]write your own application using the Timestream SDK to query data and save it to the destination of your choice” (page 100). There isn’t a mechanism or support to DELETE or UPDATE existing data.<strong> </strong></p><p><strong>The only way to remove data is to drop the entire table.</strong> Furthermore, there is no way to recover a deleted table since it is an atomic action that cannot be recovered through any Amazon API or Console.</p><p>In contrast, TimescaleDB is built on PostgreSQL, which means it inherits the 25+ years of hard, careful engineering work that the entire PostgreSQL community has done to build a rock-solid database that supports millions of mission-critical applications worldwide. </p><p>When operating TimescaleDB, one inherits all of the battle-tested tools that exist in the PostgreSQL ecosystem: <a href="https://www.postgresql.org/docs/9.6/static/app-pgdump.html">pg_dump</a>/<a href="https://www.postgresql.org/docs/9.6/static/app-pgrestore.html">pg_restore</a> and <a href="http://www.postgresql.cn/docs/9.6/app-pgbasebackup.html">pg_basebackup</a> for backup/restore, high-availability/failover tools like <a href="https://github.com/zalando/patroni">Patroni</a>, load balancing tools for clustering reads like <a href="http://www.pgpool.net/mediawiki/index.php/Main_Page">Pgpool</a>/<a href="https://www.tigerdata.com/blog/using-pgbouncer-to-improve-your-postgresql-database-performance" rel="noreferrer">pgbouncer</a>, etc. Since TimescaleDB looks and feels like PostgreSQL, there are minimal operational learning curves. TimescaleDB “just works,” as one would expect from PostgreSQL.</p><h2 id="query-language-ecosystem-and-ease-of-use">Query Language, Ecosystem, and Ease-Of-Use</h2><p>We applaud Amazon Timestream’s decision to adopt SQL as their query language. Even if Amazon Timestream functions like a NoSQL database in many ways, opting for SQL as the query interface lowers developers’ barrier to entry—especially when compared to other databases like MongoDB and InfluxDB.</p><p>That said, because Amazon Timestream is <strong>not </strong>a relational database, it doesn’t support normalized datasets and JOINs across tables. Also, because Amazon Timestream enforces a specific narrow table model on your data, deriving value from your data relies heavily on CASE statements and Common Table Expressions (CTEs) when requesting multiple measurement values (defined by “measurement_name”), leading to some clunky queries (see example later in this post).</p><p>TimescaleDB, on the other hand, has fully embraced all parts of the SQL language from day one—and <a href="https://docs.timescale.com/v2.0/api#analytics">extended SQL with functions custom-built to simplify time-series analysis</a>. TimescaleDB is also a relational database, allowing developers to store their metadata alongside their time-series data and JOIN across tables as necessary. Consequently, with TimescaleDB, new users have a minimal learning curve and are in full control when querying their data. </p><p>Full SQL means that TimescaleDB supports everything that SQL has to offer, including normalized datasets, cross-table JOINs, subqueries, stored procedures, and user-defined functions. </p><p>Supporting SQL also enables TimescaleDB to support everything in the SQL ecosystem, including Tableau, Looker, PowerBI, Apache Kafka, Apache Spark, Jupyter Notebooks, R, native libraries for every major programming language, and much more.<strong> </strong></p><p>For example, if you already use <a href="https://docs.timescale.com/use-timescale/latest/integrations/observability-alerting/tableau/" rel="noreferrer">Tableau to visualize data</a> or Apache Spark for data processing, TimescaleDB can plug right into the existing infrastructure due to its compatible connectors. And, given its roots, TimescaleDB supports everything in the PostgreSQL ecosystem, including tools like EXPLAIN that help pinpoint why queries are slow and identify ways to improve performance.</p><p>By contrast, even though Amazon Timestream speaks a variant of SQL, it is “SQL-like,” not full SQL. Thus, tooling that normally works with SQL—e.g., the Tableau and Apache Spark examples cited above—are unable to utilize Amazon Timestream data unless that tool incorporates specific Amazon Timestream drivers and SQL-like dialect. </p><p>This means, for example, that the tooling you might normally use to help you improve query performance doesn’t currently support Amazon Timestream. And, unfortunately, the current Amazon Timestream UI doesn’t give us any clues about why queries might be performing poorly or ways to improve performance (e.g., via settings or query hints).</p><p>In short, if you use PostgreSQL and any tools or extensions with your applications, they will “just work” when connected to TimescaleDB. The same isn’t true for Amazon Timestream. </p><p>So, while adopting a SQL-like query language is a great start for Amazon Timestream, we found a lot to be desired for a true “easy” developer experience.</p><h2 id="cloud-offering">Cloud Offering</h2><p>Amazon Timestream is only offered as a serverless cloud on Amazon Web Services. As of writing, it is available in three U.S. regions and one E.U. region. </p><p>Conversely, TimescaleDB can be run in your own infrastructure or fully managed <a href="https://www.timescale.com/products">through our cloud offering</a>, which make TimescaleDB available on AWS in over 75 regions and many different possible region/storage/compute configurations.</p><p>With TimescaleDB, our goal is to give our customers their choice of cloud and the ability to choose the region closest to their customers and co-locate with their other workloads.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream--most-clouds-and-regions-of-any-managed-service.png" class="kg-image" alt="TimescaleDB is available in all 3 clouds and more than 75 regions. Amazon Timestream is only available in 3 regions" loading="lazy" width="1457" height="1274" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream--most-clouds-and-regions-of-any-managed-service.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream--most-clouds-and-regions-of-any-managed-service.png 1000w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream--most-clouds-and-regions-of-any-managed-service.png 1457w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Cloud coverage comparison between Amazon Timestream and TimescaleDB (as of November 2020)</em></i></figcaption></figure><h2 id="what-we-like-about-amazon-timestream">What We Like About Amazon Timestream</h2><p>Despite the results of this comparison, we liked several things about Amazon Timestream. </p><p>The highest on the list is the simplicity of setting up a serverless offering. There aren’t any sizing decisions or knobs to tweak. Simply create a database, table, and then start sending data. </p><p>This brings up a second advantage of a serverless architecture: even if throughput isn’t ideal from a single client, the service appears to handle thousands of connections without issue. <a href="https://docs.aws.amazon.com/timestream/latest/developerguide/timestream.pdf">According to their documentation</a>, Amazon Timestream will eventually add more resources to keep up additional ingest or query threads. This means that an application shouldn’t be limited by the resources of one particular server for reads or writes.</p><p>Like with other NoSQL databases, some may find the schemaless nature of Amazon Timestream appealing, especially when a project is just getting off the ground. </p><p>Although schemas become more necessary as workloads grow for performance and data validation reasons, one of the reasons databases like MongoDB have grown in popularity is that they don’t require the same upfront planning as more traditional SQL databases.</p><p>Lastly, SQL. It shouldn’t come as a surprise that we like SQL as an efficient interface for the data we need to examine. And, although Amazon Timestream lacks some support for standard SQL dialects, most users will find it pretty straightforward to start querying data (after they understand Amazon Timestream’s narrow table model).</p><h3 id="but-why-is-amazon-timestream-so-expensive-slow-and-underwhelming">But why is Amazon Timestream so expensive, slow, and underwhelming?</h3><p>The reality is that Amazon Timestream, despite taking two years post-announcement to launch, still seems half-baked.</p><p>Why is Amazon Timestream so expensive, slow, and seemingly underdeveloped? We assume the reason is because of its underlying architecture. </p><p>Unlike other systems that we and others have benchmarked via the <a href="https://github.com/timescale/tsbs">Time-Series Benchmark Suite</a> (e.g., <a href="https://timescale.ghost.io/blog/blog/timescaledb-vs-influxdb-for-time-series-data-timescale-influx-sql-nosql-36489299877/">InfluxDB</a> and <a href="https://timescale.ghost.io/blog/blog/how-to-store-time-series-data-mongodb-vs-timescaledb-postgresql-a73939734016/">MongoDB</a>), Amazon Timestream is completely closed-source. </p><p>Based on our usage and experience with other Amazon services, we suspect that under the hood Amazon Timestream is backed by a combination of other Amazon services similar to Amazon ElastiCache, Athena, and S3. But because we cannot inspect the source code (and because Amazon does not make this sort of information public), this is just a guess.</p><p>By comparison, all of the <a href="https://github.com/timescale/timescaledb">source code for TimescaleDB</a> is available for anyone to inspect. We built TimescaleDB on top of PostgreSQL, giving it a rock-solid foundation and large ecosystem, and then spent years adding advanced capabilities to increase performance, lower costs, and improve the developer experience. </p><p>These capabilities include <a href="https://docs.timescale.com/latest/using-timescaledb/hypertables">auto-partitioning via hypertables and chunks</a>, faster queries via <a href="https://docs.timescale.com/latest/using-timescaledb/continuous-aggregates">continuous aggregates</a>, lower costs via 94&nbsp;%+ <a href="https://docs.timescale.com/use-timescale/latest/compression/about-compression/" rel="noreferrer">native compression</a>, high performance (10+ million inserts a second), and more. </p><p>We believe the real reason behind the difference between the two products is the companies building these products and how each approaches software development, community, and licensing. (And kind reader, this is where our bias may sneak in a little bit.)</p><h2 id="amazon-vs-timescale">Amazon vs. Timescale</h2><p>The viability of our company, Timescale, is 100&nbsp;% dependent on the quality of TimescaleDB. If we build a sub-par product, we cease to exist. </p><p>Amazon Timestream is just another of the <a href="https://en.wikipedia.org/wiki/List_of_Amazon_products_and_services">200+ services that Amazon is developing</a>. Regardless of the quality of Amazon Timestream, that team will still be supported by the rest of Amazon’s business—and if the product gets shut down, that team will find homes elsewhere within the larger company.</p><p>One can see this difference in how the two companies approach the developer community. Without a doubt, <a href="https://www.zdnet.com/article/the-top-cloud-providers-of-2020-aws-microsoft-azure-google-cloud-hybrid-saas/">Amazon Web Services is a leader in all things cloud computing</a>. However, with its enormous catalog of cloud services, many originally derived from external open-source projects, Amazon’s attention is spread over hundreds of products. </p><p>Case in point, when Amazon Timestream was announced in 2018, there was strong interest in when it would be released and how it would perform. However, after a two-year delay, with no information from Amazon, <a href="https://www.reddit.com/r/aws/comments/i5gomj/aws_timestream_still_happening_in_2020/">many gave up on waiting for the product</a>. When the product was finally released on September 30, 2020, there was <a href="https://news.ycombinator.com/item?id=24645416">very little fanfare from the community</a>.</p><p>In contrast, Timescale develops its <a href="https://github.com/timescale/timescaledb">source code</a> out in the open, and developers can reach us for help anytime directly via our <a href="https://slack.timescale.com/">Slack channel</a> (which is staffed by our engineers), whether they are a paying customer or not. We’ve continued to invest in our community by making all our software available for free while also serving our customers with our <a href="https://www.timescale.com/products/">hosted and fully managed cloud services</a>. </p><p>Building a high-performance, cost-effective, reliable, and easy-to-use time-series database is a hard and increasingly business-critical problem. For us, building TimescaleDB into a best-in-class time-series developer experience is an existential requirement. Without it, we cease to exist. For Amazon, Amazon Timestream is just a checkbox, another service to list on their website.</p><h3 id="when-amazon-is-forced-to-compete-on-product-quality-all-open-source-companies-have-a-shot-at-building-great-businesses">When Amazon is forced to compete on product quality, all open-source companies have a shot at building great businesses</h3><p>Amazon has a history of offering services that take advantage of the R&amp;D efforts of others: for example, <a href="https://aws.amazon.com/elasticsearch-service/">Amazon Elasticsearch Service</a>, <a href="https://aws.amazon.com/msk/">Amazon Managed Streaming for Apache Kafka</a>, <a href="https://aws.amazon.com/elasticache/redis/">Amazon ElastiCache for Redis</a>, and many others. </p><p>If Amazon wanted to launch a time-series database service that supported SQL, why did they build one from scratch and not just offer managed TimescaleDB?</p><p>Answer: our innovative licensing. The core of TimescaleDB is open-source, licensed under Apache 2. But advanced capabilities, such as compression and continuous aggregates, are licensed under the Timescale License, a source-available license that is open-source in spirit and makes all software available for free—but contains a critical restriction: preventing companies from offering that software via a hosted database-as-a-service. </p><p>The Timescale License is an example of a <em>“Cloud Protection License,”</em> which are licenses recognizing that the cloud has increasingly become the dominant form of open-source commercialization. </p><p>So these licenses protect the right of offering the software in the cloud for the main creator/maintainer of the project (who often contributes 99&nbsp;% of the R&amp;D effort). (Read more about <a href="https://timescale.ghost.io/blog/blog/building-open-source-business-in-cloud-era-v2/">how we're building a self-sustaining open-source business in the cloud era</a>.)</p><p>This “cloud protection” prevents Amazon from just distributing our R&amp;D and forces them to develop their own offering and compete on product quality, not just distribution. And as we can see from Amazon Timestream, building best-in-class database technologies is not easy, even for a company like Amazon.</p><p><strong>The truth is that when Amazon is forced to compete on product quality, all open-source companies have a shot at building great businesses. </strong></p><p>We welcome Amazon’s new entry to the time-series database market and appreciate that developers now have even more choices for storing and analyzing their time-series data. Competition is good for developers and helps drive further innovation. </p><p>For those who want to dig deeper into our benchmarking and comparison, we include detailed notes and methodology below.</p><p>For those who want to try Timescale, <a href="https://console.cloud.timescale.com/signup">create a free account </a>to get started with a fully managed TimescaleDB instance (100&nbsp;% free for 30 days). </p><p>Want to host TimescaleDB yourself? <a href="https://github.com/timescale/timescaledb">Visit our GitHub</a> to learn more about options, get installation instructions, and more (and, if you like what you see, ⭐️  are always appreciated!).</p><p>Join our <a href="http://slack.timescale.com/">Slack community</a> to ask questions, get advice, and connect with other developers (I, as well as our co-founders, engineers, and passionate community members are active on all channels).</p><h2 id="performance-comparison-details">Performance Comparison Details</h2><p>Here is a quantitative comparison of the two databases across insert and query workloads.</p><p><em>Note: We've released all the code and data used for the below benchmarks as part of the open-source Time Series Benchmark Suite (TSBS) (</em><a href="https://github.com/timescale/tsbs"><em>GitHub</em></a><em>, </em><a href="https://timescale.ghost.io/blog/blog/time-series-database-benchmarks-timescaledb-influxdb-cassandra-mongodb-bc702b72927e/?utm_source=timescale-influx-benchmark&amp;utm_medium=blog&amp;utm_campaign=july-2020-advocacy&amp;utm_content=tsbs-announcemement-blog"><em>announcement</em></a><em>), so you can reproduce our results or run your own analysis. </em></p><p><em>Typically, when we conduct performance benchmarks (for example, in our previous benchmarks versus </em><a href="https://timescale.ghost.io/blog/blog/timescaledb-vs-influxdb-for-time-series-data-timescale-influx-sql-nosql-36489299877/"><em>InfluxDB</em></a><em> and </em><a href="https://timescale.ghost.io/blog/blog/how-to-store-time-series-data-mongodb-vs-timescaledb-postgresql-a73939734016/"><em>MongoDB</em></a><em>) we use five different dataset configurations. These configurations increase metric loads and cardinalities, to simulate a breadth of time-series workloads for inserts and queries. </em></p><p><em>However, as you’ll see below, because of performance issues with Amazon Timestream, we were unable to look at Amazon Timestream’s performance under higher cardinalities and were limited to testing just our lowest-cardinality dataset.</em><br></p><h3 id="machine-configuration">Machine Configuration</h3>
<!--kg-card-begin: html-->
<p>
    <strong>Amazon Timestream </strong>
    <br style="display: block" />
    Amazon Timestream is a serverless offering, which means that a user cannot provision a specific service tier. The only meaningful configuration option that a user can modify is the “memory store retention” period and the “magnetic store retention” period. In Amazon Timestream, data can only be inserted into a table if the timestamp falls within the memory store retention period. Therefore, the only setting that we modified to insert data for our first test was to set the memory store retention period to 865 hours (~36 days) to provide padding to account for a slower insert rate.
</p>
<!--kg-card-end: html-->
<p></p><p><strong>It did not take long for us to realize that Amazon Timestream’s insert performance was dramatically slower than other time-series databases we’ve benchmarked</strong>. Therefore, we took extra time to test insert performance using three different Amazon EC2 instance configurations, each launched in the same region as our Amazon Timestream database:</p><ul><li>t3.medium running Ubuntu 18 LTS, 2 vCPUs, 4&nbsp;GB mem, up to 5 Gb network</li><li>c5n.2xlarge running Ubuntu 20 LTS, 8 vCPUs, 29&nbsp;GB mem, up to 25 Gb network</li><li>m5n.12xlarge running Ubuntu 18 LTS, 48 vCPUs,192&nbsp;GB mem, 50 Gb network</li></ul><p>After numerous attempts to insert data with each of these instance types, we determined that the size of the client did not noticeably impact insert performance at all. Instead, we needed to run multiple client instances to ingest more data. </p><p>In the end, we chose to write data from 1 and 10 t3.medium clients, each running 20 threads of TSBS. In the case of 10 clients, each covered a portion of the 30 days to avoid writing duplicate data (Amazon Timestream does not support writing duplicate data).</p>
<!--kg-card-begin: html-->
<p>
    <strong>TimescaleDB</strong>
    <br style="display:block">
    To test the same insert and read latency performance on TimescaleDB, we used the following setup:
</p>
<!--kg-card-end: html-->
<ul><li>Version: TimescaleDB <a href="https://github.com/timescale/timescaledb/releases/tag/1.7.4">version 1.7.4</a>, with PostgreSQL 12</li><li>One remote client machine and one database server, both in the same cloud data center</li><li>Instance size: both client and database server ran on DigitalOcean virtual machines (droplets) with 32 vCPU and 192&nbsp;GB memory each</li><li>OS: both server and client machines ran Ubuntu 18.04.3</li><li>Disk Size: 4.8&nbsp;TB of disk in a raid0 configuration (EXT4 filesystem)</li><li>Deployment method: TimescaleDB was deployed using <a href="https://hub.docker.com/r/timescale/timescaledb">Docker images from the official Docker hub</a></li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Datasets-for-benchmarking-table-1.png" class="kg-image" alt="TimescaleDB achieves 6000 times higher inserts than Amazon Timestream" loading="lazy" width="1355" height="891" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-Datasets-for-benchmarking-table-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-Datasets-for-benchmarking-table-1.png 1000w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Datasets-for-benchmarking-table-1.png 1355w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Results of the insert performance between TimescaleDB and Amazon Timestream</span></figcaption></figure><p><strong>In our tests, TimescaleDB outperformed Amazon Timestream by a shocking 6,000x on inserts.</strong> </p><p>The lackluster insert performance of Amazon Timestream took us by surprise, especially since we were using the Amazon Timestream SDK and modeling our TSBS code from examples in their documentation. </p><p>In the interest of being thorough and fair to Amazon Timestream, we tried increasing the number of clients writing data, made some code modifications to increase concurrency (in ways that weren’t necessary for TimescaleDB), and worked to eliminate any possible thread contention, and then ran the same benchmark with 10 clients on Amazon Timestream. </p><p>After this effort, we were able to increase Amazon Timestream performance to 5,250 metrics/second (across 10 clients)—but even then, TimescaleDB (with only one client and without any extra code modifications) outperformed Amazon Timestream by 600x.</p><p><em>(Hypothetically, we could have started a lot more clients to increase insert performance on Amazon Timestream (assuming no bottlenecks), but with an average ingest rate of ~523 metrics/second per client, we would have had to start ~61,000 EC2 instances at the same time to finish inserting metrics as fast as one client writing to TimescaleDB.)</em></p><p>In particular, with this low performance, we were only able to test our lowest cardinality workload, not our usual five—even though we worked at it for more than a week. This scenario attempts to insert 100 simulated devices, each generating 10 CPU metrics every 10 seconds for ~100M reading intervals (for one billion metrics). </p><p>We never actually made it to the full one billion metrics with Amazon Timestream. After nearly 40 hours of inserting data from 10 EC2 clients, we were only able to insert slightly over 410 million metrics. <em>(</em><a href="https://github.com/timescale/tsbs/blob/master/docs/timestream.md"><em>The dataset was created using Time-Series Benchmarking Suite, using the cpu-only use case.</em></a><em>)</em></p><p>Let us put it another way:</p><ul><li>We first tested Amazon Timestream and TimescaleDB with one client writing data.</li><li>Then, in an attempt to be fair to Amazon Timestream, we tested it with 10 separate EC2 instances over a two-day period, inserting batches of 1,000 readings (100 hosts, 10 measurements per host) as fast as possible.</li><li>It’s also worth noting that most clients started to receive a fatal connection error from Amazon Timestream between the 28 and 32-hour mark and didn’t recover. Only one client made uninterrupted inserts for more than 40 hours before we manually stopped it. It’s possible that with some additional error checking with the Amazon Timestream SDK response, TSBS could have recovered on its own and continued to send metrics from all 10 clients.</li></ul><p>In total, this means that we inserted data into Amazon Timestream for 332.5 hours and achieved slightly more than 410 million metrics.</p><p><strong>TimescaleDB inserted one billion metrics from one client in just under five minutes.</strong></p><p>Amazon claims that Amazon Timestream will learn from your insert and query patterns and automatically adjust resources to increase performance. Their documentation <a href="https://docs.aws.amazon.com/timestream/latest/developerguide/writes.html">specifically warns that writes may become throttled</a>, with the only remedy to keep inserting at the same (or higher) rate until Amazon Timestream adjusts. </p><p>However, in our experience, 332.5 hours of inserting data at a very consistent rate was not enough time for it to make this adjustment.</p><p><em><strong>The issue of cardinality: </strong></em>One other side-effect of Amazon Timestream taking so long to ingest data: we couldn’t compare how it performs with higher cardinalities, which are common in time-series scenarios, where we need to ingest a <em>relentless</em> stream of metrics from devices, apps, customers, and beyond. (<a href="https://timescale.ghost.io/blog/blog/what-is-high-cardinality-how-do-time-series-databases-influxdb-timescaledb-compare/">Read more about the role of cardinality in time series and how TimescaleDB solves it</a>.)<br><br>We’ve shown in <a href="https://timescale.ghost.io/blog/blog/what-is-high-cardinality-how-do-time-series-databases-influxdb-timescaledb-compare/#:~:text=In%20the%20world%20of%20databases,%E2%80%9D)%20that%20describes%20that%20data.">previous benchmarks</a> that TimescaleDB actually sees better performance relative to other time-series databases as cardinality increases, with moderate drop off in terms of absolute insert rate. TimescaleDB surpasses many other popular time-series databases, like InfluxDB, in terms of insert performance for the configurations of 4,000, 100,000, 1 million, and 10 million devices.</p><p>But again, we were unable to test this given Amazon Timestream’s (lack of) performance.</p><p><strong>Insert performance summary:</strong></p><ul><li>TimescaleDB outperforms Amazon Timestream in raw numbers that we found hard to believe. However, despite our best efforts to optimize Amazon Timestream, TimescaleDB still outperformed Amazon Timestream by 6,000x (600x if using 10 clients on Amazon Timestream to TimescaleDB’s one).</li><li>In the time it took us to make a pot of coffee, TimescaleDB inserted one billion metrics for a 31-day period. With Amazon Timestream, we got two nights' sleep and inserted less than half the metrics.</li><li>That said, if your insert performance is far below these benchmarks (e.g., a few thousand metrics/second), then insert performance will not be your bottleneck.</li></ul><p><strong>Full results:</strong></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2020/12/image-14.png" class="kg-image" alt="Table showing TimescaleDB vs. Amazon Timestream Insert Rate comparison ratios in metrics/second" loading="lazy" width="2000" height="1200" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2020/12/image-14.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2020/12/image-14.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2020/12/image-14.png 1600w, https://timescale.ghost.io/blog/content/images/2020/12/image-14.png 2280w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">TimescaleDB vs. Amazon Timestream Insert Rate comparison ratios</em></i></figcaption></figure><p><strong>More information on database configuration for this test:</strong></p>
<!--kg-card-begin: html-->
<p>
    <em>Batch size</em>
    <br style="display:block">
    From our research and community members’ feedback, we’ve found that larger batch sizes generally provide better insert performance. (It’s one of the reasons we created tools like Parallel COPY to help our users insert data in large batches concurrently). 
</p>
<!--kg-card-end: html-->
<p>In our benchmarking tests for TimescaleDB, the batch size was set to 10,000, something we’ve found works well for this kind of high throughput. The batch size, however, is completely configurable and often worth customizing based on your application requirements.<br><br>Amazon Timestream, on the other hand, has a fixed batch size limit of 100 values. This seems to require significantly more overhead and insert latency increases dramatically as the number of metrics we try to insert at one time increases. This is one of the first reasons we believe insert performance was so much slower with Amazon Timestream.<br></p>
<!--kg-card-begin: html-->
<p>
    <em>
        Additional database configurations
    </em>
    <br style="display: block">
    For TimescaleDB, we set the chunk time depending on the data volume, aiming for 7-16 chunks in total for each configuration (see our documentation for more on hypertables - "chunks"). 
</p>
<!--kg-card-end: html-->
<p><br>With Amazon Timestream, there aren’t additional settings you can tweak to try and improve insert performance—at least not that we found, given the tools provided by Amazon. </p><p>As mentioned in the machine configuration section above, we had to set the memory store retention period equal to ~36 days to ensure we could get all of our data inserted before the magnetic store retention period kicked in.</p><h2 id="query-performance-comparison">Query Performance Comparison</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-175x-faster-queries-1.png" class="kg-image" alt="TimescaleDB achieved 5 to 175 times better query performance than Amazon Timestream" loading="lazy" width="1562" height="1160" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-175x-faster-queries-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-175x-faster-queries-1.png 1000w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-175x-faster-queries-1.png 1562w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Note: Several queries’ ratios (high-cpu-all, lastpoint, groupby-orderby-limit) are “undefined” because Amazon Timestream did not finish executing them within the default 60 second timeout period that Timestream imposes, while TimescaleDB completed them in less than a single second</em></i></figcaption></figure><p>Measuring query latency is complex. Unlike inserts, which primarily vary on cardinality size, the universe of possible queries is essentially infinite, especially with a language as powerful as SQL. </p><p>Often, the best way to benchmark read latency is to do it with the actual queries you plan to execute. For this case, we use a broad set of queries to mimic the most common time-series query patterns.</p><p>For benchmarking query performance, we decided to use a c5n.2xlarge EC2 instance to perform the queries with Amazon Timestream. Our hope was that having more memory and network throughput available to the query application would give Amazon Timestream a better chance. The client for TimescaleDB was unchanged.</p><p>Recall that we ran these queries on Amazon Timestream with a dataset that was 40 % that of the one we ran on TimescaleDB (410 million vs. one billion metrics), owing to the insert problems we had above. </p><p>Also, because we had to set the memory store retention period to ~36 days, all the data we queried was in the fastest storage available. These two advantages <em>should</em> have given Amazon Timestream a considerable edge.</p><p><strong>That said, TimescaleDB still outperformed Amazon Timestream by 5x to 175x, depending on the query, with Amazon Timestream unable to finish several queries. </strong></p><p>The results below are the average from 1,000 queries for each query type. Latencies in this chart are all shown as milliseconds, with an additional column showing the relative performance of TimescaleDB compared to Amazon Timestream.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Query-Performance-1.png" class="kg-image" alt="Table showing latency ratios for various queries - run on 100 devices x 10 metrics - in milliseconds" loading="lazy" width="1710" height="1496" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-Query-Performance-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-Query-Performance-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/08/timescale-vs-amazon-timestream-Query-Performance-1.png 1600w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Query-Performance-1.png 1710w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Results of benchmarking query performance between TimescaleDB and Amazon Timestream</em></i></figcaption></figure><p><strong>Results by query type:</strong></p>
<!--kg-card-begin: html-->
<p>
    <em>SIMPLE ROLLUPS</em>
    <br style="display: block">
    For simple rollups (i.e., groupbys), when aggregating one metric across a single host for 1 or 12 hours, or multiple metrics across one or multiple hosts (either for 1 hour or 12 hours), TimescaleDB significantly outperforms Amazon Timestream by 11x to 28x.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
    <em>AGGREGATES</em>
    <br style="display: block">
    When calculating a simple aggregate for one device, TimescaleDB again outperforms Amazon Timestream by a considerable margin, returning results for each of 1,000 queries more than 19x faster.
</p>
    
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
    <em>DOUBLE ROLLUPS</em>
    <br style="display: block">
    For double rollups aggregating metrics by time and another dimension (e.g., GROUPBY time, deviceId), TimescaleDB again achieves significantly better performance, 5x to 12x.
</p>
    
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
    <em>THRESHOLDS</em>
    <br style="display: block">
   When selecting rows based on a threshold (CPU > 90 %), we see Amazon Timestream really begin to fall apart. Finding the last reading for one host greater than 90 % performs 170x better with TimescaleDB compared to Amazon Timestream. And the second variation of this query, trying to find the last reading greater than 90% for all 100 hosts (in the last 31 days), never finished in Amazon Timestream.
</p>
    
<!--kg-card-end: html-->
<p><em>Again, to be fair and ensure our query was returning the data we expected, we did manually run one of these queries in the Amazon Timestream Query interface of the AWS Console. It would routinely finish in 30-40 seconds (which would still be 36x slower than TimescaleDB). </em></p><p><em>In addition, running 100 of these queries at a time with the benchmark suite appears to be too much for the query engine, and results for the first set of 100 queries didn’t complete after more than 10 minutes of waiting.</em></p>
<!--kg-card-begin: html-->
<p>
    <em>COMPLEX QUERIES</em>
    <br style="display: block">
   Likewise, for complex queries that go beyond rollups or thresholds, there is no comparison. TimescaleDB vastly outperforms Amazon Timestream, in most cases because Amazon Timestream never returned results for the first set of 100 queries. Just like the complex aggregate above that failed to return any results when queried in batches of 100, these complex queries never returned results with the benchmark client.
</p>
<!--kg-card-end: html-->
<p>In each case, we attempted to run the queries multiple times, ensuring that no other clients or processes were inserting or accessing data. We also ran at least one of the queries manually in the AWS Console to verify that it worked and that we got the expected results. However, when running these kinds of queries in parallel, there seems to be a major issue with Amazon Timestream being able to satisfy the requests.</p><p>For these more complex queries that return results from Amazon Timestream, TimescaleDB provides real-time responses (e.g., 10-100s of milliseconds), while Amazon Timestream sees significant human-observable delays (seconds). </p><p>And remember, this dataset only had a cardinality of 100 hosts, the lowest cardinality we typically test with the Time-Series Benchmarking Suite, and we were unable to test higher cardinality datasets because of Amazon Timestream issues).</p><p>Notice that Timescale exhibits 48x-175x the performance of Amazon Timestream on these complex queries, many of which are common to historical time-series analysis and monitoring.</p><p><strong>Read latency performance summary</strong></p><ul><li>For simple queries, TimescaleDB outperforms Amazon Timestream in every category.</li><li>When selecting rows based on a threshold, TimescaleDB outperforms Amazon Timestream by a significant margin, being over 175x faster.</li><li>For complex queries with even low cardinality, Amazon Timestream was unable to return results for sets of 100 queries within the 60-second default query timeout.</li><li>Concurrent query load over the same time range seems to impact Amazon Timestream in a dramatic way.</li></ul><h2 id="cost-comparison-details">Cost Comparison Details</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-154x-cheaper-than-Amazon-Timestream-1.png" class="kg-image" alt="To run this test TimescaleDB took less than an hour at a cost of $2.18. The same test took a week and cost $336 in Amazon Timestream" loading="lazy" width="1382" height="746" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-154x-cheaper-than-Amazon-Timestream-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-154x-cheaper-than-Amazon-Timestream-1.png 1000w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-154x-cheaper-than-Amazon-Timestream-1.png 1382w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Results of cost comparison between TimescaleDB and Amazon Timestream</span></figcaption></figure><p>These performance differences between TimescaleDB and Amazon Timestream lead to massive cost differences for the same workloads. </p><p>To compare costs, we calculated the cost for our above insert and query workloads, which store one billion metrics in TimescaleDB and ~410 million metrics in Amazon Timestream (because we could not load the full one billion), and run our suite of queries on top.</p><p>Pricing for Amazon Timestream is rather complex. In all, our bill for testing Amazon Timestream over the <strong>course of seven days</strong> cost us $336.39, <strong>which does not include any Amazon EC2 charges </strong>(which we needed for the extra clients). During that time, our bill shows that:</p><ul><li>We inserted 100&nbsp;GB of data (~500 million metrics total across <strong><em>all</em></strong> of our attempts to ingest data)</li><li>Stored a lot of data in memory (and we continue to be charged per hour for that data)</li><li>Queried 21 TB of data when running 25,000 real-world queries</li></ul><p>For comparison, our tests for TimescaleDB (inserts and queries) completed in far less than an hour, and our Digital Ocean droplet costs $1.50/hour. We also ran this test on Timescale, our fully managed TimescaleDB service, and it was also completed in far less than an hour, and the instance (8 vCPU, 32&nbsp;GB, 1&nbsp;TB) cost $2.18/hour.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2020/12/image-12.png" class="kg-image" alt="Screenshot of Amazon Timestream bill with various charges, totaling 336.40 USD" loading="lazy" width="2000" height="804" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2020/12/image-12.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2020/12/image-12.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2020/12/image-12.png 1600w, https://timescale.ghost.io/blog/content/images/2020/12/image-12.png 2280w" sizes="(min-width: 720px) 720px"></figure><p><strong>$336.39 for Amazon Timestream vs. $2.18 for fully managed TimescaleDB ($1.50 if you would rather self-manage the instance yourself), which means that TimescaleDB is 154x cheaper than Amazon Timestream (224x cheaper if self-managed)—and it loaded <em>and</em> queried over twice the number of metrics.</strong></p><h3 id="now-let%E2%80%99s-dig-a-little-deeper-into-our-amazon-timestream-bill">Now, let’s dig a little deeper into our Amazon Timestream bill</h3><p>When using Amazon Timestream, users are charged for usage in four main categories: data ingested, data stored in both memory and magnetic storage and the amount of data scanned to satisfy your queries.</p><p>Data that resides in the memory store costs $0.036 GB/hour, while data that is eventually moved to magnetic storage costs only $0.03 GB/month. For our one-month memory store setting (which was required to insert 30 days of historical data), <strong>that’s more than a 720x difference in cost for the same data</strong>.</p><p> What’s more, since Amazon Timestream doesn’t expose any information about how data is stored in the Console or otherwise, we have no idea how well-compressed this data is, or if there is anything more we could do to reduce storage.</p><p>The real surprise, however, came with querying data because the charges don’t scale with performance. Instead, you will be charged for the amount of data scanned to produce a query result, no matter how fast that result comes back. In almost all other database-as-a-service offerings, you can modify the storage, compute, or cluster size (at a known cost) for better performance.</p><p>After waiting nearly two days to insert 410 million metrics, we created the traditional set of query batches (as outlined above) and began to run our queries. <br><br>In total, we had 15 query files, each with 1,000 queries, for a total of 15,000 queries to run against both Amazon Timestream and TimescaleDB. </p><p>While some of the queries are certainly complex, others just ask for the most recent point for each host (and remember, this dataset only had 100 hosts).  Also, recall from our query performance comparison that a few of the most complex queries were unable to return results for just 100 queries, let alone the full 1,000 query test. </p><p><strong>With Amazon Timestream, you are still charged for the data that was read, even if the query was ultimately canceled or never returned a result.</strong></p><p>To validate results, we ran each query file twice. In the case where three of the query files failed to return results, we attempted to execute them five times, hoping for some result. </p><p>Doing the math, this means that we ran around 25,000 queries. In doing so, Amazon says that we scanned 21,598.02 GB of data, which cost $215.98. There were certainly a few other ad hoc queries performed through the AWS Console UI, but before we started running the benchmarking queries, the total cost for scanning data was about $15.00.</p><p>Furthermore, as we’ve mentioned a few times, there is no built-in support to help you identify which queries are scanning too much data and how you might improve them. For comparison, both Amazon Redshift and Amazon RDS provide this kind of feedback in their AWS Console interface.</p><p>When we consider some of the recent user applications that we have highlighted elsewhere on our blog, like <a href="https://timescale.ghost.io/blog/blog/how-flightaware-fuels-flight-prediction-models-with-timescaledb-and-grafana/">FlightAware</a> or <a href="https://timescale.ghost.io/blog/blog/how-clevabit-builds-data-pipeline-for-agricultural-iot/">clevabit</a>, 25,000 queries of various shapes and sizes would easily be run in a few hours or less.</p><p>While the bytes scanned might improve over time as partitioning improves, if you don’t need to scale storage beyond a few petabytes of data, it’s hard to see how this would be less costly than a fixed Compute and Storage cost.</p><h2 id="reliability-comparison-details">Reliability Comparison Details</h2><p>Another cardinal rule for a database: it cannot lose or corrupt your data. In this respect, the serverless nature of Amazon Timestream requires that you trust Amazon will not lose your data and all of the data will be stored without corruption. Usually, this is probably a pretty safe bet. In fact, many companies rely on services like Amazon S3 or Amazon Glacier to store their data as a reliable backup solution. </p><p>The problem is that we don’t know where our time-series data is stored in Amazon Timestream—because Amazon does not tell us.</p><p>This presents a specific challenge that Amazon hasn’t addressed natively: validating or backing up your data.</p><p>In their <a href="https://docs.aws.amazon.com/timestream/latest/developerguide/timestream.pdf">240-page development guide</a>, the words “recovery” and “restore” don’t appear at all, and the word “backup” appears only once to tell the developer that there isn’t a backup mechanism. Instead, you can “write your own application using the Timestream SDK to query data and save it to the destination of your choice” (page 100).</p><p>This is not to say that Amazon Timestream will lose or corrupt your data. As we mentioned, Amazon S3, for instance, is a widely known and used service for data storage. The issue here is that we’re unable to learn or easily verify where our data resides and how it’s protected in a service interruption.</p><p>We also found it worrisome that with Amazon Timestream, there isn’t a mechanism or support to DELETE or UPDATE existing data. <strong>The only way to remove data is to drop the entire table.</strong> Furthermore, there is no way to recover a deleted table since it is an atomic action that cannot be recovered through any Amazon API or Console. </p><p>Even if one were to write their own backup and restore utility, there is no method for importing more than the most recent year of data because of the memory store retention period limitation. </p><p>As an Amazon Timestream user, all these limitations put us in a precarious position. There’s no easy way to back up our data or restore it once we’ve accumulated more than a year's worth. Even if Amazon never loses our data, deleting an essential table of data <a href="https://www.geekwire.com/2015/starbucks-back-in-business-internal-report-blames-deleted-database-table-indicates-outage-was-global/">through human error is not uncommon</a>.</p><p>TimescaleDB uses a dramatically different design principle: build on PostgreSQL. As noted previously, this allows TimescaleDB to inherit over 25 years of dedicated engineering effort that the entire PostgreSQL community has done to build a rock-solid database that supports millions of applications worldwide. (In fact, this principle was at the core of our initial <a href="https://timescale.ghost.io/blog/blog/when-boring-is-awesome-building-a-scalable-time-series-database-on-postgresql-2900ea453ee2/?utm_source=timescale-influx-benchmark&amp;utm_medium=blog&amp;utm_campaign=july-2020-advocacy&amp;utm_content=timescale-launch-blog">TimescaleDB launch announcement</a>.)</p><h2 id="query-language-ecosystem-and-ease-of-use-comparison-details">Query Language, Ecosystem, and Ease-Of-Use Comparison Details</h2><p>We applaud Amazon Timestream’s decision to adopt SQL as their query language. We have always been big fans and vocal advocates of SQL, which has become the query language of choice for data infrastructure, is well-documented, and currently ranks as the <a href="https://insights.stackoverflow.com/survey/2020#most-popular-technologies">third-most commonly used programming language among developers</a> (<a href="https://timescale.ghost.io/blog/blog/why-sql-beating-nosql-what-this-means-for-future-of-data-time-series-database-348b777b847a/">see our SQL vs. NoSQL comparison</a> for more details). </p><p>Even if Amazon Timestream functions like a NoSQL database in many ways, opting for SQL as the query interface lowers developers’ barrier to entry—especially when compared to other databases like MongoDB and InfluxDB with their proprietary query languages.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Comparisson-table.png" class="kg-image" alt="Screenshot of Stack Overflow Developer survey results, showing percentage breakdown of various languages" loading="lazy" width="1290" height="1617" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/08/timescale-vs-amazon-timestream-Comparisson-table.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/08/timescale-vs-amazon-timestream-Comparisson-table.png 1000w, https://timescale.ghost.io/blog/content/images/2023/08/timescale-vs-amazon-timestream-Comparisson-table.png 1290w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Most popular Programming, Scripting, and Markup Languages. Source: </em></i><a href="https://insights.stackoverflow.com/survey/2020#most-popular-technologies"><i><em class="italic" style="white-space: pre-wrap;">2020 Stack Overflow Developer Surve</em></i></a><i><em class="italic" style="white-space: pre-wrap;">y</em></i></figcaption></figure><p>As discussed earlier in this article, Amazon Timestream is not a relational database, despite feeling like it could be because of the SQL query interface. It doesn’t support normalized datasets, JOINs across tables, or even some common “tricks of the trade” like a simple LATERAL JOIN and correlated subqueries. </p><p>Between these SQL limitations and the narrow table model that Amazon Timestream enforces on your data, writing efficient (and easily readable) queries can be a challenge. </p>
<!--kg-card-begin: html-->
<p>
    <em>Example</em>
    <br style="display: block">
    To see a brief example of how Amazon Timestream’s “narrow” table model impacts the SQL that you write, let’s look at an example given in the Timestream documentation, <a href="https://docs.aws.amazon.com/timestream/latest/developerguide/sample-queries.iot-scenarios.html" target="_blank">Queries with aggregate functions.</a>
</p>
<!--kg-card-end: html-->
<p>Specifically, we’ll look at the example to “<em>find the average load and max speed for each truck for the past week</em>”:</p><p><strong>Amazon Timestream SQL (CASE statement needed)</strong></p><pre><code class="language-SQL">SELECT
    bin(time, 1d) as binned_time,
    fleet,
    truck_id,
    make,
    model,
    AVG(
        CASE WHEN measure_name = 'load' THEN measure_value::double ELSE NULL END
    ) AS avg_load_tons,
    MAX(
        CASE WHEN measure_name = 'speed' THEN measure_value::double ELSE NULL END
    ) AS max_speed_mph
FROM "sampleDB".IoT
WHERE time &gt;= ago(7d)
AND measure_name IN ('load', 'speed')
GROUP BY fleet, truck_id, make, model, bin(time, 1d)
ORDER BY truck_id</code></pre><p><strong>TimescaleDB SQL</strong></p><pre><code class="language-SQL">SELECT
    time_bucket(time, '1 day') as binned_time,
    fleet,
    truck_id,
    make,
    model,
    AVG(load) AS avg_load_tons,
    MAX(speed) AS max_speed_mph
FROM "public".IoT
WHERE time &gt;= now() - INTERVAL '7 days'
GROUP BY fleet, truck_id, make, model, binned_time
ORDER BY truck_id</code></pre><p>If the above is any indication, even the most simple aggregate queries in Amazon Timestream require multiple levels of CASE statements and column renaming. We were unable to write this query or pivot the results more easily.</p><p>Conversely, as evidenced in the above example, with TimescaleDB, we use standard SQL syntax. Additionally, any query that already works with your PostgreSQL-supported applications will “just work.” The same isn’t true for Amazon Timestream.</p><p>So while the decision to adopt a SQL-like query language is a great start for Amazon Timestream, there is still a lot to be desired for a truly frictionless, developer-first experience.</p><h2 id="summary">Summary</h2><p>No one wants to invest in a technology only to have it limit their growth or scale in the future, let alone invest in something that's the wrong fit today.</p><p>Before making a decision, we recommend taking a step back and analyzing your stack, your team's skills, and your needs (now and in the future). It could be the difference between infrastructure that evolves and grows with you and one that forces you to start all over.</p><p>In this post, we performed a detailed comparison of TimescaleDB and Amazon Timestream. We don’t claim to be <a href="https://www.tigerdata.com/blog/so-long-timestream-how-and-why-to-migrate-before-its-too-late" rel="noreferrer">Amazon Timestream</a> experts, so we’re open to suggestions on improving this comparison—and invite you to perform your own and share your results.</p><p>In general, we aim to be as transparent as possible about our data models, methodologies, and analysis, and we welcome feedback. We also encourage readers to raise any concerns about the information we’ve presented to help us with benchmarking in the future.</p><p>We recognize that <a href="http://www.timescale.com/?utm_source=timescale-influx-benchmark&amp;utm_medium=blog&amp;utm_campaign=july-2020-advocacy&amp;utm_content=homepage">Timescale</a> isn’t the only time-series solution on the market. There are situations where it might not be the best time-series database choice, and we strive to be upfront in admitting where an alternate solution may be preferable.</p><p>We’re always interested in holistically evaluating our solution against others, and we’ll continue to share our insights with the greater community.</p><h2 id="want-to-learn-more-about-timescale">Want to Learn More About Timescale?</h2><p><a href="https://console.cloud.timescale.com/signup">Create a free account </a>to get started with a fully managed TimescaleDB instance (100&nbsp;% free for 30 days). </p><p>Want to host TimescaleDB yourself? <a href="https://github.com/timescale/timescaledb">Visit our GitHub</a> to learn more about options, get installation instructions, and more (and, as always, ⭐️  are appreciated!)</p><p>Join our <a href="http://slack.timescale.com/">Slack community</a> to ask questions, get advice, and connect with other developers (I, as well as our co-founders, engineers, and passionate community members are active on all channels).</p>]]></content:encoded>
        </item>
    </channel>
</rss>