Back to blog
Data
IntermediateForBackend EngineersPlatform EngineersData Engineers
9 min

Postgres vs MySQL in 2026: Performance, Syntax and the Real Differences

Where Postgres and MySQL genuinely differ — storage engines, MVCC, connection models, SQL syntax and operations — plus where SQLite and MariaDB fit, and how to actually choose.

postgres-vs-mysqlpostgresqlmysqldatabase-comparisonsql-performancemariadb
Contents

Most Postgres versus MySQL comparisons are a feature checklist from 2015 with the dates changed. Both databases have added most of what the other used to have, so the checklist tells you almost nothing.

What still differs is deeper: how they store rows, how they handle concurrent versions of those rows, how they treat a connection, and what happens when a migration fails halfway. Those differences do not show up in a feature table, and they are the ones you live with.

Postgres vs MySQL in 2026: storage, concurrency, connections and syntax compared.

The 30-second answer

  • Complex queries, analytics, JSON, geospatial, extensions? Postgres.
  • Huge numbers of simple connections, key-based lookups, read replicas? MySQL.
  • Team already fluent in one of them? That one. Seriously.
  • Starting fresh with no constraint? Postgres, because the extension ecosystem gives you more escape hatches later.
  • Single process, no network, no ops? Neither — that is SQLite.

Everything below is why.

What actually differs under the hood

Storage: clustered vs heap

This single choice explains most performance differences.

MySQL (InnoDB) clusters the table on the primary key. The row data physically lives inside the primary key index. Look up by primary key and you traverse one B-tree and you are holding the row.

The cost: secondary indexes store the primary key, not a row location. So a lookup via a secondary index traverses that index to find a primary key, then traverses the primary key index to find the row. Two traversals. It also means a wide primary key inflates every secondary index on the table.

Postgres stores rows in a heap — an unordered pile of pages — and every index, including the primary key, points into it. Every index lookup costs an extra fetch to reach the row.

The benefit: all indexes are equal, so a wide primary key does not tax the others, and Postgres can use index-only scans when its visibility map says a page is fully visible.

What this means in practice: MySQL is excellent at “give me the row with this ID”. Postgres is more even-handed when your access patterns are varied.

MVCC: where old row versions live

Both give you readers that never block writers. They pay for it differently.

Postgres writes a new version of the row into the table itself and leaves the old one behind until VACUUM reclaims it. Heavy-update tables therefore bloat, and autovacuum keeping up is a real operational concern rather than a theoretical one.

MySQL writes old versions into undo logs, so the table stays compact. The cost moves elsewhere: long-running transactions force the undo log to grow, and a reader on an old snapshot has to walk back through those versions.

Neither approach is free. Postgres makes you think about vacuum; MySQL makes you think about long transactions and undo growth.

Connections: process vs thread

Postgres forks a process per connection. A process is not cheap. A few hundred mostly-idle connections consume real memory, and you will want PgBouncer or an equivalent pooler in front of any serious deployment. Serverless platforms that open a connection per request make this acute.

MySQL uses a thread per connection, which is dramatically cheaper. Thousands of connections is an ordinary configuration.

If your architecture opens many short-lived connections and you cannot pool, that fact alone may pick your database.

Postgres vs MySQL performance

Nobody can honestly tell you which is faster, because they are fast at different shapes of work. Here is what each actually favours.

Where Postgres and MySQL win: clustered vs heap storage and their different MVCC costs.

MySQL tends to win at:

  • Primary-key point lookups — one traversal, row in hand
  • Very high connection counts
  • Simple, high-volume OLTP where the query plan is obvious
  • Read-scaling via replicas, with a long-mature binlog replication story

Postgres tends to win at:

  • Complex joins, subqueries and aggregations — a stronger planner
  • Analytical queries alongside transactional ones
  • Anything with partial, expression or covering indexes
  • Write patterns that benefit from HOT updates avoiding index churn
  • JSON, arrays, geospatial and vector work, because of extensions

A word on benchmarks. Any number you read — including any I could publish — is one workload, one schema, one hardware profile and one configuration. Tuning moves results more than the engine choice does. If performance is genuinely your deciding factor, benchmark your queries on your data. Everything else is someone else’s workload.

If you are optimising rather than choosing, the techniques in SQL query optimisation apply to both engines.

Postgres vs MySQL syntax differences

This is the section that matters when you are porting something. These are the ones that actually break.

Postgres vs MySQL syntax differences
What Postgres MySQL
String concatenation 'a' || 'b' CONCAT('a','b')
Quote an identifier "my table" `my table`
Auto-incrementing key GENERATED ALWAYS AS IDENTITY AUTO_INCREMENT
Upsert INSERT ... ON CONFLICT DO UPDATE INSERT ... ON DUPLICATE KEY UPDATE
Return the inserted row INSERT ... RETURNING * not supported
Boolean real BOOLEAN type alias for TINYINT(1)
String comparison case-sensitive by default case-insensitive in common collations
Limit with offset LIMIT 10 OFFSET 20 LIMIT 10 OFFSET 20 or LIMIT 20, 10
Current timestamp NOW(), CURRENT_TIMESTAMP NOW(), CURRENT_TIMESTAMP
Arrays native array types not supported
Regex match ~, ~* REGEXP, RLIKE

Three of these cause most of the pain:

RETURNING. Postgres hands you back the row you just wrote in the same statement. MySQL does not support it, so you insert and then select, which is a second round trip and a race unless you are careful. MariaDB does support it.

-- Postgres: one statement
INSERT INTO users (email) VALUES ('a@b.com')
RETURNING id, created_at;

Case sensitivity. In Postgres, WHERE email = 'Bob@x.com' will not match a stored bob@x.com. In MySQL with a typical collation, it will. Ports in either direction silently change behaviour, and the bug surfaces in production as “login sometimes doesn’t work”.

Unquoted identifier folding. Postgres lowercases unquoted identifiers, so CREATE TABLE MyTable produces mytable. Once you quote something as "MyTable", you must quote it forever. The usual advice — stick to lower_snake_case and never quote — is worth following in both.

Operations: the difference nobody mentions

Postgres has transactional DDL. You can wrap schema changes in a transaction and roll them back:

BEGIN;
ALTER TABLE orders ADD COLUMN status text;
-- something fails here
ROLLBACK; -- the column never existed

MySQL commits implicitly on DDL. A migration that fails on step four of six leaves you with three applied changes and no way back except a hand-written down migration.

If you deploy schema changes often, this is a bigger day-to-day quality-of-life difference than any query benchmark.

Two more operational notes:

  • Extensions. Postgres lets you add PostGIS for geospatial, pgvector for embeddings, TimescaleDB for time series — inside the same database, with the same backups and the same transactions. MySQL has no comparable mechanism. If you might need a vector database, this matters.
  • Replication. MySQL’s binlog replication is old, well understood and has excellent tooling. Postgres has streaming physical replication and logical replication; both work well, and logical replication has improved substantially in recent releases.

Where SQLite fits

SQLite is frequently put in this comparison, and the framing is usually wrong.

SQLite is not a small server database. It is a library that runs inside your process. There is no server, no port, no user accounts and no network round trip — a function call reads a file.

That makes it excellent for:

  • Application-local state, caches and config
  • Test suites, where a database per test costs microseconds
  • Edge and embedded deployments
  • Read-heavy workloads served from a single machine

And unsuitable the moment you need several application servers writing concurrently. SQLite serialises writers: one at a time.

The honest rule: if exactly one process ever touches the data, SQLite is likely the right answer and the simplest one. If two might, you want a server.

MariaDB vs MySQL

MariaDB began as a MySQL fork after Oracle’s acquisition. Years later, “drop-in replacement” is no longer accurate.

They have diverged: separate optimiser development, different storage engines, and features on each side the other lacks. MariaDB has RETURNING; MySQL does not. Version numbers no longer correspond at all.

Drivers and basic dumps are largely interchangeable. Replication between them and any recent syntax are not.

Choose MariaDB for community governance and its specific feature set. Choose MySQL if you depend on Oracle’s ecosystem, a managed offering built on it, or its exact replication semantics.

Which versions to run

As of September 2026:

Which versions to run
Database Current Notes
PostgreSQL 18 (18.6) 19 in beta; 14 reaches EOL Nov 2026
MySQL 9.7 LTS (9.7.3) 8.4 LTS supported to 2029; 8.0 hit EOL Apr 2026
MariaDB 12.3 LTS (12.3.3) 11.8 and 11.4 LTS still supported

Two things to act on: if you are on MySQL 8.0, it is out of support — plan the move to 8.4 or 9.7. If you are on Postgres 14, you have until November.

Postgres ships a major version yearly with five years of support. MySQL splits into quarterly Innovation releases and roughly biennial LTS releases; unless you need a specific new feature, run LTS.

The decision table

The decision table
Your situation Pick
Complex queries, reporting alongside OLTP Postgres
Geospatial, vectors, time series in one database Postgres — extensions
Frequent schema migrations Postgres — transactional DDL
Thousands of connections, no pooler possible MySQL
Key-value style access by primary key MySQL — clustered index
Heavy read scaling via replicas MySQL — mature binlog tooling
One process owns the data, no network SQLite
Want community governance over Oracle MariaDB or Postgres
Team already deeply fluent in one That one

The bottom line

The feature gap that made this an interesting argument a decade ago has mostly closed. What is left is structural: clustered versus heap storage, old row versions in the table versus in undo logs, a process versus a thread per connection, and whether a failed migration can roll back.

Those four differences will shape your operational life far more than any feature checklist. Pick on those, and on which one your team can debug at 3am.

And if you find yourself deciding on a benchmark you did not run, you are choosing based on someone else’s workload.

Frequently asked questions

Which is faster, Postgres or MySQL?

There is no honest general answer, because they lose speed in different places. MySQL with InnoDB stores rows inside the primary key index, so a lookup by primary key fetches the row in one traversal — very fast for key-value style access. Postgres stores rows in a heap with separate indexes, which costs an extra fetch on lookups but makes complex joins, aggregations and partial indexes cheaper. Postgres also has a more capable query planner for analytical work. Any benchmark you read is really measuring one workload on one schema on one configuration.

What are the main syntax differences between Postgres and MySQL?

The ones that break real ports are: string concatenation uses double pipe in Postgres and CONCAT in MySQL; identifiers are quoted with double quotes in Postgres and backticks in MySQL; UPSERT is INSERT ON CONFLICT versus INSERT ON DUPLICATE KEY UPDATE; Postgres supports RETURNING and MySQL does not; Postgres has a real boolean type while MySQL aliases it to TINYINT; and Postgres string comparison is case-sensitive by default while MySQL's common collations are not.

Postgres vs MySQL vs SQLite — when should I use each?

SQLite is not a smaller server database, it is a library that runs inside your process with no network layer and no server to operate. It is excellent for local application state, tests, edge deployments and read-heavy embedded use, and it handles one writer at a time. Choose Postgres or MySQL the moment you need several application servers writing concurrently, real user and role management, or replication. Use SQLite when the database only ever serves one process.

Should I use MariaDB or MySQL?

Treat them as related databases rather than interchangeable ones. MariaDB started as a MySQL fork but has diverged for years — it has its own optimiser work, its own storage engines, and features MySQL lacks such as RETURNING. Drivers and dumps are largely compatible, but replication between them and newer syntax are not guaranteed to be. Pick MariaDB if you want community governance and its specific features; pick MySQL if you depend on Oracle's ecosystem, managed offerings or its exact replication behaviour.

Is Postgres harder to operate than MySQL?

In two specific ways, yes. Postgres uses a process per connection, so a few hundred idle connections consume real memory and you need PgBouncer or an equivalent pooler in front of it. And its MVCC keeps old row versions in the table itself, so heavy-update tables bloat and depend on autovacuum being tuned. MySQL is cheaper on connections and keeps old versions in undo logs. In exchange, Postgres gives you transactional DDL, which makes failed migrations far less painful.

Can I migrate from MySQL to Postgres?

Yes, and it is a normal project rather than a research one, but it is not a dump and restore. Expect work on data types (MySQL's zero dates and unsigned integers have no direct equivalent), auto-increment columns becoming identity columns, case-sensitivity assumptions in queries, UPSERT statements, and anything relying on MySQL's implicit type coercion. The application query layer is usually more work than the data itself.

Which Postgres and MySQL versions should I run in 2026?

As of September 2026, PostgreSQL 18 is the current major with 18.6 as its latest minor, and PostgreSQL 19 is in beta. Postgres 14 reaches end of life in November 2026, so anything on 14 or older needs a plan now. On the MySQL side, 9.7 is the current LTS released in April 2026, 8.4 is the previous LTS supported into 2029, and MySQL 8.0 reached end of life in April 2026. MariaDB's current LTS is 12.3.

From the community

Discussion on the Fediverse

Replies from Mastodon and Bluesky — straight from the open web, no tracking.

Loading replies …

ENDE