Database latency is the most common bottleneck in modern web apps. While adding memory or CPU power to PostgreSQL works temporarily, structuring indexes correctly and analyzing queries is the only sustainable way to scale.
๐ฏ The Tool: EXPLAIN ANALYZE
Before changing anything, understand what Postgres is doing. Prefixing any SQL query with EXPLAIN (ANALYZE, BUFFERS) reveals the execution plan, actual timings, and buffer usage.
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE user_id = 142857
ORDER BY created_at DESC
LIMIT 10;Look for:
- โธSeq Scan (Sequential Scan): This means Postgres is scanning the entire table row-by-row. If your table has 10M rows, this will kill performance.
- โธIndex Scan: This means Postgres is querying a tree-based index directly. This is extremely fast (O(log N)).
๐ก Indexing Strategies
1. B-Tree Indexes
Standard index type, great for comparisons (=, >, <) and sorting.
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);2. Partial Indexes
Only index the rows that matter. If 90% of your orders are successfully processed, and you only query active/failed orders, write a partial index.
CREATE INDEX idx_orders_failed ON orders(id)
WHERE status = 'FAILED';3. GIN (Generalized Inverted Index)
Essential for indexing JSONB data types or performing text searches.
CREATE INDEX idx_products_tags ON products USING gin(tags);โก Connection Pooling with PgBouncer
PostgreSQL spawns a separate process for every client connection, costing around 10MB of memory. When your server handles hundreds of concurrent API requests, connection overhead will crash the DB. Integrating PgBouncer as a lightweight proxy layer pools database connections, reducing query latency by up to 40% and keeping database resource usage minimal.