PostgreSQL Table Partitioning

Partitioning splits one huge table into smaller pieces called partitions. Applications still see a single table and use its name as usual. Behind the scenes, PostgreSQL stores each row in the piece that fits it.

  Table: sales (one name for your queries)
     |
     +-- sales_2024  (rows from 2024)
     +-- sales_2025  (rows from 2025)
     +-- sales_2026  (rows from 2026)

Picture a filing cabinet. One giant drawer holding every paper makes searching slow. Separate drawers labeled by year let you open only the drawer you need.

Why Partition a Table

  • Queries that filter by the partition key read only the matching partitions.
  • Deleting old data takes a fraction of a second, because you drop a whole partition instead of deleting millions of rows.
  • Maintenance such as VACUUM runs on small pieces.
  • Indexes stay smaller and fit in memory more easily.

Partitioning pays off for tables with many millions of rows. Small tables gain nothing from it.

Partition Methods

MethodSplits Rows ByTypical Use
RANGEA span of valuesDates and numeric ranges
LISTA fixed list of valuesCountry, region, or status
HASHA hash of the valueEven spread without natural groups

Partitioning Examples

Each method needs a parent table and a set of partitions. Pruning then keeps queries fast.

Range Partitioning Example

Create the parent table and name the partition key.

CREATE TABLE sales (
  sale_id   BIGSERIAL,
  sale_date DATE NOT NULL,
  amount    NUMERIC(10,2),
  PRIMARY KEY (sale_id, sale_date)
) PARTITION BY RANGE (sale_date);

A primary key on a partitioned table must include the partition key column. That rule explains why sale_date joins the key.

Create one partition for each year. The upper limit is exclusive.

CREATE TABLE sales_2025 PARTITION OF sales
  FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

CREATE TABLE sales_2026 PARTITION OF sales
  FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

Insert rows into the parent. PostgreSQL routes each row to the right partition.

INSERT INTO sales (sale_date, amount) VALUES ('2025-06-10', 120.00);
INSERT INTO sales (sale_date, amount) VALUES ('2026-02-14', 80.50);
  Row dated 2025-06-10  --> sales_2025
  Row dated 2026-02-14  --> sales_2026

An insert with a date that fits no partition fails with an error. Create a default partition to catch strays.

CREATE TABLE sales_other PARTITION OF sales DEFAULT;

Partition Pruning

Pruning means skipping partitions that cannot hold matching rows. The planner reads your WHERE clause and ignores the rest.

EXPLAIN SELECT * FROM sales
WHERE sale_date = '2026-02-14';

The plan shows a scan of sales_2026 only. The 2025 partition never gets touched. Filter on the partition key in every query that should benefit.

List Partitioning Example

CREATE TABLE customers (
  id      BIGSERIAL,
  name    TEXT,
  country TEXT NOT NULL,
  PRIMARY KEY (id, country)
) PARTITION BY LIST (country);

CREATE TABLE customers_india PARTITION OF customers
  FOR VALUES IN ('India');

CREATE TABLE customers_europe PARTITION OF customers
  FOR VALUES IN ('France', 'Germany', 'Spain');

Hash Partitioning Example

Hash partitioning spreads rows evenly. Choose the number of partitions up front.

CREATE TABLE events (
  id      BIGINT,
  payload TEXT
) PARTITION BY HASH (id);

CREATE TABLE events_p0 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE events_p1 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE events_p2 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE events_p3 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 3);

Managing Partitions

Dropping, indexing, and planning decide whether partitioning stays easy over the years.

Removing Old Data Fast

Detach a partition to remove it from the table while keeping its data. Drop it when you no longer need the rows.

ALTER TABLE sales DETACH PARTITION sales_2025;
DROP TABLE sales_2025;

Deleting a year of rows with DELETE would create millions of dead rows and take minutes. Dropping a partition finishes almost instantly.

Indexes on Partitions

An index created on the parent table builds a matching index on every partition, and on partitions added later.

CREATE INDEX idx_sales_amount ON sales (amount);

Planning Tips

  • Pick a partition key that appears in most WHERE clauses.
  • Avoid thousands of tiny partitions, because planning time grows with the count.
  • Create future partitions ahead of time, or automate it with a scheduled job.
  • Test queries with EXPLAIN to confirm pruning happens.

Practice Exercise

Create a table named visits partitioned by month on a visit_date column. Add three monthly partitions and a default partition. Insert a few rows per month. Run EXPLAIN on a query for one month and confirm that only one partition appears in the plan. Detach the oldest partition afterward.