Cheatsheet for partitioning with SQLAlchemy.

When to partition

  • Table > 100 GB.
  • Heavy date-range queries.
  • Need to drop old data fast (DROP partition vs DELETE).
  • Per-tenant isolation at partition level.

Declarative partitioned table

class Event(Base):
    __tablename__ = "events"
    __table_args__ = {"postgresql_partition_by": "RANGE (occurred_at)"}
    
    id: Mapped[int] = mapped_column(primary_key=True)
    occurred_at: Mapped[datetime] = mapped_column(primary_key=True)
    payload: Mapped[dict] = mapped_column(JSONB)

Partition key MUST be in the primary key.

Partition definitions (in migrations)

def upgrade():
    op.execute("""
        CREATE TABLE events_2026_01 PARTITION OF events
        FOR VALUES FROM ('2026-01-01') TO ('2026-02-01')
    """)
    op.execute("""
        CREATE TABLE events_2026_02 PARTITION OF events
        FOR VALUES FROM ('2026-02-01') TO ('2026-03-01')
    """)

pg_partman (automation)

CREATE EXTENSION pg_partman;

SELECT partman.create_parent(
    p_parent_table := 'public.events',
    p_control      := 'occurred_at',
    p_type         := 'range',
    p_interval     := 'monthly',
    p_premake      := 4
);

Maintenance job creates new partitions / drops old:

SELECT partman.run_maintenance();

Schedule via pg_cron.

List partitioning

__table_args__ = {"postgresql_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');

Hash partitioning

__table_args__ = {"postgresql_partition_by": "HASH (user_id)"}
CREATE TABLE messages_p0 PARTITION OF messages FOR VALUES WITH (modulus 8, remainder 0);
-- ... 8 total

Pruning verification

explain = await session.execute(
    text("EXPLAIN " + str(stmt.compile(compile_kwargs={"literal_binds": True})))
)
for row in explain:
    print(row[0])
# look for Append over only matching partitions

Drop old partition

DROP TABLE events_2024_01;        -- instant

vs DELETE — hours on big tables.

Common mistakes

  • Partitioning <100GB tables — overhead with no benefit.
  • Wrong partition key — no pruning.
  • Manual partitioning without pg_partman — forget; inserts fail.
  • Hundreds of partitions — planner overhead.

Read this next

If you want my pg_partman + retention script, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .