Database Performance Strategies
Database performance is not determined by a single optimization. Query patterns, indexes, schema design, concurrency, replication, and data distribution all influence how a database behaves as traffic and data volume increase.
A useful optimization process starts by understanding the workload, then applying the least complex technique that removes the actual bottleneck. This guide covers six major database performance strategies in order: performance factors, indexing, denormalization, locking, replication, and sharding.
Table of Contents
- Factors Affecting Database Performance
- Database Indexing
- Denormalization
- Database Locking
- Database Replication
- Sharding
- Production Design Example
- A Practical Optimization Order
- Common Database Performance Mistakes
- Production Checklist
- Frequently Asked Questions
- Conclusion
Factors Affecting Database Performance
Database optimization should begin with the workload rather than with a particular technology. The same database architecture can perform extremely well for one workload and poorly for another.
Important factors include item size, item type, dataset size, concurrency, consistency expectations, geographic distribution, high-availability requirements, and workload variability.
Item Size and Type
Row or document size affects how much data must move through memory, storage, indexes, caches, and the network.
Compare two workloads:
Workload A
10,000 rows × 500 bytes
≈ 5 MB
Workload B
10,000 rows × 50 KB
≈ 500 MB
The number of records is identical, but the I/O profile is completely different.
Large JSON documents, binary objects, long text fields, and wide relational rows can reduce cache efficiency and increase read and write costs. Frequently accessed columns should not automatically share the same access path as large infrequently used payloads.
The type of data matters as well. Integer comparisons, text search, geospatial queries, JSON filtering, vector search, and time-series access patterns require different storage and indexing strategies.
Dataset Size
A database that fits almost entirely in memory behaves differently from one whose working set is much larger than available RAM.
Dataset: 20 GB
Working set: 8 GB
Memory: 32 GB
→ large portion of active data can remain cached
Dataset: 4 TB
Working set: 800 GB
Memory: 64 GB
→ storage access becomes much more important
Dataset growth also changes the cost of scans, indexes, backups, schema migrations, maintenance, and recovery.
Large tables may eventually benefit from partitioning so queries and maintenance can operate on smaller relevant portions of the data. What Is Database Partitioning? covers this strategy in detail.
Concurrency
Database load is not just requests per second. The number of operations executing concurrently matters because each operation can consume connections, CPU, memory, locks, and I/O capacity.
A rough relationship is:
Concurrency ≈ throughput × operation duration
At 5,000 database operations per second with an average duration of 10 ms:
5,000 × 0.010 ≈ 50 concurrent operations
If latency rises to 100 ms:
5,000 × 0.100 ≈ 500 concurrent operations
The traffic rate did not change, but the database now has roughly ten times as much concurrent work.
This is why latency degradation can trigger connection-pool exhaustion and additional queuing. What Is Connection Pooling? explains how bounded pools protect database concurrency.
Consistency Expectations
Consistency requirements affect which performance optimizations are safe.
A payment balance may require a strongly consistent read immediately after a write. A product recommendation count may tolerate being several seconds behind.
Payment authorization
→ fresh authoritative state required
Analytics dashboard
→ slightly stale data may be acceptable
Relaxing freshness requirements can enable replicas, caches, asynchronous processing, and precomputed views. Strong consistency can require coordination that increases latency.
The correct choice comes from business correctness requirements, not from performance alone.
Geographic Distribution
Network latency becomes part of database latency when applications and databases are geographically separated.
Application
Dallas
↓
10 ms database round trip
Application
Singapore
↓
180 ms database round trip
A request performing five sequential database round trips can amplify the difference substantially.
Multi-region databases, regional replicas, caching, and moving computation closer to data can reduce latency, but each approach introduces consistency and operational trade-offs.
High Availability Expectations
A database architecture designed for occasional maintenance downtime is different from one expected to survive infrastructure failures with minimal interruption.
High availability can require:
- replication;
- automatic failover;
- redundant storage;
- multiple availability zones;
- health detection;
- connection failover;
- tested recovery procedures.
These mechanisms consume additional resources and can affect write latency, especially when durability requires synchronous replication.
Workload Variability
Average traffic can hide the real capacity requirement.
Normal traffic: 2,000 queries/sec
Peak traffic: 15,000 queries/sec
Flash event: 40,000 queries/sec
Performance design should account for peaks, batch jobs, migrations, reporting workloads, traffic bursts, and retry storms.
A database running at 90% capacity during normal traffic has little room for these events.
Database Indexing
Indexes are often the highest-value database performance optimization because they let the database locate relevant rows without scanning an entire table.
Consider:
SELECT id, email
FROM customers
WHERE email = 'alice@example.com';
Without an appropriate index, a large table may require scanning many rows.
Table Scan
row 1
row 2
row 3
...
row 50,000,000
An index on email provides a separate search structure that points toward matching table data:
CREATE INDEX idx_customers_email
ON customers (email);
Email Index
↓
alice@example.com
↓
Matching row
The result can reduce an operation from examining millions of rows to traversing a relatively small index structure and retrieving a small number of matching records.
Index usefulness depends on the actual query. Columns involved in filtering, joining, ordering, and uniqueness constraints are common candidates, but indexing every column is not an optimization.
Composite and Covering Indexes
Production queries often filter using several columns:
SELECT id, total_cents, created_at
FROM orders
WHERE customer_id = $1
AND status = 'pending'
ORDER BY created_at DESC
LIMIT 50;
A composite index might be:
CREATE INDEX idx_orders_customer_status_created
ON orders (
customer_id,
status,
created_at DESC
);
The order of columns matters because indexes are ordered structures. A useful composite index should be designed around real query predicates and ordering requirements rather than individual columns in isolation.
Some databases also support covering techniques that allow a query to retrieve required values from the index without visiting the main table for every result.
For a deeper treatment of selectivity, composite indexes, range queries, ordering, covering indexes, and query plans, see What Is Database Indexing?.
Index Trade-Offs
Indexes make many reads faster, but they are not free.
Every additional index can add work to:
INSERT
UPDATE
DELETE
bulk loading
maintenance
storage
backup
replication
An update to an indexed column may require both table and index changes.
UPDATE customer email
↓
Update table data
+
Update email index
Unused and redundant indexes can therefore reduce write performance while consuming storage and memory.
Denormalization
Normalized relational schemas reduce duplication and make updates easier to keep consistent. The trade-off is that read paths may require joins across several tables.
Consider an order page that needs:
Orders
+
Customers
+
Products
+
Segments
↓
Customer Order View
A normalized query might join all four tables whenever the page is requested.
SELECT
o.id,
c.name,
p.name AS product_name,
s.name AS segment_name,
o.amount_cents
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN products p ON p.id = o.product_id
JOIN segments s ON s.id = c.segment_id
WHERE o.id = $1;
Denormalization deliberately stores some duplicated or precomputed data to make important reads simpler or cheaper.
For example, a read-optimized structure might contain:
customer_orders
id
product_name
segment_name
customer_name
order_id
order_amount
The read now needs fewer joins because information has already been combined.
When Denormalization Helps
Denormalization is most useful when a known read path is expensive enough to justify additional write complexity.
Typical examples include:
- high-volume dashboards;
- reporting tables;
- precomputed aggregates;
- search documents;
- materialized views;
- read models;
- frequently accessed duplicated attributes.
For example, calculating an account's lifetime statistics on every request:
SELECT
COUNT(*) AS orders,
SUM(total_cents) AS total
FROM orders
WHERE customer_id = $1;
may eventually become expensive across hundreds of millions of orders.
A maintained summary can make reads constant-sized:
customer_stats
customer_id
order_count
lifetime_total_cents
last_order_at
The Cost of Denormalization
The main cost is synchronization.
If a customer name is stored in five places, changing the name can require updating several representations or accepting temporary inconsistency.
Source record changes
↓
Derived copy A
Derived copy B
Derived copy C
Denormalization therefore exchanges read-time computation for write-time complexity and additional storage.
It should be driven by measured access patterns rather than used as a default schema-design strategy.
Database Locking
Database performance is also affected by how concurrent operations coordinate access to shared data.
Consider two transactions attempting to reduce the same account balance:
Initial balance = 40
Transaction A reads 40
Transaction B reads 40
A subtracts 20
B subtracts 20
Without appropriate concurrency control, application logic can produce incorrect results even when every individual query is fast.
Locks protect correctness, but excessive lock contention reduces throughput.
A transaction waiting on another transaction consumes time while making no useful progress:
Transaction A
holds row lock
↓
Transaction B
waits
↓
Transaction C
waits
↓
Queue grows
The objective is not to remove locking at any cost. It is to preserve correctness while keeping lock scope and transaction duration as small as practical.
Optimistic Concurrency
Optimistic concurrency assumes conflicts are relatively uncommon and detects whether data changed before applying an update.
A version column is a common implementation:
UPDATE accounts
SET balance = 20,
version = version + 1
WHERE id = 1
AND version = 7;
If another transaction already changed the row, the affected row count is zero.
Read version 7
↓
Another writer updates → version 8
↓
UPDATE ... WHERE version = 7
↓
0 rows changed
↓
Conflict detected
The application can reject or retry the operation according to business semantics.
Pessimistic Locking
Pessimistic locking acquires a lock before performing work that must have exclusive access to a row or resource.
BEGIN;
SELECT balance
FROM accounts
WHERE id = 1
FOR UPDATE;
UPDATE accounts
SET balance = balance - 20
WHERE id = 1;
COMMIT;
This can be appropriate when conflicts are likely or when a workflow requires a stable locked row.
The downside is waiting. A transaction holding the lock for 500 ms prevents competing transactions from updating that row for roughly the same period.
Reducing Lock Contention
Important techniques include:
- keeping transactions short;
- avoiding external API calls inside transactions;
- updating rows in a consistent order;
- indexing predicates used by updates and deletes;
- reducing unnecessary transaction scope;
- avoiding hot rows when possible;
- monitoring lock waits and deadlocks.
Database locking and transaction behavior are covered in more detail in Database Locks and Transactions.
Database Replication
Replication maintains additional copies of database data on other database instances or nodes.
A common architecture uses one primary for writes and replicas for reads:
Primary
Read + Write
/ \
/ \
Replication Replication
↓ ↓
Replica A Replica B
Read Only Read Only
If the primary is overloaded mainly by reads, moving appropriate queries to replicas can increase total read capacity.
Read Scaling with Replicas
Suppose a database receives:
Writes: 5,000/sec
Reads: 60,000/sec
The primary must process all writes, but many reads may be distributed:
Primary
5,000 writes/sec
10,000 reads/sec
Replica A
25,000 reads/sec
Replica B
25,000 reads/sec
This reduces read pressure on the primary and creates separate capacity for expensive reporting or analytical workloads.
Replication is particularly useful when the workload is read-heavy because replicas do not fundamentally remove the primary's write workload.
Replication Lag
Replicas can be behind the primary.
Primary:
order.status = 'paid'
Replica:
order.status = 'pending'
This creates a read-after-write problem:
1. Client writes to primary
2. COMMIT succeeds
3. Client immediately reads replica
4. Old value returned
Freshness-sensitive reads may need to stay on the primary for a period, wait for a known replication position, or use another consistency-aware routing strategy.
Replication therefore improves read scalability only when the application understands the consistency consequences. What Is Database Replication? covers the underlying architecture and trade-offs.
Sharding
Replication creates additional copies of the same dataset. Sharding divides a dataset so different database nodes own different subsets.
For example, users might be distributed using a hash of user_id:
user_id → hash → shard
User 1001 → Shard A
User 1002 → Shard C
User 1003 → Shard B
Each shard stores only part of the total dataset and processes only part of the workload.
This enables horizontal database scaling when a single database instance can no longer provide enough storage, write throughput, or compute capacity.
Choosing a Shard Key
The shard key determines where data lives and therefore affects almost every operation in the system.
A strong shard key generally aims for:
- balanced data distribution;
- balanced request distribution;
- efficient routing;
- few cross-shard operations;
- stable growth characteristics.
For a multi-tenant application, tenant_id can be attractive when most operations stay within one tenant:
tenant_id
↓
Shard Router
↓
One target shard
However, a few very large tenants can create severe imbalance.
Hot Shards
Evenly distributing storage does not guarantee evenly distributed traffic.
Shard A → 10% traffic
Shard B → 15% traffic
Shard C → 65% traffic ← hot
Shard D → 10% traffic
A shard key based on geography, tenant, time, or another skewed attribute can create hotspots.
The design should therefore consider both current and future access distribution, not just row count.
Cross-Shard Operations
Queries become more complicated when the shard key is not known.
Instead of:
Request
↓
Shard B
↓
Result
the system may need:
Request
↓
Shard A ─┐
Shard B ─┼→ merge results
Shard C ─┤
Shard D ─┘
Cross-shard joins, aggregations, transactions, unique constraints, and pagination are significantly harder than their single-database equivalents.
Sharding is therefore normally a later-stage scaling technique, not the first response to a slow database. What Is Database Sharding? explains shard-key design and operational trade-offs in more detail.
Production Design Example
Consider an e-commerce platform with 80 million customers and 2 billion orders.
Production traffic is:
Normal:
8,000 writes/sec
45,000 reads/sec
Peak:
15,000 writes/sec
120,000 reads/sec
The first performance problem is the customer order-history endpoint:
SELECT
id,
status,
total_cents,
created_at
FROM orders
WHERE customer_id = $1
ORDER BY created_at DESC
LIMIT 50;
Query plans show repeated large scans. An index is added:
CREATE INDEX idx_orders_customer_created
ON orders (
customer_id,
created_at DESC
);
Latency drops substantially without changing the architecture.
The next bottleneck is the home dashboard. It repeatedly joins and aggregates several large tables to display customer statistics.
A read model is introduced:
customer_summary
customer_id
total_orders
total_spent_cents
last_order_at
favorite_category
This denormalizes expensive derived values for the high-volume read path.
During flash sales, payment workers then begin contending on a small set of inventory rows.
Transactions are inspected and an external fraud request is found inside a database transaction:
BEGIN
↓
Lock inventory
↓
Call fraud service
↓
Wait 400 ms
↓
Update inventory
↓
COMMIT
The workflow is redesigned so slow external work does not unnecessarily extend the lock duration.
Read traffic continues growing, so two replicas are introduced:
Primary
Writes + fresh reads
/ \
↓ ↓
Replica A Replica B
catalog reporting
browsing analytics
Consistency-sensitive payment and order-confirmation reads remain on the primary. Less freshness-sensitive catalog and reporting traffic can use replicas.
Eventually the primary reaches a different limit: write throughput and storage growth remain too high even after read traffic has been moved away.
The order dataset is then evaluated for sharding.
customer_id
↓
Shard Router
┌───┼───┬───┐
↓ ↓ ↓ ↓
S1 S2 S3 S4
Because most customer-order operations already include customer_id, requests can usually route directly to one shard.
The progression is important:
Measure workload
↓
Fix query/index problems
↓
Optimize expensive read models
↓
Reduce contention
↓
Scale reads with replicas
↓
Shard only when one write/storage domain
is no longer sufficient
Each technique solves a different bottleneck. Applying all of them from the beginning would create unnecessary complexity.
A Practical Optimization Order
Database performance work is most effective when simpler problems are eliminated before architectural complexity is introduced.
A practical sequence is:
- Measure. Identify slow queries, throughput, latency percentiles, lock waits, CPU, memory, I/O, connection pressure, and dataset growth.
- Inspect query plans. Determine whether queries scan too much data, sort unnecessarily, or use poor join strategies.
- Fix indexes. Add useful indexes and remove indexes whose maintenance cost provides little value.
- Reduce data accessed. Avoid unnecessary columns, rows, joins, and repeated queries.
- Fix transaction behavior. Keep transactions short and investigate contention, deadlocks, and hot rows.
- Optimize the schema or read model. Partition or denormalize when measured workloads justify it.
- Scale reads. Add replicas or caching when read load is the limiting factor.
- Scale the database vertically when appropriate. More memory, CPU, IOPS, or storage can be simpler than distributing the database.
- Shard when necessary. Distribute data when one database can no longer satisfy write, storage, or compute requirements economically or operationally.
The ordering is not absolute, but it prevents a common failure mode: solving a missing-index problem with a distributed database architecture.
Common Database Performance Mistakes
- Optimizing without measurements. High latency can come from queries, locks, storage, connections, network calls, or application behavior.
- Adding indexes to every column. Excessive indexing increases write cost, storage, and maintenance.
- Looking only at average latency. P95 and P99 behavior often exposes contention and saturation hidden by averages.
- Using denormalization before identifying an expensive read path. Duplication creates consistency work that must have a measurable benefit.
- Holding transactions open during network calls. Locks and transactional resources remain occupied while waiting for another service.
- Increasing connection pools whenever requests wait. More database concurrency can make an already saturated database slower.
- Sending every read to replicas. Some requests require fresh primary state.
- Treating replication as write scaling. Replicas primarily help distribute reads and provide redundancy; writes still need an authoritative path.
- Sharding too early. Sharding introduces routing, rebalancing, distributed queries, and operational complexity.
- Choosing a shard key from storage distribution alone. Request distribution matters just as much.
- Ignoring growth. An architecture that works at 100 GB may behave very differently at 10 TB.
Production Checklist
- Measure database P50, P95, and P99 latency.
- Track query throughput and transaction throughput separately.
- Enable slow-query visibility.
- Review execution plans for important queries.
- Track sequential scans and index usage.
- Monitor CPU, memory, storage latency, IOPS, and throughput.
- Monitor active, idle, and waiting connections.
- Set bounded connection pools.
- Track transaction duration and idle transactions.
- Monitor lock waits and deadlocks.
- Measure replica lag before routing freshness-sensitive reads.
- Monitor dataset and index growth.
- Identify hot tables, rows, partitions, and shards.
- Load-test peak traffic rather than only average traffic.
- Test backup, restore, failover, and recovery procedures.
- Revisit capacity assumptions as traffic and data distribution change.
Frequently Asked Questions
Database performance strategies solve different constraints. The most important decision is identifying which resource or operation is actually limiting the workload.
Should Indexes Be the First Database Optimization?
Indexes are often one of the first areas to inspect because a missing or poorly designed index can turn a targeted query into a large scan.
They should still be selected from query plans and real access patterns. If the bottleneck is lock contention, storage saturation, connection pressure, or network latency, another index may accomplish little.
When Should a Database Be Denormalized?
Denormalization is useful when an important read path repeatedly performs expensive joins, calculations, or aggregations and the system can manage the additional synchronization complexity.
Normalized data should generally remain the starting point for transactional relational models. Denormalization is an optimization for known access patterns rather than a replacement for deliberate schema design.
What Is the Difference Between Replication and Sharding?
Replication copies data. Multiple nodes contain the same or substantially the same dataset, commonly to provide read scaling and redundancy.
Sharding divides data. Different nodes own different subsets of the dataset, allowing storage and workload to be distributed horizontally.
Replication:
DB A → copy → DB B
→ copy → DB C
Sharding:
Dataset
↓
A–F → Shard 1
G–M → Shard 2
N–Z → Shard 3
When Should a Database Be Sharded?
Sharding becomes relevant when a single database can no longer meet storage, write-throughput, compute, or workload-isolation requirements and simpler approaches are insufficient.
Before sharding, it is usually worth validating queries, indexes, schema design, connection management, hardware capacity, partitioning, caching, and replication. Sharding solves scale limits, but it also makes routing, transactions, joins, migrations, and operations substantially more complicated.
Conclusion
Database performance comes from matching the architecture to the workload. Item size, dataset size, concurrency, consistency, geographic distribution, availability requirements, and traffic variability establish the constraints.
Indexes reduce unnecessary data access. Denormalization makes selected read paths cheaper. Correct locking preserves consistency while minimizing contention. Replication distributes read workloads. Sharding distributes data and write capacity when a single database is no longer sufficient.
The core principle is: measure the bottleneck first, apply the simplest strategy that removes it, and add distributed database complexity only when the workload actually requires it.
Comments (0)