Skip to content

Comparison

PostgreSQL vs MySQL

Two mature, free, thoroughly proven relational databases that will both outlive your application. The choice comes down to what you already run, what your data actually looks like, and which set of operational habits your team is willing to build.

What the choice is actually between

This comparison used to be much sharper than it is now. For years PostgreSQL was the standards-compliant, feature-rich, slightly awkward one and MySQL was the fast, simple, everywhere one, and the two were genuinely different products for genuinely different jobs. That gap has narrowed considerably. Modern MySQL on InnoDB gives you proper ACID transactions, row-level locking, foreign keys, crash recovery, check constraints, a JSON type, window functions and common table expressions, and it is strict about invalid data by default rather than silently mangling it. Anyone still arguing against MySQL on the basis of how it behaved fifteen years ago is arguing with a database that no longer exists.

What has not converged is the shape of the two projects. PostgreSQL is built as an extensible, standards-oriented data platform: an unusually rich type system, several genuinely different index types, transactional schema changes, and an extension mechanism that lets it absorb whole categories of specialised infrastructure. PostGIS turns it into a serious geospatial database, pgvector adds similarity search over embeddings, and its built-in full-text search covers a great deal of what teams stand up a separate search engine to do. MySQL is built as a fast, dependable, extremely well-understood relational store for web-shaped workloads, with replication that has been battle-tested at a scale few systems have matched, and an ecosystem so ubiquitous that whatever operational problem you hit has been documented by thousands of people who hit it first.

So the honest framing is not that one is good and one is bad. It is that they optimise for different things, and the balance of those things has shifted. For a new application with a free choice, Postgres gives you more room to grow into and fewer decisions you will later regret, which is why it is our default. For an organisation with an existing MySQL estate, a team fluent in it, and a stack of application software that assumes it, MySQL is not a compromise and switching would be an expensive way to acquire a different set of problems. Both positions are defensible, and which one is yours is usually decided before you start reading.

The short answer

For a new application where you genuinely have a free choice, PostgreSQL is the better default, and we will say that plainly because the evidence supports it. The reason is not a single killer feature, it is range. A richer type system means you can model your domain honestly instead of approximating it. JSONB gives you real indexed document storage inside the same transaction as your relational data, which removes the most common reason teams add a second database. Several index types rather than one means the awkward query has an answer. Transactional DDL means a failed migration rolls back cleanly rather than leaving your schema half-changed at two in the morning. Extensions mean geospatial, vector search and time-series work can live in the database you already operate. You very rarely regret starting with Postgres, and you fairly often regret not having.

MySQL is the right answer more often than that framing suggests, and the conditions are specific rather than sentimental. If you already run a MySQL estate, if your team knows its operational behaviour in their bones, if your workload is read-heavy web traffic that scales out across replicas, or if your application software assumes MySQL, then MySQL is correct and the theoretical advantages of Postgres do not come close to paying for the disruption of changing. WordPress, Magento and a very large body of PHP software are built and tested against MySQL every day. Going against that grain buys you problems, not benefits. Familiarity is a real engineering asset, not a failure of ambition, and a database your team can diagnose at three in the morning beats a more capable one they cannot.

Be aware that Postgres asks for something in return, and teams who skip this are the ones who end up disliking it. Its concurrency model leaves dead row versions behind that autovacuum must reclaim, and under-tuned vacuum on a write-heavy table produces bloat, slow queries and, at the extreme, a transaction-ID wraparound problem you do not want to meet. Each connection is a full backend process, so an application opening hundreds of them directly will exhaust the server long before it exhausts the hardware, and a pooler in front is not optional. Major-version upgrades take more planning than MySQL’s, and automatic failover is something you assemble rather than something you switch on. None of that is difficult once you know to do it. All of it is a genuine cost, and it is the honest counterweight to everything in the paragraph above.

Side by side

DimensionPostgreSQLMySQL
Default for a new applicationOur default. More range, stricter correctness, and fewer decisions you have to unpick later as the data model grows into corners you did not anticipate.A perfectly sound choice, and rarely the one we would reach for first unless something in your situation points at it.
Type systemUnusually rich: arrays, ranges, enums, UUID, network address types, composite types, domains with constraints, and user-defined types. You can model the domain rather than approximate it.The standard set plus ENUM, SET and JSON. No array or range types, so multi-valued and interval data is modelled with extra tables or encoded into columns.
JSON and document dataJSONB stores JSON in a decomposed binary form that is indexable with GIN, queryable with operators and containment tests, and sits in the same transaction as your relational columns.A native JSON type with a good function library and functional indexes over extracted paths. Workable and genuinely thinner than JSONB for anything document-heavy.
ExtensionsA first-class mechanism and the single biggest differentiator. PostGIS for geospatial, pgvector for embeddings, TimescaleDB for time series, pg_trgm for fuzzy matching, pg_stat_statements for query analysis.A plugin architecture exists, but there is no comparable ecosystem. Capabilities beyond the core generally mean adding another system rather than extending this one.
Indexing optionsB-tree, hash, GIN, GiST, SP-GiST and BRIN, plus partial indexes, expression indexes and covering indexes. Awkward query patterns usually have a purpose-built answer.B-tree for almost everything, with full-text and spatial index types alongside. Fewer choices, and the choices are well understood and hard to get wrong.
Table storage layoutHeap storage with all indexes pointing at row locations. No index is privileged, and index-only scans depend on the visibility map being current.InnoDB clusters the table on the primary key, so primary key lookups are fast and secondary indexes carry the primary key and take a second step. A real design consideration for key choice.
Full-text searchBuilt in with tsvector and tsquery: stemming, dictionaries, ranking and GIN indexes. Enough to postpone or avoid a dedicated search engine for catalogues and in-app search.InnoDB full-text indexes with natural language and boolean modes, plus an ngram parser for languages that do not word-break. Serviceable, and less capable than Postgres for ranking and language handling.
Concurrency modelMVCC where an update writes a new row version and leaves the old one behind for readers still entitled to see it. Readers never block writers, writers never block readers.MVCC too, with old versions kept in the undo log rather than in the table, and a purge thread clearing them once no transaction needs them.
Housekeeping and bloatAutovacuum must reclaim dead tuples and maintain visibility and freeze information. Under-tuned on a write-heavy table it produces bloat and slow queries, and it is the thing teams most often neglect.Purge handles undo cleanup with less operational attention. Long-running transactions still grow the undo history, and the failure is generally quieter and slower to arrive.
Connection handlingOne operating system process per connection. Expensive at high connection counts, so PgBouncer or an equivalent pooler in front is effectively mandatory for a busy application.One thread per connection, which is lighter. High connection counts are more forgiving out of the box, and ProxySQL is common at scale rather than required early.
ReplicationPhysical streaming replication from the write-ahead log for read replicas and standbys, plus logical replication with publications and subscriptions for selective, cross-version and change-capture use.Binary log replication with GTIDs, well trodden to the point of folklore, with row, statement and mixed formats, semi-synchronous options and built-in multi-source replication.
High availability and failoverReplication is built in, automatic failover is not. You assemble it with Patroni, repmgr or a managed service, and that assembly is real engineering.Group Replication and InnoDB Cluster provide an integrated, vendor-supported route to automatic failover, and the orchestration tooling around MySQL is unusually mature.
Isolation and correctness defaultsRead committed by default, with a genuinely serialisable mode implemented through serialisable snapshot isolation. Strict about invalid data with no permissive history to configure away.Repeatable read by default with consistent snapshot reads, and locking reads that behave differently from plain ones. Strict SQL mode is the modern default, after a long permissive history.
Schema migrationsTransactional DDL, so a multi-statement migration either fully applies or fully rolls back. Many operations can be done without a long lock, and some still need planning on very large tables.Individual DDL statements are atomic, but a migration cannot be wrapped in a transaction and rolled back. Online DDL covers many changes, and gh-ost or pt-online-schema-change cover the rest.

Choose PostgreSQL when

  • You are starting a new application with a free choice and want the option value. Postgres gives you more room to grow into: richer types, more index options, real document storage, and extensions that can absorb geospatial, search and vector work later without adding another system to operate.
  • Your data model has awkward parts. Arrays, ranges, intervals, per-tenant custom fields, event payloads with variable shape, hierarchies you will query recursively. Postgres lets you model these honestly instead of encoding them into strings or spreading them across side tables that the application then has to reassemble.
  • You want the database to enforce correctness rather than trusting every service that will ever connect to it. Check constraints, exclusion constraints, deferrable constraints, domains, partial unique indexes and row-level security are all enforced where a bug in the application cannot get around them.
  • You need geospatial. PostGIS is not merely better than the alternative here, it is one of the strongest pieces of open-source data infrastructure in existence, and having spatial queries run against your operational data rather than a separate system kept in sync with it is a substantial architectural simplification.
  • You are building AI retrieval or semantic search. pgvector puts embeddings next to the rows they describe, so one query can filter with ordinary SQL predicates and rank by vector similarity, and you can defer standing up a dedicated vector store until your scale genuinely demands one.
  • Your migrations are frequent and your tolerance for a half-applied schema is zero. Transactional DDL means a failed migration rolls back cleanly instead of leaving the database in a state somebody has to reason about manually while the deployment is stuck.
  • You expect meaningful reporting and analytical queries against operational data. Window functions, recursive common table expressions, materialised views, filtered aggregates and lateral joins let you express hard questions in the database rather than fetching rows into application code and looping over them.

Choose MySQL when

  • You already run MySQL. This is the strongest argument on the page and it beats every theoretical advantage above. An existing estate, working replication, backup and restore procedures your team has actually rehearsed, monitoring that is tuned, and engineers who know how it behaves under load are worth far more than a better type system you would spend a year migrating to.
  • Your team is fluent in MySQL and not in Postgres. The database somebody can diagnose at three in the morning is better than the one they cannot. Postgres has specific operational habits, notably vacuum tuning and connection pooling, and a team that has not built those habits will have a worse experience with the more capable database than with the familiar one.
  • Your application software assumes MySQL. WordPress, Magento and a very large body of PHP applications, plugins, themes and integrations are built and continuously tested against it. If your product lives in that world, MySQL is the substrate the ecosystem validates against, and choosing otherwise buys friction rather than benefit.
  • Your workload is read-heavy web traffic with a straightforward data model. Lookups, page renders, session-shaped access patterns and a schema without exotic corners. MySQL is fast at exactly this, its replication story for fanning reads across replicas is as well trodden as anything in the industry, and none of Postgres’s range would be doing anything for you.
  • You need integrated automatic failover without assembling it. Group Replication and InnoDB Cluster give a supported route to a self-healing cluster, and the surrounding orchestration and proxy tooling is unusually mature. Postgres replicates perfectly well and leaves failover to you or to a managed provider.
  • You are constrained to cheap or shared hosting, or to a platform where MySQL is what is offered. Its ubiquity in commodity hosting is genuine, and a database you can run on the infrastructure you actually have beats one you would need to change providers to adopt.
  • You expect very high connection counts from an application you cannot easily put a pooler in front of. MySQL’s thread-per-connection model is more forgiving here than Postgres’s process-per-connection model, and while a pooler is the right answer in both worlds, MySQL is less punishing when you have not got one yet.
  • You are hiring for the largest available pool of people who have operated the database in production. MySQL’s ubiquity means deeper recruitment and an enormous body of documented operational experience, which lowers the long-term cost of running a system you will own for years.

Types, JSONB and modelling the parts that resist a schema

The type system is where the difference is most immediate and least discussed. Postgres gives you arrays, ranges, enums, UUIDs, network address types, composite types and domains, which are named types carrying their own constraints, plus the ability to define your own. That sounds like a checklist until you meet the modelling problems it solves. A booking system with overlapping intervals is a range type with an exclusion constraint that makes double-booking impossible at the database level, rather than three columns and a query that hopes. A set of tags is an array with a GIN index rather than a join table you did not want. An email address that must always be valid is a domain, defined once, rather than a check constraint copied into every table that stores one.

MySQL gives you the standard scalar types plus ENUM, SET and JSON. This is enough for the very large majority of schemas, and there is a real argument that a smaller type system produces more portable, more legible tables that any developer can read without learning your database’s vocabulary. What you give up is the ability to express certain shapes directly, so multi-valued and interval data gets modelled around the gap: extra tables, encoded columns, or invariants enforced in application code where a second service, a script or a manual fix can bypass them.

JSON is the sharpest version of this. Postgres has two types and the distinction matters: json stores the text and reparses it, jsonb decomposes it into a binary form that is faster to query, supports containment and path operators, and can be indexed with GIN so you can look inside documents at speed. In practice this means you can keep the structured core of your domain in properly typed relational columns and put the genuinely variable parts, per-tenant custom fields, integration payloads, event bodies, in JSONB beside them, under one transaction, in one database, queryable together. That single capability removes the most common reason teams end up running a document store alongside their relational one and then reconciling two sources of truth forever.

MySQL’s JSON type is real and useful, with a good function library and the ability to index extracted paths via generated columns or functional key parts. For a handful of semi-structured fields it is entirely adequate. It is thinner once documents become a load-bearing part of the model: the operator vocabulary is narrower, indexing inside a document takes more setup, and the ergonomics push you towards extracting values into columns, which is often the right instinct anyway. If your data is genuinely document-shaped in places, that difference will be felt weekly rather than theoretically, and it is one of the clearer reasons to prefer Postgres for a new build.

Extensions, and why they change the architecture rather than the feature list

Extensions are the biggest structural difference between these two databases and the one most likely to change what your whole system looks like. Postgres has a first-class mechanism for loading new types, functions, index methods and background workers into the server, and the ecosystem built on it is serious infrastructure rather than a collection of conveniences. The consequence is architectural: capabilities that would otherwise be separate systems, each with its own deployment, backup, security posture, monitoring and consistency problem, can live inside the database you already run.

PostGIS is the strongest example. It brings geometry and geography types, spatial indexing and a very large library of functions for distance, containment, intersection and transformation, and it turns Postgres into a genuine geospatial database rather than one that can store coordinates. Nearest-branch queries, delivery and catchment zones, geofencing and map layers run against your live operational data, in the same transaction as the rows they relate to. MySQL does have spatial types and spatial indexes, and for simple proximity work they are fine. For anything that is really a GIS problem, the gap is wide and worth knowing about before you commit.

The pattern repeats. pgvector adds vector columns and similarity search with approximate index types, so embeddings sit next to the rows they describe and a single query can filter with ordinary predicates and rank by semantic distance. TimescaleDB makes time-series storage and roll-ups efficient. pg_trgm gives you trigram similarity for fuzzy matching and accelerates pattern searches that would otherwise scan. pg_stat_statements aggregates query statistics so you can find the queries that actually dominate your load instead of the ones you assume do. Built-in full-text search, with tsvector, tsquery, dictionaries, stemming and ranking, covers a large share of what teams reach for a dedicated search engine to do.

The honest counterpoint is that extensions have costs. They must be available on your platform, which constrains managed-hosting choices and occasionally pins you to a provider that supports the one you need. They must be version-compatible when you upgrade the server, and an extension that has not kept up can block a major-version upgrade entirely. And they tempt teams into using the database for jobs it should not have, which is its own failure mode. The discipline is to reach for an extension to avoid adding a system, not to avoid thinking. But used deliberately, this is how a proposed architecture with four data stores becomes one well-configured Postgres, and that is a simplification worth a great deal at three in the morning.

MVCC, vacuum and the operational bill Postgres sends you

Both databases use multi-version concurrency control, so readers do not block writers and writers do not block readers, and both are pleasant to work with as a result. They implement it differently, and the difference is the single most important operational fact in this comparison.

When Postgres updates a row it writes a new version and leaves the old one in the table, visible to transactions that are still entitled to see it. Those dead versions accumulate and must be reclaimed, which is what vacuum does. Autovacuum runs in the background and handles this automatically, and on a modest workload you will never think about it. On a write-heavy or update-heavy table with default settings you may well think about it a great deal. Dead tuples that are not reclaimed fast enough become bloat: the table and its indexes occupy more space than the live data justifies, scans read more pages, the cache holds less useful data, and queries slow down in a way that looks mysterious if you do not know to look. Vacuum also maintains the visibility map that index-only scans depend on, and it freezes old row versions to prevent transaction-ID wraparound, which is the rare and serious failure mode at the end of this road. Long-running transactions make everything worse, because vacuum cannot clean up anything that an open transaction might still need to see.

InnoDB keeps old row versions in the undo log rather than in the table, and a purge thread removes them once no transaction needs them. The practical effect is that the equivalent housekeeping demands less of your attention, and the characteristic failure is quieter: a long-running transaction causes the undo history to grow, which costs space and slows reads that have to walk it, but you are less likely to discover it as a sudden performance cliff. MySQL asks for less operational vigilance here, and for a team without dedicated database expertise that is a real and underrated advantage.

The other half of Postgres’s bill is connections. Each one is a full operating system process, with its own memory, which means an application server pool that opens several hundred direct connections will exhaust the database long before it exhausts the machine. The answer is well known and not optional: put PgBouncer or an equivalent pooler in front, and size the actual backend count against your cores rather than your application’s optimism. MySQL uses a thread per connection, which is lighter, so it tolerates high connection counts better out of the box. A proxy such as ProxySQL is still the right answer at scale, but you are less likely to be forced into it in month one.

None of this is difficult, and none of it is a reason to avoid Postgres. It is a reason to be honest that Postgres expects a specific set of operational habits: tuned autovacuum on the tables that need it, a connection pooler, monitoring for bloat and for long-running transactions, and someone who knows what those numbers mean. Teams who build those habits find Postgres extremely reliable. Teams who treat it as fire-and-forget get bitten in the same predictable ways, and then tell people the database was the problem.

Indexing, planning and getting the awkward query to run

Most database performance problems in either system are the same three things: a missing or wrong index, a query written as an application loop instead of set-based SQL, and a configuration left at packaged defaults that were chosen to start anywhere rather than to run your workload. Fix those and the choice of database rarely determines whether your application is fast. Where the two differ is what you can do when the ordinary answers have been exhausted.

Postgres gives you several genuinely different index methods. B-tree for equality and ranges, GIN for JSONB documents, arrays and full-text vectors, GiST and SP-GiST for geometric, range and nearest-neighbour work, and BRIN for very large tables with natural physical ordering, where a tiny index summarising block ranges replaces one that would otherwise be enormous. On top of that sit partial indexes, which cover only the rows matching a predicate and are wonderful for the common case of a large table where you only ever query a small active subset, and expression indexes over a computed value. Covering indexes can carry extra columns so a query is answered from the index alone. The point is not that you will use all of these. It is that when a query pattern is awkward, there is usually a purpose-built answer rather than a workaround.

MySQL is B-tree for almost everything, with full-text and spatial index types alongside, and functional key parts for indexing an expression. Fewer choices means fewer ways to be clever and fewer ways to be wrong, which is a legitimate virtue. The structural thing to understand is clustering: InnoDB stores the table itself in primary key order, so primary key lookups are very fast and range scans on the primary key are efficient, while secondary indexes store the primary key rather than a row pointer and therefore take a second step to reach the row. This makes primary key choice a genuine design decision. A wide or randomly ordered primary key inflates every secondary index and scatters inserts, which is why the conventional advice is a compact, monotonically increasing key. Postgres stores tables as unordered heaps with all indexes pointing at row locations, so no index is privileged, at the cost that index-only scans depend on the visibility map being current, which brings you back to vacuum.

On query planning, both have capable cost-based planners and both will occasionally choose something you disagree with. Postgres exposes more information about why, particularly through EXPLAIN with buffers and timing, and pg_stat_statements is one of the best tools in either ecosystem for finding the queries that genuinely dominate load rather than the ones you suspect. MySQL has EXPLAIN, EXPLAIN ANALYZE and the slow query log, which together answer most questions in practice. Postgres has more sophisticated join and aggregation strategies available and it will use them on the analytical queries that appear inside most applications eventually, which is a reasonable part of why it suits reporting-adjacent work better. Neither planner rescues a query that asks for the wrong thing.

Replication, availability and the skills each one asks for

Both databases replicate well, and this is one of the few areas where MySQL has a clear structural advantage rather than merely a familiar one. Its binary log replication is among the most thoroughly exercised mechanisms in the industry: GTIDs make failover and topology changes tractable, row-based and mixed formats cover the correctness pitfalls of statement replication, semi-synchronous options let you trade a little latency for a stronger durability guarantee, and multi-source replication is built in. More importantly, MySQL offers an integrated route to automatic failover through Group Replication and InnoDB Cluster, with mature orchestration and proxy tooling around it. If you want a self-healing cluster that a vendor supports as a product, MySQL gets you there with less assembly.

Postgres replicates through the write-ahead log. Physical streaming replication gives you read replicas and hot standbys that are byte-identical to the primary, and it is simple, fast and dependable. Logical replication publishes row-level changes by table, which is the mechanism behind selective replication, replication between different major versions, near-zero-downtime upgrades, blue-green cutovers, and feeding a warehouse or search index without dual writes from the application. That last set of capabilities is genuinely valuable and worth knowing about before you plan an upgrade you assumed needed a long maintenance window.

What Postgres does not ship is automatic failover. Replication is built in, promotion is a decision somebody or something has to make, and building a cluster that fails over safely means adopting Patroni, repmgr, pg_auto_failover or a managed service that has done this work for you. That is real engineering with real ways to get it wrong, and split-brain is not a hypothetical if you improvise it. For many teams the honest answer is to use a managed Postgres and let the provider own that problem, which is a perfectly respectable engineering decision rather than an admission of anything.

Which brings us to the operational skills each database asks for, because that is what this section is really about. Postgres asks you to understand vacuum and bloat, to run a connection pooler and size the backend count deliberately, to plan major-version upgrades rather than assume them, and to choose and operate a failover mechanism. MySQL asks you to understand replication lag and what your application does when a replica is behind, to size the InnoDB buffer pool so the working set stays in memory, to be deliberate about primary key design because of clustering, and to keep strict mode and real constraints in place rather than inheriting a permissively configured server from an older era. Both lists are learnable. Neither is optional. And the list your team has already learned is a legitimate input into the decision, arguably a bigger one than any feature in the table above.

Correctness, isolation and what the database refuses to let you do

MySQL’s historical reputation for laxness was earned. Older versions would silently truncate values that did not fit, coerce nonsense into columns, accept invalid dates and shrug at data that should have been rejected outright. That is genuinely fixed: strict SQL mode is the modern default, check constraints are enforced, and a properly configured modern MySQL rejects bad data at the door. The residual risk is not the software, it is inheritance. Long-lived MySQL estates carry configuration, schemas and habits from the permissive era, and an application built against a server where strict mode was off has usually accumulated data that will not survive turning it on. That is a migration project rather than a setting.

Postgres has always been strict and has no permissive history to configure away, which is a smaller advantage than it once was but still means the default posture is correct rather than something you have to verify. Where it remains genuinely ahead is in the constraint vocabulary. Exclusion constraints can enforce that no two rows overlap on a range, which is how you make double-booking structurally impossible. Deferrable constraints let a transaction pass through a temporarily inconsistent intermediate state and be checked at commit, which is how you move rows between parents without disabling foreign keys. Partial unique indexes let a uniqueness rule apply only to the rows it should, such as one active record per customer while allowing many archived ones. Domains attach constraints to a named type so a rule is defined once rather than copied. Row-level security enforces tenant isolation inside the database, where an application bug cannot bypass it.

On isolation, the defaults differ in a way that surprises people moving between them. Postgres defaults to read committed, where each statement sees a fresh snapshot. MySQL’s InnoDB defaults to repeatable read, where the whole transaction reads from a consistent snapshot taken at first read, and where locking reads behave differently from plain ones in a way that is worth understanding before you rely on it. Neither default is wrong, but application code that assumes one and runs on the other can behave subtly differently under concurrency, which is the sort of bug that appears at load and never in testing. Postgres also offers a genuinely serialisable isolation level implemented through serialisable snapshot isolation, which detects conflicting transactions and aborts one rather than holding locks, so you get true serialisability with retry logic instead of a lock-heavy execution. Where correctness under concurrency is critical and the alternative is hand-rolled application locking, that is a strong tool.

The most useful thing to take from this section is not a scoreboard. It is that if your invariants matter, put them in the database. Both of these systems will enforce what you declare and neither will invent rules you did not write down. Postgres gives you a richer vocabulary for declaring them, and a schema that expresses its own rules is the single most durable thing you can build, because it keeps holding while services, frameworks and languages come and go around it.

What moving between them actually involves

Moving a database between these two is a well-trodden path and considerably more work than the marketing around migration tools suggests. The table structures translate almost mechanically and the tooling handles that part fine. The work is in everything around the tables, and it lands in four places: types and defaults, SQL dialect, application behaviour, and operations.

Going from MySQL to PostgreSQL, the recurring items are consistent. Auto-increment columns become identity columns or sequences, and the sequence values must be reset after the data load or your first insert collides with existing rows. Unsigned integers do not exist in Postgres and need a wider type plus a check constraint. Zero dates and other permissive artefacts have to be cleaned before they will load at all, and that cleanup often surfaces data quality problems the old configuration was hiding. ENUM columns become either a Postgres enum type or a lookup table with a foreign key, and the second is usually the better long-term answer. Case sensitivity changes: MySQL collations are commonly case-insensitive for comparison, Postgres is case-sensitive by default, and queries that relied on the old behaviour will quietly return different results unless you use citext or normalise explicitly. Backtick quoting, LIMIT with two arguments, and MySQL-specific functions all have to be rewritten. Then the application layer: ORMs mostly absorb this, raw SQL does not, and neither does whatever reporting tool, script or spreadsheet connector somebody in finance built four years ago and nobody has inventoried.

Going from PostgreSQL to MySQL is less common and, where the Postgres schema used the features that make Postgres worth choosing, considerably harder. Arrays, ranges, composite types, domains and exclusion constraints have no direct equivalent and must be remodelled, usually into extra tables plus application-enforced rules. JSONB usage translates only partly, and anything relying on GIN containment queries will need rethinking. Extensions do not come with you: if you use PostGIS, pgvector or TimescaleDB, you are not migrating a database, you are replacing a capability. And transactional DDL is gone, which means your migration tooling and its rollback assumptions need revisiting.

Whichever direction you are going, the mechanics should be boring and the plan should be dull. Load a full copy into the target and run your application against it in a non-production environment for long enough to find the dialect issues and the behavioural differences. Then use logical replication or a change data capture tool to keep the target current from the live source, so the eventual cutover is a short pause rather than a long outage while a dump restores. Verify with row counts and, more usefully, with checksums or aggregate comparisons on the tables that matter, because a migration that silently drops rows is worse than one that fails loudly. Keep the old system running and readable for a defined period afterwards, and know in advance what your rollback actually is.

The last point is the one worth saying loudest. Do not migrate a working database because a comparison page told you the other one is better. The risk is real, the effort lands on your best engineers for months, and the benefit is usually smaller than it looked when somebody read a feature list. Good reasons to move exist: you need a capability that only exists on the other side, such as PostGIS or pgvector, your data model has outgrown what your current database can express and the application is carrying invariants it should not be, or your organisation is consolidating on one database and the cost of two is genuinely larger than the cost of one migration. Those are worth doing properly. General dissatisfaction is not a reason, and is usually better answered by fixing the indexes, the queries and the configuration you already have.

Technologies involved

How we help

Common questions

Which should we use for a new project, PostgreSQL or MySQL?

PostgreSQL, if you genuinely have a free choice, and we will say that without hedging because the evidence supports it. The reason is range rather than any single feature: a richer type system so you can model your domain honestly, JSONB for the parts that resist a schema, several index types so awkward queries have a purpose-built answer, transactional DDL so a failed migration rolls back cleanly, and an extension ecosystem that lets geospatial, vector search and full-text search live in the database you already run. MySQL becomes the right answer when you already run it, when your team is fluent in it, when your workload is read-heavy web traffic with a straightforward model, or when your application software assumes it. Those conditions are common, and when they apply they beat everything in the first list.

Is PostgreSQL faster than MySQL?

Not in any way that generalises, and anyone quoting you a multiplier is quoting a benchmark that was not your workload. Performance in either database is dominated by the same three things: whether the right indexes exist, whether queries are written as set-based SQL rather than application loops, and whether the configuration was tuned for your data instead of left at packaged defaults. Broad tendencies do exist. MySQL is very quick at simple primary key lookups on a table clustered by that key, which is exactly the read-heavy web pattern it grew up serving. Postgres tends to do better on complex joins, analytical queries and anything using its more specialised index types. The difference between a good and a bad schema on either one dwarfs the difference between the two.

Is MySQL still bad about data integrity?

No, and it is worth retiring that criticism properly. The reputation was earned by permissive historical defaults that silently truncated values, coerced nonsense into columns and accepted invalid dates. Modern MySQL runs in strict mode by default, enforces check constraints, and rejects bad data at the door. The residual risk is inherited rather than inherent: long-lived estates carry configuration and habits from that older era, and an application built against a permissive server has usually accumulated data that will not survive turning strict mode on, which makes fixing it a project rather than a setting. Postgres has always been strict and has a richer constraint vocabulary, notably exclusion constraints, deferrable constraints, partial unique indexes and domains, so it can express more of your invariants in the schema itself.

What is JSONB and does MySQL have an equivalent?

JSONB is Postgres storing JSON in a decomposed binary form rather than as text, which makes it fast to query, supports containment and path operators, and lets you build GIN indexes that reach inside documents. In practice it means the structured core of your domain stays in properly typed relational columns while the genuinely variable parts, per-tenant custom fields, integration payloads, event bodies, sit beside them in the same table and the same transaction. MySQL does have a native JSON type with a solid function library, and you can index extracted paths using generated columns or functional key parts. It is entirely adequate for a handful of semi-structured fields and noticeably thinner once documents become load-bearing, both in operator vocabulary and in how much setup indexing inside a document takes.

What are the real downsides of PostgreSQL?

Three, and they are worth taking seriously rather than waving away. First, vacuum. Its concurrency model leaves dead row versions in the table and autovacuum must reclaim them, so a write-heavy table with default settings can accumulate bloat that slows queries, and in the extreme case transaction-ID wraparound becomes a genuine emergency. Long-running transactions make it worse because vacuum cannot clean up anything they might still need. Second, connections: each one is a full operating system process, so an application opening several hundred directly will exhaust the server long before the hardware, and a pooler such as PgBouncer is effectively mandatory. Third, upgrades and failover: major-version upgrades need planning with pg_upgrade or logical replication rather than being routine, extensions must be compatible or they will block you, and automatic failover is something you assemble rather than switch on. All of it is learnable and none of it is a surprise if you know to expect it.

We run WordPress and Magento. Should we move to PostgreSQL?

No. Those platforms and the enormous ecosystem of plugins, themes, extensions and integrations around them are built and continuously tested against MySQL. Going against that grain means fighting your own software stack for no benefit, and support from anyone in that ecosystem becomes your problem to reproduce. This is one of the clearest cases in the whole comparison. Keep MySQL, configure it properly with strict mode and honest column types, size the InnoDB buffer pool so the working set lives in memory, index around the queries that actually run, and put read replicas in when traffic justifies it. If some new part of your business genuinely needs Postgres capabilities such as PostGIS or pgvector, run it as a separate service with its own database rather than trying to move the platform.

How do the two compare on replication and high availability?

This is where MySQL has a structural advantage rather than just a familiar one. Its binary log replication with GTIDs is among the most exercised mechanisms in the industry, multi-source replication is built in, semi-synchronous options let you trade latency for durability, and Group Replication with InnoDB Cluster gives an integrated, supported route to automatic failover with mature orchestration and proxy tooling around it. Postgres replicates through the write-ahead log, with physical streaming replication for read replicas and standbys and logical replication for selective, cross-version and change-capture work, which is what makes near-zero-downtime major upgrades achievable. What it does not ship is automatic failover: you adopt Patroni, repmgr or pg_auto_failover, or you use a managed service that has already solved it. Assembling that yourself is real engineering and split-brain is not hypothetical.

How hard is it to migrate from MySQL to PostgreSQL?

The tables translate almost mechanically and the tooling handles that fine. The work is everywhere else. Auto-increment becomes identity or sequences and the sequences must be reset after loading. Unsigned integers do not exist and need a wider type with a check constraint. Zero dates and other permissive artefacts must be cleaned before they will load, which usually surfaces data quality problems the old configuration was hiding. ENUM columns become an enum type or, better, a lookup table. Case sensitivity changes, because MySQL comparisons are commonly case-insensitive and Postgres is case-sensitive by default, so queries can quietly return different results. Then the application layer: ORMs absorb most of this, raw SQL does not, and neither does the reporting tool or script nobody has inventoried. Plan on loading a full copy, running the application against it for long enough to find the differences, then using logical replication or change data capture to keep the target current so the cutover is a short pause rather than a long outage.

Do we need MongoDB or a separate search or vector database alongside either of these?

Less often than an architecture diagram suggests, and starting with fewer systems is almost always the better bet. If you are on Postgres, JSONB covers most document-shaped needs, built-in full-text search covers a lot of catalogue and in-app search, and pgvector covers most retrieval and semantic search workloads, all in one database with one backup, one security posture and one transaction boundary. Add a specialised store when the evidence demands it rather than at the design stage, because every additional system is another thing to secure, monitor, back up and keep consistent with the others. If you are on MySQL, the equivalent needs push you towards separate systems sooner, since its JSON and full-text capabilities are thinner and there is no vector story in the core, which is itself one of the better technical arguments for choosing Postgres on a new build.

Weighing PostgreSQL against MySQL?

Tell us the volumes, the compliance position and who would run it day to day. We will tell you which one we would choose for your case, and say so plainly when it is not the one we sell.

  1. 01A senior engineer reads it. Not a form queue, and not an account manager.
  2. 02We reply either with questions or with a straight answer that we are not the right fit.
  3. 03If it looks like a fit, a technical call with the person who would actually run the delivery.
  4. 04Then scope, effort and risk in writing, before anyone signs anything.

Two fields required. We reply to real enquiries. No list, no sequence.