Back to Engineering Blog
Database Engineering22 min readApril 10, 2026
Scaling PostgreSQL to 1 Billion Rows

Scaling PostgreSQL to 1 Billion Rows

A highly technical breakdown of indexing strategies, query optimization, and table partitioning required to maintain sub-10ms query times on massive relational datasets.

ZG
Zohaib Global Engineering
Lead Infrastructure Team
Share Article

The Relational Limit

PostgreSQL is arguably the most powerful open-source relational database in existence. It handles complex joins, strict ACID compliance, and advanced JSONB querying with extreme elegance. However, physics is physics.

As tables approach the billion-row mark (or roughly the 500GB+ barrier for a single table), the rules of the game change entirely. Naive `SELECT * WHERE user_id = 5` queries that used to take 2 milliseconds suddenly cause catastrophic CPU spikes. Disk I/O saturates. Application servers drop connections because they are waiting on database locks. Your entire system grinds to a halt.

"Scaling a database is not about writing better code. It is an exercise in managing physical disk latency and RAM constraints. Every optimization at scale is a deliberate manipulation of how data is laid out on a physical SSD."

Advanced Indexing Strategies

Most developers understand the basic B-Tree index. A B-Tree is fantastic for looking up a single unique ID. But what happens when you need to query time-series data? For example, fetching all financial transactions that occurred in the last 15 minutes out of a table containing 10 years of history.

Block Range Indexes (BRIN)

If your data is naturally ordered (like timestamps on log entries or created_at fields), a B-Tree index will consume a massive amount of RAM—often larger than the actual table data itself. This causes the index to fall out of memory, destroying performance.

Instead, we implement BRIN (Block Range Indexes). BRIN indexes do not store every single row. They only store the minimum and maximum values for a physical "block" of data on the disk. They are incredibly small. A 50GB B-Tree index can often be replaced by a 5MB BRIN index, instantly freeing up memory for query caching.

GIN and GiST Indexes for JSONB

When working with massive JSONB payloads, searching inside the JSON object without an index forces Postgres to do a sequential scan (reading every single row on disk). We use GIN (Generalized Inverted Index) to map all keys and values inside the JSON structure, allowing sub-millisecond lookups even when searching deep within nested JSON arrays.

Query Pro-Tip

Never run a `COUNT(*)` on a massive Postgres table. MVCC (Multi-Version Concurrency Control) forces Postgres to physically scan rows to check if they are visible to your transaction. Instead, use `EXPLAIN` estimates for dashboard counters, or maintain a separate trigger-updated summary table.


Declarative Table Partitioning

When a table hits 100GB+, routine maintenance like `VACUUM` becomes a nightmare. Index rebuilds take hours and lock production traffic. To solve this, we use Declarative Partitioning.

Hardware Circuit Board

Instead of one massive `transactions` table, we instruct Postgres to split the table by month at the disk level:

-- Creating a partitioned table
CREATE TABLE transactions (
    id UUID NOT NULL,
    amount DECIMAL NOT NULL,
    created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);

-- Creating physical partitions
CREATE TABLE transactions_2026_01 PARTITION OF transactions 
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
    
CREATE TABLE transactions_2026_02 PARTITION OF transactions 
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

The application code still queries the main `transactions` table, but Postgres performs Partition Pruning. If the query includes `WHERE created_at = '2026-02-15'`, Postgres completely ignores all other partitions on the disk. It only scans the tiny February partition. This turns a multi-second query into a 5ms query instantly.


Connection Pooling: PgBouncer

PostgreSQL handles connections by spawning a new OS process for every connected client. Each process consumes roughly 10MB of RAM. If you have 50 serverless lambda functions scaling up to 1,000 instances during a traffic spike, they will attempt to open 1,000 direct database connections. This will immediately consume 10GB of RAM just for connection overhead, crashing the database.

At Zohaib Global, any high-traffic application is placed behind PgBouncer.

PgBouncer sits between the application and the database. It accepts 10,000 lightweight incoming connections from the application, but multiplexes them across a tiny pool of just 50 actual database connections. It holds the transaction in memory, routes it to an available database connection, and returns the result. This stabilizes database CPU and memory, ensuring that even under extreme DDoS-level traffic, the database engine remains calm and performant.

The Art of Database Scaling

Throwing larger EC2 instances at a slow database is a temporary bandage that costs thousands of dollars a month. True database scaling is an architectural art form. If your application is suffering from deadlocks, extreme latency, or failing vacuums, contact Zohaib Global. We engineer relational databases that bend physics.

Topics Covered

#PostgreSQL#Database Tuning#PgBouncer#Data Partitioning#Performance

Database buckling under load?

If your application is suffering from deadlocks or catastrophic query times, we engineer relational databases that bend physics.

Request a Database Audit