The short answer
Pick PostgreSQL for a new app you write or choose yourself: in our tests it was slower on reads but level on writes, used the least memory at idle, restored a dump five to eight times faster, and had the features the others lack, such as migrations that roll back completely, JSON indexes, Swedish full-text search and extensions like pgvector. Pick MySQL or MariaDB when the app expects them, as WordPress and most PHP apps do. Which of those two is the subject of MySQL or MariaDB?.
What we tested
We installed all three on the same fresh Melonslab VC-P Titanium (4 vCPU, 16 GB) with Debian 13, by following our guides Run a PostgreSQL database server, Run a MySQL database server and Run a MariaDB database server step by step, with the private WireGuard setup and the settings each guide gives for an 8 GB server. PostgreSQL runs from Debian packages as a system service. MySQL and MariaDB run as one rootless Podman container each, under a user of their own.
MySQL and MariaDB both use port 3306, and their guides say to run one per server. To run them side by side, we gave MariaDB port 3307 with one extra file next to its guide's settings, ~/mariadb/conf.d/port.cnf:
[mariadb]
port = 3307
and opened 3307 on the tunnel instead of 3306 (ufw allow in on wg0 to any port 3307 proto tcp). Apps then connect to port 3307. Everything else, including the health check, worked unchanged.
Then we ran the same tests on each, one database at a time with the other two stopped, and each held to the same 6 GB of memory with a systemd limit:
- sysbench
oltp_read_onlyandoltp_read_writeon eight tables of a million rows (about 2 GB), with 1, 8 and 32 clients, 60 seconds each, every run twice, from the server itself; - pgbench on PostgreSQL as a cross-check;
- a CSV file of five million orders (519 MB) imported, indexed, dumped and restored with each guide's own backup commands, and a physical backup with each one's own tool;
- the same SQL for migrations, constraints, JSON, full-text search and vectors in all three;
- 90 idle connections to each, and 500 to PostgreSQL through PgBouncer.
The numbers
| PostgreSQL 18.6 | MySQL 9.7.2 | MariaDB 12.3.3 | |
|---|---|---|---|
| How it runs | Debian packages, system service | 1 container | 1 container |
| Steps in our guide | 8 | 11 | 11 |
| Install and first start | 39 seconds | 50 seconds | 26 seconds |
| Memory, idle after a reboot | about 100 MB | 820 MB | 1.2 GB |
| Disk after setup | 0.3 GB of packages, 47 MB of data | 952 MB image, 1.2 GB of data | 346 MB image, 1.1 GB of data |
| Read-only, 8 clients (transactions/s) | 1,374 | 1,583 | 1,531 |
| Read-write, 8 clients (transactions/s) | 767 | 317 | 787 |
| Import of 5 million rows, then two indexes | 28 + 16 seconds | 56 + 22 seconds | 38 + 20 seconds |
| Nightly dump, as in its guide | 21 seconds, 118 MB | 21 seconds, 122 MB | 23 seconds, 122 MB |
| Restore of that dump | 28 seconds | 220 seconds | 140 seconds |
| Memory per idle connection | about 1.9 MB | about 0.5 MB | about 0.1 MB |
| Licence | PostgreSQL Licence | GPL 2, or commercial from Oracle | GPL 2 |
Memory is what each service's cgroup used a few minutes after a reboot, with nothing connected; for the containers, podman stats showed the same. The data on disk after setup is mostly the 1 GB redo log that the MySQL and MariaDB guides configure. Install times are the time the commands took on our server, image downloads included, not the time it takes to read the guide.
Speed
The benchmark numbers are transactions per second, each the average of two runs. A sysbench read-only transaction is 14 queries, a read-write one 18.
| Clients | PostgreSQL | MySQL | MariaDB |
|---|---|---|---|
| Read-only, 1 | 292 | 396 | 357 |
| Read-only, 8 | 1,374 | 1,583 | 1,531 |
| Read-only, 32 | 1,252 | 1,470 | 1,477 |
| Read-write, 1 | 116 | 91 | 137 |
| Read-write, 8 | 767 | 317 | 787 |
| Read-write, 32 | 754 | 598 | 765 |
On reads, MySQL and MariaDB were 10 to 35 % faster than PostgreSQL. On writes, PostgreSQL and MariaDB were level, and MySQL was far behind with 8 clients. The difference is MySQL's binary log: it is on by default and written to disk at every commit, as a record for replication and for restoring to a point in time. MariaDB has it off by default, and PostgreSQL keeps a single log, its WAL, for both. With the binary log switched off for one test, MySQL reached 569 transactions per second with 8 clients and 798 with 32, close to the others. Keep it on: without it you lose point-in-time recovery.
MariaDB retried a few transactions after deadlocks in the read-write test (up to 111 in a 60-second run), the others almost none. sysbench retries them by itself, as an app should.
pgbench, PostgreSQL's own benchmark, gave 1,483 and 2,199 transactions per second read-write and about 27,000 read-only, with 8 and 32 clients. Its transactions are simpler than sysbench's, so the numbers only compare with each other. The data fitted in memory in all tests, as a small app's usually does; on a database larger than memory, the disk decides more than the database does.
None of the three is fast or slow enough for this to decide your choice. Run your own app's queries if speed matters.
Import, dumps and restores
The same 519 MB CSV of five million orders went into the same table in each, with COPY in PostgreSQL and LOAD DATA INFILE in the other two, followed by two indexes:
| PostgreSQL | MySQL | MariaDB | |
|---|---|---|---|
| Import | 28 s | 56 s | 38 s |
| Two indexes | 16 s | 22 s | 20 s |
| Table with indexes | 847 MB | 733 MB | 716 MB |
Dump (pg_dump / mysqldump / mariadb-dump) | 21 s, 118 MB | 21 s, 122 MB | 23 s, 122 MB |
| Restore of the dump | 28 s | 220 s | 140 s |
Physical backup (pg_basebackup / XtraBackup / mariadb-backup) | 8 s | 68 s | 11 s |
All three restored all five million rows. The dumps were the same size and took the same time, but a MySQL or MariaDB dump is a file of INSERT statements that has to run again, while pg_dump's custom format loads with COPY and builds the indexes afterwards. For a large database that is the difference between minutes and an hour when you need it back. Our MySQL and MariaDB guides add a physical backup for that reason. MySQL's took longest; its time includes starting XtraBackup's own 1.6 GB container.
PostgreSQL stored the table in 15 to 18 % more space. Its rows carry more bookkeeping, and its VACUUM reclaims the space of deleted and updated rows in the background.
Migrations and data rules
Migrations that fail half-way. We ran the same migration in a transaction in all three: add a column, create a table, then add a constraint that the existing data breaks. In PostgreSQL the error rolled everything back, and the table looked as before. In MySQL and MariaDB, the column and the new table were still there after ROLLBACK: each ALTER or CREATE commits by itself, and each statement is atomic, but a migration of several is not. When a deploy fails half-way, PostgreSQL leaves you where you were; the other two leave you to clean up by hand.
Constraints. All three refused a negative price against a CHECK (price >= 0), and all three refused 'abc' in a number column and seven letters in a varchar(5). MySQL and MariaDB have done that by default for years, through strict SQL mode. But the mode is a setting an app can change, and some older PHP apps do. With sql_mode='', both stored 0 and toolo with only a warning. PostgreSQL has no such mode.
They also differ on mistakes that are not errors in MySQL and MariaDB: SELECT 1/0 gave NULL in both, and the date 2026-02-30 became NULL. PostgreSQL stopped with division by zero and date/time field value out of range.
Case and accents. By default, MySQL and MariaDB compare text without regard to case or accents: 'Anna' = 'anna' and 'Åsa' = 'Asa' were both true. A unique email column then also treats Anna@example.com and anna@example.com as the same, which is often what you want, and a search for Asa finds Åsa, which a Swedish app may not. PostgreSQL compared both as different. For case-insensitive text in PostgreSQL, use the citext extension or a case-insensitive ICU collation; MySQL and MariaDB have Swedish collations that keep Å, Ä and Ö apart.
Table names work the other way. In PostgreSQL, CREATE TABLE Customers creates customers, and both spellings find it. In MySQL and MariaDB on Linux, table names are case-sensitive: SELECT FROM customers failed with Table 'demo.customers' doesn't exist. An app developed on Windows or macOS can trip over this on a Linux server.
Characters through the container. The mysql command inside the MySQL container used latin1 when we piped SQL into it, and stored Åsa as Ã…sa. MariaDB's used utf8mb4. Dumps are not affected, as they set their own character set, but add --default-character-set=utf8mb4 when you type or pipe text with å, ä or ö into MySQL that way.
JSON
All three store JSON and refused invalid JSON. They index it differently.
PostgreSQL's jsonb takes one GIN index on the whole column, and that index then answers questions about any key:
CREATE INDEX events_doc ON events USING gin (doc jsonb_path_ops);
SELECT count(*) FROM events WHERE doc @> '{"type": "buy", "user": 42}';
SELECT count(*) FROM events WHERE doc @> '{"tags": ["t7"]}';
On 100,000 events, EXPLAIN showed Bitmap Index Scan on events_doc for both queries.
MySQL and MariaDB index one key at a time, through a generated column:
ALTER TABLE events ADD COLUMN type varchar(10) AS (JSON_VALUE(doc, '$.type')) VIRTUAL,
ADD COLUMN user int AS (JSON_VALUE(doc, '$.user')) VIRTUAL, ADD INDEX type_user (type, user);
SELECT count(*) FROM events WHERE type = 'buy' AND user = 42;
Both used the index for that query, but a JSON_CONTAINS on the same keys still read the whole table. MySQL can also index the values of an array, with a multi-valued index and MEMBER OF, which it used for the tags. MariaDB has no multi-valued index and no ->> shorthand, and its JSON type is a LONGTEXT with a validity check: the documents are stored as text and parsed at every query, where PostgreSQL and MySQL store a binary format.
If your app keeps flexible data in JSON and searches it in ways you don't know yet, PostgreSQL is the easier fit.
Full-text search
All three have full-text indexes. We searched six short texts in Swedish and English:
| Search | PostgreSQL (to_tsquery) | MySQL and MariaDB (MATCH ... AGAINST) |
|---|---|---|
hund | Hundarna springer i parken, En hund i stan, Hundens koppel | En hund i stan |
dog | The dogs are running, A dog sleeps | A dog sleeps |
hund* in boolean mode | all three Swedish texts |
PostgreSQL reduces words to their stem in 29 languages, Swedish included, and ranks the results. MySQL and MariaDB match whole words, so hund does not find hundarna. A wildcard helps for endings, but not for other forms. For a search box on a small site, any of the three works; for search in Swedish that finds what people mean, PostgreSQL does it without another program.
SQL features
The features that were missing ten years ago are there now. A recursive WITH query with SUM() OVER and ROW_NUMBER() OVER ran the same in all three and gave the same result, and all three had INTERSECT and CHECK constraints. The rest differed:
| Feature | PostgreSQL | MySQL | MariaDB |
|---|---|---|---|
INSERT ... RETURNING | yes | no | yes |
FULL OUTER JOIN | yes | no | no |
LATERAL | yes | yes | no |
MERGE | yes | no | no |
count(*) FILTER (WHERE ...) | yes | no | no |
| Sequences | yes | no | yes |
| Arrays as a column type | yes | no | no |
Each "no" has a workaround. Together they are why ORMs and query builders often support more of their features on PostgreSQL.
Extensions and vectors
PostgreSQL can be extended with new types, indexes and functions, packaged in the same repository as PostgreSQL itself. Two are reasons people choose it: PostGIS for maps and geodata, and pgvector for similarity search on AI embeddings. We tried pgvector:
apt install postgresql-18-pgvector
CREATE EXTENSION vector;
CREATE TABLE items(id bigserial PRIMARY KEY, embedding vector(3));
CREATE INDEX items_hnsw ON items USING hnsw (embedding vector_l2_ops);
SELECT id FROM items ORDER BY embedding <-> '[0.5,0.5,0.5]' LIMIT 5;
pgvector 0.8.7 built the index for 100,000 vectors in 24 seconds, and EXPLAIN showed Index Scan using items_hnsw. Our test vectors had 3 dimensions; real embeddings have hundreds, so plan for more memory and disk.
MariaDB has vectors built in since 11.7: a VECTOR type, a VECTOR INDEX and VEC_DISTANCE_EUCLIDEAN(), and EXPLAIN showed the index in use. MySQL 9.7 has the VECTOR type, but no vector index, and DISTANCE() failed with FUNCTION demo.DISTANCE does not exist: Oracle's documentation says it is only available in MySQL HeatWave on Oracle's cloud and MySQL AI, not in Community Server.
MySQL and MariaDB have spatial types and functions built in, without PostGIS, which covers distances and areas for many apps. PostGIS goes much further.
Connections
PostgreSQL starts a process for each connection; MySQL and MariaDB a thread. With 90 idle connections, after one query each, PostgreSQL used about 170 MB more, MySQL 45 MB and MariaDB 12 MB.
PostgreSQL's guide keeps the default max_connections = 100, and the 98th connection was refused with remaining connection slots are reserved for roles with the SUPERUSER attribute. An app with many workers, or many apps on one database server, needs a connection pooler. We put PgBouncer 1.26 from Debian in front, in transaction mode with 20 connections to PostgreSQL: 500 pgbench clients ran through it for 30 seconds at 6,907 transactions per second, with no errors. A pooler adds a step, though: with 32 clients, read-only, PgBouncer gave 9,375 transactions per second against 18,616 directly. Use it when you need more connections, not by default. Many frameworks have a pool of their own, which is often enough.
The MySQL and MariaDB guides allow 200 connections, and their threads are cheap, so most apps never need a pooler for them.
Resources
At idle, PostgreSQL used about 100 MB. It reads through the system's file cache, so its guide gives it a quarter of the memory as its own cache and lets the rest of the memory work as cache too. MySQL and MariaDB keep their own cache, 4 GB in their guides, and MariaDB filled more of it at start: 820 MB and 1.2 GB after a reboot. At the end of the benchmark, MySQL used 5.9 GB of its 6 GB limit, half of it its own cache; PostgreSQL 5.3 GB, nearly all of it file cache that the system can give back; and MariaDB 2.8 GB.
On disk, PostgreSQL's packages were about 0.3 GB, most of it LLVM for its query compiler. The MySQL image was 952 MB, MariaDB's 346 MB. Both start with a 1 GB redo log. After all our tests, PostgreSQL and MariaDB each used 2.1 GB of data, and MySQL 4.5 GB, of which 2.5 GB was binary log from the benchmarks. Its guide keeps seven days of it, so plan disk for a week of changes.
Updates, backups and replication
Updates. PostgreSQL's guide installs minor versions from the PostgreSQL repository with unattended-upgrades; the MySQL and MariaDB containers update with podman auto-update. All three restart for a few seconds. A major version of PostgreSQL comes once a year and is supported for five; the guide moves the data with pg_upgradecluster. MySQL has an LTS release about every two years, MariaDB a long-term release every year with three years of support, and both upgrade their data when the new version starts.
Backups. All three guides dump every database each night with a timer and restore it in the test. The difference is in what you can do beyond that:
- PostgreSQL's
pg_basebackupcopied the whole cluster, 946 MB, in 8 seconds, with no extra package. Restoring to a moment in time needs WAL archiving and a tool such as pgBackRest, which its guide does not cover. - MySQL has the binary log on, with GTIDs, so the changes since the last backup are already on disk and
mysqlbinlogcan replay them up to a moment. Its physical backups need Percona XtraBackup, a separate download. - MariaDB has
mariadb-backupin the same image, and it took 11 seconds. Its binary log is off by default, so restoring to a moment needslog_binswitched on first.
Replication. All three can keep a copy of the database on a second server. PostgreSQL's WAL is ready for it by default (wal_level = replica, 10 WAL senders), and a replica starts from a pg_basebackup. MySQL and MariaDB replicate from the binary log, and MariaDB also has Galera for a cluster where every node takes writes. We did not set up a replica in this test.
Which apps expect which
Many apps decide for you. We checked their own pages on 14 October 2026:
- WordPress asks for MySQL 8.0 or newer, or MariaDB 10.11 or newer, and does not mention PostgreSQL. Most PHP apps are the same, and so are some of the apps in our guides: BookStack, Kimai, Nextcloud, Passbolt and ERPNext all run on MariaDB here.
- Django 6.1 supports PostgreSQL 15+, MariaDB 10.11+ and MySQL 8.4+, and Rails has adapters for all three, so an app you write can use any of them. Both have features that only work on PostgreSQL, such as Django's
ArrayFieldand full-text search. - Most newer self-hosted apps choose PostgreSQL: 16 of the app guides on this site use it, among them Immich, Mattermost, Outline, Paperless-ngx, n8n and Authentik.
If the app supports both, use the one it lists first in its documentation. That is the one its developers test most.
Licence and cost
We checked the projects' own pages on 14 October 2026.
- PostgreSQL is open source under the PostgreSQL Licence, a short licence like BSD and MIT: you may use, change and sell it, in your own products too, as long as you keep the copyright notice. It is made by the PostgreSQL Global Development Group, a community project, and no company owns it.
- MySQL Community Server is open source under the GPL version 2, owned by Oracle in the United States. Oracle sells a commercial licence to companies that ship MySQL inside a product they do not want to publish under the GPL, and MySQL Enterprise Edition with extra tools and support. Running it as the database for your own app needs neither. Some features, such as vector
DISTANCE(), are only in Oracle's cloud service. - MariaDB is open source under the GPL version 2, and its client libraries under the LGPL. It is developed by MariaDB plc in the United States and the non-profit MariaDB Foundation. MariaDB plc sells MariaDB Enterprise Server with support to subscribers.
All three are free to self-host. The GPL only matters if you distribute the database itself with your product; an app that connects to it over the network is not affected.
Which one to pick
PostgreSQL suits a new app, or any app that supports it well: migrations that roll back completely, strict data rules that no setting turns off, JSON you can index as it is, Swedish full-text search and extensions such as pgvector and PostGIS. It used the least memory at idle and restored a dump five to eight times faster. Its weak points are slower reads in our benchmark, a process per connection, so busy apps need a pooler such as PgBouncer, more disk for the same table, and VACUUM to keep an eye on. Follow the PostgreSQL guide, on a VC-P Alloy.
MySQL suits apps written for it, such as WordPress, and teams that know it well. It was fastest on reads, and its binary log with GTIDs is ready for point-in-time recovery from the start. Its weak points are DDL that cannot be rolled back, write speed with the binary log on, slow restores of dumps, a large image, physical backups that need a separate tool, and features such as vector distance that Oracle keeps for its cloud. Follow the MySQL guide, on a VC-P Alloy.
MariaDB suits the same apps as MySQL, with a smaller image, built-in vector search, RETURNING and sequences, and mariadb-backup in the box. It was as fast as PostgreSQL on writes in our test. Its weak points are the same DDL and text rules as MySQL, JSON stored as text, and the most memory at idle. Follow the MariaDB guide, on a VC-P Alloy. For the differences between MySQL and MariaDB, read MySQL or MariaDB?.
Each runs comfortably on a VC-P Alloy with the settings from its guide. Our VC-P Titanium ran all three at once, which you only need if you run apps that need different databases on one server.