> Splitting a large table into smaller physical partitions by range, list, or hash to improve query performance, simplify maintenance, and enable efficient data lifecycle management.
Scanned 9/11/2026
Install to Claude Code
npx -y skills add Intense-Visions/harness-engineering --skill db-table-partitioning --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Db Table Partitioning?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/intense-visions-db-table-partitioning-96c629b6)More formats (shields.io, HTML) on the badges page.
# Table Partitioning
> Splitting a large table into smaller physical partitions by range, list, or hash to improve query performance, simplify maintenance, and enable efficient data lifecycle management.
## When to Use
- Tables exceeding 100 GB or 100M rows where queries target a known subset
- Time-series data where queries filter by date range
- Multi-tenant tables where queries filter by tenant ID
- Tables requiring periodic data archival or deletion (drop partition vs DELETE)
- Queries with sequential scans on large tables that could benefit from partition pruning
## Instructions
### Key Concepts
**1. Partitioning Types**
**Range partitioning:** Split by a continuous range, typically timestamps or numeric IDs.
```sql
CREATE TABLE events (
id BIGSERIAL,
tenant_id INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
payload JSONB
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2024_q1 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE events_2024_q2 PARTITION OF events
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
CREATE TABLE events_2024_q3 PARTITION OF events
FOR VALUES FROM ('2024-07-01') TO ('2024-10-01');
CREATE TABLE events_2024_q4 PARTITION OF events
FOR VALUES FROM ('2024-10-01') TO ('2025-01-01');
```
**List partitioning:** Split by discrete values, typically status or region.
```sql
CREATE TABLE orders (
id BIGSERIAL,
region TEXT NOT NULL,
total NUMERIC
) PARTITION BY LIST (region);
CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('us-east', 'us-west');
CREATE TABLE orders_eu PARTITION OF orders FOR VALUES IN ('eu-west', 'eu-central');
CREATE TABLE orders_ap PARTITION OF orders FOR VALUES IN ('ap-south', 'ap-east');
```
**Hash partitioning:** Distribute evenly across N partitions for balanced load.
```sql
CREATE TABLE sessions (
id UUID NOT NULL,
user_id INT NOT NULL,
data JSONB
) PARTITION BY HASH (user_id);
CREATE TABLE sessions_p0 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_p1 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_p2 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_p3 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 3);
```
**2. Partition Pruning**
PostgreSQL's query planner eliminates partitions that cannot contain matching rows:
```sql
-- Only scans events_2024_q3, skips all other partitions
EXPLAIN SELECT * FROM events
WHERE created_at >= '2024-07-15' AND created_at < '2024-08-01';
-- Verify pruning in EXPLAIN output:
-- "Partitions removed: 3" or "Subplans Removed: 3"
```
Pruning requires the partition key in the `WHERE` clause. Without it, PostgreSQL scans all partitions.
**3. Partition Maintenance**
```sql
-- Add a new partition before data arrives
CREATE TABLE events_2025_q1 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
-- Archive old data by detaching a partition (instant, no row-level locks)
ALTER TABLE events DETACH PARTITION events_2024_q1;
-- Now events_2024_q1 is a standalone table; move to cold storage or drop
-- Drop old data without expensive DELETE + VACUUM
DROP TABLE events_2024_q1;
```
### Worked Example
Scenario: an IoT platform stores 500M sensor readings per month. Queries always filter by `recorded_at` within a single day or week.
```sql
-- Create the partitioned parent table
CREATE TABLE sensor_readings (
id BIGSERIAL,
device_id INT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL,
value DOUBLE PRECISION,
metadata JSONB
) PARTITION BY RANGE (recorded_at);
-- Create monthly partitions (automate via cron or pg_partman)
CREATE TABLE sensor_readings_2024_01 PARTITION OF sensor_readings
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
-- ... one partition per month
-- Create index on each partition (inherited automatically in PG 11+)
CREATE INDEX ON sensor_readings (device_id, recorded_at);
-- Query with partition pruning
EXPLAIN ANALYZE
SELECT avg(value) FROM sensor_readings
WHERE device_id = 42
AND recorded_at >= '2024-06-01'
AND recorded_at < '2024-06-08';
-- Only scans sensor_readings_2024_06
-- Without partitioning: full table scan of 6B+ rows
-- Monthly maintenance: archive data older than 1 year
ALTER TABLE sensor_readings DETACH PARTITION sensor_readings_2023_01;
-- Move to S3 or drop
```
Automated partition creation with `pg_partman`:
```sql
CREATE EXTENSION pg_partman;
SELECT create_parent(
'public.sensor_readings',
'recorded_at',
'native',
'monthly'
);
-- pg_partman creates future partitions and manages retention automatically
```
### Anti-Patterns
1. **Partitioning tables under 10 GB.** Small tables gain nothing from partitioning. The overhead of partition management and query planning across partitions can make queries slower than scanning a single small table.
2. **Choosing a partition key that queries do not filter on.** If queries never include the partition key in WHERE clauses, every query scans all partitions. The partition key must align with the dominant query pattern.
3. **Creating too many partitions.** Each partition is a separate table with its own file descriptors and catalog entries. Thousands of partitions slow query planning and `pg_dump`. Daily partitions on a 10-year table means 3,650 partitions -- monthly is usually sufficient.
4. **Forgetting to create future partitions.** Inserts into a partitioned table fail if no matching partition exists. Automate partition creation with `pg_partman` or a scheduled job.
5. **Using partitioning instead of proper indexing.** Partitioning is not a substitute for indexes. A well-indexed unpartitioned table often outperforms a poorly-indexed partitioned table. Add partitioning when indexes alone are insufficient.
### PostgreSQL Specifics
- Declarative partitioning (PostgreSQL 10+) replaces the old inheritance-based approach. Always use declarative partitioning for new tables.
- PostgreSQL 11+: indexes created on the parent table are automatically created on all partitions.
- PostgreSQL 12+: foreign keys can reference partitioned tables.
- PostgreSQL 14+: `DETACH PARTITION ... CONCURRENTLY` avoids blocking concurrent queries during detach.
- `enable_partition_pruning = on` (default) must be set for pruning to work.
## Details
### Advanced Topics
**Sub-Partitioning:** Partition by range on date, then sub-partition by list on region:
```sql
CREATE TABLE events (id BIGSERIAL, region TEXT, created_at TIMESTAMPTZ)
PARTITION BY RANGE (created_at);
CREATE TABLE events_2024_q1 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01')
PARTITION BY LIST (region);
CREATE TABLE events_2024_q1_us PARTITION OF events_2024_q1
FOR VALUES IN ('us');
CREATE TABLE events_2024_q1_eu PARTITION OF events_2024_q1
FOR VALUES IN ('eu');
```
Use sparingly -- sub-partitioning multiplies partition count and management complexity.
**Partition-Wise Joins (PostgreSQL 11+):** When two tables are partitioned on the same key, PostgreSQL can join partition-to-partition instead of gathering all data first:
```sql
SET enable_partitionwise_join = on;
-- Joins events_2024_q1 with metrics_2024_q1 directly
```
### Engine Differences
MySQL supports range, list, hash, and key partitioning. Key differences from PostgreSQL: MySQL does not support partition-wise joins, does not support sub-partitioning with declarative syntax (uses a different mechanism), and requires the partition key to be part of every unique index including the primary key. This constraint often forces awkward composite primary keys in MySQL.
MySQL's `ALTER TABLE ... REORGANIZE PARTITION` can split or merge partitions online but acquires a metadata lock that can block queries.
### Real-World Case Studies
A financial analytics platform stored 2 billion transaction records in a single table. Queries filtered by `transaction_date` but full table scans took 45 minutes. After partitioning by month (24 partitions for 2 years of data), queries targeting a single month completed in under 10 seconds. Monthly data retention became trivial: `DROP TABLE transactions_2022_01` completed in milliseconds instead of a 3-hour DELETE operation that generated massive WAL and required extended VACUUM.
## Source
- [PostgreSQL Table Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html)
- [pg_partman](https://github.com/pgpartman/pg_partman)
## Process
1. Identify the partition key by analyzing the dominant query filter pattern.
2. Choose range (time-series), list (categorical), or hash (even distribution) based on the data and query patterns.
3. Create the partitioned table and initial partitions; automate future partition creation with pg_partman.
4. Verify partition pruning with `EXPLAIN` on representative queries.
## Harness Integration
- **Type:** knowledge -- this skill is a reference document, not a procedural workflow.
- **No tools or state** -- consumed as context by other skills and agents.
- **related_skills:** db-horizontal-sharding, db-vertical-partitioning, db-btree-index, db-explain-reading
## Success Criteria
- The partition key aligns with the dominant query filter pattern, enabling partition pruning on most queries.
- EXPLAIN confirms partition pruning eliminates irrelevant partitions for representative queries.
- Future partitions are created automatically via pg_partman or a scheduled job.
- Data retention uses `DETACH PARTITION` and `DROP TABLE` instead of row-level DELETE operations.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!