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.
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.
| Criterion | PostgreSQL | MySQL |
|---|---|---|
| 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.
-
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.
-
One query instead of two
RETURNINGgives back the new row with its id and defaults at once; MySQL needs a second query. -
JSON without preparation
One GIN index in PostgreSQL covers any key; in MySQL each searchable key needs its own generated column.
-
One database instead of three
Geodata, vectors and time series come to PostgreSQL as extensions; with MySQL they usually mean separate systems.
-
The cost of a connection
Hundreds of direct connections are cheap for MySQL; PostgreSQL needs a pooler in front of it from day one.
-
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.
| Task | Take | Why |
|---|---|---|
| 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.
-- 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.
-- 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.
-- 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.
-
01
pgloader does the heavy lifting
It moves the schema and data and converts most types automatically.
-
02
Upserts are rewritten
ON DUPLICATE KEY UPDATEbecomesON CONFLICT … DO UPDATE. -
03
Quotes around names
MySQL backticks become double quotes, and unquoted names in PostgreSQL are lower-case.
-
04
Zero dates
0000-00-00does not exist in PostgreSQL — such values becomeNULLbefore the move. -
05
Case and sorting
MySQL often compares strings without case, PostgreSQL with it — searches by email or name need
lower()orcitext. -
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
-
Choosing by fashion
A blog on WordPress does not need PostgreSQL, and moving a working CMS for fashion only creates risk.
-
Thinking MariaDB is MySQL
MariaDB is a fork that has diverged: JSON, replication and some functions behave differently.
-
PostgreSQL without a pooler
A traffic peak opens more connections than the database accepts, and every site on it goes down.
-
MySQL in a lax mode
Old setups silently cut strings and accept wrong dates. Strict mode must be on.
-
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.
-
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.