Partitioning, extensions and advanced deployment

Declarative range and list partitioning, partition pruning, maintenance, extension management, PostGIS and pgvector, and upgrade strategies.

Declarative partitioning

create table event (
  id         bigint generated always as identity,
  occurred_at timestamptz not null,
  tenant_id  bigint not null,
  payload    jsonb not null,
  primary key (id, occurred_at)
) partition by range (occurred_at);

create table event_2026_09 partition of event
  for values from ('2026-09-01') to ('2026-10-01');
create table event_2026_10 partition of event
  for values from ('2026-10-01') to ('2026-11-01');

create index on event_2026_09 (tenant_id, occurred_at desc);

-- partition pruning shows in the plan: only matching partitions are scanned
explain (analyze) select * from event
where occurred_at >= '2026-09-10' and occurred_at < '2026-09-11';

-- detach keeps the data, drop removes it: both are fast
alter table event detach partition event_2026_09;
drop table event_2026_09;
  • The partition key must be part of every unique constraint on the parent, which is why the primary key above includes occurred_at.
  • Pruning only happens when the predicate is on the partition key and the planner can prove it at plan time; a function call on the column usually defeats it.
  • Partitioning helps most when the access pattern is time-based and old data can be dropped in bulk - it is not a general performance fix for a badly indexed table.
  • detach partition concurrently avoids the long lock, and a detached partition is a normal table you can archive or attach elsewhere.
  • Automate partition creation. A missing partition means inserts fail outright, which is a self-inflicted outage at midnight on the first of the month.
💡
A partitioned table with hundreds of partitions slows planning down, because the planner must consider each one. Aim for tens, not thousands, and consider sub-partitioning only when a single partition is genuinely too large.

Extensions worth knowing

ExtensionProvidesTypical use
pg_stat_statementsAggregated query statisticsAlways install this first
pgcryptogen_random_uuid, digests, encryptionId generation, hashing
pg_trgmTrigram similarityFuzzy search, fast ilike '%x%'
postgisGeometry types and spatial indexesMaps, geo queries
pgvectorvector type and ANN indexesEmbeddings and similarity search
pg_partmanPartition maintenanceAutomated creation and retention
pgcronCron inside the databaseVacuum jobs, refreshes
-- pgvector: nearest neighbours without another service
create extension if not exists vector;
alter table document add column embedding vector(768);
create index document_embedding_idx on document
  using hnsw (embedding vector_cosine_ops);

select id, left(content, 80) as preview
from document
order by embedding <=> '[...]'::vector
limit 5;

Upgrading and operating

pg_upgrade --old-datadir /var/lib/postgresql/16/main \
           --new-datadir /var/lib/postgresql/17/main \
           --old-bindir /usr/lib/postgresql/16/bin \
           --new-bindir /usr/lib/postgresql/17/bin --check

# logical alternative: a clean replica on the new version, then a switch
# 1. create a logical replication slot on the old primary
# 2. subscribe from the new cluster
# 3. wait until lag is zero, then promote and repoint the application
  • pg_upgrade is fast because it copies files rather than data, but it preserves bloat and requires the extensions to be available for the new version.
  • The logical route allows near-zero downtime and a rollback path, at the cost of a full copy and the caveats of logical replication.
  • Run analyze after an upgrade: statistics are rebuilt, and plans can change dramatically in the first hours.
  • Read the release notes for behavioural changes, not just new features - some defaults and error conditions change between major versions.

FAQ

When should I partition a table?
When it is large, the access pattern is time-based, and you need to drop or archive old data cheaply, or when a single index no longer fits in memory. For a few hundred thousand rows, a good index is simpler and faster to maintain.
PostGIS or a GIS service?
PostGIS when the spatial data lives beside your relational data and the queries are geometry operations - it avoids a second system and a synchronisation problem. A dedicated service when you need global-scale tile serving or heavy raster processing.

Roles, permissions, schemas and row-level security Indexes: B-tree, GIN, GiST, partial and expression

Last refreshed 2026-09-18.