Partitioning & Sharding Strategy: Implementing Declarative Table Partitioning by Range or Hash to Keep Ultra-Large Datasets Performant

When a table crosses hundreds of millions of rows, the symptoms show up everywhere at once: VACUUM runs for hours, index bloat creeps into gigabytes of dead weight, a single DELETE locks up the application, and a query that used to return in 40ms now takes four seconds because the planner is scanning a heap it can no longer hold in memory. This is the point where "add another index" stops working and the real fix — dividing the table itself — becomes unavoidable.

This guide walks through the two techniques that solve this at different layers of the stack: declarative partitioning, which splits one logical table into smaller physical tables inside a single database instance, and sharding, which splits data across multiple database instances or nodes entirely. We'll cover range and hash partitioning in depth, show working DDL, and explain when partitioning alone is enough versus when you actually need to shard.

Partitioning vs. Sharding: Two Different Problems

It's easy to use these terms interchangeably, but they solve different bottlenecks.

Partitioning happens inside a single database engine. The table orders might look like one table to every query and every application, but under the hood it's actually a parent "routing" object with no rows of its own, pointing to a set of child tables (partitions) that each hold a slice of the data. The engine — PostgreSQL, MySQL, or otherwise — handles routing inserts to the right partition and pruning irrelevant partitions out of a query plan automatically. Your compute and storage are still on one machine (or one primary plus replicas); you're just organizing the data more intelligently.