PostgreSQL in Depth Module 14 – Applications, scaling and what comes next
Scaling PostgreSQL: replicas, caches, shards
When one PostgreSQL server is not enough: measure first, then see what read replicas, caches and shards each relieve, and why a replica can miss a fresh write.
What you will learn
- Pick the next scaling step from measurements, among tuning, a bigger machine, replicas, caches, partitioning and sharding
- Route reads to replicas without breaking read-your-writes
- Maintain summary data with materialized views or incremental view maintenance
- Explain what sharding by tenant changes for queries and transactions
Before you start
What you’ll learn · Before you start
What you will learn
- Pick the next scaling step from measurements, among tuning, a bigger machine, replicas, caches, partitioning and sharding
- Route reads to replicas without breaking read-your-writes
- Maintain summary data with materialized views or incremental view maintenance
- Explain what sharding by tenant changes for queries and transactions
Before you start
On this page
One slice of an API’s traffic made PostgreSQL touch 168,717 pages of its shared buffers in the run below, and one statement of the three was 96.2 % of them. One index cut that statement from 162,300 pages to 15,618, which is more relief than a second server would have given.
Every further step relieves a different limit and adds parts of its own: read replicas add read capacity, caches save repeated work at the price of freshness, and shards spread writes over several servers. This lesson orders the steps and shows on a recorded replica why a read can miss a fresh write.
Measure first: one statement often does most of the work
The place to start is pg_stat_statements, an extension that ships with PostgreSQL. It must be loaded at server start through shared_preload_libraries, and it then groups statements by their shape, so that 300 page loads for 300 customers count as one statement with 300 calls. The session below sends 2,000 lookups by id, 300 pages of a customer’s orders and 100 new orders, and ranks the statements by the pages they touched; then it adds one index and sends the same traffic again:
measure_session.py
"""Measure before scaling, on PostgreSQL 18.6: api_traffic.py (psycopg 3.3.6) sends the same slice of traffic twice,
and pg_stat_statements ranks its statements by the shared buffers they touched, before and after one index. Buffers
are counted instead of times, because times change from run to run on a shared computer and buffer counts do not."""
from pg_session import Server
SETTINGS = {"autovacuum": "off", "shared_preload_libraries": "'pg_stat_statements'",
"default_statistics_target": "1000", "max_parallel_workers_per_gather": "0", "jit": "off"}
with Server(settings=SETTINGS) as pg:
pg.up()
pg.silent("CREATE EXTENSION pg_stat_statements")
pg.psql("measure_setup.sql", flags=("-q",))
pg.silent("SELECT pg_stat_statements_reset()")
pg.app("python3", "api_traffic.py")
pg.psql("top_statements.sql", note="before: which statement costs the most")
pg.psql("add_index.sql", note="one index on the column the slow statement filters on")
pg.app("python3", "api_traffic.py")
pg.psql("top_statements.sql", note="after: the same traffic again") Output
$ psql -X -q -f measure_setup.sql
$ python3 api_traffic.py
sent: 2,000 lookups, 300 customer pages, 100 inserts
-- before: which statement costs the most
$ psql -X -a -f top_statements.sql
-- The application's statements, ranked by the shared buffers each touched (hits plus reads).
SELECT regexp_replace(query, '\s+', ' ', 'g') AS statement, calls, rows,
shared_blks_hit + shared_blks_read AS blocks,
round(100.0 * (shared_blks_hit + shared_blks_read)
/ sum(shared_blks_hit + shared_blks_read) OVER (), 1) AS pct_of_blocks
FROM pg_stat_statements
WHERE query LIKE '% orders %' AND query NOT LIKE '%pg_stat_statements%'
ORDER BY blocks DESC;
statement | calls | rows | blocks | pct_of_blocks
---------------------------------------------------------------+-------+-------+--------+---------------
SELECT id, total_cents FROM orders WHERE customer_id = $1 | 300 | 15000 | 162300 | 96.2
SELECT id, total_cents FROM orders WHERE id = $1 | 2000 | 2000 | 6003 | 3.6
INSERT INTO orders (customer_id, total_cents) VALUES ($1, $2) | 100 | 100 | 414 | 0.2
(3 rows)
-- one index on the column the slow statement filters on
$ psql -X -a -f add_index.sql
CREATE INDEX orders_customer_id ON orders (customer_id);
CREATE INDEX
SELECT pg_stat_statements_reset() IS NOT NULL AS statistics_reset;
statistics_reset
------------------
t
(1 row)
$ python3 api_traffic.py
sent: 2,000 lookups, 300 customer pages, 100 inserts
-- after: the same traffic again
$ psql -X -a -f top_statements.sql
-- The application's statements, ranked by the shared buffers each touched (hits plus reads).
SELECT regexp_replace(query, '\s+', ' ', 'g') AS statement, calls, rows,
shared_blks_hit + shared_blks_read AS blocks,
round(100.0 * (shared_blks_hit + shared_blks_read)
/ sum(shared_blks_hit + shared_blks_read) OVER (), 1) AS pct_of_blocks
FROM pg_stat_statements
WHERE query LIKE '% orders %' AND query NOT LIKE '%pg_stat_statements%'
ORDER BY blocks DESC;
statement | calls | rows | blocks | pct_of_blocks
---------------------------------------------------------------+-------+-------+--------+---------------
SELECT id, total_cents FROM orders WHERE customer_id = $1 | 300 | 15016 | 15618 | 70.2
SELECT id, total_cents FROM orders WHERE id = $1 | 2000 | 2000 | 6003 | 27.0
INSERT INTO orders (customer_id, total_cents) VALUES ($1, $2) | 100 | 100 | 612 | 2.8
(3 rows)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 measure_session.py
api_traffic.py
"""One slice of an API's traffic: 2,000 order lookups by id, 300 "orders of a customer" pages and 100 new orders."""
import psycopg
with psycopg.connect(autocommit=True) as conn:
for n in range(2000):
# ids from 40,001 up: psycopg sends a Python int as the smallest type that holds it, and pg_stat_statements
# keeps a statement apart for each parameter type, so one range keeps the lookups in one line
conn.execute("SELECT id, total_cents FROM orders WHERE id = %s", (40001 + (n * 7919) % 60000,)).fetchone()
for n in range(300):
conn.execute("SELECT id, total_cents FROM orders WHERE customer_id = %s", ((n * 13) % 2000 + 1,)).fetchall()
for n in range(100):
conn.execute("INSERT INTO orders (customer_id, total_cents) VALUES (%s, %s)", ((n * 17) % 2000 + 1, 1000 + n))
print("sent: 2,000 lookups, 300 customer pages, 100 inserts") measure_setup.sql
-- 100,000 orders of 2,000 customers, with no index on customer_id yet.
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id integer NOT NULL,
total_cents integer NOT NULL
);
INSERT INTO orders (customer_id, total_cents)
SELECT n % 2000 + 1, (n * 37) % 9000 + 100 FROM generate_series(1, 100000) AS n;
ANALYZE orders; top_statements.sql
-- The application's statements, ranked by the shared buffers each touched (hits plus reads).
SELECT regexp_replace(query, '\s+', ' ', 'g') AS statement, calls, rows,
shared_blks_hit + shared_blks_read AS blocks,
round(100.0 * (shared_blks_hit + shared_blks_read)
/ sum(shared_blks_hit + shared_blks_read) OVER (), 1) AS pct_of_blocks
FROM pg_stat_statements
WHERE query LIKE '% orders %' AND query NOT LIKE '%pg_stat_statements%'
ORDER BY blocks DESC; add_index.sql
CREATE INDEX orders_customer_id ON orders (customer_id);
SELECT pg_stat_statements_reset() IS NOT NULL AS statistics_reset; The customer page ran 300 times and touched 162,300 pages, 541 a call: the whole table, because nothing told PostgreSQL where one customer’s rows were. With the index the same 300 calls touched 15,618 pages, about 52 a call, the index and the 50 rows of each customer. The 2,000 lookups by id stayed at 6,003 pages, three a call.
A replica would have run the same 541-page scan, and a cache would only have hidden it. So fix the statements and indexes the measurement points at, pool short connections and move to a bigger machine first; only then add servers or stored results.
Checkpoint
Check: Measuring before scaling
Replicas, caches and shards each relieve a different limit
Once a tuned server on the biggest machine is not enough, the limit decides the step. Read replicas are standbys that answer read-only queries, long reports among them, while they replay the primary’s WAL: the hot standby: A standby server that answers read-only queries while it replays the primary's write-ahead log. The setting hot_standby is on by default, so a standby built with pg_basebackup is a hot standby. More in “Streaming replication: standbys, slots and lag” of the streaming replication lesson. Replicas add capacity for reads only: every write still goes to the one primary, and each replica answers from what it has replayed so far.
Caches keep a computed result so that it is read instead of computed again, in the application or inside PostgreSQL as a materialized view: A view whose result PostgreSQL stores like a table. It shows the data from its last REFRESH MATERIALIZED VIEW, not the current state of the tables it reads. or a summary table. Caches trade freshness for work, because a stored result is as old as its last update.
Shards split the rows of a table across several servers by a shard key: The column whose value decides which server, or shard, holds a row, such as the tenant id when each tenant's rows are kept together., such as the tenant a row belongs to. Shards scale writes as well as reads, but a query or a transaction that needs rows from several shards becomes the application’s job, or an extension’s.
Partitioning splits one table into smaller ones on the same server. Each option comes with methods of its own:
- read-your-writes: A guarantee that a user who has just changed something sees that change on the next read, even when reads normally go to a replica that may be behind. routing reads from the primary after a user’s write, or waits for the replica.
- REFRESH MATERIALIZED VIEW CONCURRENTLY recomputes a view without locking out its readers.
- incremental view maintenance: Keeping a stored query result current by applying each change of its base tables to it, instead of computing the whole query again; the pg_ivm extension does it for PostgreSQL. with pg_ivm updates a summary as the change happens.
- Two-phase commit prepares a change on every shard before committing it on any.
- Citus is an extension that spreads tables over several PostgreSQL servers.
Answer four questions about your own database, and the table rules out the steps that cannot help yet:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Step | More reads | More writes | Every read sees the last write | Moving parts added |
|---|---|---|---|---|
| Fix statements and indexes Changes: the work each query does | Good one index cut a query from 162,300 buffers to 15,618 | Good fewer indexes and cheaper statements help writes too | Good nothing is copied, so nothing is stale | Good none: the same server and the same code paths |
| A bigger machine Changes: CPU, memory and disk of one server | Good more memory keeps more of the data cached | Good more CPU and faster disks for the one primary | Good still one server, so every read is current | Good a restart or failover, and a larger bill |
| Read replicas Changes: where reads run | Good each replica answers reads with a full copy | Poor every write still goes to the one primary | Poor a replica answers from what it has replayed | Mixed servers to run and reads to route |
| Caches and summary tables Changes: how often a result is computed | Good a stored result is read instead of recomputed | Mixed keeping the result current costs work | Poor the stored result is as old as its last update | Mixed something must refresh or invalidate it |
| Partitioning Changes: how one table is stored | Mixed queries on the key read only some partitions | Mixed smaller indexes, old data dropped in one step | Good still one server and one copy of each row | Mixed a partition key and partition upkeep |
| Sharding Changes: which server holds which rows | Good each server reads only its own part of the rows | Good each server takes the writes for its part | Good each row has one home, read where it is written | Poor queries across shards need extra work |
How the answers decide
- “In pg_stat_statements, does one statement use most of the buffers or time?” Yes, one statement stands out: rules out Read replicas and Sharding. Fix the statement first. More servers would each run the same wasteful plan, while one index cut it from 162,300 buffers to 15,618 in the run.
- “In pg_stat_statements, does one statement use most of the buffers or time?” Yes, one statement stands out: adds a caveat to Caches and summary tables. A cache only hides the costly statement, because every miss still runs its full scan; fix the statement before you store its result.
- “Are most of the requests that strain the database reads?” Writes strain it too: rules out Read replicas and Caches and summary tables. Replicas and caches take load off reads only; every write still lands on the one primary.
- “Must every read show the latest write, the moment after it?” Yes, every read: adds a caveat to Read replicas and Caches and summary tables. A replica answers from what it has replayed, and a stored result is as old as its last update, so reads that must be current need a rule of their own.
- “Is the primary already tuned and on the largest machine you can buy?” No, not yet: rules out Sharding. Sharding adds the most moving parts of all six; tuning and a bigger machine come first.
- “Is the primary already tuned and on the largest machine you can buy?” No, not yet: adds a caveat to Read replicas and Caches and summary tables. Tune the statements and size up the one server first; replicas and stored results each bring parts of their own to run.
Checkpoint
Check: What replicas, caches and shards scale
A replica answers from what it has replayed
The manual’s chapter on hot standby says that data takes time to reach a standby, so the same query on the primary and a standby can give different results at nearly the same moment. The session below makes that delay hold still. It pauses replay on the replica with pg_wal_replay_pause(), takes an order on the primary, asks the replica, and then lets it catch up:
lag_session.py
"""A read replica that lags, on PostgreSQL 18.6: a primary and one streaming hot standby, its replica. Replay on the
replica is paused with pg_wal_replay_pause(), so the lag that real replicas have now and then holds still long
enough to look at; the primary takes an order, and the replica is asked about it before and after replay resumes.
The session waits (unprinted) until the replica has replayed the setup, and later the new order, before it reads."""
from pg_session import Server
with Server(settings={"autovacuum": "off"}) as pg:
pg.up()
pg.psql("lag_setup.sql", flags=("-q",))
replica = pg.standby()
pg.wait_for("SELECT count(*) FROM orders", "100000", port=replica)
pg.psql("lag_pause.sql", port=replica, note="the replica (port 5433)")
pg.wait_for("SELECT pg_get_wal_replay_pause_state()", "paused", port=replica)
pg.psql("lag_write.sql", note="the primary (port 5432)")
pg.psql("lag_read.sql", port=replica, note="the replica, while it is behind")
pg.psql("lag_resume.sql", port=replica, note="the replica applies the WAL it has received")
pg.wait_for("SELECT count(*) FROM orders", "100001", port=replica)
pg.psql("lag_read.sql", port=replica, note="the replica, after it has caught up") Output
$ psql -X -q -f lag_setup.sql
-- the replica (port 5433)
$ psql -X -a -f lag_pause.sql
-- On the replica: stop applying WAL for a moment, which is how a replica that lags behind looks from outside.
SELECT pg_wal_replay_pause();
pg_wal_replay_pause
---------------------
(1 row)
-- the primary (port 5432)
$ psql -X -a -f lag_write.sql
-- On the primary: a customer places an order.
INSERT INTO orders (customer_id, total_cents) VALUES (42, 1999) RETURNING id;
id
--------
100001
(1 row)
INSERT 0 1
SELECT count(*) AS orders_on_primary FROM orders;
orders_on_primary
-------------------
100001
(1 row)
-- the replica, while it is behind
$ psql -X -a -f lag_read.sql
-- On the replica: the same questions, answered from what it has replayed so far.
SELECT count(*) AS orders_on_replica FROM orders;
orders_on_replica
-------------------
100000
(1 row)
SELECT count(*) AS new_order_visible FROM orders WHERE id = 100001;
new_order_visible
-------------------
0
(1 row)
SELECT pg_is_in_recovery() AS is_replica, pg_is_wal_replay_paused() AS replay_paused;
is_replica | replay_paused
------------+---------------
t | t
(1 row)
-- the replica applies the WAL it has received
$ psql -X -a -f lag_resume.sql
SELECT pg_wal_replay_resume();
pg_wal_replay_resume
----------------------
(1 row)
-- the replica, after it has caught up
$ psql -X -a -f lag_read.sql
-- On the replica: the same questions, answered from what it has replayed so far.
SELECT count(*) AS orders_on_replica FROM orders;
orders_on_replica
-------------------
100001
(1 row)
SELECT count(*) AS new_order_visible FROM orders WHERE id = 100001;
new_order_visible
-------------------
1
(1 row)
SELECT pg_is_in_recovery() AS is_replica, pg_is_wal_replay_paused() AS replay_paused;
is_replica | replay_paused
------------+---------------
t | f
(1 row)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lag_session.py
lag_setup.sql
CREATE TABLE orders (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer_id integer NOT NULL, total_cents integer NOT NULL);
INSERT INTO orders (customer_id, total_cents) SELECT n % 2000 + 1, 500 FROM generate_series(1, 100000) AS n; lag_pause.sql
-- On the replica: stop applying WAL for a moment, which is how a replica that lags behind looks from outside.
SELECT pg_wal_replay_pause(); lag_write.sql
-- On the primary: a customer places an order.
INSERT INTO orders (customer_id, total_cents) VALUES (42, 1999) RETURNING id;
SELECT count(*) AS orders_on_primary FROM orders; lag_read.sql
-- On the replica: the same questions, answered from what it has replayed so far.
SELECT count(*) AS orders_on_replica FROM orders;
SELECT count(*) AS new_order_visible FROM orders WHERE id = 100001;
SELECT pg_is_in_recovery() AS is_replica, pg_is_wal_replay_paused() AS replay_paused; lag_resume.sql
SELECT pg_wal_replay_resume(); The primary counted 100,001 orders and the replica 100,000, and order 100001 was not there; after pg_wal_replay_resume() the replica showed it. Nothing was wrong with the replica. It answered correctly for the point in the WAL it had reached, and the answer carried no sign that it was old. Step through the same order, server by server:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The order is written. The app inserts order 100001 on the primary, the only server that accepts writes. The primary commits it and answers.
- The WAL travels. The primary streams the WAL of that commit to the replica, which receives it. Receiving is not yet applying it.
- A read arrives too early. The app's next page reads from the replica. Its replay is behind, so it answers from the state before the order and finds 100,000 orders.
- The old answer. The replica returns a correct but old answer. Nothing failed; the order is simply not there yet.
- Replay catches up. The replica applies the WAL it received. From now on every query on it sees order 100001.
A user who saves a profile and is then shown the old one has met this replication lag: How far a standby is behind its primary: in bytes, as the gap between two WAL positions, or in time, from a commit on the primary until the standby has replayed it. More in “Streaming replication: standbys, slots and lag”, so the reads that follow a user’s own write need a rule of their own.
Key idea
A replica gives correct answers for an earlier moment: more replicas add read capacity, never freshness, and every write still lands on the one primary.
Checkpoint
Check: Why a replica can be behind
Reading your own writes from a replica, on 18 and on 19 Pro: opens with a Pro, Premium or Ultimate pass
Sending every read to a replica breaks the first page a user loads after saving something. On PostgreSQL 18 the fix is to read that user’s pages from the primary for a while, or to wait until the replica has replayed the write; PostgreSQL 19 adds a command for the waiting.
In this part:
- Three ways to read right after a write, recorded on 18
- WAIT FOR LSN on PostgreSQL 19, recorded on 19 RC 1
2 runnable examples (profiles_setup.sql, read_your_writes.py, ryw_session.py, wait_lsn.py, wait_session.py) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Summary tables kept current: REFRESH, CONCURRENTLY and pg_ivm Pro: opens with a Pro, Premium or Ultimate pass
A dashboard of daily totals should not add up 100,000 orders on every page view. Three ways keep such totals stored, and each pays in a different place: the readers, the refresh, or every write.
In this part:
- What a refresh locks and what it reads, recorded
- pg_ivm: the summary changes with every write, recorded
2 runnable examples (ivm.sql, ivm_insert.sql, ivm_reads.sql, ivm_session.py, ivm_setup.sql, matview_session.py, mv_concurrently.sql, mv_cost.sql, mv_index.sql, mv_reader.sql, sales_setup.sql) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Sharding by tenant: routing, fan-out queries and two-phase commit Pro: opens with a Pro, Premium or Ultimate pass
Sharding moves work from the database into the application, because every query must know where its rows live. The runs below shard seven tenants over two clusters and show which queries stay simple and which do not.
In this part:
- A router and the three kinds of query, recorded
- Moving money across shards with two-phase commit, recorded
- Citus: sharding inside PostgreSQL
1 diagram · 2 runnable examples (blocked_update.sql, prepare_disabled.sql, prepared_list.sql, resolve.sql, shard_router.py, shards_session.py, shards_setup.sql, transfer.py, twophase_session.py, wallets_setup.sql) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: choosing the next step
These questions use this lesson’s measurements and its replica, starting with the idea you have shown least:
Practice that adapts to you
Practise this lesson
9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Three design questions follow: a cart that empties once replicas arrive, a dashboard that times out during each refresh, and tenants that outgrow one primary.
-
Mid to senior
A shop moved all reads to two streaming replicas. Now some customers add an item to the cart, land on the cart page and see it empty; a reload a second later shows the item. Replication lag is usually under 200 ms. What is happening, and how do you fix it without giving up the replicas?
-
Mid to senior
A sales dashboard reads a materialized view of totals per store and hour, built from a 60 million row orders table. A job runs REFRESH MATERIALIZED VIEW every 5 minutes, and dashboard requests time out for about 40 seconds each time. What do you change, and what does each option cost?
-
Senior to staff
An invoicing service holds 4 TB for 30,000 tenants on one tuned PostgreSQL primary at 85 % CPU, mostly from writes. Five tenants produce 35 % of the writes, and finance runs reports across all tenants each month. Plan the move to several servers.
Flashcards · 3 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why measure before scaling, and what comes before adding servers?
pg_stat_statements groups statements by shape, and one statement often does most of the work (a customer page touched 162,300 pages, cut to 15,618 by one index). Fix statements and indexes, pool short connections and move to a bigger machine before adding servers or caches.
-
What limit does each of replicas, caches and shards relieve?
Replicas add capacity for reads only, since the primary still takes all the writes, and each answers from what it has replayed so far; caches (a materialized view or a summary table) trade freshness for work; shards split rows by a shard key and scale writes as well, but a query or transaction across shards becomes the application's job, or an extension's.
-
Why can a read replica return a stale answer?
A replica answers from the point in the WAL it has replayed, so it can miss a just-committed write (replication lag). The answer is correct for an earlier moment and carries no sign it is old, so reads that follow a user's own write need a read-your-writes rule.
Key takeaways
- Measure with pg_stat_statements before adding servers: one statement was 96.2 % of the pages touched, and one index cut it from 162,300 pages to 15,618.
- Replicas scale reads, caches trade freshness for work, and shards spread writes as well as reads, at the price of queries across shards.
- A replica answers correctly for the WAL it has replayed, so a read right after a write can show the old row.
Terms in this lesson
- incremental view maintenance
- Keeping a stored query result current by applying each change of its base tables to it, instead of computing the whole query again; the pg_ivm extension does it for PostgreSQL.
- materialized view
- A view whose result PostgreSQL stores like a table. It shows the data from its last REFRESH MATERIALIZED VIEW, not the current state of the tables it reads.
- read-your-writes
- A guarantee that a user who has just changed something sees that change on the next read, even when reads normally go to a replica that may be behind.
- shard key
- The column whose value decides which server, or shard, holds a row, such as the tenant id when each tenant's rows are kept together.
References
- PostgreSQL 18 documentation: F.32 pg_stat_statements (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 26.4 Hot Standby (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 9.28 System Administration Functions (replay control, WAL positions) (The PostgreSQL Global Development Group)
- PostgreSQL 19 documentation: WAIT (The PostgreSQL Global Development Group)
- PostgreSQL 19 release notes (in preparation) (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 39.3 Materialized Views (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: REFRESH MATERIALIZED VIEW (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 27.2 The Cumulative Statistics System (pg_stat_user_tables) (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: PREPARE TRANSACTION (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 67.4 Two-Phase Transactions (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 5.12 Table Partitioning (The PostgreSQL Global Development Group)
- pg_ivm 1.17 README (incremental view maintenance for PostgreSQL) (SRA OSS and the pg_ivm contributors)
- Citus documentation: Concepts (coordinator and workers, distributed and reference tables) (Citus Data, the Citus project)
- Citus documentation: Prepare Application for Citus (keys that include the distribution column) (Citus Data, the Citus project)
- Citus repository (AGPL-3.0 licence, distributed and reference tables) (Citus Data, the Citus project)
- PostgreSQL 18 documentation: Chapter 29 Logical Replication (consolidating databases for analysis) (The PostgreSQL Global Development Group)
- Psycopg 3 documentation: psycopg.sql, SQL string composition (Literal for utility commands) (The Psycopg Team)
- Psycopg 3 documentation: Differences from psycopg2 (server-side binding and the statements it does not cover) (The Psycopg Team)
Report a problem with this lesson
Kept only in this browser. Your Learn progress