Every few years someone asks me which database they should use for a new project, and my answer has stayed remarkably consistent: it depends less on MySQL versus PostgreSQL than you think, and far more on whether you actually know how to operate either one. I have been building and migrating database infrastructure for twenty years, and I have run large MySQL deployments that hummed along beautifully and PostgreSQL setups that were a disaster because nobody understood autovacuum. The database engine matters, but the team’s knowledge of it matters more.
That said, the landscape has shifted meaningfully. MySQL 8.4 LTS is the current long-term supported release, supported through April 2032, and MySQL 8.0 reached end of life in April 2026. PostgreSQL 18 shipped in September 2025 with a rewritten asynchronous I/O subsystem, UUIDv7 support, and native OAuth 2.0 authentication. The extension ecosystem, the cloud-managed options, and the AI/vector search story have all evolved. If you are starting something new today or evaluating a migration, here is how I think about the choice.
Where Things Stand in 2026
MySQL is owned by Oracle and licensed under GPL v2, with commercial editions for enterprise features. The current Long-Term Support version is MySQL 8.4, with 8.4.9 as the latest patch as of mid-2026. The innovation cadence on the 9.x series (quarterly releases, no LTS status) gives you access to newer features including a native VECTOR datatype, but most production teams running on AWS RDS or Aurora are on 8.4. MySQL 8.0 reached end of life in April 2026: if you are still on it, the upgrade path to 8.4 is well-documented and overdue.
PostgreSQL is a community-controlled open-source project with no single commercial owner, which is one of its genuine structural advantages. PostgreSQL 17 remains the most widely deployed version in managed services. PostgreSQL 18, which released in September 2025, adds a new async I/O subsystem that Oracle claims speeds up sequential scans, bitmap heap scans, and vacuum operations, native support for UUIDv7, virtual generated columns, temporal constraints for PRIMARY KEY, UNIQUE, and FOREIGN KEY, and OAuth 2.0 authentication. It ranks #4 in the DB-Engines ranking as of July 2026, and according to the DB-Engines methodology, it was the only top-four database gaining score year-over-year at that snapshot.
Both databases are mature, both handle ACID transactions correctly, and both will serve your application well if you understand them. The differences are real but often mischaracterized.

Under the Hood: MVCC, Locking, and the InnoDB Story
Both MySQL and PostgreSQL implement multiversion concurrency control for read-write isolation, but they implement it differently, and those differences have operational consequences.
MySQL’s InnoDB engine stores old row versions in a separate undo tablespace. When a transaction reads a row, InnoDB reconstructs the version the transaction should see by applying undo log entries. This means the undo log grows during long-running transactions, and you can run into problems if you start a large transaction, walk away, and come back to find InnoDB has accumulated gigabytes of undo records. The undo tablespace has grown significantly more manageable since 8.0, with truncation happening automatically, but long-running transactions on write-heavy tables still deserve your attention.
PostgreSQL stores old row versions inline with the current version in the heap. A dead tuple stays in the table file until VACUUM cleans it up. This is the root cause of the “table bloat” problem that catches PostgreSQL newcomers off guard: write-heavy tables accumulate dead tuples, reads get slower as the page density drops, and index scans start touching pages they should not need. Autovacuum handles this automatically, but it needs tuning on tables with high write rates. I have seen teams run into serious bloat problems simply because they had never touched autovacuum settings and the defaults were not aggressive enough for their workload.
For locking behavior, MySQL’s default isolation level is REPEATABLE READ, while PostgreSQL defaults to READ COMMITTED. This distinction matters in practice. MySQL’s REPEATABLE READ uses gap locks to prevent phantom reads, which can lead to more lock contention in concurrent write patterns. PostgreSQL’s READ COMMITTED avoids gap locks entirely, which often produces better concurrency at the cost of slightly weaker isolation semantics. Both can be tuned, but understanding the defaults will save you from puzzling over deadlocks.
MySQL’s storage engine abstraction is a historical artifact. InnoDB is the only engine you should use for production transactional workloads. MyISAM, MEMORY, and ARCHIVE exist but you will rarely need them. PostgreSQL has a single storage engine with a table access method API that allows extensions (like columnar storage) to plug in. For most teams, this distinction is academic.
SQL Compliance and Data Types
PostgreSQL has a well-earned reputation for strict SQL standards compliance, and this shows in production. When you run a GROUP BY query in PostgreSQL, every column in the SELECT list must either appear in the GROUP BY clause or be an aggregate. MySQL in its default mode has historically been permissive about this, returning a non-deterministic value for unaggregated columns not in the GROUP BY. With ONLY_FULL_GROUP_BY enabled (the default since 5.7), MySQL enforces the standard, but many legacy MySQL codebases were written without it and will break when you enable it. I have inherited more than one migration project where the first discovery was a thousand half-broken GROUP BY queries.
PostgreSQL’s native data types are considerably richer. You get JSONB (binary-encoded JSON with indexing support), UUID natively, range types (daterange, tsrange, int4range), geometric types, network address types (inet, cidr, macaddr), array types, and enum types with proper schema-level semantics. MySQL 8.x supports JSON storage and partial indexing through generated columns, but the ecosystem built around PostgreSQL’s JSONB is more mature and the operator set is broader.
One specific win for PostgreSQL: RETURNING in INSERT, UPDATE, and DELETE statements. This lets you return the rows affected by a write in a single round trip, which is enormously useful for getting auto-generated IDs or the final state of an updated row. MySQL requires a separate SELECT after the write, which means an additional round trip to the database unless you cache the ID from LAST_INSERT_ID() in the application.
PostgreSQL 18’s UUIDv7 support is worth calling out explicitly. UUID version 7 encodes a millisecond-precision timestamp in the high bits, which means new UUIDs sort roughly in insertion order. This dramatically reduces B-tree index fragmentation compared to UUID version 4, which is random. If you are building a new service that uses UUIDs as primary keys, PostgreSQL 18’s native gen_random_uuid() now generates v4 by default but you can generate v7 with a function. MySQL can store UUIDs as binary, but you have to handle the version selection and storage format in application code.
Extensions: PostgreSQL’s Structural Advantage
This is where the gap is widest and where I consistently steer teams toward PostgreSQL for non-trivial workloads.
PostgreSQL’s extension system allows third-party code to add new data types, operators, index access methods, and background workers that integrate deeply with the storage engine. The ecosystem includes PostGIS (geospatial queries that would take months to build from scratch), TimescaleDB (time-series storage with automatic partitioning and compression), Citus (horizontal sharding via columnar storage and coordinator-worker routing), pgAudit (granular audit logging), pg_partman (automated partition management), and pg_cron (in-database job scheduling). As covered in our deep-dive on the PostgreSQL extensions ecosystem, a well-chosen set of extensions can replace entire purpose-built services.
MySQL’s plugin architecture exists but is considerably more limited. You can add storage engines, authentication plugins, and audit plugins, but you cannot add new index types or redefine how the query planner understands your data. If you need geospatial queries, MySQL supports a subset of the OGC geometry types, but it does not approach PostGIS’s feature set. If you need time-series partitioning, you are writing it yourself.
For most CRUD-heavy web applications, this difference is invisible. But the moment your requirements expand to include geospatial queries, analytics over time-windowed data, full-text search, or AI vector embeddings, PostgreSQL’s extension ecosystem means you stay on one database instead of adding a specialized service alongside your OLTP store. This is a significant operational simplification.
AI Workloads and Vector Search
This has become a real differentiator in the past two years. pgvector, the PostgreSQL extension for storing and querying vector embeddings, is production-grade and widely used. You can store embedding vectors as columns, create IVFFlat or HNSW indexes for approximate nearest-neighbor search, and run hybrid queries that combine semantic similarity with traditional filters. Managed services like Aurora PostgreSQL, AlloyDB, and Azure Flexible Server all support pgvector as a first-class extension. When building RAG systems, we often run pgvector alongside a relational schema rather than standing up a separate vector database, which simplifies the operational footprint considerably. Our writeup on vector databases and pgvector covers the architecture in detail.
MySQL 9.x added a native VECTOR datatype, but as of mid-2026, the indexing and ANN search capabilities were still maturing. The MySQL 9.x innovation releases are not LTS, and the major managed services (RDS, Aurora) have not yet rolled them to production. If vector search is a hard requirement today, PostgreSQL is the more practical choice by a significant margin.
Replication, High Availability, and Logical Replication
Both databases support physical and logical replication, but the maturity and operational experience differ.
MySQL’s replication story has evolved considerably. Row-based binary log replication has been the default since 5.7, and MySQL 8.0 introduced Group Replication and InnoDB Cluster for multi-primary or single-primary high availability with automatic failover. MySQL Router handles application-side routing. The tooling works, but it requires running multiple processes and the operational model is less familiar to teams coming from a PostgreSQL background.
PostgreSQL’s physical streaming replication is simple and rock-solid. A standby connects to the primary, streams WAL records, and applies them. Promoting a standby is a single-command operation. Patroni, the de facto HA manager for self-hosted PostgreSQL, wraps this in a distributed coordinator (etcd, Consul, or ZooKeeper) to handle automatic failover with leader election, as we cover in the PostgreSQL high availability guide.
Logical replication is where things get more interesting for migrations and CDC pipelines. PostgreSQL’s logical replication is based on a publication/subscription model and supports selective table replication. Debezium, the most popular change data capture tool, has had mature PostgreSQL support via the pgoutput plugin for years. MySQL’s Debezium connector works well, but PostgreSQL’s pluggable output decoding architecture means the CDC story is slightly more flexible.

One area where MySQL wins on replication is cross-version flexibility. MySQL’s binary log format allows replicas to run newer minor versions than the primary, which makes rolling upgrades easier. PostgreSQL requires that replicas be the same major version as the primary for physical replication. Logical replication in PostgreSQL does support cross-version replication (which is how zero-downtime major version upgrades work), but the setup is more involved. If you are managing large fleets and need rolling major version upgrades with minimal downtime, the MySQL tooling is somewhat more forgiving.
For managing schema changes without downtime, both databases require careful technique. Our guide to zero-downtime database migrations covers the expand-contract pattern that works for both engines, but the tooling available for each differs. gh-ost, one of the most reliable online schema change tools, supports MySQL. For PostgreSQL, pg_repack and native logical replication-based migrations are the standard approaches.
Cloud-Managed Options: Where You Will Actually Run These
For most teams, the self-hosted MySQL versus PostgreSQL debate is secondary to the managed service choice. Here is how the landscape looks as of late 2026.
AWS: RDS MySQL (8.4 LTS) and RDS PostgreSQL (17 and 18 available) are the baseline offerings. Aurora MySQL and Aurora PostgreSQL are the high-performance alternatives with distributed storage, up to 15 read replicas, and global database options. Aurora PostgreSQL supports pgvector and has aggressively added PostgreSQL-compatible features. Amazon RDS also supports Aurora Limitless, the distributed OLTP option that distributes writes across shards behind a transaction router. If you are already on AWS and need PostgreSQL scale-out beyond what a single instance can provide, Aurora Limitless is worth evaluating, as covered in the Aurora DSQL and distributed SQL comparison.
Google Cloud: Cloud SQL supports both MySQL 8.0/8.4 and PostgreSQL 15/16/17. AlloyDB is Google’s PostgreSQL-compatible database with a disaggregated storage architecture and claimed performance improvements for read-heavy analytics workloads alongside OLTP. AlloyDB is PostgreSQL-compatible but not identical to community PostgreSQL: not all extensions are supported. Our managed PostgreSQL comparison covers what you lose and gain with each.
Azure: Azure Database for MySQL Flexible Server (8.0 and 8.4) and Azure Database for PostgreSQL Flexible Server (14 through 17) are the primary offerings. Azure Database for PostgreSQL supports pgvector and has solid availability zone support for high availability configurations.
The practical implication: if you are on AWS and want the most ecosystem breadth and extension support, Aurora PostgreSQL is my default recommendation. The pricing is higher than RDS PostgreSQL for similar configurations, but the operational simplicity and read replica scalability are worth it for most applications above a certain scale. For smaller projects or cost-sensitive environments, RDS PostgreSQL is excellent. RDS MySQL remains the right answer for teams migrating existing MySQL applications or running WordPress-family stacks at scale.

Performance: Where Each Shines
I am deliberately not citing benchmark numbers here, because the benchmarks that matter are your workload, not synthetic TPC-C runs on hardware you do not own.
What I can say from operational experience:
MySQL’s InnoDB has excellent write performance for high-concurrency INSERT/UPDATE workloads, particularly when the working set fits in the buffer pool. The relatively simple buffer management and the InnoDB architecture make it predictable under sustained write load. Teams running high-volume logging or event-capture applications on MySQL often get clean, consistent latency profiles.
PostgreSQL’s autovacuum overhead is real. On tables with very high update rates (say, a sessions table that every request touches), PostgreSQL can spend meaningful I/O budget on vacuum if autovacuum is not tuned. The payoff is that VACUUM also updates statistics and freezes transaction IDs to prevent wraparound, but the operational cost needs to be planned for. We cover the specific tuning knobs in the PostgreSQL VACUUM and autovacuum guide.
For read-heavy analytical queries on normalized schemas, PostgreSQL’s query planner is generally considered more sophisticated, particularly for complex joins. MySQL’s planner has improved substantially in 8.x but PostgreSQL still tends to win on complex query execution plans with many tables and subqueries.
For simple point lookups and INSERT-dominated workloads with small row sizes, MySQL often has a slight edge in raw throughput. But for workloads that mix analytical queries with transactional writes, or that heavily use CTEs, window functions, and complex aggregations, PostgreSQL tends to be faster in practice. Connection pooling configuration matters for both, but PgBouncer’s transaction-mode pooling is widely used for PostgreSQL in a way that has no direct MySQL equivalent.
The Migration Question
If you have an existing MySQL application and are considering moving to PostgreSQL, the query compatibility is reasonably close but not complete. The major friction points I encounter:
The GROUP BY strictness issue mentioned earlier. Any query relying on MySQL’s permissive GROUP BY semantics will fail in PostgreSQL. An audit pass before migration is necessary.
Auto-increment versus sequences. MySQL uses AUTO_INCREMENT columns; PostgreSQL uses sequences, accessed via SERIAL or IDENTITY columns. The semantics are slightly different, particularly around transaction rollback behavior.
String case sensitivity. MySQL’s default string collation is case-insensitive; PostgreSQL’s default is case-sensitive. An application that relies on case-insensitive string matching in WHERE clauses will behave differently, and you will need explicit ILIKE or citext column types if you need case-insensitivity.
JSON handling. MySQL JSON functions use a different syntax from PostgreSQL’s JSONB operators. A significant JSON query layer will need rewriting.
The pgloader tool handles many of these transformations automatically, but it does not catch everything. A migration from MySQL to PostgreSQL on a non-trivial application typically takes two to four weeks of testing and fixing, depending on the size of the SQL surface area.
If you are running on Oracle and considering open-source, the Oracle to PostgreSQL migration guide covers the deeper issues including PL/SQL to PL/pgSQL conversion.
When to Choose MySQL
Use MySQL when:
Your team has deep MySQL operational expertise and no compelling reason to change. The knowledge asymmetry matters more than the feature delta for most CRUD applications.
You are running a WordPress, Drupal, or other PHP-stack application. The entire ecosystem assumes MySQL. You are swimming upstream if you switch.
You are on an e-commerce platform where the existing ecosystem of connectors, plugins, and managed service tooling is heavily MySQL-oriented.
Your write workload is extremely high-throughput INSERT-dominated and you have tuned InnoDB thoroughly. MySQL’s write path is efficient and well-understood for this pattern.
You are migrating an existing MySQL application to a managed service and want to preserve the ability to run the same codebase locally and in production. MySQL 8.4 to Aurora MySQL is a well-worn path.
When to Choose PostgreSQL
Use PostgreSQL when:
You are starting a new project and have no legacy constraints. It is the default for new applications in my practice, and most of my peers agree.
You need geospatial, time-series, full-text search, or vector embeddings. PostgreSQL’s extension ecosystem handles all of these without adding a separate service.
You are building on Python, Go, or Java with a modern ORM. SQLAlchemy, GORM, and Hibernate have all added PostgreSQL-specific features that use pgvector, JSONB, and array types to meaningful effect.
Your team will eventually need to run complex analytical queries alongside transactional workloads. The query planner, the JSONB support, and the CTE handling will pay dividends.
You are building AI-adjacent infrastructure where vector search, semantic similarity, and hybrid retrieval matter. pgvector on PostgreSQL is a production-tested choice. The polyglot persistence guide discusses when to keep it all in one store versus splitting services.
You want the best connection between your database schema and your infrastructure tooling. Schema-as-code tools like Atlas and Flyway have stronger PostgreSQL support for advanced types. Our database schema-as-code guide covers how to manage this in CI/CD.
A Note on Testing Your Specific Workload
The one thing I would add to any comparison like this: run your actual query mix against both engines before committing. pg_bench and sysbench both have OLTP modes that can run against either database, but nothing replaces replaying production query logs against a realistic data set. Cloud providers make this relatively easy with point-in-time restores and read replicas you can divert for benchmarking. I have been surprised more than once when a theoretically-superior engine performed worse on a specific application’s access patterns.
Both MySQL 8.4 LTS and PostgreSQL 18 are excellent, production-grade databases. The choice you make is mostly a bet on which ecosystem of tooling, expertise, and managed service features will serve your team over the next five years. For most teams building something new, that bet tilts toward PostgreSQL. For teams with existing MySQL investments and skills, there is rarely enough incremental value to justify a migration unless you hit a specific capability wall.
Pick the one your team knows, build good observability around it, keep up with minor versions, and spend your energy on the application rather than the database engine.
Get Cloud Architecture Insights
Practical deep dives on infrastructure, security, and scaling. No spam, no fluff.
By subscribing, you agree to receive emails. Unsubscribe anytime.
