A startup I advised had chosen MongoDB because their data was "flexible." Eighteen months in, they were doing three-collection lookups on every request, maintaining referential integrity in application code, and had written a nightly job to find and repair orphaned documents. Their data was not flexible. It was relational, and they had been implementing a relational database badly on top of a document store.
The reverse mistake exists too. I have seen a team model event payloads as forty nullable columns in Postgres because the schema was sacred, when a JSONB column would have taken ten minutes.
Choosing a database is not really a technology decision. It is a question about the shape of your data and how you will read it — and that question is answerable if you ask it before writing code rather than after.
The Question That Decides It
Forget the SQL versus NoSQL framing for a moment and ask: are the relationships between your entities important to how you read the data?
If you frequently need to answer questions that span entities — orders joined to customers joined to products, filtered by something on each — you have relational data. Use a relational database. Document stores make you do those joins in application code, which is slower, more code, and loses the consistency guarantees you would otherwise get for free.
If your access pattern is overwhelmingly "fetch this one document by its id, use the whole thing," and the document is genuinely self-contained, a document store fits naturally and will feel pleasant to work with.
The trap in the middle is that most business applications look self-contained at the start and turn out to be relational by month six. Customers have orders, orders have items, items reference products, products belong to suppliers. That is relational data, whatever it looked like during the prototype.
PostgreSQL: The Default I Argue For
If someone asks me what to use and gives me no other information, the answer is Postgres, and it is not close.
It gives you real transactions and constraints, so the database refuses to hold invalid state rather than trusting every code path to be correct. It has an outstanding query planner. And critically, it has absorbed most of the reasons people used to leave: JSONB for schemaless data with indexes on it, full-text search that is good enough for most applications, arrays, ranges, and extensions for geospatial and vector search.
That last point is the practical one. A single Postgres instance can serve as your relational store, your document store for the genuinely unstructured parts, your search index and your vector database. Four systems become one, and the operational simplicity of that is worth more than any individual component being marginally better.
The constraint that actually bites: connection handling. Every connection is a process, and a few hundred of them will hurt. If you are running serverless functions or many application instances, you need a pooler in front — PgBouncer, RDS Proxy, or the equivalent — and you need it before the traffic arrives rather than during the outage.
Where the Others Genuinely Fit
MySQL. Perfectly good, very widely deployed, easy to find people who know it. Postgres has richer types and extensions; MySQL has slightly simpler replication and a large hosting ecosystem. If you already run MySQL and it works, there is no reason to migrate.
MongoDB. Genuinely good when the document model matches — content management, product catalogues with wildly varying attributes, event or telemetry documents read as a whole. Also good at horizontal sharding when you truly need it. Bad when you find yourself writing lookups on every query, which is the signal you chose wrong.
Redis. Not a primary database, and treating it as one is a mistake I have seen cause data loss. It is the right answer for caching, session storage, rate limiting counters, distributed locks, leaderboards and lightweight queues. Everything in memory, single-threaded, extremely fast, and durability is a compromise rather than a guarantee. Use it heavily, and never as the only copy of something you cannot lose.
The Performance Problems, In the Order You Will Meet Them
Almost every "our database is slow" investigation I have done ended in one of these five.
A missing index. The most common by a wide margin. A query filtering on an unindexed column scans the whole table, which is instant on ten thousand rows and catastrophic on ten million. The pattern is a system that was fine for a year and then degraded over a few weeks as one table grew past the point where sequential scans hurt.
Learn to read the query plan. In Postgres, EXPLAIN (ANALYZE, BUFFERS) tells you what actually happened rather than what you assumed. A sequential scan on a large table in a frequent query is your answer.
N+1 queries. The ORM's fault, structurally. Fetch fifty orders, then loop and lazily load each customer, and you have made fifty-one round trips instead of one join. Every ORM has an eager-loading mechanism; the bug is not knowing to use it. Log your query counts per request in development — the moment a page issues sixty queries, you will see it.
// 1 + N round trips
const orders = await Order.findAll({ limit: 50 });
for (const o of orders) o.customer = await Customer.findByPk(o.customerId);
// one query
const orders = await Order.findAll({ limit: 50, include: [Customer] });
Connection pool exhaustion. Presents as timeouts under load while the database itself looks idle. The pool is full because something is holding connections — a long transaction, a slow external API call inside a transaction, or too many application instances each with their own generous pool.
Over-indexing. The opposite failure, and it appears later. Every index must be updated on every write. A table with twelve indexes has slow writes and a lot of wasted storage. Check for unused indexes periodically; databases will tell you which ones have never been read.
Doing work in the wrong place. Pulling a hundred thousand rows into application memory to sum a column is work the database would have done far faster. Conversely, complex business logic buried in stored procedures is invisible to your tests and your code review. Aggregate in the database; keep the rules in code.
Migrations, Where Downtime Comes From
Schema changes are the most common cause of self-inflicted outages, because a change that is instant on a small table can lock a large one for minutes.
The pattern that avoids it is always the same. Adding a column is safe if it is nullable without a default. Removing one is not — deploy code that stops using it, wait a release, then drop it. Renaming is the same in two steps: add the new, write to both, backfill, switch reads, remove the old. Adding an index on a large table in Postgres should be CREATE INDEX CONCURRENTLY, which does not block writes.
And every migration should be expand-then-contract, so that the old code and the new code can both run against the schema during a rolling deploy. Otherwise your deployment strategy requires downtime whether you planned for it or not.
How I Would Actually Choose
Start with Postgres. Add Redis when you have a caching or rate-limiting need, which will be soon. Reach for something else when you can name the specific thing Postgres cannot do for your workload — genuinely enormous write throughput, a document model that truly fits, or a specialised access pattern like time-series at scale.
"It scales better" is not a reason at the point where you are choosing. Postgres will carry you further than almost any team expects, and the problems you will actually hit — missing indexes, N+1 queries, unbounded connection counts — are the same problems in every database. Better to have them in the one with the best tooling for finding them.



