Automating Partition Creation with pg_partman

The partitioned metrics table was set up by hand with monthly partitions through December. Nobody wrote down that someone would have to create January’s. At 00:00:00 on January 1st every insert began failing with no partition of relation "metrics" found for row, the ingest pipeline backed up, and the on-call engineer created the partition by hand at 00:14. Partitioning moves a table’s maintenance from row-level work (deleting, vacuuming) to calendar-level work (creating future partitions, retiring old ones), and that calendar work must be automated. pg_partman is the standard PostgreSQL extension for it: it creates partitions ahead of time, retires old ones according to a retention policy, and runs as a scheduled maintenance job. This guide sets it up for an existing partitioned table and wires in the monitoring that catches failures before they become outages. It belongs to Partitioning Live Tables Without Downtime.

Partition Horizon With and Without Automation Timeline across five months. Manually created partitions cover September to December; inserts fail from January 1. With pg_partman premaking four partitions and running maintenance hourly, the horizon always extends four months ahead of the current date and old partitions beyond retention are detached. Partition Horizon With and Without Automation Jan 1: manual set runs out Manual partitions Sep – Dec created by hand inserts fail pg_partman always 4 months premade ahead Sep Oct Nov Dec Jan Feb covered no partition rolling buffer
The premake horizon turns a hard deadline into a rolling buffer that maintenance keeps topped up.

Symptom / Error Signatures

Without automation, the failure is abrupt:

ERROR:  no partition of relation "metrics" found for row
DETAIL:  Partition key of the failing row contains (recorded_at) = (2027-01-01 00:00:00.041+00).

Softer symptoms come first, if anyone is looking: a shrinking number of future partitions, retention jobs that nobody runs so old partitions accumulate, and hand-written partition DDL scattered through the migration history with inconsistent naming. With pg_partman installed but misconfigured, the symptoms are maintenance that never runs (partman.part_config shows a stale premake horizon) or retention that detaches nothing because retention is unset.

Root Cause Analysis

Declarative partitioning routes each row to the partition whose bounds contain its key; there is no automatic creation. For time-based partitioning, the set of partitions must therefore always extend beyond “now” by a safe margin. pg_partman stores a configuration row per partitioned table in partman.part_config — interval, how many partitions to premake, retention — and its maintenance function (partman.run_maintenance(), or the procedure partman.run_maintenance_proc()) creates missing future partitions and applies retention each time it runs. Maintenance must be scheduled: through pg_partman’s background worker, pg_cron, or an external scheduler.

Setting Meaning Typical value
p_interval size of each partition '1 day', '1 week', '1 month'
p_premake partitions to keep ahead of now 4–7 intervals
retention age after which partitions are removed '13 months'
retention_keep_table detach but keep the table vs drop it true while validating, then false
maintenance schedule how often run_maintenance runs hourly for daily partitions

Partition creation takes a brief lock on the parent, so maintenance itself should run with a lock timeout and be retried; with premake of several intervals, one failed run is harmless because the buffer absorbs it.

Moving Parts of a pg_partman Setup The partitioned parent table metrics is registered in partman.part_config with interval, premake and retention. A scheduler (pg_cron or the pg_partman background worker) calls run_maintenance_proc hourly, which creates future partitions and detaches or drops partitions older than retention. A monitoring query checks the newest partition's upper bound. Moving Parts of a pg_partman Setup partman.part_config interval 1 day, premake 7, retention 90 days Scheduler pg_cron hourly / BGW run_maintenance_proc create future, retire old metrics partitions rolling window Horizon alert days of partitions ahead
Configuration, a scheduler and a horizon check — all three are needed; the extension alone does nothing until maintenance runs.

Immediate Mitigation

1. If inserts are failing now, create the missing partition by hand. It is fast and takes only a brief lock on the parent.

-- PostgreSQL · migration role · emergency partition for the current interval
SET lock_timeout = '3s';
CREATE TABLE IF NOT EXISTS metrics_p20270101 PARTITION OF metrics
  FOR VALUES FROM ('2027-01-01') TO ('2027-01-02');
-- ROLLBACK PATH: not needed; pg_partman will adopt partitions following its naming convention.

2. Install pg_partman and register the table. On pg_partman 5.x, create_parent takes the control column, interval and premake; it creates the template and any missing partitions.

-- PostgreSQL 14+ · superuser or extension owner · pg_partman 5.x syntax
CREATE SCHEMA IF NOT EXISTS partman;
CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman;
SELECT partman.create_parent(
  p_parent_table => 'public.metrics',
  p_control      => 'recorded_at',
  p_interval     => '1 day',
  p_premake      => 7
);
UPDATE partman.part_config
   SET retention = '90 days', retention_keep_table = true, infinite_time_partitions = true
 WHERE parent_table = 'public.metrics';
-- ROLLBACK PATH: DELETE FROM partman.part_config WHERE parent_table = 'public.metrics';  (partitions remain)

Existing partitions must follow pg_partman’s naming convention for it to manage them; check the documentation for your version, and rename or detach hand-made partitions accordingly.

3. Schedule maintenance. With pg_cron:

-- PostgreSQL · requires pg_cron · hourly maintenance with a lock timeout
SELECT cron.schedule('partman-maintenance', '7 * * * *',
  $$SET lock_timeout = '5s'; CALL partman.run_maintenance_proc()$$);

Permanent Fix / Long-Term Pattern

Treat partition maintenance as production infrastructure with an owner and an alert. Premake enough intervals that several consecutive maintenance failures are harmless — a week of daily partitions, a few months of monthly ones — and alert when the horizon drops below half of that. The horizon check is a simple catalog query on the newest partition’s upper bound:

-- PostgreSQL · read-only · days of partitions remaining ahead of now
SELECT max(upper(bounds)) - now() AS horizon
FROM (
  SELECT tstzrange(
           (regexp_match(pg_get_expr(c.relpartbound, c.oid), 'FROM \(''([^'']+)''\)'))[1]::timestamptz,
           (regexp_match(pg_get_expr(c.relpartbound, c.oid), 'TO \(''([^'']+)''\)'))[1]::timestamptz) AS bounds
  FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
  WHERE i.inhparent = 'metrics'::regclass
) p;

Start with retention_keep_table = true so retention detaches old partitions without dropping them, verify the behaviour for a few cycles, then switch to dropping — or archive detached tables first, as described in dropping old data with partition detach instead of DELETE. Keep pg_partman’s configuration in version control (the create_parent call and part_config updates as migrations) so environments stay consistent. Add the horizon metric to the dashboards in Migration Observability.

Horizon Alert Thresholds for Daily Partitions Scale of days of partitions remaining ahead of now, from 0 to 10. Zone 0 to 2 days: page. Zone 2 to 4 days: warn. Zone 4 to 10 days: healthy. Markers show premake of 7 days and a typical value after a missed maintenance run. Horizon Alert Thresholds for Daily Partitions premake = 7 after one missed run page warn healthy 0 d 2 d 4 d 6 d 8 d 10 d
With seven days premade, a missed run moves the horizon by one day — alerts fire long before inserts can fail.

Two details make pg_partman behave well alongside schema migrations. First, changes to the parent — new columns, new indexes, new constraints — propagate to future partitions only if they are applied to the parent (or, for some properties, to pg_partman’s template table); a migration that alters only existing partitions leaves new ones inconsistent. Apply schema changes to the parent and let PostgreSQL cascade them, and check pg_partman’s template table for properties that do not cascade, such as some storage parameters. Second, coordinate maintenance with migrations: a long migration holding a lock on the parent will make maintenance wait or time out. With a generous premake, simply let maintenance fail and retry on its next run rather than raising its lock timeout.

When sizing premake, think in terms of the longest plausible outage of the maintenance job itself — a broken extension upgrade, a failed pg_cron worker, a database restored from backup without its cron schedule. A restored database is the classic case: pg_cron jobs live in the cron schema and may not be restored with the data, so verify the schedule exists after any restore.

Verification Checklist

Frequently Asked Questions

Does pg_partman replace declarative partitioning? No. Modern pg_partman manages native declarative partitions; it automates creating and retiring them. The partitioning itself is PostgreSQL’s.

What happens if maintenance fails for a day? Nothing visible, as long as premake covers the gap: the existing future partitions keep accepting inserts. The horizon alert tells you to fix the maintenance job before the buffer runs out.

Does creating a partition lock the table? It takes a lock on the parent briefly while the new partition is added to the catalog. With a lock timeout and hourly retries, contention with long transactions is harmless.

Can pg_partman manage a table I partitioned by hand? Yes, after registering it with create_parent, provided existing partitions follow its naming convention for the chosen interval. Rename or detach non-conforming partitions first.