DEV Community

Mubashir Taj
Mubashir Taj

Posted on

Postgres vs MySQL: Picking the Right Database for a New Service

Every new service starts with the same argument: Postgres or MySQL. Both are excellent, both are free, both have powered companies far larger than yours. The argument usually ends with someone's preference winning, which is a bad way to pick infrastructure that you will live with for years. The choice is not about which database is better. It is about which set of tradeoffs matches the workload you are actually building.

Here is the comparison that matters, stripped of benchmark wars.

The one-line difference

Postgres is the database you pick when your data model is complicated and your queries are interesting. MySQL, specifically InnoDB, is the database you pick when your workload is simple, write-heavy, and must stay fast under enormous concurrency.

That sentence explains most of the rest. Postgres invests in query planning, data types, and correctness features: a sophisticated planner, JSONB with real indexing, arrays, ranges, full-text search, partial and expression indexes, common table expressions that you can actually build logic on. MySQL invests in the storage engine: InnoDB is a superb machine for primary-key lookups and high-concurrency writes, and MySQL's replication story is simpler to operate at very large scale.

If your service is a CRUD app with simple queries, both will serve you fine and the decision barely matters. If your service does analytics inside the database, complex joins, or JSON-heavy documents, Postgres pulls ahead quickly. If your service is a write firehose with simple reads, MySQL's engine earns its keep.

Where Postgres wins clearly

JSON is the clearest example. Postgres JSONB is not a text blob with a JSON label; it is a binary format with GIN indexes, path operators, and containment queries that the planner understands:

-- Find orders containing a specific SKU, using a GIN index, not a scan.
SELECT id, payload
FROM orders
WHERE payload @> '{"items": [{"sku": "A-100"}]}';
Enter fullscreen mode Exit fullscreen mode

In MySQL, JSON columns work, but indexing into them is more limited and the query patterns that stay fast are narrower. If your service stores semi-structured documents and queries inside them, Postgres is the calmer choice.

Window functions, lateral joins, recursive queries, and custom types are the same story. Postgres has the deeper SQL toolbox, and teams that learn it stop pushing logic into application code that the database could have done in one query. That is not a minor win: every query you avoid writing in application code is a round trip and a bug class you never ship.

Postgres also wins on data integrity habits. It defaults to strictness: foreign keys, check constraints, deferrable constraints, and transactional DDL. You can rename a column inside a transaction and roll the whole migration back if it fails. MySQL's DDL has historically been less forgiving, and its famous silent data coercion (truncating strings, converting bad dates) has bitten more teams than admit it. Modern MySQL with strict mode is much better, but the defaults still reward checking.

Where MySQL wins clearly

Raw write throughput on simple workloads is MySQL's home turf. InnoDB's clustered primary key design means primary-key lookups are one B-tree walk, and its row-level locking and MVCC implementation handle thousands of concurrent small transactions with very low overhead. This is why enormous write-heavy systems, from early Facebook to Shopify's core storefronts, ran on MySQL: the workload was simple, the volume was huge, and the engine stayed out of the way.

Replication is the second win. MySQL's asynchronous replication is simple to set up, simple to reason about, and battle-tested at scales where operational simplicity is a feature, not a luxury. Postgres streaming replication is excellent too, but MySQL's ecosystem of tooling for replicas, failover, and read scaling grew up around exactly this use case and it shows.

There is also the ecosystem argument. If your team already runs MySQL, if your ORM and your hosting and your on-call playbooks all assume MySQL, switching costs are real. A database migration is a project with risk; "Postgres is nicer" is rarely a sufficient reason to pay it mid-flight.

The decision framework

Stop asking which database is better and ask four questions about the service you are building.

First, how complex are the queries? Joins across five tables, JSON containment, analytics inside the database: Postgres. Single-row lookups and simple writes: either, MySQL slightly ahead.

Second, how write-heavy is it? Sustained thousands of small writes per second with simple reads: MySQL's engine is built for this. Mixed workloads with heavy reads and reporting: Postgres.

Third, what does the team already operate? The database your team can debug at 3 AM beats the one with the better feature list. Operational familiarity is a real feature.

Fourth, what does the data look like in two years? If the schema will grow JSON fields, search features, or analytical queries, Postgres gives you room. If the access pattern will stay simple and the volume will explode, MySQL gives you headroom.

A final note: the most common regret is not the choice itself but the lack of one. Teams that let each service pick a different database "because both are fine" end up operating two databases, two backup strategies, and two sets of on-call knowledge. Pick one as the default for your organization, allow exceptions with a written reason, and revisit the default once a year. That discipline matters more than the logo on the database.

Switching later: possible, but price it honestly

Teams often ask whether they can start on one database and switch later if the choice proves wrong. The honest answer is yes, with a cost that grows with data size and query complexity. Migrating tables and rows is the easy part; migrating queries, indexes, stored procedures, and application assumptions is the project. Postgres window functions rewritten for MySQL, or MySQL auto-increment assumptions rewritten for Postgres sequences, each carry subtle behavioral differences that only surface under load.

If you suspect a switch is likely, the cheapest insurance is discipline now. Keep database-specific features behind a thin data-access layer, avoid stored procedures for business logic, and write migrations that both databases could plausibly run. That discipline costs little when the codebase is young and saves weeks when the migration becomes real. The teams that switch painlessly are the ones who built as if they might.

One more consideration: managed services have narrowed the operational gap. A managed Postgres or managed MySQL removes backups, patching, and failover from your on-call load, which used to be a major differentiator. With operations outsourced, the decision tilts further toward workload fit and team familiarity, because the operational burden that once dominated the choice is now someone else's problem. Choose the engine that fits the queries you will actually write, on the team that will actually run it.

A last check before you commit

Before the decision meeting, run one practical test. Take your three most common queries from the current service, or the three queries you expect to write first, and write them in both databases. Not in a benchmark harness, in the actual SQL you would ship. Time them on realistic data volumes, read the query plans, and note which one needed a workaround. That afternoon of real queries beats a week of reading comparisons, because it tests your workload instead of someone else's. The database that makes your queries boring is the right one. Boring queries are fast queries, and fast queries are the ones nobody pages you about.

Top comments (1)

Collapse
 
lin7c profile image
xcl •

The final test with three real queries is the strongest part, and it could go one step further: run those same queries under concurrent writes, not just on a static snapshot. Postgres and MySQL can look identical in a single-user timing run, while locking behaviour diverges once MVCC and row locks contend — especially for the JSONB containment case, where a GIN index helps reads but adds maintenance cost on every payload update. Checking the update rate of that payload column alongside the read plan would make the boring-query test also cover the write-heavy side of the decision framework.