<?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:10:05 GMT</lastBuildDate>
        <atom:link href="https://www.tigerdata.com/blog" rel="self" type="application/rss+xml"/>
        <ttl>60</ttl>
        <item>
            <title><![CDATA[Benchmarking PostgreSQL Batch Ingest]]></title>
            <description><![CDATA[See what PostgreSQL batch ingest method is right for your use case: in this article, we benchmark INSERT (VALUES and UNNEST) vs. COPY (text and binary).]]></description>
            <link>https://www.tigerdata.com/blog/benchmarking-postgresql-batch-ingest</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/benchmarking-postgresql-batch-ingest</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[PostgreSQL Performance]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Tue, 26 Nov 2024 14:00:51 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2024/11/to_char-11111---FM9-999-999-----2-.png">
            </media:content>
            <content:encoded><![CDATA[<p>In a previous <a href="https://timescale.ghost.io/blog/tag/performance/"><u>article</u></a> in this <a href="https://timescale.ghost.io/blog/tag/performance/"><u>series</u></a>, I explored the magic of <code>INSERT...UNNEST</code> for improving PostgreSQL batch <code>INSERT</code> performance. While it’s a fantastic technique, I know it’s not the fastest option available (although it is very flexible). Originally, I hadn't intended to loop back and benchmark all the batch ingest methods, but I saw a lot of confusion out there, so I'm back, and this time I'm looking at <code>COPY</code> too. As usual for this series, it’s not going to be a long post, but it is going to be an informative one.&nbsp;</p><p>I flipped my approach for this post, comparing not just the PostgreSQL database performance in isolation but the practical performance from an application. To do this, I built a custom benchmarking tool in <a href="https://www.rust-lang.org/">Rust</a> to measure the end-to-end performance of each method. In this article, I’ll walk you through the batch ingest options you’ve got, and how they stack up (spoiler alert, the spread is over 19x!).</p><h2 id="the-introduction-batch-ingest-in-postgresql">The Introduction: Batch Ingest in PostgreSQL</h2><p>I’m defining batch ingest as writing a dataset to PostgreSQL in batches or chunks. You’d usually do this because the data is being collected in (near) real time (think a flow of IoT data from sensors) before being persisted into PostgreSQL (hopefully with <a href="https://docs.timescale.com/">TimescaleDB</a>, although that's out of scope for this post). </p><p>Writing a single record at a time is incredibly inefficient, so writing batches makes sense (the size probably depends on how long you can delay writing). Just to be clear this isn't about loading a very large dataset in one go, I’d call that <a href="https://www.timescale.com/learn/testing-postgres-ingest-insert-vs-batch-insert-vs-copy">bulk ingest </a>not batch ingest (and you'd usually do that from a file).</p><p>Broadly speaking, there are two methods for ingesting multiple values at once in PostgreSQL: <code>INSERT</code> and <code>COPY</code>. Each of these methods has a few variants, so let's look at the differences.</p><h2 id="insert-values-and-unnest">INSERT: VALUES and UNNEST</h2><p>The most common method to ingest data in PostgreSQL is the standard <code>INSERT</code> statement using the <code>VALUES</code> clause. Everyone recognizes it, and every language and ORM (object-relational mapper) can make use of it. While you can insert a single row, we are interested in batch ingest here, passing multiple values using the following syntax (this example is a batch of three for a table with seven columns).</p><pre><code class="language-SQL">INSERT INTO sensors
VALUES&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;($1,  $2,  $3,  $4,  $5,  $6,  $7),
&nbsp;&nbsp;&nbsp;&nbsp;($8,  $9,  $10, $11, $12, $13, $14),
    ($15, $16, $17, $18, $19, $20, $21);</code></pre><p>The other method is <code>INSERT...UNNEST</code>. Here, instead of passing a value per attribute (so <code>batch size * columns</code> total values), we pass an array of values per column.</p><pre><code class="language-SQL">INSERT INTO sensors
SELECT * FROM unnest(
&nbsp;&nbsp;&nbsp;&nbsp;$1::int[],
&nbsp;&nbsp;&nbsp;&nbsp;$2::timestamp[],
&nbsp;&nbsp;&nbsp;&nbsp;$3::float8[],
    $4::float8[],
    $5::float8[]
    $6::float8[]
    $7::float8[]
);</code></pre><p>If you’re after a discussion on the difference between the two, then check out <a href="https://timescale.ghost.io/blog/boosting-postgres-insert-performance/"><u>Boosting Postgres INSERT Performance by 2x With UNNEST</u></a>.</p><p>Each of these queries can be actually sent to the database in a few ways:</p><ul><li>You could construct the query string manually with a literal value in place of the <code>$</code> placeholders. I haven’t benchmarked this because it’s bad practice and can open you up to SQL injection attacks (never forget about <a href="https://www.explainxkcd.com/wiki/index.php/327:_Exploits_of_a_Mom"><u>Little Bobby Tables</u></a>).&nbsp;</li><li>You could use your framework to send a parameterized query (which looks like the ones above with <code>$</code> placeholders) which sends the query body and the values to Postgres as separate items. This protects against SQL injection and speeds up query parsing.</li><li>You could use a prepared statement (which would also be parameterized) to let the database know about your query ahead of time, then just send the values each time you want to run it. This provides the benefits of parameterization, and also speeds up your queries by reducing the planning time.</li></ul><p>Most frameworks implement prepared statements using the binary protocol directly, but you can use the <code>PREPARE</code> and <code>EXECUTE SQL</code> commands to do the same thing from SQL.</p><p>Keep in mind that PostgreSQL has a limit of 32,767 parameterized variables in a query. So if you had seven columns, then your maximum batch size for <code>INSERT…VALUES</code> would be 4,681. When you’re using <code>INSERT…UNNEST</code>, you’re only sending one parameter per column. Because PostgreSQL can support at most 1,600 columns, you'll never hit the limit.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">When using PREPARED statements for batch ingest, ensure that <code spellcheck="false" style="white-space: pre-wrap;">plan_cache_mode</code> is not set to <code spellcheck="false" style="white-space: pre-wrap;">force_custom_plan</code>. This setting is designed for queries that benefit from being re-planned for each execution, which isn’t the case for batch inserts. <br><br>By default, <code spellcheck="false" style="white-space: pre-wrap;">plan_cache_mode</code> is set to <code spellcheck="false" style="white-space: pre-wrap;">auto</code>, meaning PostgreSQL will use custom plans for the first five executions before switching to a generic plan. To optimize performance, you could consider changing your session to <code spellcheck="false" style="white-space: pre-wrap;">force_generic_plan</code>, ensuring the query is planned just once and reused for all subsequent executions.</div></div><h2 id="copy-text-and-binary">COPY: Text and Binary</h2><p><code>COPY</code> is a PostgreSQL-specific extension to the SQL standard for bulk ingestion (strictly speaking we are talking about <code>COPY FROM</code> here because you can also <code>COPY TO</code> which moves data from a table to a file). <br><br><code>COPY</code> shortcuts the process of writing multiple records in a number of ways, with two of the most critical ones being:</p><ol><li><strong>WAL MULTI-INSERT:</strong> PostgreSQL optimizes <code>COPY</code>write-ahead operations by writing&nbsp; <code>MULTI_INSERT</code><strong> </strong>records to the WAL (write-ahead log) instead of logging each row individually. This results in less data being written to the WAL files, which means less I/O (input/output) on your database.</li><li><strong>COPY ring buffer:</strong> To avoid polluting shared buffers, <code>COPY</code> uses a dedicated ring buffer for its I/O operations. This minimizes the impact on the buffer cache used for regular queries, preserving performance for other <a href="https://www.tigerdata.com/learn/guide-to-postgresql-database-operations" rel="noreferrer">database operations</a>. So, less about raw speed and more about not being a noisy neighbor.</li></ol><p><code>COPY</code> can read data from multiple sources, local files, local commands or standard input. For batch ingestion, standard input makes the most sense as the data can be sent directly from the client without an intermediate step. I was actually surprised by the amount of people <a href="https://www.reddit.com/r/PostgreSQL/comments/1gsynek/boosting_postgres_insert_performance_by_50_with/" rel="noreferrer">who reached out on Reddit</a> following my last post saying they couldn’t use <code>COPY</code> because they would need to write out their stream of data as a file, that’s 100 percent what the <code>STDIN</code> setting is for!</p><p><code>COPY</code> can use two different protocols, text and binary. </p><ul><li>Text supports formats like CSV and involves sending text strings over the wire to be parsed by the server before ingestion. You can actually just dump raw CSV records into <code>COPY</code>.</li><li>Binary supports writing data in the native PostgreSQL format from the client, removing the need for parsing on the server side. It’s much faster but also much less flexible, with limited support in many languages. To do this you need to type your data in your client, so you know the format to write it in.</li></ul><p>The two variants of <code>COPY</code> we'll be testing are the text version using:</p><pre><code class="language-SQL">COPY sensors FROM STDIN;</code></pre><p>And the binary version using:</p><pre><code>COPY sensors FROM STDIN WITH (FORMAT BINARY);</code></pre><p><code>COPY</code> isn't a normal SQL statement, so it can’t exist within a larger query. If also can’t perform an upsert (like <code>INSERT … ON CONFLICT</code>), although from <a href="https://www.postgresql.org/docs/current/sql-copy.html">PostgreSQL 17</a>, a text <code>COPY</code> can now simulate <code>INSERT … ON CONFLICT DO NOTHING</code> by ignoring errors with <code>ON_ERROR IGNORE</code>.</p><h2 id="the-setup">The Setup&nbsp;</h2><p>I created my own <a href="https://github.com/jamessewell/pgingester">Rust CLI tool</a> to run this benchmark. That might seem like overkill, but I did it for the following reasons:</p><ul><li>I needed something that supported <code>COPY FROM STDIN</code> and <code>COPY WITH (FORMAT BINARY)</code> directly, ruling out <a href="https://k6.io/">Grafana K6</a> and the PostgreSQL native <a href="https://www.postgresql.org/docs/current/pgbench.html">pgbench</a>.</li><li>I needed something that would let me run parameterized and prepared queries using the binary protocol directly, not using <code>PREPARE</code> and <code>EXECUTE</code>, because this is how most frameworks operate.</li><li>I wanted to measure the timing from an application's viewpoint, including data wrangling, network round-trip latency, and database operations.</li><li>I start measuring time after I've read the CSV file from disk and loaded it into a Rust data structure. This is to avoid measuring the I/O limits of the benchmark client. Batch ingest normally takes place in a stream without data being read from files. </li><li>I love Rust (if you’re after more Rust x PostgreSQL content, check out <a href="https://www.youtube.com/watch?v=C9TopAI1Hnk"><u>my talk </u></a>at PGConf.EU 2024)!</li></ul><p>The tool tests the insertion of data from a CSV file into the database (the default file is one million records) with multiple batch sizes and ingest methods into a table with five metric columns (the actual schema isn’t important, I just love the fact a lot of power and renewables companies use Timescale ♺):</p><pre><code class="language-sql">CREATE TABLE IF NOT EXISTS power_generation (
    generator_id INTEGER,&nbsp;
    timestamp TIMESTAMP WITH TIME ZONE,
    power_output_kw DOUBLE PRECISION,&nbsp;
    voltage DOUBLE PRECISION,
    current DOUBLE PRECISION,
    frequency DOUBLE PRECISION,
    temperature DOUBLE PRECISION
&nbsp;);</code></pre><p>It supports a combination of the following methods (or you can use the <code>–all</code> shortcut) for insertion over multiple batch sizes per run:</p><ul><li>Batch insert (parameterized)</li><li>Prepared batch insert</li><li><code>UNNEST</code> insert (parameterized)</li><li>Prepared <code>UNNEST</code> insert</li><li><code>COPY</code></li><li>Binary <code>COPY</code>comma-separated</li></ul><p>The tool supports a few options, the most important being the comma-separated list of batch sizes.</p><pre><code>Usage: pgingester [OPTIONS] [METHODS]...

Arguments:
  [METHODS]...  [possible values: insert-values, prepared-insert-values, insert-unnest, prepared-insert-unnest, copy, binary-copy]

Options:
  -b, --batch-sizes &lt;BATCH_SIZES&gt;              [default: 1000]
  -t, --transactions                           
  -c, --csv-output                             
  -a, --all                                    
  -c, --connection-string &lt;CONNECTION_STRING&gt;  [env: CONNECTION_STRING=]
  -f, --input-file &lt;INPUT_FILE&gt;                [default: ingest.csv]
  -h, --help                                   Print help
  -V, --version                                Print version</code></pre><p>I tested with a connection to a Timescale 8&nbsp;CPU/32&nbsp;GB memory server (although it was only using a single connection, so this is overkill).&nbsp;</p><h2 id="the-results">The Results</h2><p>Running the CLI tool with the following arguments will output a bit list of ingest performance, including the relative speed for each tested method.</p><pre><code>pgingester --all --batch-sizes 1000,5000,10000,100000,1000000</code></pre><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2024/11/carbon--8-.png" class="kg-image" alt="" loading="lazy" width="1594" height="1116" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/11/carbon--8-.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2024/11/carbon--8-.png 1000w, https://timescale.ghost.io/blog/content/images/2024/11/carbon--8-.png 1594w" sizes="(min-width: 720px) 720px"></figure><p>I ran all queries with multiple batch sizes with the default CSV input (one million lines). The <code>Insert VALUES</code> and <code>Prepared Insert VALUES</code> queries will only run for the 1,000 batch size as above there are too many parameters to bind (the warnings to standard error have been removed below).</p><p>We can make a number of interesting conclusions from this data:</p><ol><li>With a larger batch size (anything other than 1,000) binary, <code>COPY</code> is substantially faster (at least 3.6x)&nbsp; than anything else (19x faster than a naive parameterized <code>INSERT...VALUES</code>). This is because it doesn’t have to do any data parsing on the server side. The more data you load in a single batch, the more pronounced the difference will become.</li><li>Text <code>COPY</code> also performs well, but surprisingly it’s surpassed in speed by prepared statements for batches of 10,000 or less.&nbsp;</li><li>Both <code>COPY</code> variants perform poorly with batches of 1,000. Interestingly, I've seen a lot of batch ingest tools actually use this.</li><li>When you’re using <code>INSERT</code> for batch ingest, prepared statements always outperform parameterized ones. If you want maximum speed, the same number of parameters regardless of the batch size, and to avoid the maximum number of parameters being hit on larger batches then use <code>INSERT…UNEST</code>.</li><li><code>INSERT...UNNEST</code> at a batch size of 100,000 does a lot better against any of the text <code>COPY</code> variants than I thought it would: there is actually only 3 ms in it 👀!</li></ol><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">I ran this with a larger dataset of 100 million rows as well. Performance is <i><em class="italic" style="white-space: pre-wrap;">slightly</em></i> worse, probably because PostgreSQL is checkpointing in the background. However, the general numbers and relative speeds remain very similar.</div></div><h2 id="so-which-should-you-use">So, Which Should You Use?</h2><p>If you're looking to optimize batch ingestion in PostgreSQL, the right method depends on your specific use case, batch size, and application requirements. Here’s how the options stack up:</p><ol><li><strong>Small Batch Sizes (&lt;= 10,000 rows)</strong>: Prepared <code>INSERT...UNNEST</code> can be surprisingly competitive. Down at a batch size of 1,000, <code>COPY</code> is actually much slower.&nbsp;</li><li><strong>Large Batch Sizes (&gt; 10,000 rows)</strong>: For maximum throughput with larger batches, binary <code>COPY</code> is unbeatable. Its ability to bypass server-side parsing and its use of a dedicated ring buffer make it the top choice for high-velocity data pipelines. If you need speed, you can have larger batches, and your application can support the binary protocol, this should be your default.</li><li><strong>Ease of Implementation</strong>: If you prioritize ease of implementation or need compatibility across a wide range of tools, text <code>COPY</code> is a great middle-ground. It doesn't require complex client-side libraries and is supported in nearly every language that interacts with PostgreSQL. You can also just throw your CSV data at it.</li><li><strong>Considerations Beyond Speed</strong>:<ul><li><strong>Upserts:</strong> If you need conflict handling (<code>INSERT...ON CONFLICT</code>), <code>COPY</code> isn't an option, and you'll need to stick with <code>INSERT</code> (unless you just want to ignore errors and you're happy with text <code>COPY</code>, in which case <a href="https://www.postgresql.org/docs/current/sql-copy.html">PostgreSQL 17 </a>has your back with <code>ON_ERROR</code>).</li><li><strong>Framework support:</strong> Ensure your preferred framework supports your chosen method; <code>COPY</code> usually requires a different API to be used and binary <code>COPY</code> may require an extension library or not be supported.</li><li><strong>Batch size limits:</strong> Watch for the 32,767-parameter limit when using parameterized <code>INSERT...VALUES</code>.</li><li><strong>Memory and disk write overheads: </strong><code>COPY</code> is designed to have the least impact on your system, writing less data to disk and not polluting shared_buffers. This is actually a big consideration! In fact, both the <code>COPY</code> methods write 62&nbsp;MB of WAL for the one million row test, while <code>INSERT</code> writes 109&nbsp;MB. This ~1.7x rule seems to hold across any ingest size.</li></ul></li></ol><h2 id="final-thoughts-for-developers">Final Thoughts for Developers</h2><p>When it comes to PostgreSQL batch ingestion, there is no one-size-fits-all solution. Each method offers trade-offs between performance, complexity, and flexibility:</p><ul><li><strong>For maximum raw speed</strong> over larger batches, binary<strong> </strong><code>COPY</code> is your best bet.</li><li><strong>For flexibility and ease of use </strong>over larger batches, text<strong> </strong><code>COPY</code> balances speed with broad support.</li><li><strong>For smaller batches or compatibility-focused workflows,</strong> prepared <code>INSERT...UNNEST</code> statement can hold its own, offering competitive speeds with maximum flexibility (but remember, if you have a heavy ingest pipeline, you risk disrupting shared_buffers, and you will be writing more to WAL).</li></ul><p>Remember, the “best” method isn’t just about ingest speed; it’s about what fits your workflow and scales with your application. Happy ingesting!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Boosting Postgres INSERT Performance by 2x With UNNEST]]></title>
            <description><![CDATA[Read how you can double your Postgres INSERT performance using the UNNEST function.]]></description>
            <link>https://www.tigerdata.com/blog/boosting-postgres-insert-performance</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/boosting-postgres-insert-performance</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[PostgreSQL Performance]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Fri, 15 Nov 2024 17:00:33 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2024/11/Untitled-design--1--2.png">
            </media:content>
            <content:encoded><![CDATA[<p>If you Google Postgres <code>INSERT</code> performance for long enough, you’ll find some hushed mentions of using an arcane <code>UNNEST</code> function (if you squint, it looks like a <a href="https://www.tigerdata.com/blog/building-columnar-compression-in-a-row-oriented-database" rel="noreferrer">columnar</a> insert) over a series of arrays to increase performance.  Any performance gains sound good to me, but what's actually going on here? </p><p>I’ve been aware of this technique for a long time (in fact, several object-relational mappers use it under the hood), but I’ve never fully understood what's happening, and any analysis I’ve seen has always left me wondering if the gains were as much about data wrangling in the programming language used as Postgres speed. This week I decided to change that and do some testing myself.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">This used to be called "Boosting Postgres INSERT Performance by 50% with UNNEST". The performance went from 2.19s to 1.03s, which 52.97% less time, but also 113% faster.<br><br>I changed the wording in this article to 2x because I think that's always clearer (thanks /u/a3kov and /u/lobster_johnson)</div></div><h2 id="the-introduction-inserts-in-postgres">The Introduction: INSERTs in Postgres</h2><p>At Tiger Data, I work with <a href="https://timescale.ghost.io/blog/time-series-introduction/" rel="noreferrer">time-series data</a>, so I gave my analysis a time-series slant. I want to simulate inserting a stream of records into my database with the <code>INSERT</code> statement (yes, I know <code>COPY</code> is a thing, see the callout below), and in doing so, I want to minimize the load I create as much as possible (saving my precious CPU cycles for my real-time analytics queries).</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">If you’re aiming to load data into your database as quickly and efficiently as possible, check out the PostgreSQL <code spellcheck="false" style="white-space: pre-wrap;">COPY</code> command—it’s almost always faster than using regular <code spellcheck="false" style="white-space: pre-wrap;">INSERT</code>. We benchmarked <a href="https://www.tigerdata.com/blog/postgres-for-everything" rel="noreferrer">Postgres data</a> ingestion methods in an earlier post.<br><br>However, even though <code spellcheck="false" style="white-space: pre-wrap;">COPY</code> is faster, many developers still prefer <code spellcheck="false" style="white-space: pre-wrap;">INSERT</code> for its flexibility. <code spellcheck="false" style="white-space: pre-wrap;">INSERT</code> supports useful features like upserts (<code spellcheck="false" style="white-space: pre-wrap;">INSERT ... ON CONFLICT</code>), returning the inserted rows, and has better integration with language libraries. Plus, it can be part of a larger SQL query, giving you more control over the data insertion process.</div></div><p><br></p><p>Let’s take a closer look at the <code>INSERT</code> queries I tested using a batch size of 1,000, 5,000, and 10,000 records.</p><p>In one corner, we have the multi-record <code>INSERT</code> variant we all know and love, using a <code>VALUES</code> clause followed by a tuple per row in the batch. These queries look long but also pretty easy to understand.</p><pre><code class="language-SQL">INSERT INTO sensors (sensorid, ts, value)
VALUES 
  ($1, $2, $3), 
  ($4, $5, $6), 
   ..., 
  ($2998, $2999, $3000);</code></pre><p>In the other corner, we have our <code>UNNEST</code> variant, using a <code>SELECT</code> query that takes one array per column and uses the <code>UNNEST</code> function to convert them into rows at execution time.</p><pre><code class="language-SQL">INSERT INTO sensors (ts, sensorid, value)&nbsp;
&nbsp;&nbsp;SELECT *&nbsp;
&nbsp;&nbsp;FROM unnest(
&nbsp;&nbsp;&nbsp;&nbsp;$1::timestamptz[],&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;$2::text[],&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;$3::float8[]
)</code></pre><p>The <a href="https://www.postgresql.org/docs/9.2/functions-array.html" rel="noreferrer">Postgres documentation describes <code>UNNEST</code></a> as a function that <em>“expands multiple arrays (possibly of different data types) into a set of rows.”</em> This actually makes sense, it’s basically flattening a series of arrays into a row set, much like the one in <code>INSERT .. VALUES</code> query. </p><p>One key difference is that where the first variant has <code>batch_size * num_columns</code> values in the query, the <code>UNNEST</code> variant only has <code>num_columns</code> arrays (each of which contains <code>batch_size</code> records when it’s flattened). This will be important later, so take note!</p><h2 id="the-setup">The Setup&nbsp;</h2><p>I ran the benchmark on a single TimescaleDB 4&nbsp;CPU/16&nbsp;GB memory instance (the spec isn't really important for this benchmark) with a very simple schema (the same table I used on the <a href="https://timescale.ghost.io/blog/skip-scan-under-load/" rel="noreferrer"><u>SkipScan performance post</u></a>).</p><pre><code class="language-sql">CREATE TABLE sensors (
    sensorid TEXT,
    ts TIMESTAMPTZ,
    value FLOAT8
);
</code></pre><p>I was hoping to use <a href="https://k6.io/" rel="noreferrer">Grafana k6</a> for all my performance articles, but in this case, it didn’t make sense. I don’t want to measure the time that application code takes to get my data into the format an  <code>INSERT .. VALUES</code> or <code>INSERT .. UNNEST</code> statement needs (especially in TypeScript), I just want the time the database spends processing the statements and loading my data.</p><p>I fell back to using good old <a href="https://www.postgresql.org/docs/current/pgbench.html" rel="noreferrer">pgbench</a> for these tests with a static file for each <code>INSERT</code> variant and batch combination. As usual, you can find the files in the <a href="https://github.com/timescale/performance" rel="noreferrer">timescale/performance GitHub repo</a>.</p><p>I ran each of the following queries to insert one million records using a single thread:</p><ul><li><code>INSERT .. VALUES</code> with a batch size of 1,000</li><li><code>INSERT .. VALUES</code> with a batch size of 5,000</li><li><code>INSERT .. VALUES</code> with a batch size of 1,0000</li><li><code>INSERT .. UNNEST</code> with a batch size of 1,000</li><li><code>INSERT .. UNNEST</code> with a batch size of 5,000</li><li><code>INSERT .. UNNEST</code> with a batch size of 10,000</li></ul><p>I used the <code>pg_stat_statments</code> (if you don’t know about this amazing extension, then do yourself a favor and <a href="https://timescale.ghost.io/blog/using-pg-stat-statements-to-optimize-queries/">look it up</a>!) statistics in the database to extract the <code>total _planning_time</code> and <code>total_exec_time</code> for each run.</p><h2 id="the-results-insert-values-vs-insert-unnest">The Results: INSERT VALUES vs. INSERT UNNEST</h2><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2024/11/Untitled-design--1--3-1.png" class="kg-image" alt="" loading="lazy" width="1200" height="502" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/11/Untitled-design--1--3-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2024/11/Untitled-design--1--3-1.png 1000w, https://timescale.ghost.io/blog/content/images/2024/11/Untitled-design--1--3-1.png 1200w" sizes="(min-width: 720px) 720px"></figure><p>The results were very clear: at the database layer, <strong><code>INSERT .. UNNEST</code> is <em>2</em>.13x faster than </strong><code>INSERT .. VALUES</code> at at batch size of 1000! This ratio held steady regardless of batch size (and even with multiple parallel jobs).</p><ul><li><strong>The primary savings come at query planning time.</strong> With the <code>INSERT .. VALUES</code> approach, Postgres must parse and plan each value individually (remember how many there were?). In contrast, <code>INSERT .. UNNEST</code> processes one array per column, which reduces the planning workload by not working with individual elements at plan time.</li><li><strong>Execution time is similar between both methods.</strong> The actual query execution was time slightly slower for <code>UNNEST</code>, which reflects the extra work that the <code>UNNEST</code> function needs to do.&nbsp;This was more than made up for by the planning gain.</li></ul><p>As you might expect adding columns makes things even better for <code>UNNEST</code>, with 10 float columns (rather than one) we get a massive 5.02x faster So if you've got a wide schema, you're in for even more performance gains (but I wanted to leave this article at what most people could reasonably expect).</p><p>If you’d like to see the graphs for the 5,000 and 10,000 batch sizes, then check out the <a href="https://www.tigerdata.com/blog/best-postgresql-gui-popsql-joins-timescale" rel="noreferrer">PopSQL</a> dashboard.</p><p>A reasonable response to this might be, "What if we prepared the <code>INSERT .. VALUES</code> query, would that reduce planning time and make it the winner?". Some quick tests (unfortunately, <code>pg_stat_statements</code> can't track statistics for <code>EXECTUTE</code> queries on prepared statements) show that this is not the case; <code>UNNEST</code> is still king.</p><h2 id="should-i-use-unnest">Should I use UNNEST?</h2><p>There’s no question that in terms of <strong>database performance</strong>, <code>INSERT .. UNNEST</code> beats <code>INSERT .. VALUES</code> for batch inserts. By minimizing planning overhead, <code>UNNEST</code> unlocks an almost magical speed boost, making it a fantastic option for scenarios where ingestion speed is critical. One thing to keep in mind is that the overhead of your language and network latency often contribute just as much to the total time you see in your application, but still, your database will be working less, which is always a good thing.</p><p>As with any optimization, there’s a trade-off. The key consideration isn’t always just speed; it’s also <strong>usability</strong>. The <code>INSERT .. VALUES</code> syntax is intuitive and widely understood, making it easier to adopt and maintain, especially in teams or projects where SQL expertise varies. Pivoting to use <code>UNNEST</code> introduces complexity. You’ll need to wrangle your data into arrays, and if you’re using an ORM, you might discover it doesn’t support this pattern at all. If you're writing raw SQL, <code>UNNEST</code> might be less familiar to future developers inheriting your codebase.</p><p>And while <code>UNNEST</code> is fast, let’s not forget about <code>COPY</code>, which <a href="https://www.timescale.com/learn/testing-postgres-ingest-insert-vs-batch-insert-vs-copy" rel="noreferrer">remains the undisputed gold standard for ingestion</a>. If you don’t need features like upserts (<code>ON CONFLICT</code> clauses), <code>COPY</code> will get your data in faster, and with less overhead.</p><h2 id="final-thoughts-for-developers">Final Thoughts for Developers</h2><p>Think of <code>INSERT .. UNNEST</code> as a magic performance hack sitting squarely between traditional <code>INSERT .. VALUES</code> and <code>COPY</code>. It delivers significant speed improvements for batch ingestion while retaining the flexibility and composability of SQL <code>INSERT</code> statements. </p><p>At Tiger Data, we love exploring the edges of what Postgres can do and techniques like <code>INSERT .. UNNEST</code> remind us why. It’s elegant, fast, and underutilized, but hopefully no longer misunderstood. If you’re aiming to push your database to its limits, we highly recommend adding this pattern to your SQL toolkit. It’s another example of how understanding Postgres deeply can help you get the most out of your system. And if you want to optimize your PostgreSQL database for <a href="https://www.tigerdata.com/blog/time-series-introduction" rel="noreferrer">time series</a>, events, real-time analytics, or vector data, <a href="https://www.tigerdata.com/docs/self-hosted/latest/install" rel="noreferrer">take TimescaleDB out for a spin</a>.</p><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PostgreSQL DISTINCT: TimescaleDB’s SkipScan Under Load]]></title>
            <description><![CDATA[We benchmarked TimescaleDB's SkipScan under load to see what effect it has on DISTINCT queries.]]></description>
            <link>https://www.tigerdata.com/blog/skip-scan-under-load</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/skip-scan-under-load</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[PostgreSQL Performance]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Thu, 07 Nov 2024 14:00:24 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2024/11/PostgreSQL-DISTINCT_response-times-2.png">
            </media:content>
            <content:encoded><![CDATA[<h2 id="the-introduction-distinct-queries-in-postgresql">The Introduction: DISTINCT Queries in PostgreSQL</h2><p>Let’s say you’re working with sensor data in PostgreSQL, with each reading containing a sensor ID, timestamp, and value. You want to power an application dashboard that needs to know the last known state of each sensor in your fleet. Your query might look like this:</p><pre><code class="language-SQL">SELECT DISTINCT ON (sensorid) *
FROM sensors
ORDER BY sensorid, ts DESC;</code></pre><p>The <code>DISTINCT ON</code> clause ensures only one record per sensor is selected, and because the query is ordered by descending timestamp, you’ll get the latest reading for each sensor (although you could also use a <code>WHERE</code> clause to get the latest value at another point in time). Simple enough, right?</p><p>In practice, this query pattern can be inefficient, even with proper indexing. In this post, I’ll explain why and walk through a benchmark demonstrating that TimescaleDB’s SkipScan can optimize this query by an astonishing 10,548x at p50 and 9,603x at p95.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">This post is about optimizing <code spellcheck="false" style="white-space: pre-wrap;">DISTINCT</code> queries to get the last values associated with an ID quickly, if you want to estimate the cardinality of your dataset (count the unique IDs) then check out the <a href="https://docs.timescale.com/use-timescale/latest/hyperfunctions/" rel="noreferrer">timescaledb-toolkit,</a> which gives you <a href="https://docs.timescale.com/use-timescale/latest/hyperfunctions/approx-count-distincts/hyperloglog/" rel="noreferrer">hyperloglog</a></div></div><h2 id="skipscan-details">SkipScan Details</h2><p>SkipScan is one of those TimescaleDB features that flies under the radar but provides impressive performance improvements—especially given it works with both Timescale’s <a href="https://docs.timescale.com/use-timescale/latest/hypertables/about-hypertables/" rel="noreferrer">hypertables</a> and standard PostgreSQL tables (although not currently on compressed hypertables).</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2024/11/Postgres-DISTINCT-TimescaleDB-s-Skip-Scan-Under-Load_tweet.png" class="kg-image" alt="A tweet from Volkan Alkilic praising SkipScan's speed for time-series data" loading="lazy" width="923" height="268" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/11/Postgres-DISTINCT-TimescaleDB-s-Skip-Scan-Under-Load_tweet.png 600w, https://timescale.ghost.io/blog/content/images/2024/11/Postgres-DISTINCT-TimescaleDB-s-Skip-Scan-Under-Load_tweet.png 923w" sizes="(min-width: 720px) 720px"></figure><p>As tables and indexes grow, <a href="https://www.timescale.com/learn/understanding-distinct-in-postgresql-with-examples" rel="noreferrer"><code>DISTINCT</code> queries</a> slow down in PostgreSQL because it doesn’t natively pull unique values directly from ordered indexes. Even if you have a perfect index in place, PostgreSQL will still scan the full index, filtering out duplicates only after the fact. This approach leads to a significant slowdown as tables grow larger.</p><p>SkipScan enhances the efficiency of <code>SELECT DISTINCT ON .. ORDER BY</code> queries by allowing PostgreSQL to directly jump to each new unique value within an ordered index, skipping over intermediate rows. This approach eliminates the need to scan the entire index and then deduplicate, as SkipScan directly retrieves the next distinct value, significantly accelerating query performance. If you're after a deep dive, <a href="https://docs.timescale.com/use-timescale/latest/query-data/skipscan/">check out the docs.</a></p><p>We’ve run <a href="https://timescale.ghost.io/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql/"><u>benchmarks on SkipScan</u></a> before, but this time, I wanted to see how it interacts in a more realistic environment with ingest and query running at the same time.</p><h2 id="the-setup">The Setup</h2><p>I set up two <a href="https://docs.timescale.com/#:~:text=What%20is%20Timescale%20Cloud%3F" rel="noreferrer">Timescale Cloud</a> instances with identical configurations (4 CPUs and 16&nbsp;GB of memory). On one instance, I disabled SkipScan (<code>SET timescaledb.skip_scan=off</code>), allowing it to default to standard PostgreSQL behavior. The other instance had SkipScan enabled to compare performance.</p><p>I created an empty test table using the following SQL (and without any TimescaleDB-specific features):</p><pre><code class="language-SQL">CREATE TABLE sensors (
  sensorid TEXT, 
  ts TIMESTAMPTZ,
  value FLOAT8);
  
CREATE UNIQUE INDEX ON sensors (sensorid, ts DESC);</code></pre><p>Using Grafana K6 (with the<a href="https://github.com/grafana/xk6-sql"> <u>xk6-sql</u></a><u> </u>extension), I ran the following test for twenty minutes:</p><ul><li><strong>Data ingest</strong>: Ingest ran at a target rate of 200K rows per second, using INSERT to ingest data from 1000 sensors, in batches of 1000, with up to 10 concurrent workers (watch this space for a deep dive into the performance of different PostgreSQL INSERT patterns coming soon).</li><li><strong>Query load</strong>: A <code>SELECT DISTINCT ON</code> query, running 10 times per second with up to 5 concurrent workers. This query pulls the latest reading for all 1000 sensors, simulating an application's needs.</li></ul><p>You'll remember the query from earlier:</p><pre><code class="language-SQL">SELECT DISTINCT ON (sensorid) *
FROM sensors
ORDER BY sensorid, ts DESC;</code></pre><p>If you’d like to recreate the benchmark, then check out the <a href="https://github.com/timescale/performance/tree/main"><u>GitHub repository</u></a> for the series.</p><h2 id="the-results-skipscan-vs-vanilla-postgresql">The Results: SkipScan vs. Vanilla PostgreSQL</h2><p><br>The graphs speak for themselves (please note the X axis in the query graph is a logarithmic scale), but here's a summary:</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2024/11/Your-paragraph-text--2-.png" class="kg-image" alt="Two line graphs, one illustrating the DISTINCT query response times at p50 and p90, and another benchmarking the data ingest performance" loading="lazy" width="2000" height="1457" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/11/Your-paragraph-text--2-.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2024/11/Your-paragraph-text--2-.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2024/11/Your-paragraph-text--2-.png 1600w, https://timescale.ghost.io/blog/content/images/2024/11/Your-paragraph-text--2-.png 2048w" sizes="(min-width: 720px) 720px"></figure><ul><li>The standard PostgreSQL server started<strong> ingesting 13&nbsp;% slower</strong> and couldn’t sustain the 200K/second goal (it only caught up as <code>DISTINCT</code>14-minute queries stopped returning).</li><li>SkipScan performed <strong>over</strong> <strong>11x faster at p50 and p95</strong> right from the start.</li><li>By the 14-minute mark, SkipScan was <strong>10,548x faster at p50 and 9,603x faster at p95</strong> than standard PostgreSQL.</li><li>SkipScan maintained stable performance throughout the run, while PostgreSQL didn’t return any results after 14 minutes (RIP your dashboard). </li></ul><p>If you’d like to interact with the data then you can check out this <a href="https://www.tigerdata.com/blog/best-postgresql-gui-popsql-joins-timescale" rel="noreferrer"><u>PopSQL dashboard</u></a>.</p><h2 id="the-conclusion">The Conclusion</h2><p>SkipScan is a pretty remarkable feature, transforming underperforming <code>DISTINCT</code> queries into highly efficient operations. While there has been some discussion on adding it to PostgreSQL, TimescaleDB has your back today. Because SkipScan is not limited to hypertables, it benefits regular PostgreSQL tables as well, giving developers a performance boost just by adding the <a href="https://github.com/timescale/timescaledb" rel="noreferrer">TimescaleDB extension</a>.</p><p>In environments where you need fast, up-to-date insights—like the dashboard example with sensor data—SkipScan lets you keep pace without sacrificing performance. It’s one of those “small but mighty” features that often goes unnoticed but has an outsized impact on real-time analytics workloads.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What We’re Excited About PostgreSQL 17]]></title>
            <description><![CDATA[As we count the days until September’s release, here are the features we’re excited about in PostgreSQL 17.]]></description>
            <link>https://www.tigerdata.com/blog/what-were-excited-about-postgresql-17</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/what-were-excited-about-postgresql-17</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <dc:creator><![CDATA[Aleksander Alekseev]]></dc:creator>
            <pubDate>Thu, 16 May 2024 12:59:30 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2024/05/What-we-re-excited-about-postgres-17--1-.png">
            </media:content>
            <content:encoded><![CDATA[<p>The next major PostgreSQL release (PostgreSQL 17) is <a href="https://www.postgresql.org/developer/roadmap/"><u>scheduled for September</u></a>.</p><figure class="kg-card kg-image-card"><img src="https://media.tenor.com/R7dseIrp4N8AAAAC/party-the-office.gif" class="kg-image" alt="" loading="lazy" width="410" height="200"></figure><p>In 2023, PostgreSQL regained the attention it deserves as a rock-solid relational database. It was voted the <a href="https://survey.stackoverflow.co/2023/?ref=timescale.com#most-popular-technologies-database-prof"><u>most popular DB in the Stack Overflow Developer Survey</u></a> and named <a href="https://db-engines.com/en/blog_post/106"><u>database management system of the year by DB-Engines</u></a>. Here at Timescale, we also consolidated our status as fierce PostgreSQL fans: besides having built Timescale on PostgreSQL, we believe PostgreSQL is evolving as a platform and becoming the <a href="https://timescale.ghost.io/blog/postgres-for-everything/"><u>bedrock for the future of data</u></a>. So, excuse us for being a <em>bit </em>excited about PostgreSQL 17.</p><p>In its latest releases, we’ve watched PostgreSQL develop toward higher performance, scalability, security, and compatibility while introducing new features to meet the evolving needs of users and applications, especially enterprise ones. The improvements to privilege administration, logical replication, and monitoring are examples of that. More importantly, during this time, <a href="https://timescale.ghost.io/blog/how-and-why-to-become-a-postgresql-contributor/"><u>we contributed</u></a>, <a href="https://timescale.ghost.io/blog/what-does-a-postgresql-commitfest-manager-do-and-should-you-become-one/"><u>managed commitfests</u></a>, and created new features and products to expand it—from <a href="https://timescale.ghost.io/blog/how-we-made-real-time-data-aggregation-in-postgres-faster-by-50-000/"><u>boosting real-time aggregation by 50,000&nbsp;%</u></a> to <a href="https://docs.timescale.com/ai/latest/"><u>powering production AI applications</u></a>.</p><p>In this blog post, we gathered Timescale contributors and enthusiasts to discuss a few of the most exciting PostgreSQL 17 commits. As we count the days until September, we’ll also examine PostgreSQL’s direction for this release. Finally, we’ll share some of our commits, as we help build up PostgreSQL as a <a href="https://timescale.ghost.io/blog/postgres-for-everything/"><u>versatile development platform for everything</u></a>.&nbsp;</p><h2 id="postgresql-17-where-it-came-from-and-where-it%E2%80%99s-headed">PostgreSQL 17: Where It Came From and Where It’s Headed</h2><p>Looking at the several PostgreSQL 17 commits, <a href="https://www.linkedin.com/in/afiskon/?ref=timescale.com"><u>Aleksander Alekseev</u></a>, long-time PostgreSQL contributor and Timescaler, says significant changes to modernize PostgreSQL are underway. “I believe the future of Postgres is bright,” he notes, adding that “new people are <a href="https://www.postgresql.org/message-id/ccbc2cfa-7711-4a52-bd8e-8746e28550a2%40joeconway.com"><u>joining the project</u></a>.” Perhaps influenced by the new wave of contributors, the changes to PostgreSQL 17 reflect the project’s commitment to embracing modern methodologies and adapting to the ever-evolving tech landscape</p><p>One such notable change in version 17, says Aleksander, is the decision to <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=0b16bb8776bb834eb1ef8204ca95dd7667ab948b"><u>drop support for AIX</u></a>, an operating system developed by IBM. AIX, while historically significant, has seen declining usage in recent years, prompting PostgreSQL to reallocate resources towards supporting more widely adopted platforms. This strategic move enables PostgreSQL to focus on enhancing compatibility with modern operating systems.&nbsp;</p><p>While they may seem more focused today, the PostgreSQL community's efforts to make PostgreSQL a solid database for modern data needs were already visible in previous versions, including the current one, PostgreSQL 16. As a specific example, Aleksander mentions the transition from Autotools to the Meson build system. Autotools, a long-standing suite of tools for configuring, building, and installing software packages, has been a stalwart in the development process of PostgreSQL.&nbsp;</p><p>However, with the advent of <a href="https://mesonbuild.com/"><u>Meson</u></a>, a contemporary build system known for its simplicity, speed, and scalability, PostgreSQL managed to streamline its development workflows. Meson offers advantages such as improved performance, easier maintenance, and better cross-platform compatibility, which PostgreSQL currently extends to its users.</p><h2 id="what-we%E2%80%99re-excited-about-postgresql-17">What We’re Excited About PostgreSQL 17</h2><p>Now that we’ve seen where PostgreSQL 17 is headed, let’s discuss some of the commits that have caught our 👀.</p><h3 id="pgcreatesubscriber">pg_createsubscriber</h3><p>Suggested by Timescaler and PostgreSQL contributor <a href="https://br.linkedin.com/in/fabriziomello"><u>Fabrízio de Mello</u></a>, <a href="https://www.postgresql.org/docs/devel/app-pgcreatesubscriber.html"><u>pg_createsubscriber is a new PostgreSQL 17 tool</u></a> that allows users to create a new logical replica from a physical standby server. “The main advantage of this tool over a common logical replication setup is the initial data copy, which can take longer on large databases and have side effects, like autovacuum issues, due to the long-running transaction to copy data from one server to another. This tool will also help reduce the catchup phase,” explains Fabrízio.</p><h3 id="support-for-merge-partitions-and-split-partitions">Support for MERGE PARTITIONS and SPLIT PARTITIONS</h3><p>While <code>ALTER TABLE</code> is a well-known statement that changes the structure of a PostgreSQL table, PostgreSQL 17 comes along with two new commands: <code>MERGE PARTITIONS</code> and <code>SPLIT PARTITIONS</code>. As the name indicates, these new DDL commands merge or split several partitions. “The current implementation has certain limitations though,” says Aleksander. “It works as a single process and holds the <code>ACCESS EXCLUSIVE LOCK</code> on the parent table during all operations. This is why the new DDL commands are not advisable for large partitioned tables under a high load,” he adds.</p><h3 id="add-support-for-incremental-file-system-backup">Add support for incremental file system backup</h3><p>“This is another feature worth mentioning,” says Aleksander. Adding support for incremental file system backup in PostgreSQL enhances the database's ability to perform efficient and effective backups. Incremental backups only save changes made since the last backup (full or incremental). This significantly reduces the volume of data to be backed up compared to full backups, which capture the entire database. And since incremental backups involve less data, the backup process is faster, minimizing the impact on system performance and reducing downtime. </p><p>Developed by Robert Haas, Jakub Wartak, and Tomas Vondra, <a href="http://rhaas.blogspot.com/2024/05/hacking-on-postgresql-is-really-hard.html"><u>this commit has been struggling with stability issues</u></a>, as explained by Robert on his blog. “Hopefully it won’t be reverted (as many other commits this month),” comments Aleksander.</p><h3 id="enable-the-failover-of-logical-slots">Enable the failover of logical slots&nbsp;</h3><p>Picked by two Timescalers, Fabrízio and our head of Developer Advocacy, <a href="https://twitter.com/jamessewell"><u>James Blackwood-Sewell</u></a>, this commit by Hou Zhijie, Shveta Malik, and Ajin Cherian lets high-availability <a href="https://www.timescale.com/learn/postgresql-database-replication-guide"><u>PostgreSQL use logical replication</u></a> and not lose downstream data in case of a failover. Enabling the failover of logical replication slots in PostgreSQL enhances the robustness and reliability of logical replication setups by allowing logical slots to be transferred and maintained across different database instances.</p><h3 id="allow-explain-to-report-optimizer-memory-usage">Allow EXPLAIN to report optimizer memory usage</h3><p>“This commit by Ashutosh Bapat is another good one,” notes Aleksander. Allowing the <code>EXPLAIN</code> command to report optimizer memory usage in PostgreSQL provides valuable insights into the resources consumed by the query planner and optimizer during the preparation of query execution plans.“It will allow the developer to choose the query that uses less memory,” explains Aleksander. This makes it especially helpful for those trying to fine-tune PostgreSQL’s performance.</p><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">💻</div><div class="kg-callout-text"><a href="https://www.timescale.com/learn/postgres-guides#:~:text=Overview-,Performance,-Guide%20to%20PostgreSQL"><u>If you’re struggling to improve your PostgreSQL performance, these resources will help you get the most out of your database</u></a>.</div></div><p><br><br>Any on this list, really</p><p><a href="https://timescale.ghost.io/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design/"><u>Bruce Momjian has always been an inspiration to us—bow tie included</u></a>—so we can safely say that any of the contributions <a href="https://momjian.us/pgsql_docs/release-17.html#RELEASE-17-SERVER"><u>on this list</u></a>, which Aleksander describes as “overall performance improvements” make us excited about getting our hands on the new PostgreSQL version.&nbsp;</p><h2 id="what-we-committed-to-postgresql-17">What We Committed to PostgreSQL 17</h2><p>In total, 90 commits (3.5 percent of all commits) were authored, co-authored, and/or reviewed by Timescalers during the PostgreSQL 17 cycle. 😎We’re not going to bother you by going over all of them, but we asked our team of upstreamers to name some of their personal favorites.</p><h3 id="the-slru-move-to-64-bit-indexes">The SLRU move to 64-bit indexes</h3><p>“Personally, I’m most excited about the series of patches that moved SLRU (simple least recently used) caches to the 64-bit indexes,” says Aleksander. While we’re not there yet, this opens the path to 64-bit XIDs, which will mitigate the problem of <a href="https://timescale.ghost.io/blog/how-to-fix-transaction-id-wraparound/"><u>XID wraparound</u></a> certain users face under specific workloads, such as mixing long-living OLAP (online analytical processing) transactions and <a href="https://www.tigerdata.com/learn/understanding-oltp" rel="noreferrer">OLTP</a> (on-line transaction processing) workloads on the same PostgreSQL instance.&nbsp;</p><h3 id="transitive-comparisons">Transitive comparisons</h3><p>Another Timescaler who contributed to PostgreSQL was database architect <a href="https://se.linkedin.com/in/matskindahl"><u>Mats Kindahl</u></a>. Mats helped with refactoring to ensure transitive comparisons in PostgreSQL, which brings several benefits to users. Transitive comparisons allow for more concise and intuitive query expressions, improve <a href="https://www.tigerdata.com/blog/best-practices-for-query-optimization-in-postgresql" rel="noreferrer">query optimization</a>, enhance index usage, and facilitate data modeling, as developers can define relationships between entities more naturally.</p><h3 id="standardexplainonequery">standard_ExplainOneQuery</h3><p>Mats also worked on the introduction of&nbsp; <code>standard_ExplainOneQuery</code> in PostgreSQL 17. This addition helps ensure consistent behavior when adding explain hooks, making it easier to predict and understand the effects of explain hooks on query explanation. Developers can focus on implementing specific hooks without worrying about the nuances of query explanation behavior, leading to more efficient development processes and facilitating query performance tuning.</p><h3 id="uuidv7">UUIDv7</h3><p>On the reviewing front, Aleksander reviewed (along with other contributors) the <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=794f10f6b920670cb9750b043a2b2587059d5051"><u>partial merge of UUIDv7 support</u></a> authored by Andrey Borodin. “While there are several UUIDv7 implementations available, the UUIDv7 standard is currently in draft condition,” explains Aleksander, adding that PostgreSQL will only support when the standard is finalized. Once it’s fully supported by PostgreSQL, UUIDv7 will help make time-based queries more efficient.&nbsp;</p><h2 id="expanding-postgresql">Expanding PostgreSQL</h2><p>Here you have it, a reflection on the direction of PostgreSQL 17, the new updates we’re excited about, and some of the contributions we made. If like us, you want to carry on (or start) building on PostgreSQL, give Timescale a try. Features like hypertables (automatically partitioned PostgreSQL tables), continuous aggregates (automatically refreshed materialized views), and advanced data management techniques will significantly enhance PostgreSQL's ability to manage your most demanding workloads effectively.</p><p>If you want to expand PostgreSQL’s capabilities while using the PostgreSQL you know and love, <a href="https://console.cloud.timescale.com/signup"><u>create a free Timescale account today</u></a>. </p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Amazon Aurora vs. PostgreSQL: 35% Faster Ingest, Up to 16x Faster Queries, and 78% Cheaper Storage]]></title>
            <description><![CDATA[Read about our journey benchmarking Amazon Aurora. Spoiler alert: 35% faster ingest, up to 16x faster queries, less than ½  the price, zero fuss.
]]></description>
            <link>https://www.tigerdata.com/blog/benchmarking-amazon-aurora-vs-postgresql</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/benchmarking-amazon-aurora-vs-postgresql</guid>
            <category><![CDATA[Benchmarks & Comparisons]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Amazon Aurora]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Wed, 22 Nov 2023 18:38:28 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless-_over-1.png">
            </media:content>
            <content:encoded><![CDATA[<p>At Timescale, we pride ourselves on making PostgreSQL fast. We started by extending PostgreSQL for new workloads, first for <a href="https://www.tigerdata.com/blog/time-series-introduction" rel="noreferrer">time series</a> with TimescaleDB, then with Timescale Vector, and soon in other directions (keep an 👀 out). We don’t modify PostgreSQL in any way. Our innovation comes from how we integrate with, run, and schedule databases.&nbsp;</p><p>Many users come to us from Amazon RDS. They started there, but as their database grows and their performance suffers, they come to Timescale as a high-performance alternative. To see why, just look at our <a href="https://timescale.ghost.io/blog/timescale-cloud-vs-amazon-rds-postgresql-up-to-350-times-faster-queries-44-faster-ingest-95-storage-savings-for-time-series-data/"><u>time-series benchmark,</u></a> our <a href="https://timescale.ghost.io/blog/savings-unlocked-why-we-switched-to-a-pay-for-what-you-store-database-storage-model/" rel="noreferrer"><u>usage-based storage pricing model</u></a>, and our <a href="https://timescale.ghost.io/blog/introducing-dynamic-postgresql/"><u>response to serverless</u></a>, which gives you a better way of running non-time-series PostgreSQL workloads in the cloud without any wacky abstractions.</p><p>Amazon Aurora is another popular cloud database option. Sometimes, users start using Aurora right away; other times, these users migrate from RDS to Aurora looking for performance from a faster, more scalable PostgreSQL. But is this what they find?</p><p>This article looks at what Aurora is, why you’d use it, and presents some interesting benchmark results that may surprise you. </p><h3 id="what-is-aurora-it%E2%80%99s-not-postgresql">What is Aurora? (It’s not PostgreSQL)</h3><p>Amazon Aurora is a database as a service (DBaaS) product released by AWS in 2015. The original selling point was of a relational database engine custom-built to combine the performance and availability of high-end commercial databases (which we guess means Oracle and SQLServer) with the simplicity and cost-effectiveness of open-source databases (MySQL and PostgreSQL).&nbsp;</p><p>Originally, Amazon Aurora only supported MySQL, but PostgreSQL support was added in 2017. There have been a bunch of updates over the years, with the most important being Aurora Serverless (and then, when that fell a bit flat, Serverless v2), which aims to bring the serverless “scale to zero” model to databases.</p><p>Aurora’s key pillars have always been performance and availability. It’s marketed as being faster than RDS (“up to three times the throughput of PostgreSQL”), supporting multi-region clusters, and highly scalable. Not much is known about the internals of Aurora (it’s closed-source, after all), but we do know that compute and storage have been decoupled, resulting in a cloud-native architecture that is PostgreSQL-compatible but isn’t Postgres.&nbsp;</p><h3 id="investigating-aurora">Investigating Aurora</h3><p>There are a few ways of running Aurora for PostgreSQL, and you’ll be asked two critical questions from the <strong>Create Database </strong>screen.</p><p>First up, you need to select a cluster storage configuration:</p><ul><li>Do you want to pay slightly less for your compute and stored data with an additional charge per I/O request (Aurora Standard)?</li><li>Or, do you want to pay a small premium on compute and stored data, but I/O is included (Aurora I/O-Optimized)?</li></ul><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_cluster-storage-confirmation.png" class="kg-image" alt="Cluster storage configuration screen in Amazon Aurora" loading="lazy" width="1972" height="878" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_cluster-storage-confirmation.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_cluster-storage-confirmation.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_cluster-storage-confirmation.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_cluster-storage-confirmation.png 1972w" sizes="(min-width: 720px) 720px"></figure><p>In our benchmark, we saw a 33&nbsp;% increase in CPU costs and a massive 125&nbsp;% increase in storage costs when moving from Standard to I/O-Optimized, although I/O-Optimized still came in cheaper once the I/O was factored in. AWS recommends using an I/O-Optimized instance if your I/O costs exceed 25&nbsp;% of your database costs.</p><p><br>I/O-Optimized turns out to be a billing construct: we saw roughly equivalent performance between the two storage configurations.</p><p>After you’ve chosen that, there’s another big decision coming up: do you want to enable Serverless v2?&nbsp;</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-config.png" class="kg-image" alt="Instance config screen in Amazon Aurora" loading="lazy" width="1972" height="956" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-config.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-config.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-config.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-config.png 1972w" sizes="(min-width: 720px) 720px"></figure><p>Although three options are shown, there are really only two: Provisioned and Serverless. Provisioned is where you choose the instance class for your database, which comes with a fixed hourly cost. Serverless is where your prices are driven by your usage.&nbsp;</p><p>If you have quiet periods, Serverless might save you money; if you burst all the time, it might not. When you choose a Provisioned type, you get a familiar “choose your instance type” dialog; when you select Serverless, you get something new.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_capacity-range.png" class="kg-image" alt="Selecting the capacity range in Amazon Aurora" loading="lazy" width="2000" height="490" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_capacity-range.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_capacity-range.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_capacity-range.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_capacity-range.png 2000w" sizes="(min-width: 720px) 720px"></figure><p>So, instead of choosing a CPU and memory allocation associated with an instance, you set a range of resources in ACUs (Aurora Capacity Unit), which your cluster will operate within.&nbsp;</p><p>So, what exactly is an ACU? That’s an excellent question and one which we still don’t entirely know the answer to. You can see that the description states an ACU provides “2 GiB of memory and corresponding compute and networking,” but what on Earth is <em>corresponding compute and networking</em>?&nbsp;</p><p>How do you compare this to Provisioned if you have no idea how many CPUs are in an ACU? Is an ACU one CPU, half a CPU, a quarter of a CPU? We actually have no idea, and we can see no way to quickly find out. The opacity was frustrating during our tests. It feels obfuscated for no good reason.&nbsp;</p><p>Confusion aside, the general idea is that, at any time, Amazon Aurora will use the number of ACUs (in half-a-point increments) that it needs to sustain your current workload within the range you specify. If your workload lets you scale up and down, Serverless might be a good idea. Or is it?</p><h3 id="aurora-costs">Aurora costs</h3><p>So, why isn’t everybody using Aurora? The other axis is price, and while <a href="https://www.tigerdata.com/blog/estimate-amazon-aurora-costs" rel="noreferrer"><u>Amazon Aurora pricing is significantly harder to model than RDS</u></a>, it’s definitely more expensive, with the difference soaring as you scale out replicas or multiple regions.</p><p>We thought so. We have had some interesting testimonials from customers telling us that they had lost confidence in Aurora. So, to draw our own conclusions, we started where any reasonable engineer would—we benchmarked.</p><h2 id="benchmarking-configuration">Benchmarking Configuration&nbsp;</h2><p>But, before we started, we had to decide what we would benchmark against. We ended up choosing the Serverless (v2) I/O-Optimized configuration because that’s what we tend to see people using in the wild when they talk to us about migration.</p><p>When deploying Amazon Aurora Serverless, we need to choose a range of ACUs (our mystery billing units). We wanted to compare with a Timescale 8&nbsp;CPU/32&nbsp;GB memory instance, so we selected a minimum of 8&nbsp;ACUs (16&nbsp;GB) and a maximum of 16&nbsp;ACUs (32&nbsp;GB memory). Again, this veneer over CPUs is very confusing. In a perfect world, one would hope that an ACU provides one CPU from the underlying instance type—but we just don’t know.</p><p>We used the <a href="https://github.com/timescale/tsbs"><u>Time Series Benchmark Suite </u></a>(TSBS) to compare Amazon Aurora for PostgreSQL because we wanted to benchmark for a specific workload type (in this case, time series) to see how the generic Aurora compared to PostgreSQL that has been extended for a particular workload (and also because we ❤️ time series).&nbsp;</p><p><em><strong>Note:</strong> Many types of workloads are actually time series, more than you would think. This doesn’t only apply to the more traditional time-series use cases (e.g., finance) but also to workloads like energy metrics, sensor data, website events, </em><a href="https://www.timescale.com/learn/types-of-data-supported-by-postgresql-and-timescale/#:~:text=If%20you%20want%20to%20know,dealing%20with%20time%2Dseries%20data."><em><u>and others</u></em></a><em><u>.</u></em></p><p>We used the following TSBS configuration across all runs (for more info about how we run TSBS, you can see our <a href="https://timescale.ghost.io/blog/timescale-cloud-vs-amazon-rds-postgresql-up-to-350-times-faster-queries-44-faster-ingest-95-storage-savings-for-time-series-data/"><u>RDS Benchmark</u></a>):</p>
<!--kg-card-begin: html-->
<table style="border:none;border-collapse:collapse;"><colgroup><col width="191"><col width="211"><col width="222"></colgroup><tbody><tr style="height:25.6787109375pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><br></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Timescale</span></p><br></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Amazon Aurora Serverless for PostgreSQL</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">PostgreSQL version</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">15.4</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">15.3 (latest available)</span></p></td></tr><tr style="height:85.5pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">PostgreSQL configuration</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">No changes</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">synchronous_commit=off</span></p><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">(to match Timescale)</span></p></td></tr><tr style="height:61.5pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Partitioning system</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">TimescaleDB (</span><a href="https://www.timescale.com/learn/is-postgres-partitioning-really-that-hard-introducing-hypertables" style="text-decoration:none;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#1155cc;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:underline;-webkit-text-decoration-skip:none;text-decoration-skip-ink:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">partitions configured transparently at ingest time</span></a><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">)</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">pg_partman (partitions manually configured ahead of time)</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Compression into columnar</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Yes</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Not supported</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Partition size</span></p></td><td colspan="2" style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">4h (each system ended up with 26 non-default partitions)</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Scale (number of devices)</span></p></td><td colspan="2" style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">25,000</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Ingest workers&nbsp;</span></p></td><td colspan="2" style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">16</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Rows ingested</span></p></td><td colspan="2" style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">868,000,000</span></p></td></tr><tr style="height:26.4287109375pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">TSBS profile</span></p><br></td><td colspan="2" style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">DevOps</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">CPU / Memory</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">8 vCPU / 32GB memory</span><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;"><br></span><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;"><br><br></span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">8-16 ACUs&nbsp;</span></p><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">(see below for more details)</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Volume size</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Dynamic</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Dynamic</span></p></td></tr><tr style="height:48pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Disk type</span></p></td><td colspan="2" style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Default provisioned IOPs (no changes)</span></p></td></tr></tbody></table>
<!--kg-card-end: html-->
<h2 id="aurora-vs-postgresql-ingest-performance-comparison">Aurora vs. PostgreSQL Ingest Performance Comparison</h2><p>We weren’t expecting Timescale to compare when it came to ingesting data (we know the gap between us and PostgreSQL for ingest has been narrowing as PostgreSQL native partitioning gets better). By separating the compute and storage layers, we thought we would see some engineered gains in Aurora. </p><p>What we actually saw when we ran the benchmark—ingesting almost one billion rows—was Timescale ingesting 35&nbsp;% faster than Aurora with 8&nbsp;CPUs. Aurora was scaled up to 16 ACUs for the entire benchmark run (including the queries in the next section). So not only was Timescale 35&nbsp;% faster, but it was 35&nbsp;% faster with 50&nbsp;% of the CPU resources (assuming 1 CPU == 1 ACU).</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_ingest-speed.png" class="kg-image" alt="Ingest speed (average rows per second)" loading="lazy" width="2000" height="1222" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_ingest-speed.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_ingest-speed.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_ingest-speed.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_ingest-speed.png 2000w" sizes="(min-width: 720px) 720px"></figure><p>At this stage, some of you might be wondering why Timescale jumped in ingest speed around the 30-minute mark. The jump happened when the platform dynamically adapted the I/O on the instance as we saw data flooding in (thanks to our amazing <a href="https://timescale.ghost.io/blog/savings-unlocked-why-we-switched-to-a-pay-for-what-you-store-database-storage-model/" rel="noreferrer">Usage Based Storage</a> implementation).</p><h2 id="aurora-vs-postgresql-query-performance-comparison">Aurora vs. PostgreSQL Query Performance Comparison</h2><p>Query performance matters with a demanding workload because your application often needs a response in real or near real-time. While the details of the TSBS query types are basically indecipherable (<a href="https://github.com/timescale/tsbs?ref=timescale.com#appendix-i-query-types-"><u>here’s a cheat sheet</u></a>), they model some common (although quite complex) time-series patterns that an application might use. Each query was run 10 times, and the average value was compared for each of our target systems.</p><p>The results here tell another very interesting story, with Timescale winning in most query categories—we were between 1.15x and 16x faster, with two queries being slightly slower. When we did a one-off test with a Timescale instance with 16&nbsp;CPUs, some queries stretched out to 81x faster, with all categories being won by Timescale.</p><p>Why is this? Timescale is optimizing for the workload by teaching the planner how to handle these analytical queries and also using our native compression—which flips the row-based PostgreSQL data into a <a href="https://www.tigerdata.com/blog/building-columnar-compression-in-a-row-oriented-database" rel="noreferrer">columnar</a> format and speeds up analysis. For more information about how our technology works and how it can help you, check out our <a href="https://timescale.ghost.io/blog/timescale-cloud-vs-amazon-rds-postgresql-up-to-350-times-faster-queries-44-faster-ingest-95-storage-savings-for-time-series-data/"><u>Timescale vs. Amazon RDS benchmark blog post</u></a>.&nbsp;</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/What-we-learned-from-benchmarling-amazon-aurora-median-query-timings.png" class="kg-image" alt="Median query timings" loading="lazy" width="1464" height="1824" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/What-we-learned-from-benchmarling-amazon-aurora-median-query-timings.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/What-we-learned-from-benchmarling-amazon-aurora-median-query-timings.png 1000w, https://timescale.ghost.io/blog/content/images/2023/11/What-we-learned-from-benchmarling-amazon-aurora-median-query-timings.png 1464w" sizes="(min-width: 720px) 720px"></figure><h2 id="aurora-vs-postgresql-data-size-comparison">Aurora vs. PostgreSQL Data Size Comparison</h2><p>What about the total size of the CPU table at the end of the benchmark? There were no surprises here. Amazon Aurora (even though it’s using a different storage backend to PostgreSQL) doesn’t seem to change the total table size, with it coming in at 159&nbsp;GB (the same as RDS did). In contrast, Timescale compresses the time-series data by 95&nbsp;% to 8.6&nbsp;GB.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_compression-ratio.png" class="kg-image" alt="Total database size" loading="lazy" width="2000" height="824" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_compression-ratio.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_compression-ratio.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_compression-ratio.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_compression-ratio.png 2000w" sizes="(min-width: 720px) 720px"></figure><h2 id="aurora-vs-postgresql-cost-comparison">Aurora vs. PostgreSQL Cost Comparison</h2><p>There is no way to sugarcoat it: Amazon Aurora Serverless is expensive. While we were benchmarking, it used 16&nbsp;ACUs constantly. First, we tried the standard Serverless product, but it charged a prohibitive amount for I/O, which is why we don’t see anyone using it for anything even remotely resembling an always-on workload. It defeats the purpose of serverless if you can’t actually ingest or query data without breaking the bank.</p><p>So, we switched to the Serverless v2 I/O-Optimized pricing, which charges a small premium on compute and storage costs and zero rates on all I/O charges. It’s supposed to help with pricing for a workload like the one we’re simulating.&nbsp;</p><p>Let’s see how Aurora I/O-Optimized really did. The bill for running this benchmark has two main components: compute and storage costs. (Although Aurora actually charges for some other facets, the costs were low in this case). These are the results:&nbsp;</p><p><strong>Compute costs:</strong></p><ul><li>Aurora Serverless v2 I/O-Optimized costs $2.56 per hour for the 16 ACUs, which were used for the duration of the benchmark.</li><li>The Timescale 8vCPU instance costs $1.26 per hour <strong>(52&nbsp;% cheaper than Aurora).</strong><br></li></ul><p><strong>Storage costs:</strong></p><ul><li>Aurora Serverless v2 I/O-Optimized needed 159&nbsp;GB of storage for the CPU table and indexes, which would be billed at $34 per month.&nbsp;</li><li>Timescale needed 8.6&nbsp;GB to store the CPU table and indexes, which would be billed at $7.60 per month (<strong>78&nbsp;% cheaper than Aurora</strong>).</li></ul><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-costs.png" class="kg-image" alt="Instance costs comparison" loading="lazy" width="1870" height="1108" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-costs.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-costs.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-costs.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_instance-costs.png 1870w" sizes="(min-width: 720px) 720px"></figure><p><strong>Timescale is 52&nbsp;% cheaper to run the machines used for the benchmark (assuming a constant workload) and 78&nbsp;% cheaper to store the data created by the benchmark.&nbsp;</strong><br></p><h2 id="our-finding">Our Finding</h2><p>The main takeaway from this benchmark was that, although Aurora Serverless is commonly used to “scale PostgreSQL” for large workloads, when compared to Timescale, it fell (very) short of doing this.</p><p>Timescale was:</p><ul><li>35&nbsp;% faster to ingest</li><li>1.15x-16x faster to query in all but two query categories</li><li>95&nbsp;% more efficient at storing data</li><li>52&nbsp;% cheaper per hour for compute</li><li>78&nbsp;% cheaper per month to store the data created</li></ul><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_summary-3.png" class="kg-image" alt="A summary of the benchmark results: Timescale vs. Amazon Aurora Serverless" loading="lazy" width="1952" height="1110" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_summary-3.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_summary-3.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_summary-3.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/What-We-Learned-From-Benchmarking-Amazon-Aurora-PostgreSQL-Serverless_summary-3.png 1952w" sizes="(min-width: 720px) 720px"></figure><p>While Aurora does replace PostgreSQL’s storage backend with newer (closed-source ☹️) technology, our investigation shows that Timescale beats it for large workloads in all dimensions.&nbsp;</p><p>Looking at this data, people might conclude that “Aurora isn’t for time-series workloads” or “of course a time-series database beats Aurora (a PostgreSQL database) for a time-series workload.” Both of those statements are true, but we would like to leave you with three thoughts:</p><ol><li>Timescale is PostgreSQL—in fact, it’s more PostgreSQL than Amazon Aurora.&nbsp;</li><li>Timescale is tuned for time-series workloads, but that doesn’t mean it’s not also great for general-purpose workloads.</li><li>A very high proportion of the “large tables” or “large datasets” that give PostgreSQL problems (and might cause people to look at Aurora) are organized by timestamp or an incrementing primary key (perhaps bigint)—both of which Timescale is optimized for, regardless of if you call your data time-series or not.</li></ol><p><a href="https://console.cloud.timescale.com/signup?ref=timescale.com"><u>Create a free Timescale account</u></a> to get started with Timescale today.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Teaching Postgres New Tricks: SIMD Vectorization for Faster Analytical Queries]]></title>
            <description><![CDATA[Read how we supercharged Postgres with vectorization and Single Instruction, Multiple Data (SIMD) to set your analytical queries on fire.]]></description>
            <link>https://www.tigerdata.com/blog/teaching-postgres-new-tricks-simd-vectorization-for-faster-analytical-queries</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/teaching-postgres-new-tricks-simd-vectorization-for-faster-analytical-queries</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Announcements & Releases]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Wed, 15 Nov 2023 13:28:58 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_cover.png">
            </media:content>
            <content:encoded><![CDATA[<p>After more than a year in the works, we’re proud to announce that the latest release of TimescaleDB (TimescaleDB 2.12) has added a vectorized query pipeline that makes Single Instruction, Multiple Data (SIMD) vectorization on our hybrid row columnar storage a reality for PostgreSQL. Our goal is to make common analytics queries an order of magnitude faster, making the <a href="https://survey.stackoverflow.co/2023/#section-most-popular-technologies-databases"><u>world’s most loved database</u></a> even better.</p><p>We’ve already built a mechanism for <a href="https://timescale.ghost.io/blog/building-columnar-compression-in-a-row-oriented-database/"><u>transforming your PostgreSQL tables into hybrid row columnar stores</u></a> with our native columnar compression. When you compress data you get the immediate benefit of significantly reducing storage size, and you get the secondary benefit of spending less CPU time waiting for disk reads. But there is another avenue for optimization that comes from columnar storage, and we are now focused on unlocking its potential to set analytical queries on fire.</p><p>(Here's a sneak preview so you can see what we're talking about.)</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_summarized-table.png" class="kg-image" alt="" loading="lazy" width="2000" height="448" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_summarized-table.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_summarized-table.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_summarized-table.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_summarized-table.png 2400w" sizes="(min-width: 720px) 720px"></figure><p>If you thought you had to turn to a specialized “analytics” columnar database to serve your queries, think twice. In this article, we walk you through how we’ve supercharged PostgreSQL with vectorization, or to be more precise, implemented a vectorized query execution pipeline that lets us transparently unlock the power of SIMD, so you can start on Postgres, scale with Postgres, and stay with Postgres—even for your analytical workloads.&nbsp;</p><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">This work started to be released in TimescaleDB 2.12 which is available today, and is continuing in TimescaleDB 2.13, which will ship in November to all time-series services in the Timescale platform. <a href="https://console.cloud.timescale.com/signup"><u>Create an account here and try it out for 30 days.</u></a></div></div><h2 id="from-postgres-scaling-issues-to-vectorization">From <a href="https://www.tigerdata.com/learn/guide-to-postgresql-scaling" rel="noreferrer">Postgres Scaling</a> Issues to Vectorization</h2><p>The decision to implement vectorized query execution in TimescaleDB comes from a long line of initiatives aimed at improving PostgreSQL’s experience and scalability. Before we get into the technical details, let’s start by discussing where developers reach the limits of Postgres and how vectorization can help.</p><p>You love Postgres (doesn’t everyone?) and chose it to power your new application because using a rock-solid, widely-used database with an incredibly diverse ecosystem that supports full SQL just makes sense. </p><p>Things are going really well, development is easy, the application launches. <a href="https://www.timescale.com/learn/types-of-data-supported-by-postgresql-and-timescale"><u>You might be working with IoT devices, sensors, event data, or financial instruments</u></a>—but whatever the use case, as time moves on, data starts piling up. All of a sudden, some of the queries that power your application mysteriously begin to get slower. Panic starts to settle in. 😱</p><p>Fast forward a few weeks or months, and something is off. You’re spending money on adding additional resources to the database and burning precious developer time trying to work out what’s broken. It doesn’t feel like anything is wrong on the application side, and tuning PostgreSQL hasn’t helped. Before you know it, someone has proposed splitting part of the workload into a different (perhaps “purpose-built”) database.&nbsp;</p><p>Complexity and tech debt rocket as the size of your tech stack balloons, your team has to learn a new database (which comes with its own set of challenges), and your application now has to deal with data from multiple siloed systems.&nbsp;</p><h2 id="teaching-postgres-new-tricks-to-make-this-journey-smoother">Teaching Postgres new tricks to make this journey smoother&nbsp;&nbsp;</h2><p>This is the painful end-state that Timescale wants to help avoid, allowing developers to scale and stay with PostgreSQL. Over the years, TimescaleDB has made PostgreSQL better with many features to help you scale smoothly, like<a href="https://www.timescale.com/learn/is-postgres-partitioning-really-that-hard-introducing-hypertables"><u> hypertables with automatic partitioning</u></a>, <a href="https://www.tigerdata.com/blog/building-columnar-compression-in-a-row-oriented-database" rel="noreferrer"><u>native columnar compression</u></a>, <a href="https://www.timescale.com/learn/real-time-analytics-in-postgres"><u>improved materialized views</u></a>, <a href="https://timescale.ghost.io/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql/"><u>query planner improvements</u></a>, and much more. If it holds you back in PostgreSQL, we want to tackle it.</p><p>Which brings us to today’s announcement…&nbsp;</p><p>For the past year we’ve been investigating how to extend PostgreSQL to unlock techniques used by specialized analytics databases custom-built for OnLine Analytical Processing (OLAP), even while retaining ACID transactions, full support for mutable data, and compatibility with the rest of the wonderful ecosystem. We don’t have the luxury of building a database from the ground up for raw performance (with all the trade-offs that typically entails), but we think where we have ended up offers a unique balance of performance, flexibility, and stability.</p><p>You <em>can</em> teach an old elephant new tricks and sometimes get an order of magnitude speedup when you do!</p><h2 id="columnar-storage-in-postgresql">Columnar Storage in PostgreSQL</h2><p>Before we launch into the vectorization and SIMD deep dive, we need to set the scene by explaining the other feature which makes it possible, our <a href="https://timescale.ghost.io/blog/building-columnar-compression-in-a-row-oriented-database/"><u>compressed columnar storage</u></a>.</p><p>By default, PostgreSQL stores and processes data in a way that is optimized for operating on data record by record (or row) as it’s inserted. The on-disk data files are organized by row, and queries use a row-based iterator to process that data. Paired with a B-tree index, a row-based layout is great for transactional workloads, which are more concerned with quickly ingesting and operating on individual records.&nbsp;</p><p>Databases that optimize for raw analytical performance take the opposite approach to PostgreSQL—they make some architectural trade-offs to organize writes with multiple values from one column grouped on disk. When a read happens, a column-based iterator is used, which means only the columns that are needed are read.&nbsp;</p><p>Column organized, or columnar, storage performs poorly when an individual record is targeted or when all columns are requested, but amazingly for the aggregate or single-column queries that are common in analytics or used for powering dashboards.</p><p>To clarify things, the following diagram shows how a row store and a column store would logically lay out data from devices measuring temperature.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_data-format-diagram.png" class="kg-image" alt="How a row store and a column store would logically lay out data from devices measuring temperature" loading="lazy" width="1998" height="817" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_data-format-diagram.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_data-format-diagram.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_data-format-diagram.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_data-format-diagram.png 1998w" sizes="(min-width: 720px) 720px"></figure><p></p><h2 id="row-vs-columnar-storage-why-not-both">Row vs. Columnar Storage: Why Not Both?&nbsp;</h2><p>Traditionally, you had to choose between a database that supported a row-based format optimized for transactional workloads or one that supported a column-based format targeted towards analytical ones. But, what we saw over and over again with our customers is that, with the same dataset, they actually wanted to be able to perform transactional-style operations on recent data and analytical operations on historical data.</p><p>Timescale is built on Postgres, so we can store data using Postgres’ native row format effortlessly. We have also built out the ability to organize data by columns through our native columnar compression (check out this <a href="https://timescale.ghost.io/blog/building-columnar-compression-in-a-row-oriented-database/"><u>recent deep dive</u></a> into the technical details). You can keep recent data in a row format and convert it to columnar format as it ages. </p><p>Both formats can be queried together seamlessly, the conversion is handled automatically in the background, and we can still support transactions and modifications on our older data (albeit less performantly).</p><p>When you’re working with columnar data, the benefit for analytical queries is immense, with some aggregate queries over columnar storage coming in <strong>5x</strong>, <strong>10x</strong>, and in some cases, even up to <strong>166x faster</strong> (due to lower I/O requirements and metadata caching) compared to row-based storage, as well as taking <strong>95&nbsp;% less space to store </strong>(due to our columnar compression) when tested using the <a href="https://github.com/timescale/tsbs"><u>Time-Series Benchmark Suite</u></a>.</p><p>But can we make this faster? Read on!</p><h2 id="vectorization-and-simd%E2%80%94oh-my">Vectorization and SIMD—Oh My!</h2><p>Now that we have data in a columnar format, we have a new world of optimization to explore, starting with vectorization and SIMD. Current CPUs are amazing feats of engineering, supporting SIMD instruction sets that can process multiple data points with a single instruction, both working faster and giving much better memory and cache locality.&nbsp; (The exact number they can process depends on the register size of the CPU and the data size; with a 128-bit register, each vector could hold 4 x 32-bit values, resulting in a theoretical 4x speedup.)</p><p>A regular (or scalar) CPU instruction receives two values and performs an operation on them, returning a single result. A vectorized SIMD CPU instruction processes two same-sized vectors (a.k.a. arrays) of values simultaneously, executing the same operation across both vectors to create an output vector in a single step. The magic is that the SIMD instruction takes the same amount of time as its scalar equivalent, even though it’s doing more work.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_scalar-vs-vectorized-diagram.png" class="kg-image" alt="Scalar vs. vectorized CPU instruction" loading="lazy" width="1427" height="762" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_scalar-vs-vectorized-diagram.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_scalar-vs-vectorized-diagram.png 1000w, https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_scalar-vs-vectorized-diagram.png 1427w" sizes="(min-width: 720px) 720px"></figure><p>Implementing vectorized query execution on top of our compressed columnar storage has been a significant focus for Timescale over the last year. It quickly became evident that implementing a vectorized query pipeline is one of the most exciting areas for optimization we can tackle—with performance increases by an order of magnitude on the table.</p><h2 id="timescale%E2%80%99s-vectorized-query-execution-pipeline">Timescale’s Vectorized Query Execution Pipeline</h2><p>As of version 2.12, TimescaleDB supports a growing number of vectorized operations over compressed data, with many more coming in 2.13 and beyond. When we were starting, one of the biggest challenges was integrating the built-in PostgreSQL operators, which process data in row-based tuples, with our new vectorized pipeline, which would be triggered as the batch was decompressed and complete when the batch was aggregated.</p><p>This becomes very clear when we look at an aggregate query. For us to vectorize aggregation, we need to have that as part of our vectorization pipeline (and not at a higher level where PostgreSQL would normally handle it).&nbsp;</p><p>However, because a single query could be returning data from an uncompressed and a compressed chunk (our abstraction which partitions tables) at the same time, we also need to return the same type of data in both cases (even though no vectorization would take place for the uncompressed data). We did this by changing both plans' output to PostgreSQL <a href="https://www.postgresql.org/docs/current/parallel-plans.html#PARALLEL-AGGREGATION"><u>Partial Aggregate nodes</u></a> (which were actually developed for parallel aggregation) nodes rather than raw tuples. PostgreSQL already knows how to deal with partial aggregates, so this gives us a common interface to work with that allows early aggregation.</p><p>The following diagram contains a query plan for an aggregation query and shows how an uncompressed chunk, a compressed chunk with vectorization disabled, and a compressed chunk with vectorization enabled all flow up to the same PostgreSQL Append node.&nbsp;</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_finalize-aggregate-diagram.png" class="kg-image" alt="A query plan for an aggregation query and shows how an uncompressed chunk, a compressed chunk with vectorization disabled, and a compressed chunk with vectorization enabled all flow up to the same PostgreSQL Append node" loading="lazy" width="1649" height="1147" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_finalize-aggregate-diagram.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_finalize-aggregate-diagram.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_finalize-aggregate-diagram.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_finalize-aggregate-diagram.png 1649w" sizes="(min-width: 720px) 720px"></figure><p>But doing this had an amazing side-effect: we could now do early aggregation for uncompressed chunks! In fact, when we committed this in TimescaleDB 2.12, we saw a consistent 10-15&nbsp;% speedup across all aggregation queries which operated on hypertables, even before we got to implementing vectorization (interestingly, a large part of this improvement comes from working with smaller datasets when aggregating, for example, smaller hash tables).</p><p>Now that we could keep PostgreSQL happy when aggregating by using Partial Aggregates, we turned our attention to the start of the pipeline. We knew that we needed to convert the compressed representation into an in-memory format, which each of our vectorization stages could use. </p><p>We chose to update our decompression node to read compressed data and output data in the Apache Arrow format, allowing us to quickly and transparently perform SIMD operations at each stage of the execution pipeline.</p><h2 id="vectorization-stages">Vectorization Stages</h2><p>So, now that we have a vectorization pipeline, we need to find operations that can benefit from SIMD to vectorize. Let’s start with an example: consider a typical dashboard query that shows some average metrics on a table with all data older than one hour compressed:</p><pre><code class="language-SQL">SELECT time_bucket(INTERVAL '5 minute', timestamp) as bucket,
       metric, sum(value)
FROM metrics
WHERE metric = 1 AND
      timestamp &gt; now() - INTERVAL '1 day' 
GROUP BY bucket, metric;
</code></pre>
<p>Among the many things this query does are four crucial, computationally expensive stages that can be vectorized to use SIMD:</p><ol><li>Decompressing the compressed data into the in-memory Apache Arrow format</li><li>Checking two filters in the WHERE clause, one for metric and one for time</li><li>Computing one expression using the time_bucket function</li><li>Performing aggregation using the SUM aggregate function</li></ol><p>All of these stages benefit from vectorization in a slightly different way; let’s dig into each of them.</p><h3 id="vectorized-decompression">Vectorized decompression</h3><p>We know that compression is a good thing, and when we decompress data, the CPU overhead incurred is almost always offset by the I/O savings from reading a smaller amount of data from disk. But what if we could use our CPU more efficiently to decompress data faster? In TimescaleDB 2.12, we answered that question with a <strong>3x decompression speedup</strong> when using SIMD over vectorized batches where the algorithms support it.</p><p>While we raised our decompression throughput ceiling to 1&nbsp;GB/second/core, there is more work to be done. Some parts of our modified Gorilla compression algorithm for floating-point values, as well as some custom algorithms for compressing small and repeated numbers (see <a href="https://timescale.ghost.io/blog/time-series-compression-algorithms-explained/"><u>this blog post for more algorithm details</u></a>), don’t allow full use of SIMD because of the way they lay out compressed data, with internal references or complex flow control blocking us from unlocking more performance.&nbsp;&nbsp;</p><p>Looking to the future, we have identified some new algorithms designed with SIMD in mind, which can go an order of magnitude faster, so watch this space. 👀</p><p>On top of the speed benefits, vectorized decompression is where we convert our on-disk compression format into our in-memory Apache Arrow format that the rest of our vectorization pipeline consumes.</p><h3 id="vectorized-filters">Vectorized filters</h3><p>The next stage of query processing is applying compute-time filters from WHERE clauses. In an ideal analytical query, most of the data that doesn't match the query filters is not even read from storage. Unneeded columns are skipped, metadata is consulted to exclude entire columnar batches, and conditions are satisfied using indexes.&nbsp;</p><p>However, the real world is not ideal, and for many queries, not all conditions can be optimized like this. For example, when a filter (e.g., a where clause on a time range) <em>partially</em> overlaps a compressed batch, then some of the batch (but not all of it) has to be used to calculate the result.</p><p>In this case, vectorized filters can provide another large performance boost. As Apache Arrow vectors stream out of our decompression node, we can use SIMD to check each filter condition very efficiently by comparing the stream to a vector of constants. Using the example from above (namely, <code>WHERE metric = 1 AND time &gt; now() - INTERVAL '1 day'</code>), we would compare the metric column against the value 1 and then also compare vectors of the time column against <code>now() - INTERVAL '1 day'</code>.</p><p>This optimization should be released in TimescaleDB 2.13, with early benchmark results against some real-world data showing up to a <strong>50&nbsp;% speedup on common queries</strong>.</p><p>But that’s not all vectorized filters can provide! Previously, even for compressed queries, all data was read from disk before filters were applied (a hold-over from the read-the-whole-row behavior that PostgreSQL employs by default).&nbsp;</p><p>Now that we are living in a columnar world, we can optimize this using a technique called “lazy column reads,” which reads the required columns for a batch early in the order they are defined in the WHERE clause. If any filters fail, the batch is discarded with no more I/O incurred. For queries with filters that remove a large number of full batches of records, this can result in an additional <strong>25&nbsp;% – 50&nbsp;% speedup</strong>.</p><h3 id="vectorized-expressions">Vectorized expressions</h3><p>Another important part of vectorized query pipelines is computing various expressions (projections) of columns that might be present in the query. In the simplest cases, this allows the use of the common vector CPU instructions for addition or multiplication, increasing the throughput.&nbsp;</p><p>More complex operations can benefit from handcrafted SIMD code, such as converting the string case, validating UTF-8, or even parsing JSON. More importantly, in some cases, the vectorized computation of expressions is a prerequisite for vectorizing the subsequent stages of the pipeline, such as grouping. For example, in the dashboard query we presented at the beginning of this section, we considered the grouping to be on <code>time_bucket</code>, so the result of this function must have a columnar in-memory representation to allow us to vectorize the grouping itself.</p><p>We haven’t made a start on vectorizing expressions yet, because aggregations will have a more immediate impact on analytics queries—but fear not, we will get to them!</p><h3 id="vectorized-aggregation">Vectorized aggregation</h3><p>Finally, the computation of most aggregate functions can also be vectorized to take advantage of SIMD. To demonstrate that this can work inside PostgreSQL as a partial aggregate, we built a high-throughput summation function that uses SIMD when working on columnar/compressed data, targeting the basic use case of <code>SELECT sum(value) FROM readings_compressed</code> (we can currently support filters on the <a href="https://docs.timescale.com/use-timescale/latest/compression/about-compression/#segment-by-columns"><strong><u>segment_by column</u></strong></a>). Without further optimization, we saw a <strong>3x speedup on compressed data</strong>. </p><p>Obviously, SUM is only one of the large set of aggregate functions that PostgreSQL provides (and TimescaleDB extends with <a href="https://docs.timescale.com/api/latest/hyperfunctions/"><u>hyperfunctions</u></a>). So, in forthcoming versions, we will optimize our approach to aggregates and deliver vectorized functions with the eventual goal of supporting the full set of built-in and hyperfunction aggregates.</p><h2 id="adding-it-all-up">Adding It All Up</h2><p>We’ve been showing you a lot of speedups, but how do they stack up in the real world?&nbsp;</p><p>We ran two simple queries, one which uses the vectorized SUM aggregate, and one which makes use of vectorized filters (unfortunately these can’t be combined at the moment). Both the queries were run on the same data (about 30 million rows) four times to show the gains from row-based, columnar (without a segment_by in this case), vectorized decompression, and then finally adding the last vectorized stage (aggregation or filter depending on the query).</p><p>We think the numbers can speak for themselves here 🔥.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_query-table-1.png" class="kg-image" alt="Vectorization made the SUM aggregate query up to 5.8x faster, and a SELECT count(*) query up to 4x faster!" loading="lazy" width="2000" height="1625" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_query-table-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_query-table-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_query-table-1.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/SIMD-Vectorization-for-Faster-Analytical-Queries_query-table-1.png 2400w" sizes="(min-width: 720px) 720px"></figure><h2 id="wrap-up">Wrap-Up</h2><p>Nothing gets us more excited at Timescale than finding smart solutions to hard problems which let people get more out of PostgreSQL. Since the first code for our vectorization pipeline hit Git, our internal Slack channels have been full of developer discussion about the optimizations and possibilities that vectorization on top of our columnar compression unlocks.</p><p>Looking forward, we are projecting that we can get even orders-of-magnitude performance improvements on some queries, and we’ve only started scratching the surface of what’s possible.&nbsp;</p><p>It’s an amazing time to be using Postgres. </p><p><a href="https://console.cloud.timescale.com/signup?ref=timescale.com"><u>Create a free Timescale account</u></a> to get started quickly with vectorization in TimescaleDB today.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Pg_partman vs. Hypertables for Postgres Partitioning]]></title>
            <description><![CDATA[Are you trying to streamline your data partitioning? Check out this head-to-head comparison on pg_partman and Timescale’s hypertables.  
]]></description>
            <link>https://www.tigerdata.com/blog/pg_partman-vs-hypertables-for-postgres-partitioning</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/pg_partman-vs-hypertables-for-postgres-partitioning</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Engineering]]></category>
            <category><![CDATA[Benchmarks & Comparisons]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Wed, 13 Sep 2023 14:26:04 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/09/pg_partman-vs-hypertables-for-postgres-partitioning.png">
            </media:content>
            <content:encoded><![CDATA[<p>You all know the feeling: You’ve got one big table in your database, and it’s getting slower and slower. Your app gets bottlenecked; user experience takes a dive. These aren’t happy times.<br></p><p>When you have data streaming into PostgreSQL constantly, sooner or later you end up with these big, slow tables. Luckily, the PostgreSQL ecosystem offers a range of <a href="https://www.timescale.com/learn/database-partitioning-what-it-is-and-why-it-matters">partitioning</a> techniques to optimize the performance and maintenance of these datasets. Among these partitioning methodologies, there are two that stand out as the most popular: Timescale's <a href="https://docs.timescale.com/use-timescale/latest/hypertables/">hypertables</a> (optimized for time-based/range partitioning) and the <a href="https://github.com/pgpartman/pg_partman">pg_partman</a> extension. <br></p><p>While both approaches aim to simplify partitioning, this article explores why we believe Timescale's hypertables present compelling advantages over pg_partman. <br></p><p>📑 <a href="https://timescale.ghost.io/blog/when-to-consider-postgres-partitioning/">Check out our previous article on when to consider partitioning— if you haven’t already.</a></p><h2 id="partitioning-strategies-in-postgresql-quick-overview">Partitioning Strategies in PostgreSQL: Quick Overview</h2><p>A partitioned table comprises many non-overlapping partitions, each covering a part of your dataset. When you select data from a partitioned table using a <code>WHERE</code> clause with a time-based restriction, PostgreSQL is able to immediately discard all the partitions that aren’t relevant before it plans the query.<br></p><p>Because we aren’t searching through all the data, we spend less time doing I/O, and the query is faster. If the total table size or (even worse) the total index size of the unpartitioned table exceeds the amount of memory Postgres uses for cache, then the difference becomes even more significant.<br></p><p>As we introduced in our <a href="https://timescale.ghost.io/blog/when-to-consider-postgres-partitioning/">previous article on partitioning</a>, you can follow different strategies and techniques to partition your PostgreSQL tables. In terms of the types of partitioning, you could choose between:</p><ul><li><strong>Range partitioning</strong>: partitions are defined by a range of values (e.g., by month, year, or an incrementing sequence).</li><li><strong>List partitioning</strong>: partitions are defined by a list of values (e.g., by country).</li><li><strong>Hash partitioning</strong>: rows are partitioned based on the hash value of the partition key to distribute data across a fixed number of partitions evenly. <br></li></ul><p>Depending on which partitioning strategy you’re using, you can choose between different methodologies, the most common being the following: </p><ul><li>Using the <code>PARTITION BY</code> clause native in PostgreSQL. This supports the three types of partitioning (e.g., <code>PARTITION BY RANGE</code>, <code>BY LIST</code>, or <code>BY HASH</code>).</li><li>Using pg_partman, an extension that automates time-based partitioning in PostgreSQL.</li><li>Using Timescale, which goes one step further than pg_partman to automate partitioning by time via the concept of hypertables. <br></li></ul><p>Here, we’ll focus particularly on range partitioning (by far the most common), comparing the last two methods: pg_partman and hypertables.</p><h2 id="pgpartman-making-postgresql-partitioning-simpler">Pg_partman: Making PostgreSQL Partitioning Simpler </h2><p>The <a href="https://github.com/pgpartman/pg_partman">pg_partman</a> extension for PostgreSQL is built on the native PostgreSQL declarative approach to partitioning tables. Declarative partitioning, introduced in PostgreSQL 10, has replaced the older method of table inheritance, introducing a more intuitive and simpler approach by providing built-in support for partitioning without triggers or rules. <br></p><p>With declarative partitioning, much of the partitioning management is automated, but for example, creating new partitions still requires manual intervention—unless you're using tools like pg_partman. Pg_partman helps to automate the creation and management of partitioned tables and partitions through a SQL API. Although new partitions aren’t added and removed automatically, this can be managed by adding another extension like <a href="https://github.com/citusdata/pg_cron">pg_cron</a> to schedule jobs.<br></p><p>Without pg_partman, declarative partitioning is a lot more complicated. Ppg_partman intends to simplify this process, and indeed, it does, but there are still important tasks and nuances that will require manual intervention. A few examples:</p><ul><li>It’s essential to ensure that the necessary partitions have been created when ingesting data to avoid a <a href="https://timescale.ghost.io/blog/how-to-fix-no-partition-of-relation-found-for-row/">No Partition of Relation Found for Row</a> error, which may block your writes.</li><li>If your workload involves sporadic or irregular data ingestions, you’ll need to ensure you aren't creating excessive, unnecessary partitions, as they could degrade query performance and lead to table bloat.</li><li>You must ensure that there are no gaps or overlaps between partitions, especially when dealing with manual partition modifications.</li><li>If you want to implement a retention policy to regularly drop old partitions regularly, you'll need to set this up.</li><li>If you need to alter the schema of your tables, such as adding or dropping columns, you'll often have to handle these changes manually to ensure they propagate correctly to all partitions.</li></ul><h2 id="hypertables-making-postgresql-partitioning-seamless">Hypertables: Making PostgreSQL Partitioning Seamless </h2><p>If pg_partman simplifies partition management, hypertables take this simplification to the next level: they completely automate the process. <strong>If pg_partman is the general toolkit, hypertables are the product.</strong><br></p><p><a href="https://docs.timescale.com/use-timescale/latest/hypertables/about-hypertables/">Hypertables</a> are an abstraction layer that allows you to automatically create and manage partitions (which in Timescale are called chunks) automatically without losing the ability to query as normal with SQL. Hypertables are optimized for time-based partitioning, although they also work for tables that aren’t based on time but have something similar, for example, a BIGINT primary key. <br></p><p>Hypertables are based on inheritance-based partitioning (which you’ll recall was the older method PostgreSQL used). While this method is harder to implement manually, it’s also more flexible, giving more granular control over the partitions. This is definitely not something that you (as an end user of partitioning) want to set up and manage, but this flexibility allows us (Timescale) to introduce some improvements over native PostgreSQL partitioning that you can directly benefit from. </p><p>What are these improvements? Let’s cover them.</p><h3 id="dynamic-partition-management-forget-about-the-%E2%80%9Cno-partition-of-relation-found-for-row%E2%80%9D-error">Dynamic partition management: Forget about the “no partition of relation found for row” error    <br></h3><p>A normal table is transformed into a Timescale hypertable using a single command (<code>create_hypertable</code>): <br></p><pre><code>CREATE TABLE conditions (
time        TIMESTAMPTZ       NOT NULL,
location    TEXT              NOT NULL,
device      TEXT              NOT NULL,
temperature DOUBLE PRECISION  NULL,
humidity    DOUBLE PRECISION  NULL
);
SELECT create_hypertable('conditions', 'time');
</code></pre>
<p>This sets up the partition column, the partition interval (seven days by default), and the unique index to support partitioning. Once the hypertable is created, new partitions (chunks) will be created on the fly as data flows into the hypertable. <br></p><p>As we said earlier, pg_partman can automate much of the partition creation process, but to routinely schedule this automation, you will need to integrate it with pg_cron—and you’ll have to ensure the necessary partitions are in place proactively. Without a predefined partition to host incoming data, you'll encounter the <a href="https://timescale.ghost.io/blog/how-to-fix-no-partition-of-relation-found-for-row/">No Partition of Relation Found for Row</a> error. (This is a common one.)  <br></p><p>Using Timescale eliminates the risk of partitions not existing, completely removing partition management from the list of things the database owner needs to consider. You get exactly the right number of partitions when you need them.<br></p><p>Another hypertables’ hidden gem is that they’ll never create an unnecessary partition. Partitions are generated on the fly, meaning if there's no data to fit a potential partition, that partition simply won't be created. This is a good thing since each active partition adds a slight overhead during query planning.</p><h3 id="reduced-table-locking-no-need-to-worry-about-data-integrity">Reduced table locking: No need to worry about data integrity</h3><p><a href="https://timescale.ghost.io/blog/how-timescaledb-solves-common-postgresql-problems-in-database-operations-with-data-retention-management/">As we covered extensively in this post,</a> DDL operations in PostgreSQL, such as adding a new partition, inherently require locks on the table. This means that during the brief period the operation is being performed, other transactions trying to write (insert, update, delete) to the table can be blocked until the operation completes.</p><p>In PostgreSQL there are two methods of adding partitions, from the <code>CREATE TABLE</code> statement and from the <code>ALTER TABLE</code> statement. The first will block writes, while the second will not. The same two methods can be used to drop partitions, although in this case both will block writes.</p><p>When pg_partman creates these partitions for its maintenance job, it performs DDL operations on the table. These operations the same locks—which can completely block writes. Other problems may also arise: the waiting time for transactions can increase, leading to unpredictably longer response time, and in systems where operations have a strict timeout, the waiting caused by locks can lead to operation failures.</p><p>Hypertables are designed to ensure that your application’s read or write operations are not interrupted. Timescale maintains its own partition catalogs and implements its own minimized locking strategy that allows reads and writes without interfering with adding or dropping partitions.</p><h3 id="easily-configurable-data-retention">Easily configurable data retention</h3><p>One of the amazing things about partitioning your data is that you can drop individual partitions instantly, which isn’t the case when writing large <code>DELETE</code> statements. </p><p>When using pg_partman, you would need to create the custom logic for removing old partitions yourself, and removing a partition will lock the master table. Also, you would need to schedule this with pg_cron or an external scheduler.<br></p><p>On the contrary, setting up automatic data retention policies for hypertables is straightforward: you don’t need further code or to manage more extensions. It only takes one command, <a href="https://docs.timescale.com/use-timescale/latest/data-retention/create-a-retention-policy/"><code>add_retention_policy</code></a>. You can define retention periods for specific time intervals, and Timescale will automatically drop outdated partitions when it needs to:<br></p><pre><code>SELECT add_retention_policy('conditions', INTERVAL '24 hours');
</code></pre>
<h3 id="query-performance-optimizations">Query performance optimizations</h3><p>Hypertables also unlock some extra features that Timescale enables for your query plans. For example, queries that reference <code>now()</code> when pruning partitions will perform better due to <a href="https://timescale.ghost.io/blog/how-we-fixed-long-running-postgresql-now-queries/"><code>now()</code></a><a href="https://timescale.ghost.io/blog/how-we-fixed-long-running-postgresql-now-queries/"> being turned into a constant,</a> and your ordered <code>DISTINCT</code> queries will benefit from <a href="https://timescale.ghost.io/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql/">SkipScan</a>.</p><h2 id="going-beyond-partitioning">Going Beyond Partitioning  </h2><p>It's worth noting that while pg_partman is more of a general-purpose partition manager for PostgreSQL, hypertables unlock a wealth of features specifically tailored for time-based (or time series) data that can get very handy for scaling your large PostgreSQL tables: </p><ul><li><a href="https://timescale.ghost.io/blog/building-columnar-compression-in-a-row-oriented-database/">Timescale compression</a> takes a hypertable and changes it from row to column-oriented. This can reduce storage utilization by up to 95 %, unlock blazing-fast analytical queries, <a href="https://timescale.ghost.io/blog/compressing-immutable-data-changing-time-series-management/">and still allow the data to be updated in place.</a></li><li><a href="https://timescale.ghost.io/blog/how-we-made-data-aggregation-better-and-faster-on-postgresql-with-timescaledb-2-7/">Continuous aggregates</a> take hypertables and let you create incrementally updated materialized views for aggregate queries. You define your query and get an aggregate table that is updated as historical data changes while also keeping up with your real-time data as it flows in.</li><li><a href="https://docs.timescale.com/api/latest/hyperfunctions/">Hyperfunctions</a> give you a blazing-fast full set of functions, procedures, and data types optimized for querying, aggregating, and <a href="https://timescale.ghost.io/blog/time-series-analysis/">analyzing time-series data</a>.</li><li>The <a href="https://timescale.ghost.io/blog/the-postgresql-job-scheduler-you-always-wanted-but-be-careful-what-you-ask-for/">Timescale job scheduler</a> lets you schedule any SQL or function-based job within PostgreSQL, meaning you don’t need an external scheduler or to load another extension like pg_cron.</li></ul><h2 id="conclusion">Conclusion</h2><p>Pg_partman is an amazing toolkit that greatly simplifies the management of declarative partitioning in PostgreSQL, but it is only that—a toolkit. </p><p>We believe hypertables are a complete product that makes partitioning much more streamlined. The dynamic partition management, reduced locking overhead, and automated retention policies make hypertables a better choice for applications dealing with large datasets. You will save time and worries, and you’ll unlock many other amazing features that will make it even easier to work with your large PostgreSQL tables. </p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making PostgreSQL Backups 100x Faster via EBS Snapshots and pgBackRest]]></title>
            <description><![CDATA[pgBackrest is an awesome tool for backup creation/restore in Postgres, but it can get slow for large databases. We mitigated this problem by incorporating EBS Snapshots to our backup strategy. ]]></description>
            <link>https://www.tigerdata.com/blog/making-postgresql-backups-100x-faster-via-ebs-snapshots-and-pgbackrest</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/making-postgresql-backups-100x-faster-via-ebs-snapshots-and-pgbackrest</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Engineering]]></category>
            <dc:creator><![CDATA[Grant Godeke]]></dc:creator>
            <pubDate>Thu, 31 Aug 2023 14:16:35 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/08/elephant-armor.png">
            </media:content>
            <content:encoded><![CDATA[<p>If you have experience running PostgreSQL in a production environment, you know that maintaining database backups is a daunting task. In the event of a catastrophic failure, data corruption, or other form of data loss, the ability to quickly restore from these backups will be vital for minimizing downtime. If you’re managing a database, maintaining your backups and getting your recovery strategy in order is probably the first check on your checklist.</p><p>Perhaps this has already given you one headache or two because<strong> creating and restoring backups for large PostgreSQL databases can be a very slow process.</strong></p><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">🗒️</div><div class="kg-callout-text">A refresher on your basic backup and restore Postgres commands:<br><br><a href="https://www.timescale.com/learn/backup" rel="noreferrer">Postgres Backup Cheat Sheet</a><br><a href="https://www.timescale.com/learn/postgres-cheat-sheet/restore" rel="noreferrer">Postgres Restore Cheat Sheet</a></div></div><p><br></p><p>The most widely used external tool for backup operations in PostgreSQL is <a href="https://pgbackrest.org/?ref=timescale.com">pgBackRest</a>, which is very powerful and reliable. But pgBackRest can also be very time-consuming, especially for databases well over 1 TB. </p><p>The problem is exacerbated when restoring backups from production databases that continue to ingest data, thus creating more WAL (write-ahead log) that must be applied. In this case, a full backup and restore can take hours or even days, which can be a nightmare in production databases.</p><p>When operating our platform (<a href="https://www.timescale.com/">Timescale</a>, a cloud database platform built on PostgreSQL), we struggled with this very thing. At Timescale, we pride ourselves in making PostgreSQL faster and more scalable for large volumes of time-series data—therefore, our customers’ databases are often large (many TBs). At first, we were completely basing our backup and restore operations in pgBackRest, and we were experiencing some pain:</p><ul><li>Creating full backups was very slow. This was a problem, for example, when our customers were trying to upgrade their PostgreSQL major version within our platform, as we took a fresh, full backup after upgrade in case there was a failure shortly after. Upgrades are already stressful, and adding a very slow backup experience was not helping. </li><li>Restoring from backups was also too slow, both restoring from the backups themselves and replaying any WAL that had accrued since the last backup. (<a href="https://docs.timescale.com/use-timescale/latest/backup-restore/backup-restore-cloud/" rel="noreferrer">In Timescale, we automatically take full and incremental backups of all our customers’ databases.</a>)</li></ul><p>In this blog post, we’re sharing how we solved this problem by combining pgBackRest with EBS snapshots. Timescale runs in AWS, so we had the advantage of cloud-native infrastructure. If you're running PostgreSQL in AWS, you can perhaps benefit from a similar approach.</p><p><strong>After introducing EBS snapshots, our backup creation and restore process got 100x faster. </strong>This significantly improved the experience for our customers and made things much easier for our team.</p><h2 id="quick-introduction-to-database-backups-in-postgresql-and-why-we-used-pgbackrest">Quick Introduction to Database Backups in PostgreSQL (And Why We Used pgBackRest)</h2><p>If you asked 100 engineers if they thought backups were important for production databases, they would all say "yes"—but if you then took those same 100 engineers and gave them a grade on their backups, most wouldn’t hit a pass mark. </p><p>We all collectively understand the need for backups, but it’s still hard to create an effective backup strategy, implement it, run it, and test that it’s working appropriately.</p><p>In PostgreSQL specifically, there are two ways to implement backups: <strong>logical database dumps</strong>, which contain the SQL commands needed to recreate (not restore) your database from scratch, and <strong>physical backups</strong>, which capture the files that store your database state.  </p><p>Physical backups are usually paired with a mechanism to store the constant stream of write-ahead logs (WALs), which describe all data mutations on the system. A physical backup can then be restored to get PostgreSQL to the exact same state as it was when that backup was taken, and the WAL files rolled forward to get to a specific point in time, maybe just before someone (accidentally?) dropped all your data or your disk ate itself.</p><p>Logical backups are useful to recreate databases (potentially on other architectures), but maintaining physical backups is imperative for any production workload where uptime is valued. Physical backups are exact: they can be restored quickly and provide point-in-time recovery. In the rest of this article, we’ll discuss physical backups.</p><h3 id="how-are-physical-backups-usually-created-in-postgresql">How are physical backups usually created in PostgreSQL?</h3><ul><li>The first option is using the <a href="https://www.postgresql.org/docs/current/app-pgbasebackup.html">pg_basebackup</a> command. <code>pg_basebackup</code> copies the data directory and optionally includes the WAL files, but it doesn’t support incremental backups and has limited parallelization capabilities. The whole process is very manual, too. If you’re using <code>pg_basebackup</code>, you’ll instantly get the files you need to bootstrap a new database in a tarball or directory, but not much else. </li><li>Tools like <a href="https://pgbackrest.org/?ref=timescale.com">pgBackRest</a> were designed to overcome the limitations of <code>pg_basebackup</code>. pgBackRest allows for full and incremental backups, multi-threaded operations, and point-in-time recovery. It ensures data integrity by validating checksums during the backup process, supports different types of storage, and much more. In other words, pgBackRest is a robust and feature-rich tool, making it our choice for PostgreSQL backup operations.</li></ul><h2 id="the-problem-with-pgbackrest">The Problem With pgBackRest</h2><p>But pgBackrest is not perfect: it reads and backs up files, causing an additional load on your system. This can cause performance bottlenecks that can complicate your backup and restore strategy, especially if you’re dealing with large databases.</p><p>Even though pgBackRest offers incremental backups and parallelization, it often gets slow when executing full backups over large data volumes or on an I/O-saturated system.  </p><p>While you can sometimes rely on differential or incremental backups to minimize data (<a href="https://docs.timescale.com/use-timescale/latest/backup-restore/backup-restore-cloud/" rel="noreferrer">like we do in Timescale</a>), there are situations in which creating full backups is unavoidable. Backups could also be taken on standby, but at the end of the day, you’re limited by how fast you can get data off your volumes. </p><p>We shared earlier the example of full database upgrades, but we're also talking about any other kind of migration, integrity checks, archival operations, etc. In Timescale, some of our most popular platform features (like <a href="https://timescale.ghost.io/blog/introducing-one-click-database-forking-in-timescale-cloud/">forks,</a> <a href="https://timescale.ghost.io/blog/high-availability-for-your-production-environments-introducing-database-replication-in-timescale-cloud/">high-availability replicas</a>, and <a href="https://timescale.ghost.io/blog/high-availability-for-your-production-environments-introducing-database-replication-in-timescale-cloud/">read replicas</a>) imply a data restore from a full backup.</p><p>Having a long-running full backup operation in your production database is not only inconvenient, it can also conflict with other high-priority DB tasks, affecting your overall performance. This was problematic for us.</p><p>The slowness of pgBackRest was also problematic when it was time to restore from these backups. It’s very good at CPU parallelization, but when you’re trying to write terabytes of data as fast as possible, I/O will be the bottleneck. When it comes to recovery time objective or RTO, every minute counts. In case of major failure, you want to get that database up as soon as possible.</p><h2 id="using-ebs-snapshots-to-speed-up-the-creation-of-backups">Using EBS Snapshots to Speed Up the Creation of Backups</h2><p>To speed up the process of creating fresh full backups, we decided to replace standard pgBackRest full backups with on-demand <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSSnapshots.html?ref=timescale.com">EBS snapshots</a>.</p><p>Our platform runs in AWS, which comes with some advantages. Using snapshots is a much more cloud-native approach to the problem of backups compared to what’s been traditionally used in PostgreSQL management. </p><p>EBS snapshots create a point-in-time copy of a particular database: this snapshot can be restored, effectively making it a backup. The key is that <strong>taking a snapshot is significantly faster than the traditional approach with pgBackRest</strong>: in our case, our p90 snapshot time decreased by over 100x. This gap gets wider the larger your database is!</p><p>How did we implement this? Basically, we did a one-to-one replacement of pgBackRest. Instead of waiting for the pgBackRest fresh full backup to complete, we now take a snapshot. We still wait for the backup to complete, but the process is significantly faster via snapshots. This way, we get the quick snapshot but also the full data copy and checksumming for datafile integrity, which pgBackRest performs.</p><p>If a user experiences a failure shortly after an upgrade, we have a fresh backup—the snapshot—that we can quickly restore (we’ll cover how we handle restores next). We still take a fresh full backup using pgBackRest (yay for redundancy), but the key difference is that this happens after the upgrade process has been fully completed. </p><p>If a failure has happened, the service is available to our customer quickly: we don’t have to force them to wait for the lengthy pgBackRest process to finish before being able to use their service again.</p><p>The trade-offs for adopting this approach were minimal. The only downside to consider is that, by taking snapshots, we now have redundant backups (both snapshots and full backups), so we incur additional storage costs. But what we’ve gained (both in terms of customer satisfaction and our own peace of mind) is worth the price.</p><h2 id="combining-ebs-snapshots-and-pgbackrest-for-quick-data-restore-taking-partial-snapshots-replaying-wal">Combining EBS Snapshots and pgBackRest for Quick Data Restore: Taking Partial Snapshots, Replaying WAL</h2><p>Solving the first problem we encountered with pgBackRest (i.e., slow creation of full backups) was relatively simple. We knew exactly when we needed an EBS snapshot to be created, as this process is always tied to a very specific workflow (e.g., performing a major version upgrade).</p><p>But we also wanted to explore using EBS snapshots to improve our data restore functionality. As we mentioned earlier, some popular features in the Timescale platform rely heavily on restores, including <a href="https://timescale.ghost.io/blog/introducing-one-click-database-forking-in-timescale-cloud/">creating forks,</a> <a href="https://timescale.ghost.io/blog/high-availability-for-your-production-environments-introducing-database-replication-in-timescale-cloud/">high-availability replicas</a>, and <a href="https://timescale.ghost.io/blog/high-availability-for-your-production-environments-introducing-database-replication-in-timescale-cloud/">read replicas,</a> all of which imply a data restore from a full backup.</p><p>This use case posed a slightly different and more difficult challenge since to restore from a full backup, such a backup needs to exist first, reflecting the latest state of the service. </p><p>To implement this, the first option we explored was taking an EBS snapshot when the user clicked “Create” a fork, read replica, or high-availability replica, to then restore from that snapshot. However, this process was still too slow for the end user. To get the performance we wanted, we had to think a bit beyond the naive approach and determine a way to take semi-regular snapshots across our fleet.</p><p>Fortunately, we already had a backup strategy for pgBackRest in place that we chose to mirror. Now, all Timescale services have EBS snapshots taken daily. For redundancy reasons and to verify file checksums, we still take our standard pgBackRest partial backups, but we don’t depend on them.</p><p>Once the strategy is solved, restoring data from an EBS snapshot mirrors a restore from pgBackRest very closely. We simply chose the corresponding EBS snapshot we wanted to restore—in the cases mentioned above, always the most recent—and then replayed any WAL that has accumulated since that restore point. Here, it is important to note that <a href="https://timescale.ghost.io/blog/how-high-availability-works-in-our-cloud-database/#what-if-theres-a-failure-affecting-your-storage">we still rely on pgBackRest to do our WAL management</a>. pgBackRest works great for us here; nothing gets close in terms of parallel WAL streaming.</p><p>This EBS snapshotting and pgBackRest approach has given us great results so far. Using snapshots for restores has helped improve our product experience, also providing our customers with an even higher level of reliability. Keeping pgBackRest in parallel has given us peace of mind that we still have a traditional backup approach that validates our data as well as snapshots.</p><p>We’re continually improving our strategy though, for example, by being smarter about when we snapshot—e.g., by looking at the accumulated WAL since the last snapshot to determine if we need to snapshot certain services more frequently. This practice helps improve restore times by reducing the amount of WAL that would need to be replayed, which is often the bottleneck in this process.</p><h2 id="on-snapshot-prewarming">On Snapshot Prewarming</h2><p>One important trade-off with this EBS snapshot approach is the balance between deployment time and initial performance. One limitation of a snapshot restore is that not all blocks are necessarily prewarmed and <a href="https://timescale.ghost.io/blog/scaling-postgresql-with-amazon-s3-an-object-storage-for-low-cost-infinite-database-scalability/">may need to be fetched from S3</a> the first time they are used, which is a slow process.</p><p>To give props to pgBackRest restore, it does not have this issue. For our platform features, our trade-off was between getting the user a running read replica (or fork or high-availability replica) as quickly as possible or making sure it was as performant as possible.</p><p>After some back and forth, we decided on our current approach on prewarming: we’re reading as much as we can for five minutes, prioritizing the most recently modified files first. The idea here is that we will warm the data the user is actively engaging with first. After five minutes, we then hand the process off to PostgreSQL to continue reading the rest of the volume at a slower pace until it is complete. For the initial warming, we use a custom <a href="https://www.educative.io/answers/what-is-a-goroutine?ref=timescale.com">goroutine</a> that reads concurrently from files.</p><h2 id="backing-it-up">Backing It Up</h2><p>We are not completely replacing our pgBackRest backup infrastructure with EBS snapshots anytime soon: it is hard to give up on the effectiveness and reliability of pgBackRest. </p><p>But by combining EBS snapshots with pgBackRest across our infrastructure, we’ve been able to mitigate its performance problem significantly, speeding up our backup creation and restore process. This allows us to build a better product, providing a better experience to our customers.</p><p>If you’re experiencing the same pains we were experiencing with pgBackRest, think about experimenting with something similar! It may cost you a little extra money, but it can be very much worth it.</p><p>We still have work to do on our end: we will continue to iterate on the ideal snapshotting strategy across the fleet to minimize deployment times as much as possible. We are also looking at smarter ways to prewarm the snapshots and more applications for snapshots in general.</p><p>If any of these problems interest you, <a href="https://www.timescale.com/careers/?ref=timescale.com">check out our open engineering roles</a> (we’re hiring!). And if you are a PostgreSQL user yourself, <a href="https://console.cloud.timescale.com/signup?ref=timescale.com">sign up for a free Timescale trial</a> and experience the result of EBS snapshots in action.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Implementing ASOF Joins in PostgreSQL and Timescale]]></title>
            <description><![CDATA[Read our step-by-step guide to implement ASOF joins in PostgreSQL and Timescale, and learn how to supercharge your queries with some Timescale magic.]]></description>
            <link>https://www.tigerdata.com/blog/implementing-asof-joins-in-timescale</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/implementing-asof-joins-in-timescale</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Thu, 15 Jun 2023 14:10:00 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/06/caspar-camille-rubin-fPkvU7RDmCo-unsplash--1-.jpg">
            </media:content>
            <content:encoded><![CDATA[<h2 id="what-is-an-asof-join">What Is an <code>ASOF</code> Join?</h2><p>An <code>ASOF</code> (or "as of") join is a type of join operation used when analyzing two sets of time-series data. It essentially matches each record from one table with the nearest—but not necessarily equal—value from another table based on a chosen column. Oracle supports this out of the box using a non-standard SQL syntax, but unfortunately, PostgreSQL does not provide a built-in <code>ASOF</code> keyword.</p><p>The chosen column needs to have some concept of range for the <code>ASOF</code> operation to work. You may think of it as being the "closest value," but not exceeding the comparison. It works for string (alphabetical), integer (ordinal), float (decimal), and any other data type that has an idea of ORDER. Because timestamps are near and dear to our hearts at Timescale, we will demonstrate with time and date columns.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">✨</div><div class="kg-callout-text">Want to understand <a href="https://www.timescale.com/learn/postgresql-join-type-theory">how the PostgreSQL parser picks a join method or join types</a>? Check out this article!</div></div><p><br></p><p>Performing this operation in PostgreSQL takes a bit of effort. This article aims to delve deeper into <code>ASOF</code>-style joins and how to implement similar functionality in PostgreSQL by subselecting data or other join types.</p><h2 id="understanding-asof-joins">Understanding <code>ASOF</code> Joins</h2><p><code>ASOF</code> joins are a powerful tool when dealing with time-series data. In simple terms, an ASOF join will, for each row in the left table, find a corresponding single row in the right table where the key value is less than or equal to the key in the left table.</p><p>This is a common operation when dealing with financial data, sensor readings, or <a href="https://timescale.ghost.io/blog/time-series-data/" rel="nofollow noopener noreferrer ugc">other types of time-series data where readings might not align perfectly by timestamp</a>.</p><p>For a simple example, consider the real-world question, "What was the temperature yesterday at this time?" It is very unlikely that a temperature reading was taken yesterday at exactly the millisecond that the question is asked today. What we really want is "What was the temperature taken yesterday up to today's time stamp?"</p><p>This simple example becomes a lot more complex when we start comparing temperatures day over day, week over week, etc.</p><h2 id="implementing-asof-joins-in-timescale">Implementing <code>ASOF</code> Joins in Timescale</h2><p>Even though PostgreSQL does not directly support <code>ASOF</code> joins, you can achieve similar functionality using a combination of SQL operations. Here's a simplified step-by-step guide:</p><h3 id="step-1-prepare-your-data">Step 1: Prepare your data</h3><p>Ensure your data is in the correct format for the <code>ASOF</code> join. You'll need a timestamp or other monotonically increasing column to use as a key for the join.</p><p>Suppose you have two tables, <code>bids</code> and <code>asks</code>, each containing a timestamp column, and you want to join them by instrument and the nearest timestamp.</p><pre><code class="language-sql">CREATE TABLE bids (
    instrument text,
    ts TIMESTAMPTZ,
    value NUMERIC
);
--
CREATE INDEX bids_instrument_ts_idx ON bids (instrument, ts DESC);
CREATE INDEX bids_ts_idx ON bids (ts);
--
CREATE TABLE asks (
    instrument text,
    ts TIMESTAMPTZ,
    value NUMERIC
);
CREATE INDEX asks_instrument_ts_idx ON asks (instrument, ts DESC);
CREATE INDEX asks_ts_idx ON asks (ts);
--
</code></pre><p>Normally you'd make both these tables into hypertables with the <code>create_hypertable</code> function (because you're a super educated Timescale user), but in this case, we aren't going to, as we won't be inserting much data (and we also have some Timescale magic to show off 🪄).</p><h3 id="step-2-insert-some-test-data">Step 2: Insert some test data</h3><p>Next, we'll create data for four instruments, <code>AAA, BBB, NCD,</code> and <code>USD</code>.</p><pre><code class="language-sql">INSERT INTO bids (instrument, ts, value)
SELECT 
   -- random 1 of 4 instruments
  (array['AAA', 'BBB', 'NZD', 'USD'])[floor(random() * 4 + 1)], 
   -- timestamp of last month plus some seconds
  now() - interval '1 month' + g.s, 
   -- random value
  random()* 100 +1
FROM (
  -- 2.5M seconds in a month
  SELECT ((random() * 2592000 + 1)::text || ' s')::interval s 
  FROM generate_series(1,3000000)) g;
INSERT INTO asks (instrument, ts, value)
SELECT 
   -- random 1 of 4 instruments
  (array['AAA', 'BBB', 'NZD', 'USD'])[floor(random() * 4 + 1)], 
   -- timestamp of last month plus some seconds
  now() - interval '1 month' + g.s, 
   -- random value
  random()* 100 +1
FROM (
  -- 2.5M seconds in a month
  SELECT ((random() * 2592000 + 1)::text || ' s')::interval s 
  FROM generate_series(1,2000000)) g;
</code></pre><h3 id="step-3-query-the-data-using-a-sub-select">Step 3: Query the data using a sub-select</h3><p>To mimic the behavior of an <code>ASOF</code> join, use a <code>SUBSELECT</code> join operation along with conditions to match rows based on your criteria. This will run the sub-query once per row returned from the target table. We need to use the <code>DISTINCT</code> clause to limit the number of rows returned to one.</p><p>This will work in vanilla Postgres, but when we are using Timescale (even though we aren't using hypertables yet), we get the benefits of a Skip Scan, which will supercharge the query (for more information on this check our <a href="https://docs.timescale.com/use-timescale/latest/query-data/skipscan/" rel="nofollow noopener noreferrer ugc">docs</a> or <a href="https://timescale.ghost.io/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql/">blog post about how Skip Scan can give you an 8,000x speed-up</a>).</p><pre><code class="language-sql">SELECT bids.ts timebid, bids.value bid,
    (SELECT DISTINCT ON (asks.instrument) value ask
    FROM asks
    WHERE asks.instrument = bids.instrument
    AND asks.ts &lt;= bids.ts
    ORDER BY instrument, ts DESC) ask
FROM bids
WHERE bids.ts &gt; now() - interval '1 week'
</code></pre><pre><code class="language-sql">                              QUERY PLAN                                                                               
-------------------------------------------------------------------------
 Index Scan using bids_ts_idx on public.bids  
    (cost=0.43..188132.58 rows=62180 width=56) 
    (actual time=0.067..1700.957 rows=57303 loops=1)
   Output: bids.instrument, bids.ts, bids.value, (SubPlan 1)
   Index Cond: (bids.ts &gt; (now() - '7 days'::interval))
   SubPlan 1
     -&gt;  Unique  (cost=0.43..2.71 rows=5 width=24) 
                (actual time=0.027..0.029 rows=1 loops=57303)
           Output: asks.value, asks.instrument, asks.ts
           -&gt;  Custom Scan (SkipScan) on public.asks  
                  (cost=0.43..2.71 rows=5 width=24) 
                  (actual time=0.027..0.027 rows=1 loops=57303)
                 Output: asks.value, asks.instrument, asks.ts
                 -&gt;  Index Scan using asks_instrument_ts_idx on public.asks  
                        (cost=0.43..15996.56 rows=143152 width=24) 
                        (actual time=0.027..0.027 rows=1 loops=57303)
                       Output: asks.value, asks.instrument, asks.ts
                       Index Cond: ((asks.instrument = bids.instrument) 
                          AND (asks.ts &lt;= bids.ts))
 Planning Time: 1.231 ms
 Execution Time: 1703.821 ms

</code></pre><h3 id="conclusion">Conclusion</h3><p>While PostgreSQL does not have an <code>ASOF</code> keyword, it does offer the flexibility and functionality to perform similar operations. When you're using Timescale, things only get better with the enhancements like Skip Scan.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Fix No Partition of Relation Found for Row in Postgres Databases]]></title>
            <description><![CDATA[Learn more about the “no partition of relation found for row” error and how to avoid it in PostgreSQL databases.]]></description>
            <link>https://www.tigerdata.com/blog/how-to-fix-no-partition-of-relation-found-for-row</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/how-to-fix-no-partition-of-relation-found-for-row</guid>
            <category><![CDATA[AWS]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[PostgreSQL Tips]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Thu, 06 Apr 2023 13:30:44 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/04/AWS-RDS-Error-message-no-partition-of-relation-found.jpg">
            </media:content>
            <content:encoded><![CDATA[<p></p><h2 id="error-no-partition-of-relation-found-for-row"><code>ERROR</code>: No Partition of Relation Found for Row</h2><p>The error message <code>ERROR: no partition of relation {table-name} found for row</code> is reported by PostgreSQL (and will appear in the console and the log) when a table has been configured with declarative partitioning, and data is <code>INSERTed</code> before a child table has been defined with constraints that match the data. This will cause the insert to fail, potentially losing the data which was in flight.</p><p>You will find this error message in other PostgreSQL-based databases, such as Amazon RDS for PostgreSQL and Amazon Aurora. But it can be avoided in Timescale when you use our <a href="https://docs.timescale.com/use-timescale/latest/hypertables/about-hypertables/">hypertable abstraction</a>. In this blog post, we’ll explain this database error in more detail to learn why.</p><h2 id="explanation">Explanation</h2><p>Let’s dive deeper into what causes a <code>no partition of relation found for row</code> error. When a table is partitioned using PostgreSQL declarative partitioning, it becomes a parent to which multiple child partitions can be attached. Each of these children can handle a specific non-overlapping subset of data. When partitioning by time (the most common use case), each partition would be attached for a particular date range. For example, seven daily partitions could be attached, representing the upcoming week.<br></p><p>When inserts are made into the parent table, these are transparently routed to the child table, matching the partitioning criteria. So an insert of a row that referenced tomorrow would be sent automatically to tomorrow’s partition. If this partition doesn’t exist, then there is a problem—there is no logical place to store this data. PostgreSQL will fail the <code>INSERT</code> and report <code>no partition of relation {table-name} found for row</code>.</p><h2 id="how-to-resolve">How to Resolve</h2><p>There are two ways around this problem, although neither is perfect. Keep reading to see the Timescale approach with <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertables</a> that avoids these pitfalls.</p><p>Partitions can be made ahead of time—perhaps a scheduler could be used to create a month's worth of partitions automatically in advance. This works in theory (as long as that scheduler keeps running!) but will cause locking issues while the partitions are being created. Plus, it doesn’t account for data in the past or the far future. </p><p>A default partition can also be added that automatically catches all data that doesn’t have a home, but this is problematic, too, as it collects data that needs to eventually be moved into freshly created partitions. As the amount of orphaned data in the default partition grows, it will also slow down query times.</p><h2 id="documentation-and-resources">Documentation and Resources</h2><ul><li>Timescale <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hypertables/">hypertables work like regular PostgreSQL tables</a> but provide a superior user experience when handling time-series data.</li><li>Need some advice on how to model your time-series data using hypertables? Read our best practices about choosing between a <a href="https://timescale.ghost.io/blog/best-practices-for-time-series-data-modeling-narrow-medium-or-wide-table-layout-2/">narrow, medium, or wide hypertable layout</a> and learn when to use <a href="https://timescale.ghost.io/blog/best-practices-for-time-series-data-modeling-single-or-multiple-partitioned-table-s-a-k-a-hypertables/">single or multiple hypertables</a>.<br></li></ul><h2 id="how-timescale-can-help">How Timescale Can Help</h2><p>As mentioned earlier, another solution is enabling the TimescaleDB extension and converting the table into a hypertable instead of using PostgreSQL declarative partitioning. This removes the need to worry about partitions (which in Timescale jargon are called chunks), as they are transparently made when inserts happen with no locking issues. </p><p>You’ll never have to see this error, worry about scheduling potentially disruptive partition creation, or think about default partitions ever again! </p><p>New to Timescale? <a href="https://console.cloud.timescale.com/signup">Sign up for Timescale</a> (30-day free trial, no credit card required) for fast performance, seamless user experience, and the best compression ratios.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Timescale vs. Amazon RDS PostgreSQL: Up to 350x Faster Queries, 44 % Faster Ingest, 95 % Storage Savings for Time-Series Data]]></title>
            <description><![CDATA[Why are developers migrating from Amazon RDS for PostgreSQL to Timescale to handle their time-series data workloads? Our benchmark answers the question: faster queries, faster ingest, and 95 % storage savings for time-series data.]]></description>
            <link>https://www.tigerdata.com/blog/timescale-cloud-vs-amazon-rds-postgresql-up-to-350-times-faster-queries-44-faster-ingest-95-storage-savings-for-time-series-data</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/timescale-cloud-vs-amazon-rds-postgresql-up-to-350-times-faster-queries-44-faster-ingest-95-storage-savings-for-time-series-data</guid>
            <category><![CDATA[Cloud]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Benchmarks & Comparisons]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Tue, 15 Nov 2022 14:19:00 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-23-Amazon-RDS-timescale-hero.png">
            </media:content>
            <content:encoded><![CDATA[<p>Since we launched Timescale, our cloud-hosted PostgreSQL service for time-series data and event and analytics workloads, we have seen large numbers of customers migrating onto it from the general-purpose Amazon RDS for PostgreSQL. These developers usually struggle with performance issues on ingest, sluggish real-time or historical queries, and spiraling storage costs.</p><p>They need a solution that will let them keep using PostgreSQL while not blocking them from getting value out of their time-series data. Timescale fits them perfectly, and this article will present benchmarks that help explain why.</p><p>When we talk to these customers, we often see a pattern: </p><ol><li>At the start of a project, developers choose PostgreSQL because it’s <a href="https://survey.stackoverflow.co/2022/#section-most-loved-dreaded-and-wanted-databases">a database they know and love</a>. The team is focused on shipping features, so they choose the path of least resistance—Amazon RDS for PostgreSQL.</li><li>Amazon RDS for PostgreSQL works well at first, but as the volume of time-series data in their database grows, they notice slower ingestion, sluggish query performance, and growing storage costs.</li><li>As the database becomes a bottleneck, it becomes a target for optimization. Partitioning is implemented, materialized views are configured (destroying the ability to get real-time results), and schedules are created for view refreshes and partition maintenance. Operational complexity grows, and more points of failure are introduced.</li><li>Eventually, in an effort to keep up, instance sizes are increased, and larger, faster volumes are created. Bills skyrocket, while the improvements are only temporary.</li><li>The database is now holding the application hostage regarding performance and AWS spending. A time-series database is discussed, but the developers and the application still rely on PostgreSQL features.<br></li></ol><p>Does it sound familiar? It’s usually at this stage when developers realize that Amazon RDS for PostgreSQL is no longer a good choice for their applications, start seeking alternatives, and come across Timescale. </p><p>Timescale runs on AWS, offering hosted PostgreSQL with added time-series superpowers. Since Timescale is still PostgreSQL and already in AWS, the transition from RDS is swift: Timescale integrates with your PostgreSQL-based application directly and plays nicely <a href="https://timescale.ghost.io/blog/do-more-on-aws-with-timescale-cloud-8-services-to-build-time-series-apps-faster/">with your AWS infrastructure</a>. </p><p>Timescale has always strived to enhance PostgreSQL with the ingestion, query performance, and cost-efficiency boosts that developers need to run their data-intensive applications, all while providing a seamless developer experience with advanced features to ease working with time-series data.</p><p>But don’t take our word for it—let the numbers speak for themselves. In this blog post, we share a benchmark comparing the performance of Timescale to Amazon RDS for PostgreSQL. You will find all the details of our comparison and all the information required to run the benchmark yourself using the <a href="https://github.com/timescale/tsbs">Time-Series Benchmarking Suite</a> (TSBS).</p><h2 id="time-series-data-benchmarking-a-sneak-preview">Time-Series Data Benchmarking: A Sneak Preview</h2><p>For those who can’t wait, here’s a summary: <strong>for a 160 GB dataset with almost 1 billion rows stored on a 1 TB volume, Timescale outperforms Amazon RDS for PostgreSQL with up to 44 % higher ingest rates, queries running up to 350x faster, and a 95 % smaller data footprint.</strong><br></p><p>When we ingested data in both Timescale and Amazon RDS for PostgreSQL (using gp3 EBS volumes for both), Timescale was <strong>34 % faster than RDS for 4 vCPU</strong> and <strong>44 %  for 8 vCPU</strong> configurations.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-time-to-run-table.png" class="kg-image" alt="A diagram of our time to run ingest benchmark between Timescale Cloud and RDS for time-series data" loading="lazy" width="1176" height="580" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-time-to-run-table.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-time-to-run-table.png 1000w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-time-to-run-table.png 1176w" sizes="(min-width: 720px) 720px"></figure><p></p><p>When we ran a variety of time-based queries on both databases, ranging from simple aggregates to more complex rollups through to last-point queries, <strong>Timescale consistently outperformed Amazon RDS for PostgreSQL in every query category, sometimes by as much as 350x</strong> (you can see all of the results in the <a href="#benchmarking-configuration" rel="noreferrer">Benchmarking section</a>).</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-median-time-to-run-table.png" class="kg-image" alt="A diagram of our median time to run CPU benchmark between Timescale Cloud and RDS for time-series data" loading="lazy" width="1176" height="580" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-median-time-to-run-table.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-median-time-to-run-table.png 1000w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-median-time-to-run-table.png 1176w" sizes="(min-width: 720px) 720px"></figure><p><strong>Timescale used 95 % less disk</strong> than Amazon RDS for PostgreSQL, thanks to Timescale’s <a href="https://timescale.ghost.io/blog/building-columnar-compression-in-a-row-oriented-database/">native columnar compression</a>, which reduced the size of the test database from 159 GB to 8.6 GB. Timescale's compression uses <a href="https://timescale.ghost.io/blog/time-series-compression-algorithms-explained/">best-in-class algorithms</a>, including Gorilla and delta-of-delta, to dramatically reduce the storage footprint.<br></p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-total-database-size.png" class="kg-image" alt="A diagram of our storage savings benchmark between Timescale Cloud and RDS for time-series data" loading="lazy" width="1176" height="580" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-total-database-size.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-total-database-size.png 1000w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-total-database-size.png 1176w" sizes="(min-width: 720px) 720px"></figure><p>And the storage savings above don’t even consider the effect of the <a href="https://timescale.ghost.io/blog/expanding-the-boundaries-of-postgresql-announcing-a-bottomless-consumption-based-object-storage-layer-built-on-amazon-s3/">object store built on Amazon S3</a> that we just announced for Timescale. This feature is available for testing via private beta at the time of writing but is not yet ready for production use. </p><p>Still, by running one SQL command, this novel functionality will allow you to tier an unlimited amount of data to the S3 object storage layer that’s now an integral part of Timescale. This layer is columnar (it’s based on Apache Parquet), elastic (you can increase and reduce your usage), consumption-based (you pay only for what you store), and one order of magnitude cheaper than our EBS storage, with no extra charges for queries or usage. This feature will make scalability even more cost-efficient in Timescale, so stay tuned for some exciting benchmarks!</p><p>In the remainder of this post, we’ll deep dive into our performance benchmark comparing Amazon RDS for PostgreSQL with Timescale, detailing our methods and results for comparing ingest rates, query speed, and storage footprint. We’ll also offer insight into <em>why</em> Timescale puts up the numbers it does, with a short introduction to its vital advantages for handling time-series, events, and analytics data.</p><p>If you’d like to see how Timescale performs for your workload, <a href="https://console.cloud.timescale.com/signup">sign up for Timescale</a> today— it’s free for 30 days, there’s no credit card required to sign up, and you can spin up your first database in minutes.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">More on RDS:<br>- <a href="https://timescale.ghost.io/blog/estimating-rds-costs/" rel="noreferrer">Estimating RDS Costs</a><br>- <a href="https://timescale.ghost.io/blog/understanding-rds-pricing-and-costs/" rel="noreferrer">Why Is RDS so Expensive?</a><br>- <a href="https://timescale.ghost.io/blog/alternatives-to-rds/" rel="noreferrer">Alternatives to RDS</a><br>- <a href="https://timescale.ghost.io/blog/amazon-aurora-vs-rds-understanding-the-difference/" rel="noreferrer">Amazon Aurora vs. RDS</a></div></div><h2 id="benchmarking-configuration">Benchmarking Configuration</h2><p><a href="https://timescale.ghost.io/blog/timescaledb-vs-amazon-timestream-6000x-higher-inserts-175x-faster-queries-220x-cheaper/">As for our previous Timescale benchmarks</a>, we used the <a href="https://github.com/timescale/tsbs">open-source Time-series Benchmarking Suite</a> to run our tests. Feel free to download and run it for yourself using the settings below. Suggestions for improvements are also welcome: comment on <a href="https://twitter.com/TimescaleDB">Twitter</a> or <a href="https://slack.timescale.com/">Timescale Slack</a> to join the conversation.</p><p>We used the following TSBS configuration across all runs:<br></p>
<!--kg-card-begin: html-->
<table style="border:none;border-collapse:collapse;"><colgroup><col width="213"><col width="235"><col width="248"></colgroup><tbody><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><br></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Timescale</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Amazon RDS for PostgreSQL</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">PostgreSQL version</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">14.5</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">14.4 (latest available)</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">&nbsp;</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">No changes</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">synchronous_commit=off</span></p><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">(to match Timescale)</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Partitioning system</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">TimescaleDB (partitions automatically configured)</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">pg_partman (partitions manually configured)</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Compression into columnar</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Yes, for older partitions</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Not supported</span></p></td></tr><tr style="height:21pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Partition size</span></p></td><td colspan="2" style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">4h (each system ended up with 26 non-default partitions)</span></p></td></tr><tr style="height:21pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Scale (number of devices)</span></p></td><td colspan="2" style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">25,000</span></p></td></tr><tr style="height:21pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Ingest workers&nbsp;</span></p></td><td colspan="2" style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">16</span></p></td></tr><tr style="height:21pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Rows ingested</span></p></td><td colspan="2" style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">868,000,000</span></p></td></tr><tr style="height:21pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">TSBS profile</span></p></td><td colspan="2" style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">DevOps</span></p></td></tr><tr style="height:21pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Instance type</span></p></td><td colspan="2" style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">M5 series&nbsp; (4 vCPU+16 GB memory and 8 vCPU+32 GB memory)</span></p></td></tr><tr style="height:21pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Disk type</span></p></td><td colspan="2" style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">gp3 (16 K IOPs, 1000 MiBps throughput)</span></p></td></tr><tr style="height:21pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Volume size</span></p></td><td colspan="2" style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">1 TB</span></p></td></tr></tbody></table>
<!--kg-card-end: html-->
<p><br><a href="https://docs.timescale.com/use-timescale/latest/hypertables/about-hypertables/" rel="noreferrer">Hypertables</a> are the base abstraction of Timescale's time-series magic. While they work just like regular PostgreSQL tables, they boost performance and the user experience with time-series data by automatically partitioning it (large tables become smaller chunks or data partitions within a table) and allowing it to be queried more efficiently.</p><p>If you’re familiar with PostgreSQL, you may be asking questions about partitioning in RDS. In the past, we have benchmarked TimescaleDB against unpartitioned PostgreSQL simply because that’s the journey most of our customers follow. However, we inevitably get questions about not comparing using <a href="https://github.com/pgpartman/pg_partman">pg_partman</a>.</p><p>Pg_partman is another <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extension</a> that provides partition creation but doesn’t seamlessly create partitions on the fly: if someone inserted data outside of the currently created partitions, it would either go into a catch-all partition, degrading performance or, worse, still fail). It also doesn’t provide any additional time-series functionality, planner enhancements, or compression.</p><p>We listen to these comments, so we decided to highlight Timescale's performance (and convenience) by enabling pg_partman on the RDS systems in this benchmark. After all, the extension is considered a <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL_Partitions.html">best practice for partitioned tables</a> in Amazon RDS for PostgreSQL, so it was only fair we’d use it.<br></p><p>On our end, we enabled native compression on Timescale, compressing everything but the most recent chunk data. To do so, we segmented by the <code>tags_id</code> and ordered by time descending and <code>usage_user</code> columns. This is something we couldn’t reproduce in RDS since it doesn’t offer any equivalent functionality. </p><p>Almost everything else was exactly the same for both databases. We used the same data, indexes, and queries: almost one billion rows of data in which we ran a set of queries 100 times each using 16 threads. The only difference is that the Timescale queries use the <code>time_bucket()</code> function for arbitrary interval bucketing, whereas the PostgreSQL queries use extract (which performs equally well but is much less flexible).</p><p>We have split the performance data extracted from the benchmark into three sections: ingest, query, and storage footprint.</p><h2 id="ingest-performance-comparison">Ingest Performance Comparison</h2><p>As we started to run Timescale and RDS through our 16-thread ingestion benchmark to insert almost 1 billion rows of data, we began to see some amazing wins. Timescale beat RDS by 32 % with 4 vCPUs and 44 % with 8 vCPUs. Both systems had the same I/O performance configured on their gp3 disk, so we kept looking to get to the bottom of why we were winning on busy systems.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-ingest-performance-graph.png" class="kg-image" alt="A diagram of our ingest performance during benchmark run between Timescale Cloud and RDS for time-series data" loading="lazy" width="1992" height="1362" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-ingest-performance-graph.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-ingest-performance-graph.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/2023-06-08-ingest-performance-graph.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-ingest-performance-graph.png 1992w" sizes="(min-width: 720px) 720px"></figure><p></p><p>To test the outcome without any disk I/O involvement, we used <a href="https://www.postgresql.org/docs/current/pgbench.html">pgbench</a> to run the following CPU-hungry SQL statement on 8 vCPU machines (using a scale of 1,000 and 16 jobs) and had some more interesting results straight away. </p><pre><code class="language-SQL">SELECT count(*) FROM (SELECT generate_series(1,10000000)) a
</code></pre>
<p>Timescale was almost twice as fast, returning an average query latency of <strong>518 ms</strong>, while RDS returned <strong>904 ms</strong>. This 50 % difference was consistent on both 4 vCPU and 8 vCPU instances.</p><p>Unfortunately, we can’t look inside the black box that is RDS to see what’s happening here. One hypothesis is that a large part of this difference is because Timescale gives you the exact amount of vCPU you provision <strong>for PostgreSQL</strong> (thanks, Kubernetes!), while Amazon RDS provides you a <strong>host with that many vCPUs</strong>. </p><p>This means that we (Timescale) pay for the operating overhead on Timescale, while on RDS, you (as the user) pay for this. As instances get very busy and processes fight with the operating system for CPU (like for an ingest benchmark or when you’re crunching a lot of data), this becomes a much bigger advantage for Timescale than we had anticipated. As usual, if anybody has any other possible reasons for this difference, please reach out, we’d love to hear from you.</p><p>Our benchmark shows Timescale not only ingests data faster across the board but also provides more predictable and faster results under heavy CPU load. Not a bad feature when you want to get the most out of your instances.</p><h2 id="query-performance-comparison">Query Performance Comparison</h2><p>Query performance is something that needs to be optimized in a time-series database. When you ask for data, you often need to have it as quickly as possible—especially when you’re powering a real-time dashboard. TSBS has a wide range of queries, each with its own somewhat hard-to-decode description (you can find a <a href="https://github.com/timescale/tsbs#appendix-i-query-types-">quick primer here</a>). We ran each query 100 times on the 4 vCPU instance types (which wasn’t quick in some cases) and recorded the results. </p><p>When we look at the table of query runtimes, we can see a clear story. Timescale is consistently faster than Amazon RDS, often by more than 100x. In some cases, Timescale performs over 350x better, and it doesn’t perform worse for any query type. The table below shows the data for 4 vCPU instances, but results are similar across all the CPU types we tested (and of course, if your instance is very busy, you could get even better results).</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-table.png" class="kg-image" alt="A table of the median query times in the benchmark between Timescale Cloud and RDS for time-series data" loading="lazy" width="1352" height="1824" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-table.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-table.png 1000w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-table.png 1352w" sizes="(min-width: 720px) 720px"></figure><p>When we examine the amount of data loaded and processed by some of the queries with the larger differences, the reason behind these improvements becomes clear. Timescale compresses data into a <a href="https://www.tigerdata.com/blog/building-columnar-compression-in-a-row-oriented-database" rel="noreferrer">columnar</a> format, which has several impacts on performance:</p><ol><li>Timescale compressed chunks group by column, not by row. When a subset of the columns for a table are required, they can be loaded individually, reducing the amount of data processed (especially for the <code>single-groupby-</code> query types).</li><li>When compressed data is loaded from disk, it takes less time, as there is simply less data to read. This is traded off against additional compute cycles to uncompress the data—a compromise that works in our favor, as you can see in the results above.</li><li>As compressed data is smaller, more of it can be cached in shared memory, meaning even fewer reads from disk (for a great introduction to this, check out <a href="https://timescale.ghost.io/blog/database-scaling-postgresql-caching-explained/">Database Scaling: PostgreSQL Caching Explained</a> by our own Kirk Roybal).</li></ol><p>And just as a reminder, RDS had pg_partman configured for this test. This shows that while Timescale provides efficient partitioning via hypertables, we also provide a lot more than that (353x more in some instances).</p><h2 id="storage-usage-comparison">Storage Usage Comparison</h2><p>Total storage size is measured at the end of the TSBS ingest cycle, looking at the size of the database which TSBS has been ingesting data into. For this benchmark on Timescale, all but the most recent partition of data is compressed into our <a href="https://timescale.ghost.io/blog/building-columnar-compression-in-a-row-oriented-database/">native columnar format,</a> which uses best-in-class algorithms, including Gorilla and delta-of-delta, to reduce the storage footprint for the CPU table dramatically. </p><p>After compression, you can still access the data as usual, but you get the benefits of it being smaller and the benefits of it being columnar.</p><p>Using less storage can mean smaller volumes, lower cost, and faster access (as we saw in the query results above). In the case of this benchmark, we saved 95 %, reducing our database from 159 GB to 8.6 GB. And this isn’t an outlier, we often see these numbers for production workloads at real customers.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-total-cpu-table-size.png" class="kg-image" alt="A diagram of the total database size in the benchmark between Timescale Cloud and RDS for time-series data" loading="lazy" width="1666" height="1090" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-total-cpu-table-size.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-total-cpu-table-size.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/2023-06-08-total-cpu-table-size.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-total-cpu-table-size.png 1666w" sizes="(min-width: 720px) 720px"></figure><h2 id="beyond-benchmarks-a-closer-look-at-timescale">Beyond Benchmarks: A Closer Look at Timescale</h2><p>Now that we’ve examined the results of the benchmark, let’s briefly explore some of the features that make these results possible. This section aims to offer insight into the performance comparison above and highlight some other aspects of Timescale that will improve your developer experience when working with time-series data.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">✨</div><div class="kg-callout-text"><i><em class="italic" style="white-space: pre-wrap;">If you’re new to Timescale, you can also </em></i><a href="https://console.cloud.timescale.com/signup"><i><em class="italic" style="white-space: pre-wrap;">sign up for free </em></i></a><i><em class="italic" style="white-space: pre-wrap;">and follow our </em></i><a href="https://docs.timescale.com/getting-started/latest/create-hypertable/"><i><em class="italic" style="white-space: pre-wrap;">Getting Started guide</em></i></a><i><em class="italic" style="white-space: pre-wrap;">, which will introduce you to our main features in a hands-on way.</em></i></div></div><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-chart.png" class="kg-image" alt="A diagram with the benefits of Timescale vs. Amazon RDS for time-series data" loading="lazy" width="1952" height="948" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-chart.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-chart.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/2023-06-08-chart.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-chart.png 1952w" sizes="(min-width: 720px) 720px"></figure><h2 id="hypertables-continuous-aggregates-and-query-planner-improvements-for-performance-at-scale">Hypertables, continuous aggregates, and query planner improvements for performance at scale</h2><p>Timescale is purpose-built to provide features that handle the unique demands of time-series, analytics, and event workloads—and as we’ve seen earlier in this post, performance at scale is one of the most challenging aspects to achieve with a vanilla PostgreSQL solution. </p><p>To make PostgreSQL more scalable, we built features like <a href="https://docs.timescale.com/use-timescale/latest/hypertables/about-hypertables/" rel="noreferrer">hypertables</a> and added query planner improvements allowing you to seamlessly partition tables into high-performance chunks, ensuring that you can load and query data quickly. </p><p>While some other solutions force you to think about creating and maintaining data partitions, Timescale does this for you under the hood, as queries come in with no performance impact. In fact, some of Timescale’s improvements work on tables that don’t even hold time-series data, like SkipScan, which <a href="https://timescale.ghost.io/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql/">dramatically improves <code>DISTINCT</code> queries on any PostgreSQL table</a> with a matching B-tree index.</p><p>Another problem that comes with time-series data at scale is slow aggregate queries as you analyze or present data. <a href="https://timescale.ghost.io/blog/how-we-made-data-aggregation-better-and-faster-on-postgresql-with-timescaledb-2-7/">Continuous aggregates</a> let you take an often run or costly time-series query and incrementally materialize it in the background, providing real-time, up-to-date results in seconds or milliseconds rather than minutes or hours. </p><p>While this might sound similar to a materialized view, it not only reduces the load on your database but also takes into account the most recent inserts and doesn’t require any management once it’s configured.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2024/07/Timescale-vs.-Amazon-RDS-for-PostgreSQL_comparison-chart.png" class="kg-image" alt="A table of PostgreSQL vs. Timescale Cloud for our time-series data benchmark" loading="lazy" width="1800" height="2066" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/07/Timescale-vs.-Amazon-RDS-for-PostgreSQL_comparison-chart.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2024/07/Timescale-vs.-Amazon-RDS-for-PostgreSQL_comparison-chart.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2024/07/Timescale-vs.-Amazon-RDS-for-PostgreSQL_comparison-chart.png 1600w, https://timescale.ghost.io/blog/content/images/2024/07/Timescale-vs.-Amazon-RDS-for-PostgreSQL_comparison-chart.png 1800w" sizes="(min-width: 720px) 720px"></figure><h2 id="hyperfunctions-job-scheduling-and-user-defined-functions-to-build-faster">Hyperfunctions, job scheduling, and user-defined functions to build faster</h2><p>Once you have time-series data loaded, Timescale also gives you the tools to work with it, offering over 100 built-in <a href="https://timescale.ghost.io/blog/how-to-write-better-queries-for-time-series-data-analysis-using-custom-sql-functions/">hyperfunctions</a>—custom SQL functions that simplify complex time-series analysis, such as <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>, and bucketing by hour, minute, month and timezone with <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>We also provide a <a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/user-defined-actions/">built-in job scheduler</a>, which saves the effort of installing and managing another PostgreSQL extension and lets you schedule and monitor any SQL snippet or database function.</p><h2 id="direct-access-to-a-global-expert-support-team-to-assist-you-in-production">Direct access to a global, expert support team to assist you in production</h2><p>If you’re running your database in production, having direct access to a team of database experts will lift a heavy weight off your shoulders. Timescale gives all customers access to a <a href="https://timescale.ghost.io/blog/how-were-raising-the-bar-on-hosted-database-support/">world-class team of technical support</a> engineers at no extra cost, encouraging discussion on any time-series topic, even if it’s not directly related to Timescale operations. You might want some help with ingest performance, tuning advice for a tricky SQL query, or best practices on setting up your schema—we are here to help.</p><p>As a comparison, <a href="https://aws.amazon.com/premiumsupport/pricing/">deeply consultative support, general guidance, and best practices start at over $5,000 per month</a> in Amazon RDS for PostgreSQL. Lower tiers have only a community forum or receive general advice. So this means that you need to pay an extra $60,000 a year just for such support on AWS, while you get this for free on Timescale.</p><h2 id="native-columnar-compression-and-object-storage-for-cost-efficiency">Native columnar compression and object storage for cost efficiency</h2><p>Cost is one of the major factors when choosing any cloud database platform, and Timescale provides multiple ways to keep your spending under control.</p><p>Timescale's best-in-class <a href="https://docs.timescale.com/timescaledb/latest/overview/core-concepts/compression/">native compression</a> allows you to compress time-series data in place while still retaining the ability to query it as normal. Compressing data in Timescale often results in savings of 90 % or more (take another look at our benchmark results, which actually saw a 95 % storage footprint reduction).</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-compression-chart.png" class="kg-image" alt="A diagram of the Timescale Cloud compression advantages for time-series data in our benchmark vs. RDS" loading="lazy" width="1346" height="332" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-compression-chart.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-compression-chart.png 1000w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-compression-chart.png 1346w" sizes="(min-width: 720px) 720px"></figure><p>Timescale also includes built-in features to manage<a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/data-retention/"> data retention</a>, making it easy to implement data lifecycle policies, which remove data you don’t care about quickly, easily, and without impacting your application. You can combine data retention policies with continuous aggregates to automatically downsample your data according to a schedule.</p><p>To help reduce costs even further, Timescale offers <a href="https://timescale.ghost.io/blog/expanding-the-boundaries-of-postgresql-announcing-a-bottomless-consumption-based-object-storage-layer-built-on-amazon-s3/">bottomless, consumption-based object storage</a> built on Amazon S3 (currently in private beta). Providing access to an object storage layer from within the database itself enables you to seamlessly tier data from the database to S3, store an unlimited amount of data, and pay only for what you store. All the while you retain the ability to query data in S3 from within the database via standard SQL.</p><h2 id="it%E2%80%99s-just-postgresql">It’s just PostgreSQL</h2><p>Last but not least, Timescale is just PostgreSQL under the hood. Timescale supports full SQL (not SQL-like or SQL-ish). You can leverage the full breadth of drivers, connectors, and extensions in the vibrant PostgreSQL ecosystem—if it works with PostgreSQL, it works with Timescale! </p><p>If you switch from Amazon RDS for PostgreSQL to Timescale, you won’t lose any compatibility, your application will operate the same as before (but it will probably be faster, as we’ve shown).</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-timescale-and-postgres.png" class="kg-image" alt="The PostgreSQL and Timescale logos together: Timescale Cloud is just PostgreSQL for time-series data" loading="lazy" width="1346" height="386" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/2023-06-08-timescale-and-postgres.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/2023-06-08-timescale-and-postgres.png 1000w, https://timescale.ghost.io/blog/content/images/2023/06/2023-06-08-timescale-and-postgres.png 1346w" sizes="(min-width: 720px) 720px"></figure><h2 id="conclusion">Conclusion</h2><p>When you have time-series data, you need a database that can handle time-series workloads. While Amazon RDS for PostgreSQL provides a great cloud PostgreSQL experience, our benchmarks have shown that even when paired with the pg_partman extension to provide partition management, it can’t compete with Timescale. According to our tests, Timescale can be over 40 % faster to ingest data, up to 350x faster for queries, and takes 95 % less space to store data when compressed. </p><p>On top of these findings, we offer a rich collection of time-series features that weren’t used in the benchmark. You can speed queries up even further by incrementally pre-computing responses with continuous aggregates, benefit from our job scheduler, configure retention policies, use analytical hyperfunctions, speed up your non-time-series queries with features like Skip Scan, and so much more. </p><p><br>If you have time-series data, don’t wait until you hit that performance wall to give us a go. Spin up an account now: y<a href="https://console.cloud.timescale.com/signup">ou can use it for free for 30 days; no credit card required</a>.</p><h3 id="further-reading">Further reading</h3><ul><li><a href="https://timescale.ghost.io/blog/estimating-rds-costs/" rel="noreferrer">Estimating RDS Costs</a></li><li><a href="https://timescale.ghost.io/blog/understanding-rds-pricing-and-costs/" rel="noreferrer">Why Is RDS so Expensive?</a></li><li><a href="https://timescale.ghost.io/blog/alternatives-to-rds/" rel="noreferrer">Alternatives to RDS</a></li><li><a href="https://timescale.ghost.io/blog/amazon-aurora-vs-rds-understanding-the-difference/" rel="noreferrer">Amazon Aurora vs. RDS</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OpenTelemetry: Where the SQL Is Better Than the Original]]></title>
            <description><![CDATA[How does OpenTelemetry differ from previous observability tools? And can these differences open a new path to an old friend as a unified query language for telemetry data? ]]></description>
            <link>https://www.tigerdata.com/blog/opentelemetry-where-sql-is-better-than-the-original</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/opentelemetry-where-sql-is-better-than-the-original</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Monitoring & Alerting]]></category>
            <dc:creator><![CDATA[James Blackwood-Sewell]]></dc:creator>
            <pubDate>Wed, 25 May 2022 11:14:00 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2022/05/jorgen-haland-4yOgRb_b_i4-unsplash--1-.jpg">
            </media:content>
            <content:encoded><![CDATA[<p><em>This blog post was originally published at TFiR on May 2, 2022.</em></p><p>OpenTelemetry is a familiar term to those who work in the cloud-native landscape by now. Two years after the first beta was released it still maintains an incredibly active and large community, only coming second to Kubernetes when compared to other Cloud Native Computing Foundation (CNCF) projects. </p><p>For those who aren’t so familiar, OpenTelemetry was born out of the need to provide a unified front for instrumenting code and collecting observability data—a framework that can be used to handle metrics, logs, and traces in a consistent manner, while still retaining enough flexibility to model and interact with other popular approaches (such as Prometheus and StatsD).</p><p>This article explores how OpenTelemetry differs from previous observability tools and how that point of difference opens up the potential for bringing back an old friend as the query language across all telemetry data.</p><h2 id="observability%E2%80%94then-and-now">Observability—Then and Now</h2><p>At a high level, the primary difference between OpenTelemetry and the previous generation of open-source observability tooling is one of scope. OpenTelemetry doesn’t focus on one particular signal type, and it doesn’t offer any storage or query capabilities. Instead, it spans the entire area that an application needing instrumentation cares about—the creation and transmission of signals. The benefit of this change in approach is that OpenTelemetry can offer developers a complete experience: one API and one SDK per language, which offers common concepts across metrics, logs, and traces. When developers need to instrument an app, they only need to use OpenTelemetry.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2022/05/20220422_OpenTelemetry-Kubecon-article-v2.0-2.jpg" class="kg-image" alt="" loading="lazy" width="1800" height="1176" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2022/05/20220422_OpenTelemetry-Kubecon-article-v2.0-2.jpg 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2022/05/20220422_OpenTelemetry-Kubecon-article-v2.0-2.jpg 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2022/05/20220422_OpenTelemetry-Kubecon-article-v2.0-2.jpg 1600w, https://timescale.ghost.io/blog/content/images/2022/05/20220422_OpenTelemetry-Kubecon-article-v2.0-2.jpg 1800w" sizes="(min-width: 720px) 720px"></figure><p>On top of that promise, OpenTelemetry can take streams of signals and transform them, enrich them, aggregate them or route them, interfacing with any backend which implements the OpenTelemetry specification. This opens up a host of new deployment possibilities—a pluggable storage provider per signal (Prometheus, Jaeger, and Loki, maybe), a unified storage provider for all of them, two subsets of metrics to two different backends, or everything being sent out of a Kubernetes cluster to an external endpoint.</p><p>Personally, the appeal of OpenTelemetry is very real to me—gathering telemetry data from a Kubernetes cluster using a single interface feels much more natural than maintaining multiple signal flows and potentially operators, and custom resource definitions (CRDs). When I think back to the pain points of getting signals out of applications and into dashboards, one of my main issues was consistently around the fractured landscape of creating, discovering, and consuming telemetry data.</p><h2 id="opentelemetry-and-the-query-babel-tower">OpenTelemetry and the Query Babel Tower</h2><p>When discussing OpenTelemetry, the question of querying signals soon comes up. It’s amazing we now have the ability to provide applications with a single interface for instrumentation, but what about when the time comes to use that information?</p><p>If we store our data in multiple silos with separate query languages, all the value we gained from shared context, linking, and common attributes is lost. Because these languages have been developed (and are still being developed) for a single signal, they reinforce the silo approach. PromQL can query metrics, but it can’t reach out to logging or tracing data. It becomes clear that a solution to this problem is needed to allow the promise of OpenTelemetry to be realized from a consumption perspective.</p><p>As it stands today, open-source solutions to this problem have mostly been offered via a user interface. For example, Grafana can allow you to click between traces and metrics that have been manually linked and correlate via time—but this soon starts to feel a bit limited.</p><h2 id="a-new-promise">A New Promise</h2><p>OpenTelemetry promises tagged attributes that could be used to join instrumentation and rich linkages between all signals. So what is the query equivalent of what OpenTelemetry promises? A unified language that can take inputs from systems that provide storage for OpenTelemetry data and allow rich joins between different signal types. </p><p>This language would need to be multi-purpose, as it needs to be able to express common queries for metrics, traces, and logs. Ideally, it could also express one type of signal as another when required—the rate of entries showing up in a log stream which have a type of ERROR or a trace based on the time between metric increments. </p><p>So, what would this language look like? It needs to be a well-structured query language that can support multiple different types of signal data; it needs to be able to express domain-specific functionality for each signal; it really needs to support complex and straightforward joins between data, and it needs to return data which the visualization layer can present. Other tools also need to support it, too. And hopefully, not just observability tools—integration with programming languages and business intelligence solutions would be perfect. </p><p>Designing such a language is not easy. While the simplicity of PromQL is great for most metric use cases, adding on trace and log features would almost certainly make that experience worse. Having three languages that were similar (one for each signal) and could be linked together by time and attributes at query time is a possibility, but while PromQL is a de facto standard, it seems unlikely that LogQL (Grafana Loki’s PromQL-inspired query language for logs) will show up in other products. And, at the time of writing, traces don’t have a common language. Sure we could develop those three interfacing languages, but do we need to?</p><h2 id="why-sql">Why SQL?</h2><p>Before working with observability data, I was in the Open Source database world. I think we can learn something from databases here by adopting the lingua franca of data analytics: SQL. Somehow, it has been pushed to the bottom of our programming languages kit but is coming back strong due to the increasing importance of data for decision-making.</p><p>SQL is a truly a language that has stood the test of time:</p><ul><li>It’s a well-defined standard built for modeling relationships and then analyzing data.</li><li>It allows easy joins between relations and is used in many, many data products.</li><li>It is supported in all major programming languages, and if tooling supports external query languages, it’s a good bet it will support SQL as one of them.</li><li>And finally, developers <em>understand</em> SQL. While it can be a bit more verbose than something like PromQL, it won’t need any language updates to support traces and metrics in addition to logs—it just needs a schema defined that models those relationships.</li></ul><p>Despite all this, SQL is a language choice that often raises eyebrows. It’s not typically a language favored by Cloud technologies and DevOps, and with the rise in the use of object-relational mapping libraries (ORMs), which abstract SQL away from developers, it’s often ignored. But, if you need to analyze different sets of data that have something in common—so they can be joined, correlated, and compared together—you use SQL. </p><p>If before we dealt with metrics, logs, and traces in different (and usually intentionally simple) systems with no commonalities, today’s systems are becoming progressively more complex and require correlation. SQL is a perfect choice for this; in fact, this is what SQL was designed to do. It even lets us be sure that we can correlate data from outside of our Observability domain with our telemetry—all of a sudden, we would have the ability to pull in reference data and enrich our signals past the labels we attach at creation time.</p><p>At Timescale, we are convinced that a single, consistent query layer is the correct approach—and are investing in developing <a href="https://timescale.ghost.io/blog/important-news-about-promscale/" rel="noreferrer">Promscale</a>, a scalable backend to store all signal data which supports SQL as its native language. Whatever the solution is, we are looking forward to being able to query seamlessly across all our telemetry data, unlocking the full potential of OpenTelemetry.</p>]]></content:encoded>
        </item>
    </channel>
</rss>