“Our database is slow” is one of the most common technical complaints I hear in my fractional CTO engagements. It’s also one of the most frequently misdiagnosed. Teams assume they need a new database, a sharding strategy, or a migration to NoSQL. In reality, they usually need to look at their queries.

I’m not being dismissive. Database scaling is a real challenge at real scale. But in my experience, 80% of database performance problems at companies under $50M in revenue can be solved with indexes, query optimization, and basic caching — the boring stuff that doesn’t require a new architecture.

Start With the Queries

Before you scale anything, find out what’s actually slow. Every major database has a slow query log. Turn it on. Let it run for a week. Sort by total execution time (frequency × duration). The top 10 queries on that list are your optimization targets.

Common findings in these audits:

Missing indexes. A query scanning 10 million rows when a properly designed index would touch 50 rows. I’ve seen single index additions reduce query time from 8 seconds to 4 milliseconds. This isn’t an edge case — it’s the most common database performance fix across my client engagements.

N+1 queries. The application loads a list of 100 items, then makes a separate database query for each item’s related data. That’s 101 queries where one query with a JOIN would suffice. ORMs make this easy to introduce accidentally. Tools like bullet (Ruby), nplusone (Python), or query logging in your framework will identify them.

Unoptimized JOINs. Joining five tables with no attention to join order, missing indexes on join columns, or selecting columns you don’t need. Analyzing the query execution plan (EXPLAIN ANALYZE in PostgreSQL, EXPLAIN in MySQL) reveals exactly where the query is spending time.

Unbounded queries. SELECT * FROM events without pagination or date range filtering. As the table grows, these queries get linearly slower. Add pagination, add time-based filtering, and question whether you really need all those columns.

These optimizations are free. They require no infrastructure changes, no downtime, and no new technology. They typically yield 5-50x performance improvements.

Read Replicas

If your workload is read-heavy (most web applications are 80-95% reads), read replicas let you distribute read traffic across multiple database instances while the primary handles writes.

The implementation is straightforward: your database provider supports creating read replicas (RDS, Cloud SQL, and managed PostgreSQL services all make this trivial), and your application routes read queries to replicas and write queries to the primary.

The caveat: replication lag. There’s a delay — usually milliseconds, sometimes seconds under load — between a write to the primary and its appearance on replicas. Your application needs to handle this. The common pattern: after a user writes data (submits a form, makes a purchase), subsequent reads for that user go to the primary for a few seconds. Everyone else reads from replicas.

Read replicas can scale your read capacity almost linearly. Two replicas handle roughly twice the read traffic. For most applications, this buys you years of growth.

Caching

Not everything needs to hit the database. Data that’s read frequently but changes rarely — user profiles, product catalogs, configuration settings, permission lists — can be cached in Redis or Memcached.

The standard pattern: check the cache first. If the data is there, return it (a cache hit takes microseconds). If it’s not, query the database, store the result in the cache with a time-to-live (TTL), and return it. The cache evicts stale data automatically.

What to cache: Anything read more than 10x for every write. Session data, feature flags, aggregated dashboards, API responses from external services.

What not to cache: Real-time data where staleness causes business problems (account balances, inventory counts during checkout). When in doubt, don’t cache financial data.

Cache invalidation — knowing when to clear or update cached data — is famously one of the two hard problems in computer science. Start with TTL-based expiration and manual invalidation on writes. More sophisticated strategies (event-driven invalidation, cache-aside pattern) come later if needed.

A well-implemented caching layer typically reduces database load by 60-80%.

Vertical Scaling: The Underrated Option

Before you re-architect your database layer, consider just getting a bigger machine. Modern database instances can be massive — AWS offers RDS instances with 768 GB of RAM and 128 vCPUs. If your database fits in RAM, almost everything is fast.

Vertical scaling is unsexy. It doesn’t demonstrate architectural sophistication. But it works, it’s fast (usually an instance resize with a few minutes of downtime), and it buys you time to plan a real strategy if you genuinely need horizontal scaling later.

I’ve seen companies spend 6 months implementing sharding when a $2,000/month instance upgrade would have solved their problem for the next 2 years. The engineering time saved was worth far more than the infrastructure cost.

When You Actually Need Horizontal Scaling

If you’ve optimized queries, added read replicas, implemented caching, and upgraded your instance — and your database is still the bottleneck — you have a genuine horizontal scaling problem. This typically happens at significant scale: billions of rows, thousands of writes per second, or data volumes that exceed what a single machine can hold.

Sharding partitions your data across multiple database instances. Each shard holds a subset of the data (by customer, by region, by date range). Queries are routed to the right shard based on a sharding key. This is powerful but adds significant application complexity: cross-shard queries are expensive, rebalancing shards is operationally complex, and your application needs to be shard-aware.

Distributed databases like CockroachDB, Vitess (for MySQL), and Google Spanner handle sharding automatically. They provide the interface of a single database while distributing data across nodes. These are excellent solutions at the right scale, but they’re overkill — and expensive — for most applications.

Do the boring things first. They’ll take you further than you expect.


Related: Cloud Cost Optimization | The Prototype-to-Production Gap | When to Replatform or Rewrite