DEV Community

Cover image for Database Scaling Indexing, replication, read replicas, sharding
Tanu Priya
Tanu Priya

Posted on

Database Scaling Indexing, replication, read replicas, sharding

Imagine you've built a simple application.
The architecture looks like this:

Client
  ↓
API Server
  ↓
Database
Enter fullscreen mode Exit fullscreen mode

At the beginning, everything works perfectly.

You have a few hundred users. Your database has a few thousand rows. Queries return quickly, and your infrastructure is easy to understand.

Then your application starts growing.

A few hundred users become 10,000.

10,000 becomes 100,000.

Your database grows from thousands of rows to millions, and eventually hundreds of millions.

The application is now receiving thousands of requests every second.

And suddenly, the database that was perfectly fine in the beginning becomes one of the biggest bottlenecks in your system.

Queries become slower.

CPU usage increases.

Memory gets exhausted.

Database connections start piling up.

Read and write operations begin competing for the same resources.

At this point, simply adding more API servers may not solve the problem.

You might have:

          ┌─────────┐
          │  API 1  │
          └────┬────┘
               │
          ┌────┴────┐
          │         │
      ┌───▼───┐ ┌──▼────┐
      │ API 2 │ │ API 3 │
      └───┬───┘ └───┬───┘
          │         │
          └────┬────┘
               ↓
          ┌──────────┐
          │ Database │
          └──────────┘
Enter fullscreen mode Exit fullscreen mode

The API layer has been scaled horizontally.

But all those API servers are still hitting the same database.

So the database remains the bottleneck.

This is where database scaling becomes important.

But database scaling isn't one single technique.

Depending on the problem, you might use:

  • Query optimization
  • Indexing
  • Caching
  • Vertical scaling
  • Replication
  • Read replicas
  • Sharding

The important system-design skill is knowing which technique solves which problem.


What Is Database Scaling?

Database scaling means improving a database system's ability to handle increasing:

  • Traffic
  • Data volume
  • Concurrent connections
  • Read operations
  • Write operations
  • Query complexity

There are two fundamental ways to scale a database:

Vertical scaling and horizontal scaling.

Understanding this distinction is important because almost every other database-scaling technique builds on these ideas.


Vertical Scaling: Make the Database Bigger

The simplest way to scale a database is to make the existing server more powerful.

Suppose your database currently has:

8 CPU cores
32 GB RAM
500 GB SSD
Enter fullscreen mode Exit fullscreen mode

You could upgrade it to:

32 CPU cores
128 GB RAM
2 TB SSD
Enter fullscreen mode Exit fullscreen mode

The architecture doesn't fundamentally change:

API
 ↓
Database
Enter fullscreen mode Exit fullscreen mode

You're simply giving the database more resources.

This is called vertical scaling, or scaling up.

Vertical scaling has a major advantage:

It's simple.

You don't need to redesign how your application accesses data.

You don't need to decide which server stores which records.

You don't need to coordinate multiple database instances.

You simply give the existing database more resources.

For many applications, this is actually a perfectly reasonable first step.


Why Not Scale Vertically Forever?

Because eventually you hit limits.

There is a maximum amount of CPU, RAM, storage, and network capacity that a single machine can provide.

And even if a larger machine exists, it can become extremely expensive.

For example:

Small Database
      ↓
More CPU/RAM
      ↓
Larger Database
      ↓
Even More CPU/RAM
      ↓
Very Expensive Database
      ↓
Physical / Cost Limit
Enter fullscreen mode Exit fullscreen mode

At some point, you need to distribute the workload.

That's where horizontal scaling comes in.


Horizontal Scaling: Add More Machines

Instead of continuously making one machine bigger, horizontal scaling distributes work across multiple machines.

For example:

              API
               ↓
        ┌──────────────┐
        │ Load Balancer│
        └───────┬──────┘
                ↓
       ┌────────┼────────┐
       ↓        ↓        ↓
   Database  Database  Database
      1         2         3
Enter fullscreen mode Exit fullscreen mode

Now multiple machines participate in handling the workload.

This can provide much greater scalability.

But there's a catch.

Distributed databases are significantly more complicated than a single database.

You now have to think about:

  • Data distribution
  • Consistency
  • Replication
  • Network failures
  • Synchronization
  • Transactions
  • Routing
  • Failover

So horizontal scaling shouldn't be your first response to every performance problem.

A good system-design approach is:

Start simple, measure the bottleneck, optimize it, and introduce distributed complexity only when necessary.

One of the simplest optimizations to start with is indexing.


1. Indexing: Make Database Queries Faster

Imagine you have a users table containing 20 million users.

users

id | name | email
----------------------------
1  | John | john@example.com
2  | Alex | alex@example.com
3  | Mike | mike@example.com
...
20,000,000 rows
Enter fullscreen mode Exit fullscreen mode

Your application frequently needs to find a user by email:

SELECT *
FROM users
WHERE email = 'john@example.com';
Enter fullscreen mode Exit fullscreen mode

Without an appropriate index, the database may need to inspect a large number of rows to find the matching record.

This can become increasingly expensive as the table grows.

An index gives the database an additional data structure that helps it locate matching records more efficiently.

Conceptually:

Without Index

Query
  ↓
Database
  ↓
Check many rows
  ↓
Find matching row
Enter fullscreen mode Exit fullscreen mode

With an index:

Query
  ↓
Index
  ↓
Locate matching row
  ↓
Return data
Enter fullscreen mode Exit fullscreen mode

For example:

CREATE INDEX idx_users_email
ON users(email);
Enter fullscreen mode Exit fullscreen mode

Now the database has an index specifically designed to help queries involving the email column.


Why Indexes Can Be So Powerful

Suppose your table contains:

50 million users
Enter fullscreen mode Exit fullscreen mode

And you frequently run:

WHERE email = ?
Enter fullscreen mode Exit fullscreen mode

Searching through the entire table for every request would be wasteful.

An index can dramatically reduce the amount of data the database needs to examine.

This is why indexes are often one of the first things engineers investigate when a query becomes slow.

Before adding replicas or sharding, you should first ask:

Is the database actually doing unnecessary work because the query isn't optimized?

Sometimes the answer is yes.

And if it is, adding more database servers may simply be solving the wrong problem.


Indexes Aren't Free

It's easy to think:

"If indexes make reads faster, why not create an index on every column?"

Because indexes have costs.

Indexes consume storage.

They also need to be maintained when data changes.

For example, when you execute:

INSERT INTO users (...)
Enter fullscreen mode Exit fullscreen mode

the database may need to update multiple indexes.

Similarly, updates and deletes can require index maintenance.

So there is a trade-off:

More Indexes
     ↓
Potentially faster reads
     ↓
More storage
     ↓
More write overhead
Enter fullscreen mode Exit fullscreen mode

This means indexing should be intentional.

You want indexes that support your application's important query patterns.


Composite Indexes

Sometimes your queries filter using multiple columns.

For example:

SELECT *
FROM orders
WHERE user_id = 42
AND status = 'completed';
Enter fullscreen mode Exit fullscreen mode

In such cases, a composite index can sometimes be useful:

CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
Enter fullscreen mode Exit fullscreen mode

The important point isn't memorizing SQL syntax.

The important point is understanding that indexes should match how your application actually queries the database.

This is why database performance tuning usually starts by examining real queries and query execution plans rather than randomly adding indexes.


2. Replication: Create Multiple Copies of Your Database

Indexing improves how efficiently one database handles queries.

But what if the database simply has too many requests?

Imagine your application receives:

100,000 requests
Enter fullscreen mode Exit fullscreen mode

And:

90,000 → Reads
10,000 → Writes
Enter fullscreen mode Exit fullscreen mode

The database has to handle both workloads.

What if we could create additional copies of the database?

That's where replication comes in.

Replication means maintaining copies of database data on multiple database instances.

A basic architecture looks like:

                 Primary
                    │
             Replication
              ┌─────┴─────┐
              ↓           ↓
          Replica 1    Replica 2
Enter fullscreen mode Exit fullscreen mode

The primary database receives changes.

Those changes are then replicated to other database instances.

Now your system has multiple copies of the same underlying data.


Why Replication Is Useful

Replication can provide several benefits.

1. Read Scaling

Multiple database instances can serve read traffic.

2. Availability

If one database instance fails, another copy may be available depending on the replication and failover setup.

3. Disaster Recovery

Maintaining additional copies of data can help with recovery strategies.

4. Geographic Distribution

In some architectures, replicas can be placed closer to users in different regions to reduce latency.

Replication therefore isn't only a performance technique.

It can also be part of a system's availability and reliability strategy.


3. Read Replicas: Scale Read-Heavy Applications

Replication becomes particularly useful when your application performs significantly more reads than writes.

Consider a social media application.

A user might perform a few write operations:

Create Post
Like Post
Follow User
Enter fullscreen mode Exit fullscreen mode

But those actions can generate many more reads:

Load Feed
View Post
View Profile
Load Comments
Load Notifications
Enter fullscreen mode Exit fullscreen mode

Imagine:

100,000 database operations

90,000 → Reads
10,000 → Writes
Enter fullscreen mode Exit fullscreen mode

Sending all 100,000 operations to one database can create unnecessary pressure.

Instead, we can separate the workloads.

                    API
                     │
          ┌──────────┴──────────┐
          ↓                     ↓
       Writes                  Reads
          ↓                     ↓
      ┌─────────┐       ┌─────────────┐
      │ Primary │       │ Read Router │
      └────┬────┘       └──────┬──────┘
           │                   │
      Replication         ┌────┴────┐
           │              ↓         ↓
     ┌─────┴─────┐   Replica 1  Replica 2
     ↓           ↓
 Replica 1    Replica 2
Enter fullscreen mode Exit fullscreen mode

Conceptually:

Writes → Primary

Reads → Read Replicas
Enter fullscreen mode Exit fullscreen mode

Now the read workload can be distributed across multiple servers.


Read Replicas Don't Mean Instant Consistency

This is one of the most important things to understand.

Suppose a user changes their name:

User changes name
       ↓
Primary
       ↓
Replication
       ↓
Replica
Enter fullscreen mode Exit fullscreen mode

Replication takes some time.

For a short period, the databases could contain:

Primary:
name = "Alex"

Replica:
name = "John"
Enter fullscreen mode Exit fullscreen mode

This is called replication lag.

If the application immediately reads from the replica after writing to the primary, it might temporarily receive the old value.

This leads to a common distributed-systems trade-off:

Do we need the newest data immediately, or is slightly stale data acceptable?

For some applications, stale reads are fine.

For example:

View count
Trending products
Analytics dashboard
Recommendation results
Enter fullscreen mode Exit fullscreen mode

A small delay may not matter.

But for something like:

Bank balance
Payment status
Order confirmation
Enter fullscreen mode Exit fullscreen mode

reading stale data can be much more problematic.

This means read replicas aren't simply:

"Add more databases and everything becomes faster."

You also need to understand the consistency requirements of your application.


4. Sharding: Split the Dataset

Now imagine the database contains:

5 billion users
Enter fullscreen mode Exit fullscreen mode

Even if you have read replicas, you may still have a fundamental problem.

Every replica contains the entire dataset.

That means you're copying:

5 billion users
Enter fullscreen mode Exit fullscreen mode

to every replica.

What if the dataset itself is too large for a single database machine?

This is where sharding becomes useful.

Sharding means splitting a dataset into multiple independent partitions called shards.

Instead of:

One Huge Database

┌─────────────────────────────┐
│ Users 1 → 5 Billion        │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

you could have:

                 Application
                      ↓
                 Shard Router
                /      |      \
               ↓       ↓       ↓
          ┌────────┐ ┌────────┐ ┌────────┐
          │Shard 1 │ │Shard 2 │ │Shard 3 │
          └────────┘ └────────┘ └────────┘
Enter fullscreen mode Exit fullscreen mode

Each shard contains only a portion of the data.

For example:

Shard 1 → Users 1 - 1,000,000
Shard 2 → Users 1,000,001 - 2,000,000
Shard 3 → Users 2,000,001 - 3,000,000
Enter fullscreen mode Exit fullscreen mode

Now no single database needs to store the entire dataset.


Choosing a Shard Key

One of the most important decisions in sharding is choosing the shard key.

The shard key determines how records are distributed.

For example:

user_id
Enter fullscreen mode Exit fullscreen mode

could be used.

The application or routing layer can determine which shard owns a particular user.

Conceptually:

Request for user 1250
        ↓
Shard Router
        ↓
Shard 2
Enter fullscreen mode Exit fullscreen mode

This sounds straightforward.

But choosing a poor shard key can create serious problems.


The Hot Shard Problem

Imagine you distribute data across three shards.

You expect:

Shard 1 → 33% traffic
Shard 2 → 33% traffic
Shard 3 → 33% traffic
Enter fullscreen mode Exit fullscreen mode

But in reality:

Shard 1 → 85% traffic
Shard 2 → 10% traffic
Shard 3 → 5% traffic
Enter fullscreen mode Exit fullscreen mode

Now Shard 1 becomes a bottleneck.

This is called a hot shard.

You technically have multiple database servers, but most of the traffic is concentrated on one of them.

So sharding isn't simply:

"Split the database into multiple servers."

It's:

Distribute data and workload in a way that keeps the shards reasonably balanced.

This is one of the hardest parts of designing a sharded database.


Different Ways to Choose a Shard Key

There are several approaches.

For example, you might shard based on:

user_id
region
tenant_id
organization_id
Enter fullscreen mode Exit fullscreen mode

The correct choice depends heavily on the application's access patterns.

For a multi-tenant SaaS application, you might consider:

tenant_id
Enter fullscreen mode Exit fullscreen mode

because many queries are naturally scoped to one organization.

For another application, user_id may be a better fit.

The important lesson is:

Choose the shard key based on how the application accesses data, not just how the data looks.


Sharding Makes Some Queries Harder

Before sharding, you might have:

SELECT *
FROM orders
WHERE user_id = 42;
Enter fullscreen mode Exit fullscreen mode

The database knows where everything is because there's only one database.

After sharding, the system first needs to determine:

Which shard contains user 42?
Enter fullscreen mode Exit fullscreen mode

Then it can execute the query there.

This is easy if the query includes the shard key.

But what happens if you ask:

SELECT *
FROM orders
WHERE status = 'pending';
Enter fullscreen mode Exit fullscreen mode

without knowing which shard contains the relevant records?

The system may need to query multiple shards.

Conceptually:

             Query
               ↓
        ┌──────┼──────┐
        ↓      ↓      ↓
      Shard 1 Shard 2 Shard 3
        ↓      ↓      ↓
        └──────┼──────┘
               ↓
          Merge Results
Enter fullscreen mode Exit fullscreen mode

This is called a scatter-gather pattern.

As the number of shards increases, such queries can become expensive.

This is one reason sharding should be introduced carefully.


Sharding and Transactions

Transactions can also become more complicated.

With a single database:

Transaction
     ↓
Database
Enter fullscreen mode Exit fullscreen mode

Everything is happening within one system.

With sharding:

Transaction
   ↙      ↘
Shard 1  Shard 2
Enter fullscreen mode Exit fullscreen mode

Now a single logical operation may involve multiple database instances.

Coordinating transactions across multiple shards can introduce significant complexity and performance costs.

This doesn't mean distributed transactions are impossible.

It means they require much more careful architecture.

A good sharding strategy often tries to design the data model so that most important operations can remain within a single shard.


Read Replicas vs Sharding

These concepts are frequently confused.

They solve different problems.

Read Replicas

Replicas contain copies of the same data.

Primary
   ↓
Replica 1
Replica 2
Replica 3
Enter fullscreen mode Exit fullscreen mode

Their main purpose is often to help handle more read traffic and improve availability.

Sharding

Shards contain different portions of the data.

Shard 1 → Part of dataset
Shard 2 → Part of dataset
Shard 3 → Part of dataset
Enter fullscreen mode Exit fullscreen mode

The goal is to distribute the dataset and workload.

A useful way to remember it:

Replication
→ Same data, multiple copies

Sharding
→ Different data, multiple partitions
Enter fullscreen mode Exit fullscreen mode

And large systems can use both.


Combining Sharding and Replication

A large system might look like:

                    Application
                         ↓
                   Shard Router
                 /       |       \
                ↓        ↓        ↓
             Shard 1  Shard 2  Shard 3
                │        │        │
             ┌──┴──┐  ┌──┴──┐  ┌──┴──┐
             ↓     ↓  ↓     ↓  ↓     ↓
          Primary Replica Primary Replica Primary Replica
Enter fullscreen mode Exit fullscreen mode

Each shard owns a portion of the data.

Each shard can then have replicas.

This provides two different forms of scaling:

Sharding
→ Distributes the dataset

Replication
→ Creates copies for availability/read scaling
Enter fullscreen mode Exit fullscreen mode

But notice how much more complicated the architecture has become.

That's why you shouldn't start here unless the problem actually requires it.


Where Does Caching Fit?

Caching is another important database-scaling technique.

Suppose your application repeatedly requests:

GET /products/trending
Enter fullscreen mode Exit fullscreen mode

If the same result can be reused, you may not need to query the database every time.

Instead:

Client
  ↓
API
  ↓
Cache
  ↓
Database
Enter fullscreen mode Exit fullscreen mode

The cache absorbs repeated requests.

This is fundamentally different from sharding.

Caching tries to avoid database work.

Read replicas try to distribute read work.

Sharding tries to distribute data and workload.

Indexing tries to make individual queries more efficient.

These techniques solve different problems.


A Useful Comparison

Technique Main Purpose What It Helps With
Indexing Faster queries Slow database queries
Caching Avoid database requests Repeated reads
Replication Multiple copies Availability and redundancy
Read Replicas Distribute reads Read-heavy traffic
Sharding Split data Huge datasets and workload
Vertical Scaling Bigger machine Resource limitations

This table is useful, but the real skill is recognizing which problem you're actually facing.


A Practical Database Scaling Journey

Most applications don't start with a distributed database architecture.

They evolve.

A realistic journey might look like this.

Stage 1: Start Simple

API
 ↓
Database
Enter fullscreen mode Exit fullscreen mode

Don't over-engineer the system.

Get the application working.

Measure its performance.

Understand the workload.


Stage 2: Optimize Queries

Before adding infrastructure, check whether queries themselves are inefficient.

Look at:

  • Slow queries
  • Missing indexes
  • Unnecessary joins
  • Large result sets
  • Repeated queries

Sometimes a poorly written query is the real bottleneck.


Stage 3: Add Indexes

Once you've identified frequently used query patterns, create appropriate indexes.

Slow Query
   ↓
Analyze
   ↓
Add Appropriate Index
   ↓
Faster Query
Enter fullscreen mode Exit fullscreen mode

This is often one of the cheapest scaling improvements.


Stage 4: Add Caching

If the same data is requested repeatedly:

API
 ↓
Cache
 ↓
Database
Enter fullscreen mode Exit fullscreen mode

Caching can prevent many requests from reaching the database at all.


Stage 5: Scale the Database Vertically

If the database needs more CPU, RAM, storage, or network capacity, upgrading the database server may be enough.

This is still relatively simple.


Stage 6: Add Read Replicas

If reads become the dominant workload:

Writes → Primary
Reads  → Replicas
Enter fullscreen mode Exit fullscreen mode

Now read traffic can be distributed.


Stage 7: Introduce Sharding

Eventually, the dataset or workload may become too large for one database cluster.

Now you may consider:

Application
     ↓
Shard Router
  /   |   \
 ↓    ↓    ↓
S1   S2   S3
Enter fullscreen mode Exit fullscreen mode

At this point, you've entered a much more complex distributed architecture.


Don't Scale Before Measuring

One of the biggest mistakes engineers make is introducing scaling techniques before understanding the actual bottleneck.

Imagine your database is slow.

You might immediately think:

"We need sharding."

But the real problem could be:

Missing index
Enter fullscreen mode Exit fullscreen mode

Or:

N+1 queries
Enter fullscreen mode Exit fullscreen mode

Or:

Unnecessary database requests
Enter fullscreen mode Exit fullscreen mode

Or:

A badly optimized query
Enter fullscreen mode Exit fullscreen mode

Or:

Connection pool misconfiguration
Enter fullscreen mode Exit fullscreen mode

Adding more database servers won't necessarily fix those problems.

That's why database scaling should begin with measurement.

Look at:

  • Query latency
  • CPU usage
  • Memory usage
  • Disk I/O
  • Connection count
  • Read/write ratio
  • Query frequency
  • Cache hit rate
  • Replication lag

The goal is to find the actual bottleneck.


A Simple Decision Framework

When your database starts struggling, ask these questions.

Are individual queries slow?

Start with:

Query optimization
      +
Indexing
Enter fullscreen mode Exit fullscreen mode

Are the same queries being repeated?

Consider:

Caching
Enter fullscreen mode Exit fullscreen mode

Are reads overwhelming the primary database?

Consider:

Read Replicas
Enter fullscreen mode Exit fullscreen mode

Is the database server running out of CPU or memory?

Consider:

Vertical Scaling
Enter fullscreen mode Exit fullscreen mode

Is the dataset becoming too large for a single database?

Consider:

Sharding
Enter fullscreen mode Exit fullscreen mode

Do you need additional copies for availability?

Consider:

Replication
Enter fullscreen mode Exit fullscreen mode

This gives you a much better approach than simply saying:

"My application is big, so I need sharding."


Common Database Scaling Mistakes

1. Adding Indexes Everywhere

Indexes can improve reads, but excessive indexes increase storage and write overhead.

Index based on actual query patterns.


2. Treating Read Replicas as Fully Synchronous

Replicas can have replication lag.

Your application needs to understand that a read from a replica may temporarily return older data.


3. Choosing a Poor Shard Key

A bad shard key can create hot shards and uneven traffic.

A distributed database is only useful if the workload is actually distributed.


4. Sharding Too Early

Sharding introduces significant complexity.

If a single database with good indexes and appropriate scaling can handle your workload, you may not need sharding yet.


5. Ignoring Writes

Read replicas are excellent for read-heavy workloads.

But they don't magically make writes disappear.

If your workload is heavily write-oriented, you need to investigate the write bottleneck separately.


6. Ignoring Consistency Requirements

Not every piece of data needs to be immediately consistent.

But some data absolutely does.

Understanding this difference is critical when using replicas and distributed databases.


The Bigger System Design Lesson

Database scaling isn't about collecting technologies.

You don't get bonus points for using:

Redis
+
Read Replicas
+
Sharding
+
Multiple indexes
Enter fullscreen mode Exit fullscreen mode

The goal is to solve the actual bottleneck.

A mature scaling strategy often looks like:

Measure
   ↓
Optimize
   ↓
Index
   ↓
Cache
   ↓
Scale vertically
   ↓
Replicate
   ↓
Add read replicas
   ↓
Shard when necessary
Enter fullscreen mode Exit fullscreen mode

Notice something important:

The architecture becomes more complicated as you move down the list.

That's intentional.

You want to use the simplest solution that solves the current problem.


Conclusion

Database scaling is not about making your database bigger just because your application is growing.

It's about understanding what is actually limiting your system.

If queries are slow, indexing and query optimization may be enough.

If the same data is requested repeatedly, caching can prevent unnecessary database work.

If your application is read-heavy, read replicas can distribute the read workload.

If you need multiple copies of your data for availability or redundancy, replication becomes important.

And if your dataset or workload becomes too large for a single database system, sharding can distribute the data across multiple machines.

The techniques solve different problems:

Indexing
→ Make queries more efficient

Caching
→ Avoid unnecessary database queries

Replication
→ Maintain multiple copies

Read Replicas
→ Scale read traffic

Sharding
→ Distribute data and workload

Vertical Scaling
→ Give the database more resources
Enter fullscreen mode Exit fullscreen mode

The most important lesson isn't knowing how to configure a read replica or create a shard.

Top comments (0)