Composite partitioning (subpartitions). — Cracked Java
// PostgreSQL · Partitioning
SeniorTheory

Composite partitioning (subpartitions).

Composite (multi-level) partitioning means a partition is itself partitioned — you declare PARTITION BY on a child, giving a tree like RANGE-by-date then HASH-by-tenant. PostgreSQL supports it directly; the only rule is that leaf partitions hold the rows, and intermediate levels are just routing layers.

How to build it

CREATE TABLE events (
    ts        timestamptz NOT NULL,
    tenant_id int         NOT NULL,
    payload   jsonb
) PARTITION BY RANGE (ts);

-- A monthly partition that is itself partitioned by tenant hash:
CREATE TABLE events_2025_06 PARTITION OF events
    FOR VALUES FROM ('2025-06-01') TO ('2025-07-01')
    PARTITION BY HASH (tenant_id);

-- Leaf partitions actually store rows:
CREATE TABLE events_2025_06_p0 PARTITION OF events_2025_06
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE events_2025_06_p1 PARTITION OF events_2025_06
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);
-- ... p2, p3

A row routes top-down: by ts into the month, then by hash(tenant_id) into a leaf. Pruning works at every levelWHERE ts >= ... AND tenant_id = 7 prunes both the date dimension (plan-time) and the hash dimension (equality).

When it earns its keep

Use it when two independent dimensions both matter: you retire data by time (RANGE on ts) and you want even sizing or isolation per tenant within each month (HASH/LIST on tenant_id). A single-level partition can only optimize one of those.

When it's overkill — and the cost

Subpartitioning multiplies partition count: 24 months × 4 hash buckets = 96 leaf tables, each needing its own indexes and ANALYZE. Planning and \d+ output get noisy, and operational scripts grow. Don't add a second level unless the second dimension genuinely changes your queries or management.

Mark your status