<?xml version="1.0" encoding="UTF-8" ?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" version="2.0"><channel><title>Marco Slot | CrunchyData Blog</title>
<atom:link href="https://www.crunchydata.com/blog/author/marco-slot/rss.xml" rel="self" type="application/rss+xml" />
<link>https://www.crunchydata.com/blog/author/marco-slot</link>
<image><url>https://www.crunchydata.com/build/_assets/marco-slot.png-TPE3Y5IS.webp</url>
<title>Marco Slot | CrunchyData Blog</title>
<link>https://www.crunchydata.com/blog/author/marco-slot</link>
<width>400</width>
<height>400</height></image>
<description>PostgreSQL experts from Crunchy Data share advice, performance tips, and guides on successfully running PostgreSQL and Kubernetes solutions</description>
<language>en-us</language>
<pubDate>Tue, 17 Dec 2024 08:30:00 EST</pubDate>
<dc:date>2024-12-17T13:30:00.000Z</dc:date>
<dc:language>en-us</dc:language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<item><title><![CDATA[ pg_incremental: Incremental Data Processing in Postgres ]]></title>
<link>https://www.crunchydata.com/blog/pg_incremental-incremental-data-processing-in-postgres</link>
<description><![CDATA[ We are excited to release a new open source extension called pg_incremental. pg_incremental works with pg_cron to do incremental batch processing for data aggregations, data transformations, or imports/exports. ]]></description>
<content:encoded><![CDATA[ <p>Today I’m excited to introduce <a href=https://github.com/crunchydata/pg_incremental>pg_incremental</a>, a new open source PostgreSQL extension for automated, incremental, reliable batch processing. This extension helps you create processing pipelines for append-only streams of data, such as IoT / time series / event data workloads.<p>Notable pg_incremental use cases include:<ul><li>Creation and incremental maintenance of rollups, aggregations, and interval aggregations<li>Incremental data transformations<li>Periodic imports or export of new data using standard SQL</ul><p>After you set up a pg_incremental pipeline, it runs forever until you tell Postgres to stop. There’s a lot you can do with pg_incremental and we have a lot of thoughts on why we think it’s valuable. To help you navigate some of if you want to jump directly to one of the examples that you feel is relevant to you:<ul><li><a href=/blog/pg_incremental-incremental-data-processing-in-postgres#why-incremental-processing>Why incremental processing</a><li><a href=/blog/pg_incremental-incremental-data-processing-in-postgres#practical-data-pipelines-using-parameterized-sql>Practical data pipelines using parameterized SQL</a><li><a href=/blog/pg_incremental-incremental-data-processing-in-postgres#example-1-unpacking-raw-json-data-with-a-sequence-pipeline>Example 1: Unpacking raw JSON data with a sequence pipeline</a><li><a href=/blog/pg_incremental-incremental-data-processing-in-postgres#example-2-complex-aggregations-with-a-time-interval-pipeline>Example 2: Complex aggregations with a time interval pipeline</a><li><a href=/blog/pg_incremental-incremental-data-processing-in-postgres#example-3-periodic-export-to-parquet-in-s3-with-a-time-interval-pipeline>Example 3: Periodic export to Parquet in S3 with a time interval pipeline</a><li><a href=/blog/pg_incremental-incremental-data-processing-in-postgres#example-4-import-new-files-with-a-file-list-pipeline>Example 4: Import new files with a file list pipeline</a></ul><h2 id=why-incremental-processing><a href=#why-incremental-processing>Why incremental processing?</a></h2><p>My team has been working on handling data-intensive workloads in PostgreSQL for many years. The most data-intensive workloads are usually the ones with a machine-generated stream of event data, and we often find that the best solution for handling those workloads in PostgreSQL involves incremental data processing.<p>For example, a common pattern in PostgreSQL is to periodically pre-aggregate incoming event data into a summary table. In that model, writes (esp. batch loads) are fast because they do not trigger any immediate processing. The incremental aggregation is fast because it only processes new rows, and queries from dashboards are fast because they hit an indexed summary table. I originally developed <a href=https://github.com/citusdata/pg_cron>pg_cron</a> for this purpose, but creating an end-to-end pipeline still required a lot of bookkeeping and careful concurrency considerations.<p>There are some existing solutions to this problem, such as incremental materialized views and logical decoding-based approaches, but the implementations are complex and come with many limitations. Moreover, there are other incremental processing scenarios such as collecting data from multiple sources, or periodic import/export. I also still hear from people about an old blog post I wrote on <a href=https://www.citusdata.com/blog/2018/06/14/scalable-incremental-data-aggregation/>incremental data processing in PostgreSQL</a>  so I know this topic remains unsolved for many Postgres users.<p>I felt it was time for a new incremental processing tool. One that isn't particularly magical -  but is simple, versatile and gets the job done. That tool is pg_incremental.<h2 id=practical-data-pipelines-using-parameterized-sql><a href=#practical-data-pipelines-using-parameterized-sql>Practical data pipelines using parameterized SQL</a></h2><p>The basic idea behind pg_incremental is simple: You define a pipeline using a SQL command that is executed with parameters ($1, $2) that specify a range of values to be processed.<p>When you first define the pipeline, it executes the command with a range that covers all existing data, and also sets up a background job using <a href=https://github.com/citusdata/pg_cron>pg_cron</a> to periodically execute the command for new ranges. Every execution of the pipeline is transactional, such that each value is processed successfully exactly once. The dimension used to identify new data can be a sequence, time, or list of files.<p>Let’s think about a sample data aggregation pipeline:<ol><li>You have an indexed raw data table of events<li>You have a summary table called view_counts that summarizes data from your daily events table<li>pg_incremental is used for incrementally upserting existing and new event data into view_counts</ol><p>Sample code:<pre><code class=language-sql>/* define the raw data and summary table */
create table events (event_id bigserial, event_time timestamptz, user_id bigint, response_time double precision);
create table view_counts (day timestamptz, user_id bigint, count bigint, primary key (day, user_id));

/* enable fast range scans on the sequence column */
create index on events using brin (event_id);

/* for demo: generate some random data */
insert into events (event_time, user_id, response_time)
select now(), random() * 100, random() from generate_series(1,1000000) s;

/* define a sequence pipeline that periodically upserts view counts */
select incremental.create_sequence_pipeline('view-count-pipeline', 'events',
  $$
    insert into view_counts
    select date_trunc('day', event_time), user_id, count(*)
    from events where event_id between $1 and $2
    group by 1, 2
    on conflict (day, user_id) do update set count = view_counts.count + EXCLUDED.count;
  $$
);

/* get the most active users of today */
select user_id, sum(count) from view_counts where day = now()::date group by 1 order by 2 desc limit 3;
┌─────────┬───────┐
│ user_id │  sum  │
├─────────┼───────┤
│      32 │ 20486 │
│      77 │ 20404 │
│      75 │ 20378 │
└─────────┴───────┘
</code></pre><p>A “sequence pipeline” takes advantage of sequence values in PostgreSQL being monotonically increasing with every insert. It is not normally safe to just start processing a range of sequence values, because there might be ongoing transactions that are about to insert lower sequence values. However, pg_incremental waits for those transactions to complete before processing a range, which guarantees that the range is safe.<p>Not every table has a sequence, and sometimes the source is not a table at all. Therefore, pg_incremental has 3 types of pipelines:<ul><li><strong>Sequence pipelines</strong> can process ranges of new sequence values in small batches with upserts.<li><strong>Time intervals pipelines</strong> can process data that falls within a time interval after the time interval has passed.<li><strong>File list pipelines</strong> (in preview) can process new files that appear in a directory.</ul><p>Let's look at some more examples:<h3 id=example-1-unpacking-raw-json-data-with-a-sequence-pipeline><a href=#example-1-unpacking-raw-json-data-with-a-sequence-pipeline>Example 1: Unpacking raw JSON data with a sequence pipeline</a></h3><p>PostgreSQL has great JSON support, but I often run into scenarios where you need to unpack raw JSON data into the columns of a table to simplify querying or add indexes and constraints.<p>Below is an example of using a pg_incremental sequence pipeline to transform raw JSON. We create a table with a sequence and a JSONB column to load raw files directly using COPY. We then set up a pipeline that extracts relevant values from the new JSON objects, and inserts them into an events table with columns.<pre><code class=language-sql>/* create a table with a single JSONB column and a sequence to track new objects */
create table events_json (id bigint generated by default as identity, payload jsonb);
create index on events_json using brin (id);

/* load some data from a local newline-delimited JSON file */
\copy events_json (payload) from '2024-12-15-00.json' with (format 'csv', quote e'\x01', delimiter e'\x02', escape e'\x01')

/* periodically unpack the new JSON objects into the events table */
select incremental.create_sequence_pipeline('unpack-json-pipeline', 'events_json',
  $$
    insert into events (event_id, event_time, user_id, response_time)
    select
      nextval('events_event_id_seq'),
      (payload->>'created_at')::timestamptz,
      (payload->'actor'->>'id')::bigint,
      (payload->>'response_time')::double precision
    from events_json
    where id between $1 and $2
  $$
);
</code></pre><p>After setting up the pipeline, future data loads into events_json will automatically be transformed and added to the events table.<h3 id=example-2-complex-aggregations-with-a-time-interval-pipeline><a href=#example-2-complex-aggregations-with-a-time-interval-pipeline>Example 2: Complex aggregations with a time interval pipeline</a></h3><p>A time interval pipeline runs after an interval has passed when all the data in the interval is available. Compared to sequence pipelines, time interval pipelines are more suitable for aggregations that cannot be merged such as exact distinct counts.<p>Below is an example of using a pg_incremental time interval pipeline to aggregate the number of unique users in an hour into a user_counts table. The $1 and $2 parameters will be set to the start and end (exclusive) of a range of time intervals.<pre><code class=language-sql>/* create a table for number of active users per hour */
create table user_counts (hour timestamptz, user_count bigint, primary key (hour));

/* enable fast range scans on the event_time column */
create index on events using brin (event_time);

/* aggregates a range of 1 hour intervals after an hour has passed */
select incremental.create_time_interval_pipeline('distinct-user-count', '1 hour',
  $$
    insert into view_counts
    select date_trunc('hour', event_time), count(distinct user_id)
    from events where event_time >= $1 and event_time &#60 $2
    group by 1
  $$
);

/* get number of active users per hour */
select hour, user_count from user_counts order by 1;
</code></pre><p>A downside of time interval pipelines is that they do not process data with older timestamps if the corresponding interval has already been processed. By default, a time interval pipeline waits for 1 minute after the interval. You can configure a higher min_delay and can also specify a source_table_name to wait for writers to finish.<h3 id=example-3-periodic-export-to-parquet-in-s3-with-a-time-interval-pipeline><a href=#example-3-periodic-export-to-parquet-in-s3-with-a-time-interval-pipeline>Example 3: Periodic export to Parquet in S3 with a time interval pipeline</a></h3><p>A common requirement with event data is to export into a remote storage system like S3, for instance using the <a href=https://www.crunchydata.com/blog/pg_parquet-an-extension-to-connect-postgres-and-parquet>pg_parquet</a> extension.<p>Below is an example of using a pg_incremental time interval pipeline to export the data in the events table to one Parquet file per day starting at Jan 1st 2024, and automatically after a day has passed.<pre><code class=language-sql>/* define a function that wraps a COPY TO command to export data */
create or replace function export_events(start_time timestamptz, end_time timestamptz)
returns void language plpgsql as $function$ begin

  /* select all rows in a time range and export them to a Parquet file */
  execute format(
    'copy (select * from events where event_time >= %L and event_time &#60 %L) to %L',
    start_time, end_time, format('s3://mybucket/events/%s.parquet', start_time::date)
  );

end; $function$;

/* export data as 1 file per day, starting at Jan 1st */
select incremental.create_time_interval_pipeline(
  'export-events',
  '1 day',
  'select export_events($1, $2)',

  source_table_name := 'events', /* wait for writes on events to finish */
  batched := false,              /* separate execution for each day     */
  start_time := '2024-01-01'     /* export all days from Jan 1st now    */
);
</code></pre><p>In this case, I disabled “batching” of time intervals, such that time intervals are processed one at a time, starting from Jan 1st 2024. I also specified a source_table_name, which means the execution waits for any ongoing writes. If the event_time is generated via now(), this helps ensure we do not skip any rows.<h3 id=example-4-import-new-files-with-a-file-list-pipeline><a href=#example-4-import-new-files-with-a-file-list-pipeline>Example 4: Import new files with a file list pipeline</a></h3><p>One of the things that triggered me to write pg_incremental was that I found myself writing a script to incrementally process new files in S3 for a <a href=https://www.crunchydata.com/blog/crunchy-data-warehouse-postgres-with-iceberg-for-high-performance-analytics>Crunchy Data Warehouse</a> use case, and I realized that processing new files in a directory had a lot in common with the other incremental processing scenarios, except we find new data by listing files.<p>Below is an example of using a pg_incremental file list pipeline to import all files that match a wildcard and automatically load new files as they appear (in Crunchy Data Warehouse). The $1 parameter will be set to the path of a file that has not been processed yet, as returned by the underlying list function.<pre><code class=language-sql>/* define function that wraps a COPY FROM command to import data */
create or replace function import_events(path text)
returns void language plpgsql as $function$ begin

  /* load a file into the events table */
  execute format('copy events from %L', path);

end; $function$;

/* load all the files under a prefix, and automatically load new files, one at a time */
select incremental.create_file_list_pipeline(
    'import-events',
    's3://mybucket/events/*.csv',
    'select import_events($1)'
);
</code></pre><p>The list function is configurable via the <code>list_function</code> argument. For instance, you could wrap around the <a href=https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-GENFILE>pg_ls_dir()</a> function to load files on the server, or use a function that returns a synthetic range to load public (not listable) data.<p>The API of the file list pipeline might still undergo small changes, hence it’s in preview.<h2 id=monitoring-pg_incremental><a href=#monitoring-pg_incremental>Monitoring pg_incremental</a></h2><p>You can see all your pipelines in the <code>incremental.pipelines</code> table and monitor the progress of your pipelines via the tables that pg_incremental uses to do its own bookkeeping, which contain the last processed value:<pre><code class=language-sql>select * from incremental.sequence_pipelines ;
┌─────────────────────┬────────────────────────────┬────────────────────────────────┐
│    pipeline_name    │       sequence_name        │ last_processed_sequence_number │
├─────────────────────┼────────────────────────────┼────────────────────────────────┤
│ view-count-pipeline │ public.events_event_id_seq │                        3000000 │
└─────────────────────┴────────────────────────────┴────────────────────────────────┘

select * from incremental.time_interval_pipelines;
┌───────────────┬───────────────┬─────────┬───────────┬────────────────────────┐
│ pipeline_name │ time_interval │ batched │ min_delay │  last_processed_time   │
├───────────────┼───────────────┼─────────┼───────────┼────────────────────────┤
│ export-events │ 1 day         │ f       │ 00:00:30  │ 2024-12-17 00:00:00+01 │
└───────────────┴───────────────┴─────────┴───────────┴────────────────────────┘
</code></pre><p>In addition, you can view the result of the underlying pg_cron jobs via the regular pg_cron tables.<pre><code class=language-sql>select jobname, start_time, status, return_message
from cron.job_run_details join cron.job using (jobid)
where jobname like 'pipeline:event-import%' order by 1 desc limit 3;
┌───────────────────────┬───────────────────────────────┬───────────┬────────────────┐
│        jobname        │          start_time           │  status   │ return_message │
├───────────────────────┼───────────────────────────────┼───────────┼────────────────┤
│ pipeline:event-import │ 2024-12-17 13:27:00.090057+01 │ succeeded │ CALL           │
│ pipeline:event-import │ 2024-12-17 13:26:00.055813+01 │ succeeded │ CALL           │
│ pipeline:event-import │ 2024-12-17 13:25:00.086688+01 │ succeeded │ CALL           │
└───────────────────────┴───────────────────────────────┴───────────┴────────────────┘
</code></pre><p>Note that the jobs run more frequently than the pipeline command is executed. The command is skipped if there is no new work to do.<h2 id=get-started-with-incremental-processing-in-postgresql><a href=#get-started-with-incremental-processing-in-postgresql>Get started with incremental processing in PostgreSQL</a></h2><p>Crunchy Data is proud to release pg_incremental under the PostgreSQL license. We believe it is a foundational building block for building IoT applications on PostgreSQL that should be available to everyone, similar to <a href=https://github.com/citusdata/pg_cron>pg_cron</a>, <a href=https://github.com/crunchydata/pg_parquet/>pg_parquet</a>, and <a href=https://github.com/pgpartman/pg_partman>pg_partman</a>.<p>You can find code and documentation on the <a href=https://github.com/crunchydata/pg_incremental>pg_incremental GitHub repo</a>, and let us know if you have any feedback (always appreciate a star!).<p>Starting today, pg_incremental is also available on <a href=https://www.crunchydata.com/products/crunchy-bridge>Crunchy Bridge</a> and <a href=https://www.crunchydata.com/products/warehouse>Crunchy Data Warehouse</a>. ]]></content:encoded>
<category><![CDATA[ Analytics ]]></category>
<author><![CDATA[ Marco.Slot@crunchydata.com (Marco Slot) ]]></author>
<dc:creator><![CDATA[ Marco Slot ]]></dc:creator>
<guid isPermalink="false">402056d593bec50e6bc664d9eea176a721bd2aaaa9603ca4c4b8cfc04de8b6dd</guid>
<pubDate>Tue, 17 Dec 2024 08:30:00 EST</pubDate>
<dc:date>2024-12-17T13:30:00.000Z</dc:date>
<atom:updated>2024-12-17T13:30:00.000Z</atom:updated></item>
<item><title><![CDATA[ Postgres Powered by DuckDB: The Modern Data Stack in a Box ]]></title>
<link>https://www.crunchydata.com/blog/postgres-powered-by-duckdb-the-modern-data-stack-in-a-box</link>
<description><![CDATA[ Marco reviews the challenges and strengths of analytical and transactional workloads and what a modern data stack that merges the two might look like. ]]></description>
<content:encoded><![CDATA[ <style>
    .black-box {
        background-color: black;
        color: white;
        padding: 20px;
        text-align: left;
        align-items: left;
        margin: 20px auto;
        border-radius: 10px;
        width: auto;
        height: auto;
    }
    .black-box a {
        color: white;
        text-decoration: underline;
    }
</style> <div class="black-box">
Looking for Postgres with the power of DuckDB? <a href="https://www.crunchydata.com/products/warehouse">Crunchy Data Warehouse</a> is the latest Postgres-native tool with full Iceberg support and DuckDB integration.
</div><p><a href=https://www.crunchydata.com/products/crunchy-bridge-for-analytics>Postgres for analytics</a> has always been a huge question mark. By using PostgreSQL's extension APIs, <a href=https://www.crunchydata.com/solutions/postgres-with-duckdb>integrating DuckDB</a> as a query engine for state-of-the-art analytics performance without forking either project could Postgres be the analytics database too?<p>Bringing an analytical query engine into a transactional database system raises many interesting possibilities and questions. In this blog post I want to reflect on what makes these workloads and system architectures so different and what bringing them together means.<h2 id=olap--oltp-never-the-twain-shall-meet><a href=#olap--oltp-never-the-twain-shall-meet>OLAP &#38 OLTP: Never the twain shall meet</a></h2><p><a href=https://www.crunchydata.com/blog/an-overview-of-distributed-postgresql-architectures>Database systems</a> have always been divided into two worlds: Transactional and Analytical (traditionally referred to as OLTP) and OLAP (Online Transactional/Analytical Processing).<p>Both types of data stores use very similar concepts. The relational data model and SQL dominate. Writes, schema management, transactions and indexes use similar syntax and semantics. Many tools can interact with both types. Why then are they separate systems?<p>The answer has multiple facets. At a high level, OLTP involves doing a very large number of small queries and OLAP involves doing a small number of very large queries. They represent two extremes of the database workload spectrum. While many workloads fall somewhere in between, they can often be optimized or split until they can reasonably be handled by conventional OLTP or OLAP systems.<p>For many applications, the database system does the bulk of the critical computational work. Deep optimizations are essential for a database system to be useful and appealing, but optimization inherently comes with complexity. Consider that relational database systems have a vast amount of functionality and need to cater to a wide range of workloads. Building a versatile yet well-optimized database system can take a very long time.<p>Optimization is most effective when specializing for the characteristics of specific workloads, which practically always comes with the trade-off of being less optimized for other workloads. As it turns out, doing many small things or a few large things, when optimized to the extreme across a wide range of system functions over a long period, results in fundamentally different system architectures. The interface may be similar, but everything from the way queries and transactions are processed down to the storage architecture is going to be vastly different.<p>Let's have a look at what the main challenges are for each type of system.<h2 id=challenges-of-transactional-systems><a href=#challenges-of-transactional-systems>Challenges of transactional systems</a></h2><p>The biggest challenge in transactional systems is the efficient handling of a high rate of small update/delete transactions, in a way that guarantees ACID properties, while also handling concurrent read queries.<p>Storage in transactional systems is organized around small in-memory buffers that can be modified in less than a microsecond and written to disk in under a millisecond. Rows are packed together in the buffers, with tree data structures across the buffers (indexes) used to efficiently find the rows.<p>An insert/update/delete command involves modifying several buffers to write the new rows and add index entries, or a larger number when modifying multiple rows at once. The changes to the buffers are also written to a write ahead log (WAL) to ensure that they can be recovered in case of a crash.<p>In the cloud, the buffers and WAL can be stored in elastic block storage or other network-attached storage systems that replicate small disk blocks with minimal latency. Only the WAL is synchronously flushed to disk when committing a transaction. Recently modified buffers may only exist in the memory of the database server. The disk can have many stale and sometimes even truncated buffers. It can only be correctly interpreted by the server to which it is attached or through a crash recovery process that restores changes from the WAL.<p>Many write operations and read queries will be running concurrently. Database systems typically try to prevent queries from seeing ongoing changes by versioning the rows during a write, such that concurrent reads can skip row versions that were added after the query started ("snapshot isolation"). This comes with additional challenges. For instance, consider that strange anomalies would occur if concurrent updates would operate on the same snapshot without considering each other’s effects. PostgreSQL resolves this through a combination of row-level locks, a chain of forward pointers from past to current row versions, and using the latest row version in the update regardless of the snapshot.<p>To build a high performance transactional system it is essential to minimize the overhead from synchronization across all these operations, as well as storage access, query processing, transaction management, I/O, and concurrency control.<h2 id=challenges-of-analytical-systems><a href=#challenges-of-analytical-systems>Challenges of analytical systems</a></h2><p>The biggest challenge in analytical systems is to process a very large number of records in as little time as possible within the context of a single query.<p>Analytical queries compute statistics and trends across historical data. The number of records involved in a single query can easily be in the billions, which means it's a billion times higher than the number of records involved in a typical transactional query (1-100). If your database system needed 1 second per record, a single query might not finish in your lifetime. Hence, spending as little time as possible per record matters above all else.<p>One of the techniques analytical systems use to minimize per-record overhead is columnar storage, which dissects records into fixed-sized vectors of values from the same column. Analytical queries often only use a subset of the columns while involving most of the records, and columnar storage enables skipping unused columns during reads. The vectors can also be compressed effectively because columns often contain many similar values.<p>Database systems that use columnar storage can be architected for vectorized execution, which means they process a vector at a time rather than a record at a time. For instance, a filter might be evaluated on a vector from column A, which produces a list of indices to be retrieved from a vector from column B. Vectorized execution minimizes the overhead of switching between different expression states, and can take advantage of modern CPU instructions that process multiple values at once (SIMD).<p>Analytical database systems also optimize for parallel execution within a single query. Data needs to be passed around efficiently between different parts of a parallel execution pipeline.<p>Managing the flows of data in a parallel, vectorized executor with minimal data copying, while also using data structures that maximize performance is the most complex part of building an analytical database system. The query planner and executor are very different than in transactional systems, with higher processing time per query, but much lower processing time per row when a query spans many rows.<p>In modern analytical database systems, storage is organized around files in distributed storage systems like Amazon S3, because they can scale the amount of storage and retrieval bandwidth, and can be accessed directly by various applications. The latency of such systems is relatively high, but acceptable for analytical queries which typically range from hundreds of milliseconds to minutes.<p>The overhead of synchronization between components and concurrent queries is also less of an issue in analytical systems than in transactional systems because they are negligible compared to the time required to execute a single query.<h2 id=can-you-build-a-unified-database-architecture><a href=#can-you-build-a-unified-database-architecture>Can you build a unified database architecture?</a></h2><p>It is technically possible to build a database engine that can simultaneously handle a high rate of low latency transactions and perform fast analytical queries on a <em>single copy of the data</em>, with ACID properties. Such systems are often referred to as HTAP (Hybrid Transactional/Analytical Processing). However, hybrid systems generally underperform against dedicated OLTP or OLAP systems or make other invasive trade-offs such as limited functionality, or lower durability.<p>It is hard to precisely identify the workloads that benefit substantially from a hybrid approach. Moreover, the incremental cost of keeping a second copy of transactional data in a format that is optimized for analytics and compression in object storage is relatively small. Hence, replicating transactional data into an analytical system with some lag has become the dominant way of doing analytics on transactional data.<p>The OLTP vs. OLAP disparity is likely to stay, though it is not without downsides. The capabilities, tools, ecosystem, and practices differ significantly between different parts of the data stack, which becomes a source of complexity and high maintenance cost. In addition, expensive, brittle data movement tools are needed to get data from transactional to analytical stores.<p>OLTP and OLAP workloads are often managed by different teams, so some differences in tooling and practices are to be expected. Still, organizations spend a huge amount of time and money on moving data and integrating different systems. Moreover, an application that is purely transactional or purely analytical is not likely to remain so for long.<p>Consider that analytics teams often create dashboards and popular dashboards end up having various types of materialized views, typically kept in transactional database systems. Data management also involves a lot of metadata and bookkeeping, and those are best done in a transactional way to avoid duplicate or missing data.<p>Application teams use transactional database systems, but often want to add value by providing their customers with insights. However, they do not want the complexity of funneling the data through several (company-wide) analytics systems.<p>OLTP and OLAP are fundamentally different workloads that benefit from fundamentally different techniques and storage solutions, but they don't necessarily benefit from using wholly different database systems. The fact that most database systems are focused on only one type of workload is because it is extremely hard for a database builder to be simultaneously successful in two different worlds.<p>The solution to this conundrum, we believe, lies in extensibility.<h2 id=database-extensibility-bringing-disparate-systems-together><a href=#database-extensibility-bringing-disparate-systems-together>Database extensibility: Bringing disparate systems together</a></h2><p>From its inception, PostgreSQL has been designed to be extensible. It supports many forms of extensibility, with extensions able to control the behavior of query processing and data storage at many different levels. There is a flourishing ecosystem of extensions.<p>DuckDB is an embedded OLAP database, which is taking the analytics landscape by storm. DuckDB takes inspiration from SQLite for its deployment model, and PostgreSQL for its functionality and extensibility.<p>With both of these systems being extensible, and having a similar interface, they can be integrated in interesting ways. In Crunchy Bridge for Analytics, we introduced the notion of analytics tables that are backed by files in S3 and integrated DuckDB as a query engine for queries that involve analytics tables. PostgreSQL gives sufficient flexibility to use DuckDB for the full query or specific parts of the query. We used DuckDB extensions to incrementally fill any gaps that DuckDB might have compared to PostgreSQL, to ensure a wide range of PostgreSQL queries can take advantage of parallel, vectorized execution on columnar Parquet files within DuckDB.<p>Our goal in Crunchy Bridge for Analytics is not necessarily to enable HTAP tables. It is meant "for analytics". We do believe it is very useful to have your analytics database use the same system as your transactional databases, and that it is also very useful to be able to handle arbitrary transactional workloads in your analytics database, including handling of metadata, fast insert queues, partitioned time series tables, materialized views, etc. We also think it is useful to be able to easily move data between transactional and analytical tables without needing external tools, or do fast analytics without needing to switch to a different set of tools, data types, syntax, and ecosystem.<p>Effectively, extensibility can reduce the traditional OLTP-OLAP gap to a difference between tables, rather than a difference between database systems. Multiple query engines that make very different trade-offs and use different storage layers can be blended together into a single environment.<h2 id=the-power-single-machine-database-systems><a href=#the-power-single-machine-database-systems>The power single machine database systems</a></h2><p>One of the most surprising and disruptive aspects of DuckDB is that it is taking fast analytics away from the complex world of distributed systems back into the much simpler single-machine realm. While transactional database systems like PostgreSQL can be distributed, the vast majority of database systems run on a single machine. Hence, we now have two state-of-the-art, open source OLAP &#38 OLTP systems which we can reasonably run together on the same machine.<p>There is still a difference between running a single machine briefly to handle a large analytics query (common for OLAP) vs. running it all the time to handle a steady rate of transactions (common for OLTP). However, we found that using a persistent machine for analytics on modern hardware has one tremendous benefit: long-lived cached in memory and on large NVMe drives.<p>Retrieving a file over S3 over a single connection is generally limited to 50-80MB/sec. Multiple concurrent connections can help, but in most scenarios the aggregate throughput on a single machine only goes a few times higher. Conversely, locally-attached NVMe drives easily reach 2-3GB/sec of read throughput and are usually big enough to hold a large part of the data. Keeping our data cached lets us take advantage of DuckDB’s processing power at a limited, predictable cost.<p>Even with a local cache, you do want your machine to have a high bandwidth connection to S3 for querying files that are not in cache or for loading into cache within a reasonable amount of time. The best way to achieve that is to run the machine on EC2 in the same AWS region as your S3 buckets. A major benefit DuckDB-in-PostgreSQL has over plain DuckDB in that regard is that it has a well-defined network protocol and an huge ecosystem of tools that support it. Moreover, PostgreSQL can be managed for you in EC2 by Crunchy Bridge.<p>Of course, when we talk about single machine systems, you can still have as many of those as you need to handle various applications and workflows with high concurrency. The big advantage is that you avoid a lot of the cost and complexity of gluing together operationally complex data processing systems, and instead you have a set of versatile units that are managed for you.<h2 id=duckdb--postgresql--the-everything-database><a href=#duckdb--postgresql--the-everything-database>DuckDB + PostgreSQL = The Everything Database?</a></h2><p>So, with <a href=https://www.crunchydata.com/products/crunchy-bridge-for-analytics>Crunchy Bridge for Analytics</a> you can get the stellar analytics performance of DuckDB along with all the familiar transactional capabilities and versatility of PostgreSQL in one box, which is managed for you by the team at Crunchy Data.<p>You can do fast ad-hoc queries on <a href=https://www.crunchydata.com/solutions/postgres-for-parquet-and-iceberg>Parquet</a> in S3 or create materialized views, you can schedule your ETL processes via <a href=https://www.crunchydata.com/blog/annoucing-the-scheduler-for-crunchy-bridge>pg_cron</a>, you can track data operations via transactions, you can efficiently import and export Parquet/CSV/JSON, you can use any PostgreSQL-compatible tool (incl. most BI tools), and you can use all the PostgreSQL <a href=https://docs.crunchybridge.com/extensions-and-languages>extensions</a> and <a href=https://docs.crunchybridge.com/concepts>managed database features</a> offered by Crunchy Bridge. Finally, you get a predictable price with great price-performance thanks to long-lived caches.<p>It might just be time for a new data stack. ]]></content:encoded>
<category><![CDATA[ Crunchy Data Warehouse ]]></category>
<author><![CDATA[ Marco.Slot@crunchydata.com (Marco Slot) ]]></author>
<dc:creator><![CDATA[ Marco Slot ]]></dc:creator>
<guid isPermalink="false">e344a825d30807b621355f47dc26ca82f2fcace91b05d63bc3c588701936cbd0</guid>
<pubDate>Fri, 16 Aug 2024 12:00:00 EDT</pubDate>
<dc:date>2024-08-16T16:00:00.000Z</dc:date>
<atom:updated>2024-08-16T16:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ An Overview of Distributed  PostgreSQL Architectures ]]></title>
<link>https://www.crunchydata.com/blog/an-overview-of-distributed-postgresql-architectures</link>
<description><![CDATA[ Marco just joined Crunchy Data and he reflects on his career in distributed systems in this post. He provides an overview of several options for approaching distributed Postgres workloads and the pros and cons of each approach. ]]></description>
<content:encoded><![CDATA[ <p>I've always found distributed systems to be the most fascinating branch of computer science. I think the reason is that distributed systems are subject to the rules of the physical world just like we are. Things are never perfect, you cannot get everything you want, you’re always limited by physics, and often by economics, or by who you can communicate with. Many problems in distributed systems simply do not have a clean solution, instead there are different trade-offs you can make.<p>While at Citus Data, Microsoft, and now Crunchy Data, the focus of my work has been on distributed PostgreSQL architectures. At the last <a href=http://PGConf.EU>PGConf.EU</a> in December, I gave a talk titled “<a href=https://www.postgresql.eu/events/pgconfeu2023/sessions/session/4826-postgresql-distributed-architectures-best-practices/>PostgreSQL Distributed: Architectures &#38 Best Practices</a>” where I went over various kinds of distributed PostgreSQL architectures that I’ve encountered over the years.<p>Many distributed database discussions focus on algorithms for distributed query planning, transactions, etc. These are very interesting topics, but the truth is that only a small part of my time as a distributed database engineer goes into algorithms, and an excessive amount of time goes into making very careful trade-offs at every level (and of course, failure handling, testing, fixing bugs). Similarly, what many users notice within the first few minutes of using a distributed database is how unexpectedly slow they can be, because you quickly start hitting performance trade-offs.<p>There are many types of distributed PostgreSQL architectures, and they each make a different set of trade-offs. Let’s go over some of these architectures.<h2 id=single-machine-postgresql><a href=#single-machine-postgresql>Single machine PostgreSQL</a></h2><p>To set the stage for discussing distributed PostgreSQL architectures, we first need to understand a bit about the simplest possible architecture: running PostgreSQL on a single machine, or "node".<p>PostgreSQL on a single machine can be incredibly fast. There’s virtually no network latency on the database layer and you can even co-locate your application server. Millions of IOPS are available depending on the machine configuration. Disk latency is measured in microseconds. In general, running PostgreSQL on a single machine is a performant and cost-efficient choice.<p>So why doesn’t everyone just use a single machine?<p>Many companies do. However, PostgreSQL on a single machine comes with operational hazards. If the machine fails, there’s inevitably some kind of downtime. If the disk fails, you’re likely facing some data loss. An overloaded system can be difficult to scale. And you’re limited to the storage size of a disk, which when full will cease to process and store data. That very low latency and efficiency clearly comes at a price.<p>Distributed PostgreSQL architectures are ultimately trying to address the operational hazards of a single machine in different ways. In doing so, they do lose some of its efficiency, and especially the low latency.<h2 id=goals-of-a-distributed-database-architecture><a href=#goals-of-a-distributed-database-architecture>Goals of a Distributed Database Architecture</a></h2><p>The goal of a distributed database architecture is to try to meet the availability, durability, performance, regulatory, and scale requirements of large organizations, subject the physics. The ultimate goal is to do so with the same rich functionality and precise transactional semantics as a single node RDBMS.<p>There are several mechanisms that distributed database systems employ to achieve this, namely:<ul><li>Replication - Place copies of data on different machines<li>Distribution - Place partitions of data on different machines<li>Decentralization - Place different DBMS activities on different machines</ul><p>In practice, each of these mechanisms inherently comes with concessions in terms of performance, transactional semantics, functionality, and/or operational complexity.<p>To get a nice thing, you’ll have to give up a nice thing, but there are many different combinations of what you can get and what you need to give up.<h2 id=the-importance-of-latency-in-oltp-systems><a href=#the-importance-of-latency-in-oltp-systems>The importance of latency in OLTP systems</a></h2><p>Of course, distributed systems have already taken over the world, and most of the time we don’t really need to worry a lot about trade-offs when using them. Why would distributed <em>database</em> systems be any different?<p>The difference lies in a combination of storing the authoritative state for the application, the rich functionality that an RDBMS like PostgreSQL offers, and the relatively high impact of latency on client-perceived performance in OLTP systems.<p>PostgreSQL, like most other RDBMSs, uses a synchronous, interactive protocol where transactions are performed step-by-step. The client waits for the database to answer before sending the next command, and the next command might depend on the answer to the previous.<p>Any network latency between client and database server will already be a noticeable factor in the overall duration of a transaction. When PostgreSQL itself is a distributed system that makes internal network round trips (e.g. while waiting for WAL commit), the duration can get many times higher.<p><img alt="latency diagram"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/41566cae-dec8-4d8d-9b32-a8c23cb70200/public><p>Why is it bad for transactions to take longer? Surely humans won’t notice if they need to wait 10-20ms? Well, if transactions take on average 20ms, then a single (interactive) session can only do 50 transactions per second. You then need a lot of concurrent sessions to actually achieve high throughput.<p>Having many sessions is not always practical from the application point-of-view, and each session uses significant resources like memory on the database server. Most PostgreSQL set ups limit the maximum number of sessions in the hundreds or low thousands, which puts a hard limit on achievable transaction throughput when network latency is involved. In addition, any operation that is holding locks while waiting for network round trips is also going to affect the achievable concurrency.<p><img alt="connections and processes diagram"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/f094a883-daa1-4a52-caa5-5590bce9fe00/public><p>While in theory, latency does not have to affect performance so much, in practice it almost always does. The CIDR ‘23 paper <a href=https://www.cidrdb.org/cidr2023/papers/p50-ziegler.pdf>“Is Scalable OLTP in the Cloud a solved problem?”</a> gives a nice discussion of the issue of latency in section 2.5.<h2 id=postgresql-distributed-architectures><a href=#postgresql-distributed-architectures>PostgreSQL Distributed Architectures</a></h2><p>PostgreSQL can be distributed at many different layers that hook into different parts of its own architecture and make different trade-offs. In the following sections, we will discuss these well-known architectures:<ul><li><strong>Network-attached block storage</strong> (e.g. EBS)<li><strong>Read replicas</strong><li><strong>DBMS-optimized cloud storage</strong> (e.g. Aurora)<li><strong>Active-active</strong> (e.g. BDR)<li><strong>Transparent Sharding</strong> (e.g. Citus)<li><strong>Distributed key-value stores with SQL</strong> (e.g. Yugabyte)</ul><p>We will describe the pros and cons of each architecture, relative to running PostgreSQL on a single machine.<p>Note that many of these architectures are orthogonal. For instance, you could have a sharded system with read replicas using network-attached storage, or an active-active system that uses DBMS-optimized cloud storage.<h3 id=network-attached-block-storage><a href=#network-attached-block-storage>Network-attached block storage</a></h3><p>Network-attached block storage is a common technique in cloud-based architectures where the database files are stored on a different device. The database server typically runs in a virtual machine in a Hypervisor, which exposes a block device to the VM. Any reads and writes to the block device will result in network calls to a block storage API. The block storage service internally replicates the writes to 2-3 storage nodes.<p><img alt="Network-attached block storage architecture in the cloud"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/4cd769ee-098e-4f1e-072c-1b15dcfcb500/public><p>Practically all managed PostgreSQL services use network-attached block devices because the benefits are critical to most organizations. The internal replication results in high durability and also allows the block storage service to remain available when a storage node fails. The data is stored separately from the database server, which means the database server can easily be respawned on a different machine in case of failure, or when scaling up/down. Finally, the disk itself is easily resizable and supports snapshots for fast backups and creating replicas.<p>Getting so many nice things does come at a significant performance cost. Where modern Nvme drives generally achieve over >1M IOPS and disk latency in the tens of microseconds, network-attached storage is often below 10K IOPS and >1ms disk latency, especially for writes. That is a ~2 order of magnitude difference.<p><strong>Pros:</strong><ul><li>Higher durability (replication)<li>Higher uptime (replace VM, reattach)<li>Fast backups and replica creation (snapshots)<li>Disk is resizable</ul><p><strong>Cons:</strong><ul><li>Higher disk latency (~20μs -> ~1000μs)<li>Lower IOPS (~1M -> ~10k IOPS)<li>Crash recovery on restart takes time<li>Cost can be high</ul><p>💡 <strong>Guideline</strong>: the durability and availability benefits of network-attached storage usually outweigh the performance downsides, but it’s worth keeping in mind that PostgreSQL can be much faster.<h3 id=read-replicas><a href=#read-replicas>Read replicas</a></h3><p>PostgreSQL has built-in support for physical replication to read-only replicas. The most common way of using a replica is to set it up as a hot standby that takes over when the primary fails in a <a href=https://www.crunchydata.com/blog/database-terminology-explained-postgres-high-availability-and-disaster-recovery>high availability set up</a>. There are many blogs, books, and talks describing the trade-offs of high availability set ups, so in this post I will focus on other architectures.<p>Another common use for read replicas is to help you scale read throughput when reads are CPU or I/O bottlenecked by load balancing queries across replicas, which achieves linear scalability of reads and also offloads the primary, which speeds up writes!<p><img alt="read replica diagram"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/67558330-e428-41c2-a876-3d6fe88ba000/public><p>A challenge with read replicas is that there is no prescribed way of using them. You have to decide on the topology and how you query them, and in doing so you will be making distributed systems trade-offs yourself.<p>The primary usually does not wait for replication when committing a write, which means read replicas are always slightly behind. That can become an issue when your application does a read that, from the user’s perspective, depends on a write that happened earlier. For example, a user clicks “Add to cart”, which adds the item to the shopping cart and immediately sends the user to the shopping cart page. If reading the shopping cart contents happens on the read replica, the shopping cart might then appear empty. Hence, you need to be very careful about which reads use a read replica.<p><img alt="digram of inserts lagging behind a read replica"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/646e4756-c11e-49e8-c4b3-b6b139e32900/public><p>Even if reads do not directly depend on a preceding write, at least from the client perspective, there may still be strange time travel anomalies. When load balancing between different nodes, clients might repeatedly get connected to different replica and see a different state of the database. As distributed systems engineers, we say that there is no “monotonic read consistency”.<p>Another issue with read replicas is that, when queries are load balanced randomly, they will each have similar cache contents. While that is great when there are certain extremely hot queries, it becomes painful when the frequently read data (working set) no longer fits in memory and each read replica will be performing a lot of redundant I/O. In contrast, a sharded architecture would divide the data over the memory and avoid I/O.<p>Read replicas are a powerful tool for scaling reads, but you should consider whether your workload is really appropriate for it.<p><strong>Pros:</strong><ul><li>Read throughput scales linearly<li>Low latency stale reads if read replica is closer than primary<li>Lower load on primary</ul><p><strong>Cons:</strong><ul><li>Eventual read-your-writes consistency<li>No monotonic read consistency<li>Poor cache usage</ul><p>💡 <strong>Guideline:</strong> Consider using read replicas when you need >100k reads/sec or observe a CPU bottleneck due to reads, best avoided for dependent transactions and large working sets.<h3 id=dbms-optimized-cloud-storage><a href=#dbms-optimized-cloud-storage>DBMS-optimized cloud storage</a></h3><p>There are a number of cloud services now like Aurora and AlloyDB that provide a network-attached storage layer that is optimized specifically for a DBMS.<p>In particular, a DBMS normally performs every write in two different ways: Immediately to the write-ahead log (WAL), and in the background to a data page (or several pages, when indexes are involved). Normally, PostgreSQL performs both of these writes, but in the DBMS-optimized storage architecture the background pages writes are performed by the storage layer instead, based on the incoming WAL. This reduces the amount of write I/O on the primary node.<p><img alt="diagram of dbms-optmized storage"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/8f4c2648-db97-40f8-1581-7d4a6ce0fa00/public><p>The WAL is typically replicated directly from the primary node to several availability zones to parallelize the network round trips, which increases I/O again. Always writing to multiple availability zones also increases the write latency, which can result in lower per-session performance. In addition, read latency can be higher because the storage layer does not always materialize pages in memory. Architecturally, PostgreSQL is also not optimized for these storage characteristics.<p>While the theory behind DBMS-optimized storage is sound. In practice, the performance benefits are often not very pronounced (and can be negative), and the cost can be much higher than regular network-attached block storage. It does offer a greater degree of flexibility to the cloud service provider, for instance in terms of attach/detach times, because storage is controlled in the data plane rather than the hypervisor.<p><strong>Pros:</strong><ul><li>Potential performance benefits by avoiding page writes from primary<li>Replicas can reuse storage, incl. hot standby<li>Can do faster reattach, branching than network-attached storage</ul><p><strong>Cons:</strong><ul><li>Write latency is high by default<li>High cost / pricing<li>PostgreSQL is not designed for it, not OSS</ul><p>💡 <strong>Guideline:</strong> Can be beneficial for complex workloads, but important to measure whether price-performance under load is actually better than using a bigger machine.<h3 id=active-active><a href=#active-active>Active-active</a></h3><p>In the active-active architecture any node can locally accept writes without coordination with other nodes. It is typically used with replicas in multiple sites, each of which will then see low read and write latency, and can survive failure of other sites. These benefits are phenomenal, but of course come with a significant downside.<p>First, you have the typical eventual consistency downsides of read replicas. However, the main challenge with an active-active setup is that update conflicts are not resolved upfront. Normally, if two concurrent transactions try to update the same row in PostgreSQL, the first one will take a “row-level lock”. In case of active-active, both updates might be accepted concurrently.<p>For instance, when you perform two simultaneous updates of a counter on different nodes, the nodes might both see 4 as the current value and set the new value to 5. When replication happens, they’ll happily agree that the new value is 5 even though there were two increment operations.<p><img alt="diagram of an active-active architecture"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/627e299d-d26a-439d-cfe4-83049c73b200/public><p>Active-active systems do not have a linear history, even at the row level, which makes them very hard to program against. However, if you are very prepared to live with that, the benefits could be attractive especially for very high availability.<p><strong>Pros:</strong><ul><li>Very high read and write availability<li>Low read and write latency<li>Read throughput scales linearly</ul><p><strong>Cons:</strong><ul><li>Eventual read-your-writes consistency<li>No monotonic read consistency<li>No linear history (updates might conflict after commit)</ul><p>💡 <strong>General guideline:</strong> Consider only for very simple workloads (e.g. queues) and only if you really need the benefits.<h3 id=transparent-sharding><a href=#transparent-sharding>Transparent sharding</a></h3><p>Transparent sharding systems like Citus distribute tables by a shard key and/or replicate tables across multiple primary nodes. Each node shows the distributed tables as if they were regular PostgreSQL tables and queries &#38 transactions are transparently routed or parallelized across nodes.<p>Data is stored in shards, which are regular PostgreSQL tables, which can take advantage of indexes, constraints, etc. In addition, the shards can be co-located by the shard key (in “shard groups”), such that joins and foreign keys that include the shard key can be performed locally.<p><img alt="transparent sharding diagram"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/1cb5e4db-c901-40e3-5327-73a5c6caa500/public><p>The advantage of distributing the data this way is that you can take advantage of the memory, IO bandwidth, storage, and CPU of all the nodes in an efficient manner. You could even ensure that your data or at least your working set always fits in memory by scaling out.<p>Scaling out transactional workloads is most effective when queries have a filter on the shard key, such that they can be routed to a single shard group (e.g. single tenant in a <a href=https://www.crunchydata.com/blog/designing-your-postgres-database-for-multi-tenancy>multi-tenant app</a>). That way, there is only a marginal amount of overhead compared to running a query on a single server, but you have a lot more capacity. Another effective way of scaling out is when you have compute-heavy analytical queries that can be parallelized across the shards (e.g. <a href=https://www.crunchydata.com/blog/postgres-citus-partman-your-iot-database>time series / IoT</a>).<p>However, there is also higher latency, which reduces the per-session throughput compared to a single machine. And, if you have a simple lookup that does not have a shard key filter, you will still experience all the overhead of parallelizing the query across nodes. Finally, there may be restrictions in terms of data model (e.g. unique and foreign constraints must include shard key), SQL (non-co-located correlated subqueries), and transactional guarantees (snapshot isolation only at shard level).<p>Using a sharded system often means that you will need to adjust your application to deal with higher latency and a more rigid data model. For instance, if you are building a <a href=https://docs.citusdata.com/en/stable/get_started/tutorial_multi_tenant.html>multi-tenant application</a> you will need to add tenant ID columns to all your tables to use as a shard key, and if you are currently loading data using INSERT statements then you might want to switch to COPY to avoid waiting for every row.<p>If you are willing to adjust your application, sharding can be one of the most powerful tools in your arsenal for dealing with data-intensive applications.<p><strong>Pros:</strong><ul><li>Scale throughput for reads &#38 writes (CPU &#38 IOPS)<li>Scale memory for large working sets<li>Parallelize analytical queries, batch operations</ul><p><strong>Cons:</strong><ul><li>High read and write latency<li>Data model decisions have high impact on performance<li>Snapshot isolation concessions</ul><p>💡 <strong>General guideline:</strong> Use for multi-tenant apps, otherwise use for large working set (>100GB) or compute heavy queries.<h3 id=distributed-key-value-storage-with-sql><a href=#distributed-key-value-storage-with-sql>Distributed key-value storage with SQL</a></h3><p>About a decade ago, Google Spanner introduced the notion of a distributed key-value store that supports transactions across nodes (key ranges) with snapshot isolation in a scalable manner by using globally synchronized clocks. Subsequent evolutions of Spanner then added a SQL layer on top, and ultimately even a PostgreSQL interface. Open source alternatives like CockroachDB and Yugabyte followed a similar approach without the requirement of synchronized clocks, at the cost of significantly higher latency.<p>These systems have built on top of existing key-value storage techniques for availability and scalability, such as shard-level replication and failover using Paxos or Raft. Tables are then stored in the key-value store, with the key being a combination of the table ID and the primary key. The SQL engine is adjusted accordingly, distributing queries where possible.<p><img alt="key value store diagram"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/de7bc03c-8f80-4384-383a-0d38dde0d800/public><p>In my view, the relational data model (or, your typical PostgreSQL app) is not well-served by using a distributed key-value store underneath. Related tables and indexes are not necessarily stored together, meaning typical operations such as joins and evaluating foreign keys or even simple index lookups might incur an excessive number of internal network hops. The relatively strong transactional guarantees that involve additional locks and coordination can also become a drag on performance.<p>In comparison to PostgreSQL or Citus, performance and efficiency are often <a href=https://gigaom.com/report/transaction-processing-price-performance-testing/>disappointing</a>. However, these systems offer much richer (PostgreSQL-like) functionality than existing key-value stores, and better scalability than consensus stores like etcd, so they can be a great alternative for those.<p><strong>Pros:</strong><ul><li>Good read and write availability (shard-level failover)<li>Single table, single key operations scale well<li>No additional data modeling steps or snapshot isolation concessions</ul><p><strong>Cons:</strong><ul><li>Many internal operations incur high latency<li>No local joins in current implementations<li>Not actually PostgreSQL, and less mature and optimized</ul><p>💡 <strong>General guideline:</strong> Just use PostgreSQL 😉 For simple applications, the availability and scalability benefits can be useful.<h2 id=conclusion><a href=#conclusion>Conclusion</a></h2><p>PostgreSQL can be distributed at different layers. Each architecture can introduce severe trade-offs. Almost nothing comes for free.<p>When deciding on the database architecture, keep asking yourself:<ul><li>What do I really want?<li>Which architecture achieves that?<li>What are the downsides?<li>What can my application tolerate? (can I change my application?)</ul><p>Even with state-of-the-art tools, deploying a distributed database system is never a solved problem, and perhaps never will be. You will need to spend some time understanding the trade-offs. I hope this blog post will help.<p>If you’re still feeling a bit lost, <a href=https://www.crunchydata.com/contact>our PostgreSQL experts</a> at Crunchy Data will be happy to help you pick the right architecture for your application. ]]></content:encoded>
<category><![CDATA[ Production Postgres ]]></category>
<author><![CDATA[ Marco.Slot@crunchydata.com (Marco Slot) ]]></author>
<dc:creator><![CDATA[ Marco Slot ]]></dc:creator>
<guid isPermalink="false">99d1188374afe4c88e5bc72203932eff398fa46f50b8d397efe201165120c3d7</guid>
<pubDate>Mon, 08 Jan 2024 08:00:00 EST</pubDate>
<dc:date>2024-01-08T13:00:00.000Z</dc:date>
<atom:updated>2024-01-08T13:00:00.000Z</atom:updated></item></channel></rss>