DatabasesAugust 25, 2026 · 9 min

PostgreSQL performance tuning for SaaS: the indexes that fix your slow endpoints

When an endpoint is slow, engineers reach for caches and bigger servers. Nine times out of ten the real cause is in the database: a missing index turning a lookup into a full-table scan, or an N+1 pattern firing hundreds of queries per request. Fix the database and the endpoint gets fast for free. Here's how I find and fix it.

Read EXPLAIN before you guess

Never optimize blind. EXPLAIN (ANALYZE, BUFFERS) shows what Postgres actually does with a query. The signals that matter: a Seq Scan on a big table where you filter or join (you're missing an index), a huge gap between estimated and actual rows (stale statistics or a bad query shape), and Sort/Hash steps spilling to disk. The plan tells you where the time goes — you stop guessing.

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM invoices
WHERE tenant_id = $1 AND status = 'open'
ORDER BY created_at DESC LIMIT 50;
-- Seq Scan on invoices  (actual rows=2,000,000)  <-- missing index

-- Fix: a composite index matching the filter + sort
CREATE INDEX CONCURRENTLY idx_invoices_tenant_status_created
  ON invoices (tenant_id, status, created_at DESC);

Index the way you query

  • Composite index column order = equality filters first, then the range/sort column (tenant_id, status, created_at).
  • A well-ordered composite index can satisfy the WHERE and the ORDER BY at once — no separate sort step.
  • Partial indexes for skewed data (WHERE status = 'open') are smaller and faster when you mostly query one slice.
  • Don't over-index: every index slows writes and costs storage — add them for real query patterns, not hypotheticals.

The N+1 that hides in your ORM

A dashboard that loads 50 rows and then lazily loads each row's relations fires 51 queries. Individually fast, collectively a disaster — and invisible until you count queries per request. The fix is to fetch related data in a single query (a join or a batched IN query / your ORM's eager-load), turning 51 round-trips into one or two.

Log query counts per request in development. 'This page runs 200 queries' is the most common performance bug in SaaS dashboards, and it never shows up in a single-query benchmark.

The rest of the toolkit

Keep statistics fresh (autovacuum/ANALYZE) so the planner makes good choices, use connection pooling (a serverless app opening a connection per invocation will exhaust Postgres), select only the columns you need instead of SELECT *, and paginate with keyset pagination on large tables instead of large OFFSETs that scan and discard rows.

Key takeaways

  • Diagnose with EXPLAIN (ANALYZE, BUFFERS) — Seq Scans and bad row estimates point straight at the fix.
  • Build composite indexes matching your filter + sort; use partial indexes for skewed slices.
  • Hunt N+1 queries — count queries per request and eager-load relations.
  • Pool connections, keep stats fresh, select only needed columns, and use keyset pagination at scale.

FAQ

How do I speed up a slow PostgreSQL query in my SaaS?

Run EXPLAIN (ANALYZE, BUFFERS) to see the plan. A sequential scan on a filtered/joined large table means you need an index — build a composite index whose column order matches your equality filters then your sort column. Also check for N+1 query patterns from your ORM, which are a more common cause than the query itself.

Does adding more indexes always make things faster?

No. Indexes speed reads but slow every write and consume storage, and too many can confuse the planner. Add indexes for your real, measured query patterns — confirmed with EXPLAIN — not for hypothetical ones, and prefer a single well-ordered composite index over several redundant single-column ones.

AA
Ali Asghar

Senior software engineer & technical lead — 6+ years shipping production multi-tenant SaaS, payments and AI integration in Next.js, Node & TypeScript.

Keep reading
Multi-tenant SaaS: RLS vs schema-per-tenant vs database-per-tenantHow I built a real-time, cookieless analytics console in Next.js