PostgreSQL or MySQL: a detailed comparison and which one to choose

How PostgreSQL and MySQL differ in practice: fourteen criteria, which database to take for which task, the same tasks in both dialects, moving from MySQL to PostgreSQL and common mistakes.

Stack and technologies Updated

In short

PostgreSQL and MySQL are the two most popular free relational databases, and both are reliable for a typical site. PostgreSQL is stricter and richer: transactional schema changes, RETURNING, jsonb with indexes, arrays, ranges and extensions like PostGIS and pgvector — it is the default choice for services, SaaS, complex data and AI search. MySQL is simpler to run, cheaper per connection, available on any shared hosting and native to WordPress and most CMS. Choose PostgreSQL when the data is complex and will grow; choose MySQL when the project lives on a CMS or the hosting dictates it.

In short: which one to choose

For a new service, SaaS, a shop with its own code or anything with complex data, take PostgreSQL. It guards data more strictly, gives more SQL and types, and grows into geodata, vector search and time series through extensions — without a second database.

For WordPress and most ready CMS, for cheap shared hosting and for a team that already runs MySQL well, MySQL is the sensible choice. It is simpler to operate, handles many connections cheaply and has very mature replication. Neither choice is a mistake for a typical site — mistakes start when the database contradicts the task.

  • Complex data and growth — PostgreSQL
  • CMS and shared hosting — MySQL
  • Both are free and reliable

PostgreSQL vs MySQL: a detailed comparison

Fourteen criteria side by side. Speed depends on the data and the queries, so the table compares capabilities and behaviour rather than benchmarks.

CriterionPostgreSQLMySQL
Owner and licence a community, PostgreSQL License Oracle, GPL v2 or a commercial licence
Data strictness strict by design strict in the default mode since 5.7; old setups may be lax
Schema changes inside a transaction, can be rolled back each statement is atomic but commits itself
Returning inserted rows RETURNING on insert, update and delete no; LAST_INSERT_ID() and a second query
JSON jsonb with a GIN index over any key JSON, indexed through generated columns
Data types arrays, ranges, uuid, inet, your own types the classic set, plus JSON and spatial types
Indexes B-tree, GIN, GiST, BRIN; partial and by expression B-tree, full-text, spatial; by expression, no partial
Extensions PostGIS, pgvector, TimescaleDB and hundreds more far fewer; what exists is built in
Vector search pgvector: indexes and millions of vectors a VECTOR type in MySQL 9; vector indexes in the paid cloud
Connections a process per connection, needs a pooler a thread per connection, cheaper
Updating rows new row versions, cleaned by VACUUM in place with an undo log, no vacuum
Replication and scaling streaming and logical; Patroni, Citus very mature; InnoDB Cluster, Vitess
Hosting any VPS and cloud; rarer on cheap shared hosting on any hosting, including the cheapest
Typical home Django, Rails, services and SaaS WordPress, Joomla, Magento, PrestaShop

6 differences you feel in everyday work

The table is about capabilities; here is how they show up in a real project.

  1. Migrations can be rolled back

    In PostgreSQL a failed migration rolls back entirely. In MySQL a half-applied migration has to be finished or undone by hand.

  2. One query instead of two

    RETURNING gives back the new row with its id and defaults at once; MySQL needs a second query.

  3. JSON without preparation

    One GIN index in PostgreSQL covers any key; in MySQL each searchable key needs its own generated column.

  4. One database instead of three

    Geodata, vectors and time series come to PostgreSQL as extensions; with MySQL they usually mean separate systems.

  5. The cost of a connection

    Hundreds of direct connections are cheap for MySQL; PostgreSQL needs a pooler in front of it from day one.

  6. Upkeep

    PostgreSQL needs autovacuum to be tuned for busy tables; MySQL has no vacuum and is a bit simpler to keep running.

Which database for which task

Twelve typical projects with a recommendation and the reason.

TaskTakeWhy
A site on WordPress MySQL the CMS supports only MySQL and MariaDB
A shop on a ready platform MySQL most shop platforms are built for it
A shop or catalogue on own code PostgreSQL jsonb for characteristics, strict data, rich SQL
SaaS or a web service PostgreSQL transactional migrations, types and growth through extensions
Corporate site either small data; often SQLite is enough
Maps, delivery zones, “nearby” PostgreSQL PostGIS is the standard for geodata
AI search and RAG PostgreSQL pgvector next to the data
Reports and analytics in the database PostgreSQL richer SQL, window functions, partial indexes
Very many simple reads either both are fast; MySQL holds connections more cheaply
Cheap shared hosting MySQL PostgreSQL is not always there
A team that knows MySQL well MySQL experience is worth more than small differences
Billions of analytics events neither a columnar database like ClickHouse

The same tasks in PostgreSQL and MySQL: 3 examples

Each example solves one task in both databases. Checked on PostgreSQL 16 and MySQL 8.4 — the results are in the comments.

Upsert: insert or update

Different syntax, the same meaning: if the item exists, add to its stock.

upsert.sql
-- PostgreSQL: add to the stock, or create the item if it is new
CREATE TABLE stock (
    sku text PRIMARY KEY,
    qty integer NOT NULL CHECK (qty >= 0)
);

INSERT INTO stock (sku, qty) VALUES ('A-100', 5)
ON CONFLICT (sku) DO UPDATE SET qty = stock.qty + EXCLUDED.qty;

INSERT INTO stock (sku, qty) VALUES ('A-100', 3)
ON CONFLICT (sku) DO UPDATE SET qty = stock.qty + EXCLUDED.qty;

SELECT sku, qty FROM stock;   -- A-100 | 8

-- MySQL 8: the same, with ON DUPLICATE KEY
CREATE TABLE stock (
    sku VARCHAR(32) PRIMARY KEY,
    qty INT NOT NULL CHECK (qty >= 0)
);

INSERT INTO stock (sku, qty) VALUES ('A-100', 5) AS new
ON DUPLICATE KEY UPDATE qty = stock.qty + new.qty;

INSERT INTO stock (sku, qty) VALUES ('A-100', 3) AS new
ON DUPLICATE KEY UPDATE qty = stock.qty + new.qty;

SELECT sku, qty FROM stock;   -- A-100 | 8

Searching inside JSON

PostgreSQL indexes the whole document at once; MySQL indexes a chosen key through a generated column.

json.sql
-- PostgreSQL: one GIN index covers any key inside jsonb
CREATE TABLE orders (
    id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    details jsonb NOT NULL DEFAULT '{}'
);
CREATE INDEX orders_details_idx ON orders USING gin (details);

INSERT INTO orders (details)
VALUES ('{"delivery": "courier", "floor": 5}'), ('{"delivery": "pickup"}');

SELECT id FROM orders WHERE details @> '{"delivery": "courier"}';   -- 1

-- MySQL 8: a JSON field is indexed through a generated column
CREATE TABLE orders (
    id       BIGINT AUTO_INCREMENT PRIMARY KEY,
    details  JSON NOT NULL,
    delivery VARCHAR(20) AS (details->>'$.delivery') STORED,
    INDEX (delivery)
);

INSERT INTO orders (details)
VALUES ('{"delivery": "courier", "floor": 5}'), ('{"delivery": "pickup"}');

SELECT id FROM orders WHERE delivery = 'courier';   -- 1

Rolling back a migration

The most noticeable difference in practice: PostgreSQL undoes a schema change, MySQL has already committed it.

migration.sql
-- PostgreSQL: a schema change inside a transaction can be rolled back
BEGIN;
ALTER TABLE stock ADD COLUMN reserved integer NOT NULL DEFAULT 0;
-- the migration went wrong: roll back, and the column is gone
ROLLBACK;

SELECT column_name FROM information_schema.columns
WHERE table_name = 'stock'
ORDER BY ordinal_position;   -- sku, qty

-- MySQL 8: ALTER TABLE commits the transaction by itself
START TRANSACTION;
ALTER TABLE stock ADD COLUMN reserved INT NOT NULL DEFAULT 0;
ROLLBACK;   -- too late: the column has already been added

SELECT column_name FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'stock'
ORDER BY ordinal_position;   -- sku, qty, reserved

Moving from MySQL to PostgreSQL: what to keep in mind

The move is common and well-trodden, but a few differences break code silently.

  1. 01

    pgloader does the heavy lifting

    It moves the schema and data and converts most types automatically.

  2. 02

    Upserts are rewritten

    ON DUPLICATE KEY UPDATE becomes ON CONFLICT … DO UPDATE.

  3. 03

    Quotes around names

    MySQL backticks become double quotes, and unquoted names in PostgreSQL are lower-case.

  4. 04

    Zero dates

    0000-00-00 does not exist in PostgreSQL — such values become NULL before the move.

  5. 05

    Case and sorting

    MySQL often compares strings without case, PostgreSQL with it — searches by email or name need lower() or citext.

  6. 06

    A pooler before launch

    Code that opened hundreds of connections to MySQL needs PgBouncer in front of PostgreSQL.

Common mistakes when choosing between them

  1. Choosing by fashion

    A blog on WordPress does not need PostgreSQL, and moving a working CMS for fashion only creates risk.

  2. Thinking MariaDB is MySQL

    MariaDB is a fork that has diverged: JSON, replication and some functions behave differently.

  3. PostgreSQL without a pooler

    A traffic peak opens more connections than the database accepts, and every site on it goes down.

  4. MySQL in a lax mode

    Old setups silently cut strings and accept wrong dates. Strict mode must be on.

  5. Staying on MySQL 8.0

    Version 8.0 reached the end of its support in April 2026; the long-term version now is 8.4.

  6. Everything into JSON

    In either database, data with known rules belongs in columns with constraints, not in a document.

Questions about PostgreSQL and MySQL

Which is faster, PostgreSQL or MySQL?

It depends on the queries. Both are fast for typical sites; MySQL is lighter on many simple connections, PostgreSQL is stronger on complex queries and special indexes.

What should I choose for a new project?

If it is your own code and the data will grow — PostgreSQL. If it is a CMS or shared hosting — MySQL.

Can WordPress run on PostgreSQL?

Officially no: WordPress supports MySQL and MariaDB. Workarounds exist but are not worth the risk.

Are both databases free?

Yes. PostgreSQL is fully free; MySQL Community is free under GPL, and Oracle also sells an Enterprise edition.

Is it hard to move from MySQL to PostgreSQL?

The data moves with pgloader; most of the work is in queries that use MySQL-specific syntax and in case-sensitive comparisons.

What about MariaDB?

It is a separate database that grew out of MySQL. Ready CMS work with it, but it is not a drop-in replacement for MySQL 8 everywhere.

Which one is better for AI search?

PostgreSQL with pgvector: vectors live next to the data and are indexed in the open-source version.

Do I need a database administrator?

For a site — no, a correctly set up database and backups are enough. For a large service — regular attention or a managed cloud database.

Online form

PostgreSQL
and MySQL

I work with both databases: I choose the one that fits the task, design the schema, speed up slow queries and move data from one to the other. Tell me about the project — I answer within one working day.

Or write to [email protected]