Comprehensive PostgreSQL reference for developers and DBAs covering versions 14–18. Use whenever the user asks about PostgreSQL syntax, DDL/DML/DQL, joins, LATERAL, CTEs, window functions, GROUPING SETS, DISTINCT ON, RETURNING, ON CONFLICT, PL/pgSQL, functions, procedures, triggers, views, materialized views, indexes (B-tree/GIN/GiST/BRIN/Hash/Bloom), MVCC, VACUUM, autovacuum, WAL, TOAST, partitioning, replication (streaming/logical), backup, PITR, HA (Patroni/repmgr), pgBouncer, EXPLAIN ANALYZE, RLS, roles, extensions (pgvector, PostGIS, TimescaleDB, Citus, pg_trgm, pg_cron), JSON/JSONB, full-text search, UUID, timestamptz, COPY, system catalogs, collations, large objects, cursors, GUC, or any Postgres administration, performance, security, replication, backup, or recovery topic.
94
94%
Does it follow best practices?
Impact
94%
1.36xAverage score across 3 eval scenarios
Passed
No known issues
External extension that automates partition lifecycle for PostgreSQL declarative partitioning. Pre-creates future partitions, drops or detaches old ones per retention policy, optionally moves data out of default partition into proper child tables.
You are operating partitioned tables — time-series, multi-tenant by tenant_id, or any range / list partitioning — and need automation for:
If you are still on native inheritance partitioning (pre-PG10), see 36-inheritance.md. If you have not yet partitioned, see 35-partitioning.md for declarative syntax first.
[!WARNING] pg_partman 5.x is a hard break from 4.x — different API, different catalog, no in-place upgrade path pg_partman v5.0.0 (2023-09-28) removed trigger-based partitioning entirely and dropped
time-static/time-dynamic/id-static/id-dynamicpartition types. Only PG declarative partitioning is supported. Many function parameters renamed;part_configcolumns reshuffled. Migration is viadoc/migrate_to_declarative.md+doc/pg_partman_5.0.0_upgrade.md— notALTER EXTENSION ... UPDATE. Any tutorial older than late-2023 likely references v4 syntax that no longer exists.12
[!WARNING] pg_partman 5.x requires PostgreSQL >= 14 Verbatim README: "Requirement: PostgreSQL >= 14"1. PG12 / PG13 are not supported by v5.x — stay on partman 4.7.x if stuck on those, then migrate Postgres major first.
Five rules:
partman automates declarative partitioning — it is not a partitioning engine. PG provides PARTITION BY RANGE / LIST / HASH (35-partitioning.md). partman pre-creates children, drops old ones, moves orphan data. Without partman you write a cron script doing the same calls by hand.
partman.create_parent() registers a parent table in partman.part_config. Parameters specify control column, interval, partition type (range or list), premake count, automatic-maintenance flag, optional template table. After registration, run_maintenance() reads part_config and acts.
run_maintenance_proc() is a procedure not a function — it commits between partition sets. Long maintenance run on many sets won't hold one giant transaction. run_maintenance() (function) exists for compatibility but cannot commit. Canonical scheduling pattern is CALL partman.run_maintenance_proc() from pg_cron (98-pg-cron.md).
Retention is opt-in and asymmetric — drop versus detach versus move-to-schema. retention_keep_table = false drops the table; true (default) detaches it leaving it queryable in the same schema. retention_schema overrides both — table is reattached-detached-and-moved into named archive schema. No automatic export to object storage.
Template table pattern is one-shot at child creation, not retroactive. Indexes, constraints, REPLICA IDENTITY, unlogged flag, autovacuum overrides, toast options defined on the template apply to NEW children created after the template was set. Existing children are not back-filled — you ALTER them by hand.
| Use case | Approach |
|---|---|
| Append-only time-series (events, logs, telemetry) | partman.create_parent(..., p_type := 'range', p_interval := '1 day') + retention + pg_cron schedule for run_maintenance_proc() |
| Multi-tenant by integer ID range | p_type := 'range', p_interval := '100000' over the ID column |
| Multi-tenant by tenant_id list | v5.1+ p_type := 'list' for single-value integer LIST |
| Hash partitioning (e.g. tenant_id-hash) | pg_partman does NOT support hash — manage hash partitions manually or via Citus (97-citus.md) |
| Year-then-month sub-partitioning | partman.create_sub_parent() |
| Pre-create 30 days of future partitions | p_premake := 30 (default 4) |
| Pull data from default partition into proper children | partman.partition_data_proc() (PROCEDURE — commits between batches) |
| Reverse a partitioned table back to a single heap | partman.undo_partition_proc() |
| Apply indexes / GRANT to future children | Set up template table, register via p_template_table |
| Retro-fix existing children with new indexes | Manual CREATE INDEX CONCURRENTLY on each leaf — partman does NOT propagate template changes backward |
| Drop old partitions silently in production | Set retention + retention_keep_table = false in part_config |
| Detach old partitions but keep queryable | Set retention + retention_keep_table = true (default) |
| Move old partitions to archive schema | Set retention + retention_schema = 'archive' |
| Schedule maintenance every hour | cron.schedule('partman', '@hourly', $$CALL partman.run_maintenance_proc()$$) cross-ref 98 |
| Schedule for a single partition set only | partman.run_maintenance(p_parent_table := 'public.events') |
| Diagnose "why did partman not create the next partition" | Inspect partman.part_config + run run_maintenance(p_jobmon := true) + read partman.run_maintenance_jobmon table |
Three smell signals:
automatic_maintenance set to off. Default-partition data should be near-zero in healthy steady state.run_maintenance_proc runs against thousands of partition sets in one cron tick — split into multiple part_config rows + multiple cron schedules, or use maintenance_order (v5.1+).-- Requires shared_preload_libraries entry NOT needed (unlike pg_cron / pg_stat_statements).
-- pg_partman is a SQL-side + plpgsql extension. Just CREATE EXTENSION.
-- Optional: dedicated schema for partman objects.
CREATE SCHEMA partman;
CREATE EXTENSION pg_partman WITH SCHEMA partman;
-- Verify.
SELECT extname, extversion FROM pg_extension WHERE extname = 'pg_partman';[!NOTE] Superuser requirement removed in v5.2.0 v5.2.0 (2024-11-22) removed the superuser requirement for
CREATE EXTENSION pg_partman. Earlier versions required superuser at install time. Thepartmanrole used at runtime can be a non-superuser with sufficient grants on the partitioned tables.2
Most managed providers either preinstall pg_partman or expose it via their extension allowlist. Self-hosted: install the OS package (postgresql-NN-partman) or build from source per [99-pg-partman.md repository INSTALL.md]1.
create_parent — register a partition setFull v5.4+ signature:3
partman.create_parent(
p_parent_table text, -- 'schema.table' fully qualified
p_control text, -- column controlling partitioning (timestamp, integer)
p_interval text, -- e.g. '1 day', '1 month', '100000', 'P1D'
p_type text DEFAULT 'range',-- 'range' | 'list' (v5.1+)
p_epoch text DEFAULT 'none', -- 'none' | 'seconds' | 'milliseconds' | 'microseconds' (v5.2+) | 'nanoseconds' for integer epoch storing time
p_premake int DEFAULT 4, -- count of future partitions to pre-create
p_start_partition text DEFAULT NULL, -- first partition bound; auto-derives from now() if NULL
p_default_table boolean DEFAULT true, -- create DEFAULT partition (v5.2 removed this column from part_config but kept as create_parent arg)
p_automatic_maintenance text DEFAULT 'on', -- 'on' | 'off' — controls whether run_maintenance() touches this set
p_constraint_cols text[] DEFAULT NULL, -- columns to apply min/max constraints for constraint-exclusion
p_template_table text DEFAULT NULL, -- 'schema.template_table'
p_jobmon boolean DEFAULT true, -- log to partman.run_maintenance_jobmon
p_date_trunc_interval text DEFAULT NULL -- e.g. 'day' to truncate bounds
) RETURNS boolean[!NOTE] PostgreSQL 5.4.0 added
create_partition()alias v5.4.0 (2026-01-05) renamedcreate_parent()tocreate_partition()for naming consistency withundo_partition(). The old name is retained for backward compatibility — both forms work.2
Canonical time-range registration:
-- Step 1: create the parent (PARTITION BY RANGE must already exist).
CREATE TABLE public.events (
event_id bigint NOT NULL,
occurred_at timestamptz NOT NULL,
tenant_id int NOT NULL,
payload jsonb NOT NULL DEFAULT '{}'::jsonb
) PARTITION BY RANGE (occurred_at);
-- Step 2: register with partman.
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'occurred_at',
p_interval := '1 day',
p_premake := 7,
p_type := 'range'
);
-- Verify.
SELECT parent_table, control, partition_interval, partition_type, premake, automatic_maintenance
FROM partman.part_config
WHERE parent_table = 'public.events';
-- Inspect created children.
SELECT inhrelid::regclass AS child
FROM pg_inherits
WHERE inhparent = 'public.events'::regclass
ORDER BY 1;After this call, partman has created events_p2026_05_07, events_p2026_05_08, ..., events_p2026_05_14, events_default and registered them under the parent.
List partitioning (v5.1+) over integer tenant_id:
CREATE TABLE public.events_by_tenant (
event_id bigint NOT NULL,
tenant_id int NOT NULL,
payload jsonb NOT NULL
) PARTITION BY LIST (tenant_id);
SELECT partman.create_parent(
p_parent_table := 'public.events_by_tenant',
p_control := 'tenant_id',
p_interval := '1',
p_type := 'list'
);run_maintenance — the workhorseTwo forms, different transaction semantics:3
| Form | Transaction | When to use |
|---|---|---|
partman.run_maintenance(p_parent_table text DEFAULT NULL, p_analyze boolean DEFAULT false, p_jobmon boolean DEFAULT true) | Function — one transaction for entire call | Ad-hoc, single set, manual invocation |
partman.run_maintenance_proc(p_wait int DEFAULT 0, p_analyze boolean DEFAULT NULL, p_jobmon boolean DEFAULT true) | Procedure — commits between partition sets | Scheduled invocation via pg_cron, many partition sets |
For each registered partition set with automatic_maintenance = 'on':
premake and current control-column value.retention (if retention is set).ANALYZE newly attached children (if p_analyze := true).-- One-shot maintenance for a single set.
SELECT partman.run_maintenance(p_parent_table := 'public.events');
-- Scheduled batch maintenance for all sets (cron-friendly).
CALL partman.run_maintenance_proc();
-- With analyze + delay between sets (delay reduces lock pressure).
CALL partman.run_maintenance_proc(p_wait := 30, p_analyze := true);[!NOTE] PostgreSQL 5.1.0 added
maintenance_orderv5.1.0 (2024-04-02) addedmaintenance_order int+maintenance_last_run timestamptzcolumns topart_config.maintenance_ordercontrols execution priority when many sets are configured — smaller integer runs earlier. NULL means default ordering.2
part_config — the state tableRead this to see what is configured; modify it to change behavior (avoid editing during a maintenance run). Columns (v5.4+):3
| Column | Meaning |
|---|---|
parent_table | schema.table — primary key |
control | Column used for partitioning |
partition_interval | e.g. '1 day', '1 month', '100000' |
partition_type | range or list (v5.0+); pre-v5 had time-static / time-dynamic / id-static / id-dynamic — all removed in v5.0 |
premake | Future partitions pre-created (default 4) |
automatic_maintenance | on / off — whether run_maintenance() touches this set |
template_table | Optional template-table reference |
retention | Interval (time) or bigint (integer) for keep-window; NULL = keep all |
retention_schema | If set, move expired partitions to this schema; overrides retention_keep_table |
retention_keep_index | Boolean — drop or keep indexes on detached children |
retention_keep_table | Boolean (default true) — detach vs drop |
epoch | none / seconds / milliseconds / microseconds (v5.2+) / nanoseconds — for integer epoch columns storing time |
constraint_cols | Columns to maintain min/max CHECK constraints on for constraint-exclusion |
optimize_constraint | Number of partitions back to maintain constraint |
infinite_time_partitions | If true, premake bounded by partition_interval not by data presence |
datetime_string | Suffix format for child table names (e.g. YYYY_MM_DD) |
jobmon | Log to partman.run_maintenance_jobmon |
sub_partition_set_full | Used for sub-partitioning |
undo_in_progress | Flag set by undo_partition_proc() |
inherit_privileges | Whether new children inherit parent privileges automatically |
constraint_valid | Whether to mark constraint VALID immediately (vs NOT VALID + later VALIDATE) |
subscription_refresh | List of subscription names to refresh after partition changes (logical replication) |
ignore_default_data | If true, do not consider default partition during maintenance |
maintenance_order (v5.1+) | Ordering priority |
retention_keep_publication (v5.1+) | Whether to keep publication membership on detached partitions |
maintenance_last_run (v5.1+) | Timestamp of last successful run |
To pause maintenance for a single set:
UPDATE partman.part_config
SET automatic_maintenance = 'off'
WHERE parent_table = 'public.events';Three behaviors, controlled by three columns:3
retention | retention_schema | retention_keep_table | Effect on expired partitions |
|---|---|---|---|
NULL | any | any | No retention enforced — keep everything |
| set | NULL | true (default) | DETACH from parent, leave in current schema |
| set | NULL | false | DETACH + DROP TABLE |
| set | 'archive' | any | DETACH + ALTER ... SET SCHEMA archive |
-- Keep 90 days of events, drop older.
UPDATE partman.part_config
SET retention = '90 days',
retention_keep_table = false
WHERE parent_table = 'public.events';
-- Keep 90 days, move older to archive schema.
CREATE SCHEMA IF NOT EXISTS archive;
UPDATE partman.part_config
SET retention = '90 days',
retention_schema = 'archive',
retention_keep_table = true
WHERE parent_table = 'public.events';[!WARNING] retention_keep_table=false is irreversible Once
run_maintenance()drops a partition, the data is gone. Pair with logical archive (cronCOPYto S3, or useretention_schemato move to detached archive schema where you control further lifecycle). The detach + move path is the safer default for compliance workloads.
PG declarative partitioning does NOT propagate every property from parent to children. Indexes on parent propagate as INVALID until attached; PRIMARY KEY must include partition key; UNIQUE must include partition key; foreign keys, REPLICA IDENTITY, unlogged flag, autovacuum overrides, toast options — none of these propagate.
partman's template-table mechanism fills the gap. You create a separate table (not under the parent) with the desired properties, then register it as p_template_table in create_parent(). Each NEW child created by partman inherits from the template at creation time.
-- Step 1: create the template (NOT a child of events).
CREATE TABLE partman.events_template (LIKE public.events);
-- Step 2: add properties you want propagated to new children.
ALTER TABLE partman.events_template REPLICA IDENTITY FULL;
CREATE INDEX events_template_tenant_idx ON partman.events_template(tenant_id);
CREATE INDEX events_template_payload_gin ON partman.events_template USING GIN (payload);
-- Step 3: register with partman.
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'occurred_at',
p_interval := '1 day',
p_premake := 7,
p_template_table := 'partman.events_template'
);
-- Or, if create_parent was already called:
UPDATE partman.part_config
SET template_table = 'partman.events_template'
WHERE parent_table = 'public.events';[!WARNING] Template is one-shot at child creation Properties on the template apply to children created AFTER the template was registered. Existing children are not back-filled. To retro-fit, you must
CREATE INDEX CONCURRENTLY/ALTER TABLEeach existing leaf by hand. Same applies to any later template change.4
partman.create_sub_parent() (v5.4.0 alias create_sub_partition()) registers a sub-level. Useful for year-then-month or tenant-then-month patterns. Same parameter shape as create_parent() with the top-parent as first arg.
-- Year-level parent.
CREATE TABLE public.events (
event_id bigint NOT NULL,
occurred_at timestamptz NOT NULL
) PARTITION BY RANGE (occurred_at);
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'occurred_at',
p_interval := '1 year'
);
-- Sub-partition each year by month.
SELECT partman.create_sub_parent(
p_top_parent := 'public.events',
p_control := 'occurred_at',
p_interval := '1 month'
);After this call, partman creates yearly children + monthly grandchildren on each yearly child. Read partman.part_config_sub to inspect sub-config rows.
partition_data_proc + undo_partition_procTwo background-job procedures for bulk data movement:3
partition_data_proc() — pull rows out of the DEFAULT partition into the proper child partitions. Use when bounds were extended too late and the default accumulated rows that belong elsewhere. Commits between batches so a long migration does not hold one giant transaction.
-- Move all data out of events_default into proper child partitions.
CALL partman.partition_data_proc(
p_parent_table := 'public.events',
p_loop_count := 100, -- batches per call
p_interval := '1 day',-- batch size in partition_interval units
p_lock_wait := 5, -- seconds to wait for lock
p_lock_wait_tries := 10,
p_wait := 1 -- seconds between batches
);undo_partition_proc() — reverse a partitioned table back to a single heap. Useful for major refactors or repartitioning under a different key. Aware: this is destructive in the sense it removes the partition structure — be sure of intent.
CALL partman.undo_partition_proc(
p_parent_table := 'public.events',
p_target_table := 'public.events_undone',
p_loop_count := 100,
p_keep_table := true, -- keep child tables after data move
p_drop_cascade := false
);Canonical pattern is to schedule run_maintenance_proc() from pg_cron (98-pg-cron.md):
-- Hourly maintenance covering all configured partition sets.
SELECT cron.schedule(
'partman-maintenance',
'@hourly',
$$CALL partman.run_maintenance_proc()$$
);
-- Verify.
SELECT jobid, schedule, command FROM cron.job WHERE jobname = 'partman-maintenance';[!NOTE] pg_cron installs only in one database per cluster pg_cron may only be installed in the database named by the
cron.database_nameGUC (defaultpostgres). To schedule partman maintenance for a partition set living in a different database, usecron.schedule_in_database()from the pg_cron database, passing the target database name. See 98-pg-cron.md gotcha #3.
[!NOTE] partman + HA failover pg_cron + partman both run on the primary only. After a failover, the new primary has the same
partman.part_config(replicated via physical replication) AND the samecron.jobrows. Maintenance resumes automatically on the new primary. If using logical replication,partman.part_configis NOT replicated by default — re-create on the subscriber side. See 77-standby-failover.md.
pg_partman is wholly external. All five PG major release notes (PG14 / PG15 / PG16 / PG17 / PG18) contain ZERO pg_partman items. All meaningful changes happen in pg_partman's own release cadence.
| Version | Released | Highlights |
|---|---|---|
| 4.7.x | 2021–2023 | Last v4 series. Supported trigger-based + declarative partitioning. time-static / time-dynamic / id-static / id-dynamic types. Stay on this branch if on PG12 / PG13. |
| 5.0.0 | 2023-09-28 | Major break. Removed trigger-based partitioning entirely. Only declarative supported. Dropped time-* and id-* types — only range. Parameters renamed; part_config columns reshuffled. Migration via doc/migrate_to_declarative.md + doc/pg_partman_5.0.0_upgrade.md. Privileges NOT preserved across upgrade — re-grant. |
| 5.1.0 | 2024-04-02 | LIST partitioning for single-value integers. maintenance_order + maintenance_last_run columns added to part_config. REPLICA IDENTITY auto-inherits from template. Experimental numeric-column range partitioning. |
| 5.2.0 | 2024-11-22 | UUIDv7 + custom-encoded methods for time-based partitioning. Microsecond epoch precision. Superuser requirement removed. default_table column removed from part_config (kept as create_parent() arg). Control column may be NULL (with care). |
| 5.3.0 | 2025-10-09 | New partition_data_async() for smaller batched moves out of default partition (time-based only). p_ignored_columns support. UUID partitioning support in partition_data_time() / _proc(). |
| 5.4.0 | 2026-01-05 | Renamed create_parent() → create_partition() + create_sub_parent() → create_sub_partition() (old names retained). New config_cleanup() strips pg_partman state while leaving partitioned table intact. p_ignore_infinity parameter for default-table handling. |
| 5.4.3 | 2026-03-05 | Latest stable at planning time. Toast inheritance from template; fixes 5.4.2 control-file version-mismatch bug. |
[!NOTE] All five PG majors contain ZERO pg_partman release-note items Verified at planning time across PG14 / PG15 / PG16 / PG17 / PG18 release notes. Any version-specific behavior change comes from pg_partman's own release cadence, not from PG. If a tutorial claims "PG18 introduced partman feature X" — verify against [pg_partman CHANGELOG.txt]2 directly.
-- Schema.
CREATE TABLE public.events (
event_id bigserial NOT NULL,
occurred_at timestamptz NOT NULL DEFAULT now(),
tenant_id int NOT NULL,
event_type text NOT NULL,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
PRIMARY KEY (event_id, occurred_at)
) PARTITION BY RANGE (occurred_at);
-- Template for indexes that should apply to every new partition.
CREATE TABLE partman.events_template (LIKE public.events);
CREATE INDEX events_template_tenant_idx ON partman.events_template (tenant_id);
CREATE INDEX events_template_payload_gin ON partman.events_template USING GIN (payload);
-- Register with partman.
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'occurred_at',
p_interval := '1 day',
p_premake := 14,
p_template_table := 'partman.events_template'
);
-- 90-day retention with drop.
UPDATE partman.part_config
SET retention = '90 days',
retention_keep_table = false
WHERE parent_table = 'public.events';
-- Schedule hourly maintenance.
SELECT cron.schedule(
'partman-maintenance',
'@hourly',
$$CALL partman.run_maintenance_proc()$$
);Bounds were extended too late. Default partition accumulated 50M rows that belong in proper children.
-- Inspect default-partition size first.
SELECT pg_size_pretty(pg_relation_size('public.events_default'));
-- Move rows in batches, commit between, avoid one big transaction.
CALL partman.partition_data_proc(
p_parent_table := 'public.events',
p_loop_count := 100,
p_interval := '1 day',
p_lock_wait := 5,
p_lock_wait_tries := 10,
p_wait := 1
);
-- Verify.
SELECT pg_size_pretty(pg_relation_size('public.events_default'));Template registers index on FUTURE partitions. Existing partitions stay un-indexed unless you walk them by hand. Use CREATE INDEX CONCURRENTLY then attach to the parent index (26-index-maintenance.md).
-- Step 1: add to template so future children get it.
CREATE INDEX events_template_event_type_idx
ON partman.events_template (event_type);
-- Step 2: walk existing leaves with CIC.
DO $$
DECLARE
leaf regclass;
BEGIN
FOR leaf IN
SELECT inhrelid::regclass
FROM pg_inherits
WHERE inhparent = 'public.events'::regclass
LOOP
EXECUTE format(
'CREATE INDEX CONCURRENTLY IF NOT EXISTS %I ON %s (event_type)',
'idx_events_event_type_' || regexp_replace(leaf::text, '^.*_p', ''),
leaf
);
END LOOP;
END;
$$;
-- Step 3: create the parent-level index referencing leaves.
-- (PG12+ creates parent index INVALID until all leaves attached)
CREATE INDEX events_event_type_idx ON ONLY public.events (event_type);
-- Step 4: attach each leaf index to the parent.
DO $$
DECLARE
leaf regclass;
leaf_idx text;
BEGIN
FOR leaf, leaf_idx IN
SELECT inhrelid::regclass,
'idx_events_event_type_' || regexp_replace(inhrelid::regclass::text, '^.*_p', '')
FROM pg_inherits
WHERE inhparent = 'public.events'::regclass
LOOP
EXECUTE format(
'ALTER INDEX %I ATTACH PARTITION %I',
'events_event_type_idx',
leaf_idx
);
END LOOP;
END;
$$;UPDATE partman.part_config
SET automatic_maintenance = 'off'
WHERE parent_table = 'public.events';
-- ... maintenance window ...
UPDATE partman.part_config
SET automatic_maintenance = 'on'
WHERE parent_table = 'public.events';
-- Catch up.
SELECT partman.run_maintenance(p_parent_table := 'public.events');CREATE SCHEMA IF NOT EXISTS archive;
UPDATE partman.part_config
SET retention = '90 days',
retention_schema = 'archive',
retention_keep_table = true
WHERE parent_table = 'public.events';
-- Verify after next run.
CALL partman.run_maintenance_proc();
SELECT relnamespace::regnamespace AS schema, relname
FROM pg_class
WHERE relkind = 'r'
AND relname LIKE 'events_p%'
ORDER BY 1, 2;CREATE TABLE public.events (
event_id bigint NOT NULL,
occurred_at timestamptz NOT NULL
) PARTITION BY RANGE (occurred_at);
-- Top level: yearly.
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'occurred_at',
p_interval := '1 year',
p_premake := 2
);
-- Sub level: monthly under each year.
SELECT partman.create_sub_parent(
p_top_parent := 'public.events',
p_control := 'occurred_at',
p_interval := '1 month',
p_premake := 3
);
-- Inspect sub-config.
SELECT * FROM partman.part_config_sub;SELECT
pc.parent_table,
pc.control,
pc.partition_interval,
pc.partition_type,
pc.premake,
pc.retention,
pc.automatic_maintenance,
pc.maintenance_last_run,
COUNT(i.inhrelid) AS leaf_count,
pg_size_pretty(SUM(pg_relation_size(i.inhrelid))) AS total_size
FROM partman.part_config pc
LEFT JOIN pg_inherits i ON i.inhparent = pc.parent_table::regclass
GROUP BY pc.parent_table, pc.control, pc.partition_interval, pc.partition_type,
pc.premake, pc.retention, pc.automatic_maintenance, pc.maintenance_last_run
ORDER BY total_size DESC;-- Step 1: is automatic_maintenance on?
SELECT parent_table, automatic_maintenance, maintenance_last_run
FROM partman.part_config
WHERE parent_table = 'public.events';
-- Step 2: is the cron job running?
SELECT * FROM cron.job_run_details
WHERE jobid = (SELECT jobid FROM cron.job WHERE jobname = 'partman-maintenance')
ORDER BY start_time DESC LIMIT 5;
-- Step 3: run maintenance manually with jobmon enabled and read the log.
SELECT partman.run_maintenance(
p_parent_table := 'public.events',
p_jobmon := true
);
-- Step 4: read jobmon table.
SELECT * FROM partman.run_maintenance_jobmon
WHERE parent_table = 'public.events'
ORDER BY started_at DESC LIMIT 10;If still on partman 4.x with trigger-based partitioning, follow the official migration docs: doc/migrate_to_declarative.md and doc/pg_partman_5.0.0_upgrade.md in the repo. High-level shape:
INSERT INTO new_parent SELECT * FROM old_child per child (or partition_data_proc() for batch-safe variant).-- Requires PG18+ for in-core uuidv7() or pgcrypto for gen_random_uuid().
-- pg_partman v5.2+ recognizes UUIDv7 as time-ordered.
CREATE TABLE public.events (
event_id uuid NOT NULL DEFAULT uuidv7(),
occurred_at timestamptz NOT NULL DEFAULT now(),
payload jsonb NOT NULL
) PARTITION BY RANGE (event_id);
-- partman extracts the timestamp from UUIDv7.
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'event_id',
p_interval := '1 day',
p_premake := 7,
p_type := 'range'
);-- One-shot consolidation of all partitions into a target table.
CREATE TABLE public.events_consolidated (LIKE public.events INCLUDING ALL);
CALL partman.undo_partition_proc(
p_parent_table := 'public.events',
p_target_table := 'public.events_consolidated',
p_loop_count := 100,
p_wait := 1,
p_keep_table := false -- drop child tables after data move
);When the partitioned table is published / subscribed, attaching or detaching partitions changes the publication membership. partman can auto-call ALTER SUBSCRIPTION ... REFRESH PUBLICATION after maintenance.
-- On the subscriber side (where partman lives mirroring the publisher's partition structure).
UPDATE partman.part_config
SET subscription_refresh = ARRAY['my_subscription']
WHERE parent_table = 'public.events';-- v5.4.0 added config_cleanup() to remove partman state cleanly.
SELECT partman.config_cleanup(p_parent_table := 'public.events');
-- After this, public.events still exists as a declarative-partitioned table
-- with its current children, but partman no longer manages it.v5 is a hard break from v4 — no in-place ALTER EXTENSION UPDATE path. Migration is via doc/migrate_to_declarative.md + doc/pg_partman_5.0.0_upgrade.md. Most older tutorials reference v4 trigger-based syntax that no longer exists.
pg_partman 5.x requires PG14 minimum. Verbatim README: "Requirement: PostgreSQL >= 14". PG12 / PG13 require staying on partman 4.7.x.
No HASH partitioning support. partman manages range and list (v5.1+) only. Hash-partitioned tables must be created and maintained manually, or use Citus (97-citus.md).
Template table changes are NOT retroactive. Indexes / constraints / REPLICA IDENTITY added to template apply to NEW children only. Existing children stay un-indexed — walk them with CREATE INDEX CONCURRENTLY per Recipe 3.
retention_keep_table = false drops the table irreversibly. Once run_maintenance() drops a partition, data is gone. Pair with logical archive (cron COPY to S3) or use retention_schema for safer detach + move pattern.
run_maintenance (function) holds one giant transaction; run_maintenance_proc (procedure) commits between sets. Use the proc form from pg_cron. Use the function form only for one-shot manual invocations on a single set.
automatic_maintenance = 'off' silently disables maintenance for a set. Read it explicitly when diagnosing "why aren't future partitions being created."
maintenance_last_run only populates if p_jobmon = true (or v5.1+ updates it regardless — verify per version). If maintenance appears to run but maintenance_last_run stays NULL, jobmon is likely disabled.
premake too low + maintenance not running = writes hit DEFAULT partition. Default partition should be near-empty in healthy steady state. Investigate if it grows.
PRIMARY KEY on partitioned table must include the partition key column. PG declarative-partitioning rule, not partman-specific. For time-partitioned tables, PK must be (id, occurred_at) not (id) alone. UNIQUE constraints likewise. See 35-partitioning.md gotcha #1.
Foreign keys referencing a partitioned parent only work from PG12+. Foreign keys FROM a partitioned table also need each leaf to have a matching covering index. See 38-foreign-keys-deep.md.
partman.part_config is NOT replicated by logical replication. Physical replication ships the catalog as-is. For logical-replication topologies, re-create part_config on the subscriber side and run maintenance independently.
partition_data_proc is time-based-partition-only for partition_data_async (v5.3+). For non-time partitioning, use partition_data_proc() synchronously.
Privileges on partman objects are NOT preserved across the v4 → v5 upgrade. Re-grant after migration.
p_interval must be a valid PostgreSQL interval value for time-based partitioning. v5.0 removed the shortcut strings (weekly, monthly, etc.) — use '1 week' / '1 month' instead.
p_epoch is required when control column is an integer storing epoch. Without p_epoch := 'seconds' (or milliseconds / microseconds), partman treats the integer column as a plain numeric range, not a time range.
infinite_time_partitions = true pre-creates partitions regardless of data presence. Useful for tables that may have gaps in incoming data; default false is data-driven (only pre-create if there is data approaching the bound).
p_constraint_cols adds CHECK constraints for non-key columns to enable constraint exclusion, useful pre-PG10. Mostly obsolete with PG declarative pruning; only enable if specific queries benefit.
Sub-partitioning multiplies leaf count. Year-then-month for 5 years = 60 leaves. Year-then-day-of-year = 1825 leaves. Each leaf has its own catalog row, autovacuum target, planner-time cost. Cap depth at 2 levels in practice.
undo_partition_proc() is destructive to the partition structure. It moves data into a single target table and (optionally) drops the children. There is no redo_partition_proc() — re-running create_parent() requires a fresh partitioned table.
automatic_maintenance = 'on' is meaningless without a scheduled call to run_maintenance_proc(). partman does not have its own scheduler — relies on pg_cron, a background worker config, or an external orchestrator.
partman objects (part_config, run_maintenance_jobmon) live in the schema where the extension was installed. CREATE EXTENSION pg_partman WITH SCHEMA partman is the canonical placement. Querying part_config without schema-qualifying requires partman in search_path.
PG14 / PG15 / PG16 / PG17 / PG18 release notes contain ZERO pg_partman items. Verified at planning time. Tutorials claiming "PG N added partman feature X" must be verified against pg_partman/CHANGELOG.txt directly — feature timeline belongs to partman, not PG.
CREATE INDEX CONCURRENTLY + attach pattern for retroactive index propagationALTER DEFAULT PRIVILEGES IN SCHEMA for new partitionspart_configsubscription_refresh interaction with partman--table + pattern matching with partitioned tablesrun_maintenance_proc()pg_partman README — https://github.com/pgpartman/pg_partman (PG14+ requirement, license, install) ↩ ↩2 ↩3
pg_partman CHANGELOG.txt — https://github.com/pgpartman/pg_partman/blob/master/CHANGELOG.txt (per-version timeline 4.x → 5.4.3) ↩ ↩2 ↩3 ↩4 ↩5
pg_partman docs — https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md (function signatures, part_config columns, parameters) ↩ ↩2 ↩3 ↩4 ↩5
pg_partman howto — https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman_howto.md (template-table pattern, sub-partitioning) ↩
evals
references