Setting up PostgreSQL Table Partitioning (Range/List/Hash)
Imagine your events table has grown to 2 billion rows. VACUUM drags on for hours, pg_dump creates a 400-gigabyte file, and deleting old data with DELETE blocks writes for hours—your business is suffering. Our clients come with such pains, and we solve them using declarative partitioning—end to end, from audit to support. In 7+ years, we've seen hundreds of tables: in 90% of cases, partitioning solves the performance problem without code changes. According to our data, properly configured partitioning reduces query time by 50x and backup volume by 70%. One client saved $15,000 per year on storage by archiving old partitions.
Partitioning divides one large table into several physical partitions. The mechanism is built into PostgreSQL since version 10 and speeds up queries, simplifies data management, and reduces database load. In this article, we'll cover choosing the partitioning type, automation, and migration with minimal downtime. We provide a transparent work plan, fixed deadlines, and SLA. Our experience is confirmed by 50+ successful projects.
Why partitioning speeds up queries?
Three key mechanisms:
- Partition pruning—the planner excludes irrelevant partitions from the plan, reducing scan time by 10–100 times.
- Instant data deletion—
DROP TABLEon a partition completes in milliseconds, does not block the table, and does not bloat the WAL. - Parallel maintenance—
VACUUM,ANALYZE, and backups can be performed on individual partitions.
On a 500 GB table, we observed query speedup from 30 seconds to 300 ms—a 100x improvement. Dropping an old month with DROP instead of DELETE is 1000 times faster.
How to choose the partitioning type?
The table below helps with the choice:
| Type | Key | Scenario | Example |
|---|---|---|---|
| Range | Range (date, ID) | Time series, logs | created_at |
| List | List of values | Categorical data | region, status |
| Hash | Hash function | Uniform distribution | user_id |
Range works for data with a natural order—logs, metrics, transactions. List is for a finite set of categories. Hash is when you need uniform distribution but no obvious key.
Range Partitioning: typical case for time-series data
-- Parent table
CREATE TABLE events (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
user_id BIGINT NOT NULL,
event_type VARCHAR(64) NOT NULL,
payload JSONB
) PARTITION BY RANGE (created_at);
-- Creating partitions (manually or via pg_partman)
CREATE TABLE events_2023_01 PARTITION OF events
FOR VALUES FROM ('2023-01-01') TO ('2023-02-01');
Index on the parent table is automatically created on all partitions:
CREATE INDEX ON events (created_at, user_id); Automation with pg_partman
Creating partitions manually is not our approach. We use pg_partman:
SELECT partman.create_parent(
p_parent_table => 'public.events',
p_control => 'created_at',
p_type => 'range',
p_interval => 'monthly',
p_premake => 3
);
-- Configure retention: delete after 2 years
UPDATE partman.part_config
SET retention = '2 years',
retention_keep_table = false
WHERE parent_table = 'public.events';
-- Run maintenance (can be scheduled via pg_cron)
SELECT partman.run_maintenance();
How does partition pruning work?
Partition pruning is an optimization by the PostgreSQL planner: it excludes from the plan partitions that do not satisfy the `WHERE` condition on the partitioning key. For pruning to work, the condition must contain the key. For example, `WHERE created_at >= '2023-01-01' AND created_at < '2023-02-01'` cuts off all months except January 2023. This reduces the amount of scanned data by orders of magnitude.Hash Partitioning: uniform distribution
Note: when there is no obvious key, use hash:
CREATE TABLE user_sessions (
id UUID DEFAULT gen_random_uuid(),
user_id BIGINT NOT NULL,
data JSONB,
expires_at TIMESTAMPTZ
) PARTITION BY HASH (user_id);
CREATE TABLE user_sessions_p0 PARTITION OF user_sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0); Case study: migrating a 500 GB log table
A client—a fintech platform—faced degradation: queries for recent logs took over 30 seconds, VACUUM saturated I/O. We performed an audit, chose Range on created_at with monthly partitions and pg_partman.
Migration process:
- Created a new partitioned table
logs_new. - Set up logical replication via pglogical for synchronization.
- Copied old data in batches of 10 million rows.
- Switched the application to
logs_new(downtime: 2 minutes).
Results: queries for a week—300 ms instead of 30 seconds; deleting outdated data—instant DROP; VACUUM load reduced by 5x. This saved up to 60% on storage by archiving old partitions. If you are facing a similar problem, don't delay—order an audit and partitioning setup today.
How to migrate a table without downtime?
Migrating an existing table to partitioning is the most critical step. We use a combination of pg_partman and logical replication. The standard approach: create a new partitioned table, synchronize data via pglogical, copy old data in batches, switch the application. For tables over 1 TB, we recommend streaming replication with a temporary write stop. In any case, we minimize downtime to a few minutes. Tell us about your task and we'll come back with a timeline estimate—contact us.
What our work includes
- Audit of candidate tables (size, access patterns, query types).
- Selection of partitioning strategy (type, key, interval).
- Design of partition scheme and automation (pg_partman + cron).
- Configuration of partition pruning and performance monitoring.
- Migration of existing tables with minimal downtime.
- Documentation, team training, and post-deploy support.
We guarantee a transparent work plan, fixed deadlines, and SLA for support. Our experience: 7+ years in PostgreSQL, 50+ successful projects.
Process
- Analysis—collect metrics, identify bottlenecks, estimate volumes.
- Design—choose partitioning type, design schema.
- Implementation—write migration scripts, configure automation.
- Testing—verify partition pruning, compare performance on staging.
- Deploy—execute migration in production, monitor.
Estimated timelines
| Data Volume | Time (working days) |
|---|---|
| New table | from 2 days |
| Existing table up to 100 GB | from 5 days |
| Table 100+ GB with testing | up to 2 weeks |
The cost is calculated individually—write to us, we'll estimate your project in 1-2 days. Contact us—we'll help you with partitioning and speed up your database.







