PostgreSQL

PostgreSQL Partitioning at Scale: Range, Hash, and Automated Maintenance

When your events table hits 800 million rows, PostgreSQL declarative partitioning becomes essential. Learn range and hash partitioning, automated management with pg_partman, and zero-downtime archival.

JusDB Team
July 22, 2025
Updated June 20, 2026
10 min read

Your events table hit 800 million rows and every query planner decision now involves scanning billions of index entries. VACUUM takes 6 hours. Dropping old data means a table lock. PostgreSQL declarative partitioning was built for exactly this — here's how to implement it correctly before your table becomes unmanageable.

TL;DR
  • PostgreSQL declarative partitioning (PG 10+) splits one logical table into physical child tables
  • Range partitioning by date is ideal for time-series and event data
  • Constraint exclusion and partition pruning mean queries only scan relevant partitions
  • Attach/detach partitions for zero-downtime old data archival and purging

PostgreSQL Partitioning Strategies

Range Partitioning (Most Common)

sql
CREATE TABLE events (
    id         bigserial,
    user_id    bigint NOT NULL,
    event_type text NOT NULL,
    payload    jsonb,
    created_at timestamptz NOT NULL DEFAULT now()
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2025_01
    PARTITION OF events
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

CREATE TABLE events_2025_02
    PARTITION OF events
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

-- Index applies to all partitions (PG 11+)
CREATE INDEX ON events (created_at, user_id);

Hash Partitioning (Horizontal Sharding)

sql
CREATE TABLE user_events (
    user_id    bigint NOT NULL,
    event_type text NOT NULL,
    created_at timestamptz NOT NULL
) PARTITION BY HASH (user_id);

CREATE TABLE user_events_p0 PARTITION OF user_events
    FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE user_events_p1 PARTITION OF user_events
    FOR VALUES WITH (MODULUS 8, REMAINDER 1);
-- repeat for p2 through p7

Automating Partition Creation with pg_partman

sql
CREATE EXTENSION pg_partman SCHEMA partman;

SELECT partman.create_parent(
    p_parent_table => 'public.events',
    p_control      => 'created_at',
    p_interval     => '1 month',
    p_premake      => 3
);

UPDATE partman.part_config
SET retention              = '12 months',
    retention_keep_table   = false,
    automatic_maintenance  = 'on'
WHERE parent_table = 'public.events';

-- Schedule via pg_cron
SELECT cron.schedule(
    'partition-maintenance',
    '0 0 * * *',
    'SELECT partman.run_maintenance_proc()'
);

Query Performance with Partitioning

Verifying Partition Pruning

sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*), event_type
FROM events
WHERE created_at BETWEEN '2025-06-01' AND '2025-06-30'
GROUP BY event_type;

-- Good: "Partitions selected: 1 out of 18"
-- Bad: all partitions scanned

-- WRONG (breaks pruning):
-- WHERE created_at::date = '2025-06-01'
-- RIGHT:
-- WHERE created_at >= '2025-06-01' AND created_at < '2025-06-02'
Warning

Implicit type casts on the partition key column defeat partition pruning. If created_at is timestamptz, always compare it with timestamptz literals — not with ::date casts or DATE_TRUNC() on the column side.

Zero-Downtime Data Archival

sql
-- Detach partition without dropping data (instant)
ALTER TABLE events DETACH PARTITION events_2024_01 CONCURRENTLY;

-- Drop old data instantly (no row-by-row delete)
DROP TABLE events_2024_01;

-- Or attach to archive table
ALTER TABLE events_archive ATTACH PARTITION events_2024_01
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
Tip

Use DETACH PARTITION ... CONCURRENTLY (PostgreSQL 14+) to detach without acquiring an ACCESS EXCLUSIVE lock on the parent table. Safe to run during business hours.

Key Takeaways
  • Declarative range partitioning by date is the standard approach for time-series and event tables exceeding 100M rows.
  • Use pg_partman to automate partition creation and retention — manually managing partitions at scale is error-prone.
  • Avoid type casts on the partition key in WHERE clauses — they disable partition pruning and cause full-table scans.
  • DETACH PARTITION CONCURRENTLY (PG 14+) enables zero-downtime archival without locking the parent table.

Working with JusDB on PostgreSQL Partitioning

JusDB designs and implements PostgreSQL partitioning strategies for engineering teams dealing with billion-row tables. We handle pg_partman configuration, partition pruning validation, and zero-downtime migration from unpartitioned tables.

Explore JusDB PostgreSQL Services →  |  Talk to a DBA

Related reading:

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

PostgreSQL 19 Beta: Every New Feature That Matters to DBAs

PostgreSQL 19 Beta 1 (June 4, 2026) brings parallel autovacuum, the native REPACK command for online table rebuilds, 2x faster inserts under foreign-key load, online logical replication without a restart, WAIT FOR LSN for read-your-writes consistency, and default changes (JIT off, lz4 TOAST, RADIUS removed). A DBA-focused walkthrough of what changed and what to test before GA.

PostgreSQL14 minJun 15, 2026
Read

PostgreSQL Performance Tuning Playbook: A Top-Down Method for Faster Queries

A repeatable, top-down method for tuning PostgreSQL: measure with pg_stat_statements, read plans with EXPLAIN (ANALYZE, BUFFERS), fix queries and indexes before parameters, then tune memory, I/O, WAL, connection pooling, and autovacuum — with a ready-to-adapt postgresql.conf baseline.

PostgreSQL22 minMay 31, 2026
Read

PostgreSQL Architecture Deep Dive: Process Model, MVCC, WAL & Replication Explained

Walk through PostgreSQL's multi-process architecture, shared/local memory layout, page-organized storage, MVCC tuple versioning, the WAL write path, the query execution pipeline, and physical + logical replication — all with ASCII flow diagrams that show how data and control actually move through the system.

PostgreSQL18 minMay 31, 2026
Read