PostgreSQL Partitioning vs Indexing vs Read Replicas: Which Scaling Move Fits?
A practical comparison of PostgreSQL partitioning, indexing, and read replicas for teams deciding how to scale reads, writes, maintenance, and cost.
Most PostgreSQL scaling decisions start with the same uncomfortable symptom: the database is still working, but a few queries, maintenance jobs, or traffic peaks are beginning to dominate the system. The tempting answer is to add more capacity immediately. Sometimes that is correct. More often, the better first question is whether the bottleneck is an access-path problem, a table-management problem, or a read-capacity problem.
Indexing, partitioning, and read replicas solve different problems. An index helps PostgreSQL find rows without scanning more data than necessary. Partitioning splits one logical table into smaller physical pieces so queries and maintenance can target less data. A read replica adds another server that can answer read-only traffic, usually with some replication lag. They can complement each other, but choosing the wrong one can add cost and operational complexity without fixing the actual bottleneck.
This guide compares the three options for managed PostgreSQL teams that need a practical scaling path rather than a theoretical architecture diagram.
The quick decision model
Start with the shape of the pain, not with the feature. If one endpoint is slow because it filters on an unindexed column, a replica only gives you another place to run the same slow query. If a large event table makes deletes, vacuum, and retention jobs painful, another B-tree index might make writes slower while the table remains hard to maintain. If reporting traffic is saturating CPU even after the key queries are well indexed, a read replica may be the cleanest next step.
| Scaling move | Best fit | What it does not fix | Operational cost |
|---|---|---|---|
| Better indexes | Repeated lookups, joins, ordering, and selective filters | Bad query shape, unlimited write volume, retention pain | Extra storage and write overhead |
| Partitioning | Very large tables with time, tenant, or range boundaries | Random small-table queries, poor predicates, most OLTP hot rows | Schema and migration complexity |
| Read replicas | Read-heavy workloads, analytics, dashboards, regional read traffic | Primary write bottlenecks, stale-read-sensitive flows | Replica lag, routing, monitoring, failover expectations |
| Vertical scaling | Broad CPU, memory, or I/O pressure without one dominant design flaw | Inefficient queries that grow with data forever | Higher plan cost and possible resize windows |
| Application caching | Repeated expensive reads with acceptable staleness | Transactional correctness and ad hoc database load | Invalidation and consistency rules |
The order matters. For most applications, indexing and query review come before replicas because they reduce work everywhere. Partitioning comes later, when table size and lifecycle management become the issue. Replicas make sense when the workload is genuinely read-heavy and the application can route traffic safely.
Indexing is usually the first scaling lever
PostgreSQL indexes are access paths. The official documentation describes several index types, with B-tree as the default and most common choice for equality and range comparisons. That sounds basic, but it is still the most common reason a managed PostgreSQL database feels too small: important queries are forcing PostgreSQL to inspect far more rows than the product experience requires.
Good indexing starts from real queries. Look at the endpoints and jobs that matter, then inspect their predicates, joins, ordering, and limits. A SaaS table queried by tenant_id and created_at often wants an index shaped around those columns in the order the query uses them. A background worker that repeatedly claims pending jobs may need an index around status and scheduled time. A search box might need a different index type entirely, depending on whether it uses equality, prefix matching, full-text search, or JSONB containment.
The tradeoff is that indexes are not free. Every insert, update, and delete has to maintain the relevant indexes. Large indexes consume memory and storage, and they can lengthen migrations. That is why the best index is not "one index per column." It is a small set of indexes that match the access paths the application actually uses.
For managed PostgreSQL users, the practical process is straightforward: find the slow query, run EXPLAIN or EXPLAIN ANALYZE in a safe environment, add the narrowest useful index, and verify that the plan changes. If the query is still slow after a good index exists, the issue may be query shape, row volume, or memory pressure rather than missing indexing.
Partitioning is for table shape and lifecycle, not generic speed
Declarative partitioning lets PostgreSQL split one logical table into child tables based on a partition key, such as a date range or tenant identifier. PostgreSQL can prune partitions when the query predicate proves that only some partitions are relevant. That can make queries and maintenance easier when the table has a natural boundary.
The most common production example is a large append-heavy events table. If the application nearly always queries recent data by time, monthly or weekly range partitions can keep recent queries focused and make retention easier. Dropping an old partition is usually cleaner than deleting millions of old rows from one monolithic table. Partitioning can also help when indexes on a single table become very large and maintenance windows become hard to control.
Partitioning is not a magic performance setting. Queries must include predicates that match the partition key for pruning to help. A query that scans all tenants across all months can still touch every partition. PostgreSQL documentation also notes limitations and operational details around partitioned tables, constraints, indexes, and maintenance. Those details matter because partitioning changes the schema model your migrations, ORMs, and operational scripts interact with.
Use partitioning when the data has a durable boundary and the application can keep that boundary visible in queries. Avoid it when the table is modest, the partition key is unclear, or the goal is only to make an unselective query faster. In those cases, query design and indexing usually give a better return with less complexity.
Read replicas scale read traffic with a consistency tradeoff
A read replica is a separate PostgreSQL server kept in sync from the primary through replication. PostgreSQL documentation describes standby servers for high availability and read-only queries, but the important application detail is that replicas are not the primary. Writes still go to the primary, and reads from a replica can be behind the primary by a small or sometimes significant amount depending on workload, network, and replay pressure.
That lag is acceptable for many use cases. Dashboards, admin pages, exports, customer-facing analytics, and expensive product discovery queries can often tolerate slightly stale data. Checkout flows, login state, permission checks, and read-after-write experiences usually cannot. If a user creates a project and the next request reads from a lagging replica, the app may appear broken even though the database is behaving correctly.
Replica routing also adds application and operational work. The app needs to know which queries are safe to send to a replica. Monitoring needs to track replication lag and failed replay. Incident procedures need to distinguish a read-scaling replica from a failover candidate. In managed PostgreSQL, the provider can simplify provisioning and monitoring, but the application still owns the consistency contract.
Read replicas are a strong move after indexes and query design are already reasonable and read traffic is the clear bottleneck. They are a weak move when slow reads are caused by missing indexes or when the primary is saturated by writes, locks, autovacuum pressure, or storage throughput.
How to choose with real workload signals
The right decision becomes clearer when you map symptoms to causes. Do not rely on average CPU alone. A database with low average CPU can still have one destructive query. A database with high CPU can be healthy during a known reporting window. Look for the pattern behind the pressure.
| Workload signal | More likely cause | Best next move |
|---|---|---|
| One endpoint slows as table size grows | Missing or mismatched access path | Review query plan and add or adjust indexes |
| Deletes and retention jobs take too long | Large table lifecycle problem | Consider time-based partitioning and partition drop retention |
| Primary CPU is high during dashboard traffic | Read-heavy workload | Move safe stale reads to a replica after query review |
| Writes slow down after many indexes were added | Index maintenance overhead | Remove unused indexes and consolidate overlapping ones |
| Queries scan every partition | Partition key not present in predicates | Revisit partition design or query shape |
| Users see missing data right after writes | Replica lag in read-after-write path | Route those reads to primary or add consistency-aware routing |
A healthy scaling review usually begins with query evidence. Capture the expensive queries, their frequency, their plans, and their business purpose. Then separate one-time fixes from architectural changes. An index can be added quickly and rolled back by dropping it if it is wrong. Partitioning a production table requires a migration plan. Replicas require traffic routing, lag monitoring, and a policy for stale reads.
A practical sequence for managed PostgreSQL teams
First, make sure the workload is observable. Enable or use the platform's query insights if available, review slow query logs, and compare the database symptoms with application traces. The goal is to avoid scaling the wrong layer. If one query consumes most of the time, fix that before buying more infrastructure.
Second, clean up the obvious access paths. Add indexes for high-value queries, but keep an eye on write-heavy tables. If you are unsure whether an index is still used, review statistics and wait through a representative traffic period before removing it. The safest indexing work is guided by real production patterns and verified on production-like data.
Third, evaluate partitioning only when the table has crossed from "large" into "hard to operate." Good signs include painful retention deletes, indexes that have become expensive to rebuild, frequent queries that naturally target recent ranges, or tenant/time boundaries that appear in nearly every query. Before migrating, test inserts, updates that touch the partition key, backups, restores, and application migrations against the partitioned shape.
Fourth, add read replicas when read demand remains the bottleneck after query work. Start with clearly safe routes such as analytics, exports, and internal dashboards. Keep read-after-write product flows on the primary unless the application can detect lag or use a session-level consistency rule. Document which classes of queries are allowed to use replicas so the behavior does not become accidental tribal knowledge.
Finally, keep vertical scaling in the conversation. Sometimes the right answer is simply a larger plan because the workload is healthy and the business has outgrown the current allocation. ArmorDB's managed PostgreSQL plans are designed to make that path straightforward, and the pricing page is useful context when the tradeoff is engineering time versus infrastructure cost. The key is to scale deliberately after removing avoidable waste.
Common mistakes
The first mistake is using replicas to hide inefficient queries. A bad query on two servers is still a bad query, and it can become more expensive once dashboards and background jobs learn that a replica exists. Fix access paths before spreading load.
The second mistake is partitioning too early. Partitioned tables are powerful, but they add migration and schema complexity. If the table is small enough that normal indexes and maintenance work comfortably, partitioning can create more operational surface area than it removes.
The third mistake is assuming consistency is somebody else's problem. Managed PostgreSQL can provide the replica, but the application decides whether a stale read is acceptable. Treat replica routing as a product decision, not only an infrastructure decision.
Sources and further reading
- PostgreSQL documentation on table partitioning: https://www.postgresql.org/docs/current/ddl-partitioning.html
- PostgreSQL documentation on indexes: https://www.postgresql.org/docs/current/indexes.html
- PostgreSQL documentation for EXPLAIN: https://www.postgresql.org/docs/current/sql-explain.html
- PostgreSQL documentation on standby servers and replication: https://www.postgresql.org/docs/current/warm-standby.html
- ArmorDB guide to PgBouncer and connection pooling: /blog/pgbouncer-connection-pooling-postgresql
Takeaway
Use indexes when PostgreSQL is doing too much work to find the rows a query needs. Use partitioning when a large table's physical shape and lifecycle have become the problem. Use read replicas when well-shaped reads still need more serving capacity and the application can tolerate the consistency model.
For most growing applications, the winning path is sequential rather than dramatic: observe the workload, improve the access paths, partition only where the data model justifies it, and add replicas for clearly read-heavy traffic. That keeps the database easier to reason about while giving the business room to grow.
Topic
Data-Specs
Updated
Jun 10, 2026
Read time
8 min read
ArmorDB Engineering writes about PostgreSQL operations, security, and infrastructure decisions for teams building production apps on ArmorDB.
Read next
Deep Dives · 9 min read
PostgreSQL WAL Archiving and PITR: A Practical Managed Database Guide
A deep dive into PostgreSQL WAL archiving, point-in-time recovery, checkpoints, and the backup questions managed PostgreSQL users should ask before production incidents.
Read articleTech-News · 6 min read
PostgreSQL 18 Skip Scan: What It Changes for Multicolumn Indexes
PostgreSQL 18 can use skip scan lookups on multicolumn B-tree indexes in more cases. Here is what to test before changing production indexes.
Read article