GuidesDevelopers and hostingPostgreSQL vs MySQL/MariaDB

PostgreSQL, MySQL or MariaDB? Which database for your app

PostgreSQL, MySQL and MariaDB side by side on one Debian 13 server, each set up with our own guide and given the same memory, with the same benchmark, the same five million rows imported, dumped and restored, and the differences that decide real projects tried in all three.

Tested on PostgreSQL 18.6, MySQL 9.7.2 and MariaDB 12.3.3 on Debian 13 (trixie) on a Melonslab server Updated October 14, 2026

Recommended server for this guide

SSD RAID 1 · ECC DDR4 · 10 Gbit

VC-P Alloy · 2 vCPU · 8 GB Memory · 60 GB Storage

Month to month, no lock-in 7-day money-back guarantee

These steps were tested on VC-P Titanium. This uses so little that VC-P Alloy, with the same number of vCPUs and memory on an older CPU, is enough at a small scale and costs less.

€8.99/mo

Deploy now
On this page

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_only and oltp_read_write on 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.6MySQL 9.7.2MariaDB 12.3.3
How it runsDebian packages, system service1 container1 container
Steps in our guide81111
Install and first start39 seconds50 seconds26 seconds
Memory, idle after a rebootabout 100 MB820 MB1.2 GB
Disk after setup0.3 GB of packages, 47 MB of data952 MB image, 1.2 GB of data346 MB image, 1.1 GB of data
Read-only, 8 clients (transactions/s)1,3741,5831,531
Read-write, 8 clients (transactions/s)767317787
Import of 5 million rows, then two indexes28 + 16 seconds56 + 22 seconds38 + 20 seconds
Nightly dump, as in its guide21 seconds, 118 MB21 seconds, 122 MB23 seconds, 122 MB
Restore of that dump28 seconds220 seconds140 seconds
Memory per idle connectionabout 1.9 MBabout 0.5 MBabout 0.1 MB
LicencePostgreSQL LicenceGPL 2, or commercial from OracleGPL 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.

ClientsPostgreSQLMySQLMariaDB
Read-only, 1292396357
Read-only, 81,3741,5831,531
Read-only, 321,2521,4701,477
Read-write, 111691137
Read-write, 8767317787
Read-write, 32754598765

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:

PostgreSQLMySQLMariaDB
Import28 s56 s38 s
Two indexes16 s22 s20 s
Table with indexes847 MB733 MB716 MB
Dump (pg_dump / mysqldump / mariadb-dump)21 s, 118 MB21 s, 122 MB23 s, 122 MB
Restore of the dump28 s220 s140 s
Physical backup (pg_basebackup / XtraBackup / mariadb-backup)8 s68 s11 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.

All three have full-text indexes. We searched six short texts in Swedish and English:

SearchPostgreSQL (to_tsquery)MySQL and MariaDB (MATCH ... AGAINST)
hundHundarna springer i parken, En hund i stan, Hundens koppelEn hund i stan
dogThe dogs are running, A dog sleepsA dog sleeps
hund* in boolean modeall 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:

FeaturePostgreSQLMySQLMariaDB
INSERT ... RETURNINGyesnoyes
FULL OUTER JOINyesnono
LATERALyesyesno
MERGEyesnono
count(*) FILTER (WHERE ...)yesnono
Sequencesyesnoyes
Arrays as a column typeyesnono

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_basebackup copied 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 mysqlbinlog can replay them up to a moment. Its physical backups need Percona XtraBackup, a separate download.
  • MariaDB has mariadb-backup in the same image, and it took 11 seconds. Its binary log is off by default, so restoring to a moment needs log_bin switched 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 ArrayField and 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.

Run it on your own server

SSD RAID 1 · ECC DDR4 · 10 Gbit

VC-P Alloy

€8.99/mo

vCPU
2
Memory
8 GB
Storage
60 GB
Transfer
10 TB
Performance
SSD · RAID 1
  • Full root access
  • Native /64 IPv6
  • RAID-protected storage
  • Malmö, Sweden
  • Month to month, no lock-in
  • 7-day money-back guarantee

These steps were tested on VC-P Titanium. This uses so little that VC-P Alloy, with the same number of vCPUs and memory on an older CPU, is enough at a small scale and costs less.

All guides