Reference·Data Stores

Data Store Selection Matrix

Thirteen storage categories, what each is genuinely good at, where PostgreSQL is still enough — and what a second store actually costs you.

Last reviewed ·revision 1·first published

This is a living document. It is revised when the underlying numbers or practice change, not on a publishing schedule.

Show revision history
  • First publication. Thirteen-category matrix, a PostgreSQL-is-enough-until table for eight specialist workloads, the polyglot tax, and six anti-patterns.

13 min
Data Store Selection Matrix
In short

Choose a data store by answering four questions in order: access pattern, write pattern, working-set size, and required consistency — then ask who operates it at 3 a.m. PostgreSQL with the right extension covers most workloads until either scale or one specialist feature breaks it. Name which one before adding a second store, because each one costs roughly a quarter to half an engineer indefinitely.

The question is never "which database is best." It is: for this access pattern, at this cardinality, with this consistency requirement, what does the wrong choice cost me in two years?

That last clause is the one people skip. Storage choices are the hardest architectural decision to reverse. Frameworks get replaced in a weekend. A data store with three years of production data, a schema every service depends on, and an operational runbook the on-call rota has memorised gets replaced in three quarters, if at all.

So the bar for deviating from the boring choice should be high, and this page exists to make the deviation defensible when it is genuinely warranted.

Start here: the honest default

Use PostgreSQL until you have a specific, measured reason not to.

This is not conservatism. A single modern Postgres instance handles tens of thousands of transactions per second, tens of terabytes, JSON documents, full-text search, geospatial queries, vector similarity, time-series partitioning, and queueing. It gives you serializable transactions, mature tooling, a large hiring pool, and managed offerings on every cloud.

Most polyglot persistence in the wild is not the result of a requirement. It is the accumulated residue of individual teams each choosing what they knew, and the bill arrives as five backup strategies, five failure modes, and a dual-write problem nobody owns.

Deviate when you have a number. "Our analytical queries scan 400 GB and take 90 seconds on Postgres" is a number. "Postgres doesn't scale" is not.

The decision sequence

Answer in this order. Each answer eliminates categories.

  1. What is the access pattern? Point lookups by key, range scans, ad-hoc multi-table joins, aggregation over billions of rows, traversal of relationships, similarity search, or full-text ranking. This single answer eliminates most of the field.
  2. What is the write pattern? Random updates, append-only, bulk load, high-cardinality time-stamped events.
  3. What is the working-set size versus total size? If the hot set fits in RAM, most of the differences between engines disappear.
  4. What consistency do you actually need? See the consistency models reference. Assume you need less than your instinct says, except for money.
  5. What is the operational cost of being wrong? Who runs it at 3 a.m.? Who restores it? Has anyone tested the restore?

Question 5 is the tiebreaker and it is almost never asked in the design review.


The matrix

CategoryFits whenBreaks whenRepresentativeThe honest trade-off
Relational (OLTP)Ad-hoc joins, transactions across entities, evolving query patterns, integrity constraintsAnalytical scans over billions of rows; single-node write ceilingPostgreSQL, MySQLBest default; write throughput is eventually bounded by one primary
Distributed SQLYou need relational semantics past a single node, or multi-region writesLatency-sensitive single-key work; small datasetsCockroachDB, Spanner, YugabyteDBSerializable at scale, paid for in per-transaction latency and operational complexity
Columnar / OLAPAggregation over billions of rows, few columns at a time, mostly appendPoint lookups, updates, transactionsClickHouse, BigQuery, Snowflake, DuckDB10–100x on scans; updates and deletes are second-class or absent
Key–valueKnown-key point access, extreme throughput, simple valuesAny query you cannot express as a keyDynamoDB, Redis, FoundationDBPredictable latency at any scale; a new access pattern means a new table or a migration
DocumentAggregates read and written whole, per-document schema varianceCross-document transactions, joins, ad-hoc analyticsMongoDB, DynamoDB (doc mode)Schema flexibility now, schema archaeology later — the schema still exists, it just lives in the application
Wide-columnMassive write volume, time-ordered rows within a partition, linear scale-outAd-hoc queries; anything needing cross-partition consistencyCassandra, ScyllaDB, HBaseWrite throughput and availability that nothing else matches; you must design the table per query, up front
GraphVariable-depth traversal is the primary workload — fraud rings, permissions closures, recommendationsRelationships exist but three joins would doNeo4j, NeptuneTraversals that are elegant here are painful in SQL; the reverse is also true, and most "graph problems" are two joins
Time-seriesHigh-cardinality timestamped metrics, downsampling, retention, time-window queriesGeneral-purpose querying; frequent updatesTimescaleDB, InfluxDB, Prometheus10–20x compression and automatic retention; cardinality explosions are the standard outage
SearchRelevance ranking, fuzzy matching, faceting, typo toleranceAs a system of record — everElasticsearch/OpenSearch, Typesense, MeilisearchRanking quality nothing else matches; it is a derived index, and it will drift from the source
VectorSemantic similarity over embeddings at scale, hybrid with keywordSmall corpora; exact-match retrievalpgvector, Qdrant, Pinecone, WeaviateSub-10 ms ANN over millions of vectors; recall is a tuning parameter, and re-embedding on model change is a migration
CacheRecomputation is expensive and staleness is tolerableDurability required; correctness depends on itRedis, MemcachedLatency; every cache is a second copy of the truth and therefore a correctness liability
Object storageLarge immutable blobs, cheap durable bulk, data-lake substrateSmall-record random access; low-latency readsS3, GCS, R2Effectively infinite and effectively free; per-object latency is tens of milliseconds
Log / streamOrdered, replayable event history; fan-out to many consumersRandom access by key; mutationKafka, Pulsar, KinesisReplay and decoupling; ordering exists only within a partition

Before you add a store: can Postgres do it?

For each specialist category, there is a point where the extension stops being enough. Knowing where that point is saves most of the arguments.

You wantPostgres optionGood enough untilThen move to
Document storageJSONB + GIN indexDocuments are a minority of your model; no sharding neededMongoDB, DynamoDB
Full-text searchtsvector + GINBasic relevance is acceptable; no faceting, typo tolerance or custom analysers; corpus under ~10M docsElasticsearch, Typesense
Vector searchpgvector + HNSWUnder ~5–10M vectors and you want them transactionally consistent with your rowsQdrant, Pinecone, Vespa
Time-seriesDeclarative partitioning, or TimescaleDBHundreds of millions of rows with modest cardinalityClickHouse, InfluxDB
AnalyticsRead replica, materialised views, or DuckDB over exportsScans stay under a few hundred GB and minutes are acceptableClickHouse, BigQuery, Snowflake
Job queueSELECT ... FOR UPDATE SKIP LOCKEDUnder a few thousand jobs/second, single-regionSQS, Kafka, Temporal
Graph traversalRecursive CTE, ltreeDepth is bounded and knownNeo4j
CachingUNLOGGED tables, or the OS page cacheYou have not measured a cache miss problem yetRedis

The pattern: the extension is right until either scale or a specialist feature breaks it, and you should be able to name which one before you add a system.

The polyglot tax

Every additional store costs you, whether or not it appears in the design document:

  • A second failure mode, with its own recovery procedure and its own on-call knowledge requirement.
  • A backup and a tested restore. Untested restore is not backup.
  • The dual-write problem. Writing to Postgres and then to Elasticsearch is two writes with no transaction between them. When the second fails, they diverge silently. The fix is the transactional outbox or change data capture — never a try/catch around the second write.
  • Consistency questions with no good answer, because now "the data" exists in several places at several ages.
  • Hiring and rotation, permanently.

A rough rule: adding a store is worth roughly 0.25 to 0.5 of an engineer, indefinitely. If the benefit does not clear that, keep the extension.

Sizing shortcuts

Fast estimates that avoid a spreadsheet. Pair these with the capacity calculator when you need the full derivation.

  • Rows per second a single Postgres primary sustains for simple writes: low tens of thousands with tuned WAL and batching; low thousands if every write fans out to five indexes and a trigger.
  • When to shard: when your working set no longer fits in RAM and the read replica route is exhausted. Not before. Sharding costs you cross-shard joins, distributed transactions, and every rebalance for the life of the system.
  • Index budget: every index is a write tax. Five indexes on a hot write table is usually two too many.
  • Retention beats scale. The cheapest way to make a store fast is to hold less in it. Ask what the data is worth at 90 days before designing for it at three years.

Anti-patterns

Search engine as the system of record. It is a derived index. It will lose documents, it will reindex, and it has no transactions. Own the truth elsewhere and rebuild the index from it — and prove you can, on a schedule.

Cache as a database. If losing the cache means losing data or serving wrong answers rather than slow ones, it is not a cache.

A queue used as a database. Replaying a topic to answer a query means you needed a materialised view, not a longer retention setting.

Choosing for peak scale you do not have. Selecting Cassandra for a hundred writes per second buys you the operational profile of a system at a million and none of the querying you actually need today.

Choosing by benchmark. Vendor benchmarks measure the workload the vendor chose. The only benchmark that matters is your access pattern, your data distribution, and your hardware — and it takes two days to run.

One store per microservice, on principle. Service independence is about the schema boundary and deploy independence, not the engine. Twelve services can own twelve schemas in one cluster and remain independent.

Sources

Frequently asked

How do you choose a database for a new system?

Answer four questions in order: what is the access pattern (point lookup, range scan, join, aggregation, traversal, similarity, relevance), what is the write pattern, does the working set fit in memory, and what consistency is genuinely required. Each answer eliminates categories. Then ask the tiebreaker nobody asks in design reviews — who operates and restores it, and has the restore been tested.

When should you move off PostgreSQL?

When you can name the specific limit you have hit: analytical scans past a few hundred gigabytes, a write rate beyond a single primary, a specialist feature such as faceted relevance ranking or linear write scale-out, or multi-region write latency. "Postgres doesn't scale" is not a reason; "our aggregation scans 400 GB and takes 90 seconds" is.

Is pgvector good enough or do you need a dedicated vector database?

pgvector with an HNSW index is usually sufficient up to roughly five to ten million vectors, and it has a real advantage: the embeddings stay transactionally consistent with the rows they describe, so there is no dual-write problem. Move to Qdrant, Pinecone or Vespa when corpus size, filtered-search performance, or specialised hybrid ranking becomes the bottleneck.

What is the real cost of adding another data store?

A second failure mode with its own runbook, a backup and a tested restore, the dual-write problem between the two stores, permanent hiring and on-call knowledge requirements, and consistency questions with no clean answer. Budget roughly a quarter to half an engineer indefinitely. If the benefit does not clear that, keep the PostgreSQL extension.

Can Elasticsearch be used as a primary database?

No. It is a derived index with no transactions, no referential integrity, and reindexing as a routine operation. Own the truth in a transactional store and rebuild the index from it — and rehearse that rebuild on a schedule, because a search cluster you cannot regenerate is an outage waiting for a mapping change.

When should you shard a relational database?

When the working set no longer fits in memory and the read-replica route is exhausted — not before. Sharding permanently costs you cross-shard joins, distributed transactions, and rebalancing for the life of the system. Partitioning, retention policies and an analytical replica solve most of what teams reach for sharding to fix.

Go deeper