Skip to content

Chapter 6 of 7

Replication, Scaling, and Partitioning

When a single server isn't enough, the first move is usually read scaling via streaming replication. A primary publishes its write-ahead log (WAL) to one or more replicas, which replay the stream and serve read traffic—routing SELECTs to replicas with awareness of replication lag. The essentials on the primary are wal_level set at least to replica, max_wal_senders sized to accommodate the replicas, and archive_mode on if you want point-in-time recovery. The standby is seeded from pg_basebackup, then connects using primary_conninfo in postgresql.auto.conf. Asynchronous replication keeps commit latency low but accepts possible data loss on failover; synchronous replication waits for at least one standby to acknowledge before COMMIT, trading latency for durability. Both are physical, byte-for-byte copies. Logical replication, by contrast, replicates row changes by primary key and supports cross-version and subset replication, making it useful for migrations and selective feeds. Lag is monitored through pg_stat_replication, where replay_lag shows how far behind a replica is.

Replication slots are convenient but risky: an inactive slot prevents WAL from being deleted, eventually filling the disk, so unused slots should be dropped promptly. WAL bloat can also be caused by a broken archive_command or by very long transactions, both of which hold onto segments until they can be released. PITR is enabled by continuous WAL archiving plus base backups, with restoration picking up at recovery_target_time, recovery_target_xid, recovery_target_lsn, or a named restore point. Tools that orchestrate WAL shipping include pgbackrest, WAL-G, and barman, all of which integrate continuous archiving with base-backup management. Routine backups come in two flavors: pg_dump produces a portable logical SQL dump that works across major versions, while pg_basebackup produces a physical copy suitable for spinning up replicas and restoring onto a compatible major version.

For very large tables—think billions of rows—partitioning is a key tool. A partition key with a clear value distribution, such as a date range, a discrete list like region, or a hash of an identifier, lets PostgreSQL prune irrelevant partitions at planning time and reuse space efficiently. RANGE partitioning on time is the most common because old partitions can be dropped in milliseconds to expire data, and it dramatically reduces vacuum cost since each partition is much smaller than the whole. Hash partitioning is useful for sharding by an identifier when there is no natural range, while LIST partitioning suits discrete keys like region. Maintenance includes DROP PARTITION for retention and ensuring ANALYZE runs after big partition operations so the planner has fresh statistics. Continuous aggregates and retention policies in TimescaleDB provide a higher-level, time-series–optimized alternative built on top of partitioning.

All chapters
  1. 1Diagnosing Slow Queries
  2. 2Indexing Strategies
  3. 3Statistics, VACUUM, and Table Health
  4. 4Concurrency, Locking, and Transactions
  5. 5Server Configuration and Connection Management
  6. 6Replication, Scaling, and Partitioning
  7. 7Schema Patterns, Migrations, and Extensions

Drill it

Reading is not remembering. These come from the Postgresql Performance Tuning deck:

Q

How do you find the slowest queries in Postgres?

Enable pg_stat_statements extension. Then: SELECT query, calls, total_exec_time, mean_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;

Q

How do you read an EXPLAIN plan?

Read bottom-up: leaf nodes first (table scans / index scans), then joins, then aggregates. Look for high cost, rows, and actual time.

Q

Difference between EXPLAIN, EXPLAIN ANALYZE, EXPLAIN (BUFFERS, ANALYZE)?

EXPLAIN: planner's estimate only.EXPLAIN ANALYZE: actually runs the query and reports real times.EXPLAIN (BUFFERS, ANALYZE): + buffer hits/misses — tells you I/...

Q

Why is a Seq Scan faster than an Index Scan sometimes?

When the planner expects to return a large fraction of the table — random I/O via the index would be slower than reading sequentially.