Skip to content

PostgreSQL vs MySQL Performance: Real Benchmarks Compared

PostgreSQL 16 vs MySQL 8.0 on the same machine: sysbench results, 2M-row queries, table bloat and an index plan surprise. Real numbers, honest caveats.

Dasun Kalupahana 8 min read 7 views 0 comments
On this page
  1. The short answer
  2. How I tested
  3. Result 1: OLTP throughput with sysbench
  4. Result 2: reporting queries over 2 million rows
  5. Result 3: adding an index made MySQL's join slower
  6. Result 4: what repeated updates do to the table
  7. Result 5: connections
  8. How to run this comparison on your own data
  9. Common mistakes when comparing databases
  10. Choosing: what the numbers do and do not say
  11. Related Dev Talk articles and tools
  12. Frequently asked questions
  13. Is PostgreSQL faster than MySQL?
  14. Should I migrate from MySQL to PostgreSQL for speed?
  15. Does Laravel work with both?
  16. What about MariaDB?
  17. Would you get the same results on bigger hardware?

Last updated: 7 November 2026. See the update log at the end.

"Which is faster, PostgreSQL or MySQL?" has no single answer, because speed depends on the query, the data, the settings and the version. Most comparisons you will find change several of those at once. So I ran both on the same machine, loaded identical data, used matching memory settings, and measured typical web-application workloads: simple reads, mixed reads and writes, reporting queries, indexes, and what happens to a table under heavy updates. The results are below, along with the places where the numbers do not mean what they seem to.

The short answer

WorkloadWhat I measuredResult in this test
Simple point readssysbench, 8 connectionsPostgreSQL about 1.7 to 2.2 times the throughput
Mixed read/write transactionssysbench, 8 connectionsPostgreSQL about 2.1 to 2.5 times the throughput
Aggregates and joins over 2M rowsGROUP BY, JOINPostgreSQL 5 to 6 times faster
Table size after heavy updates5 full-table updatesPostgreSQL grew 30 to 113 MB; MySQL stayed at 24.5 MB
Single primary-key lookup from PHPone connection, sequentialMySQL about 2 times faster (0.06 vs 0.12 ms)
Cost of opening a connectionPDO connect, localhostMySQL 0.16 ms, PostgreSQL 1.62 ms

That is one small machine, one workload generator and two specific versions. Read the caveats before you quote any of it.

How I tested

ItemSetting
Hardware2 vCPU, 8 GB RAM Linux container, client and servers on the same machine
PostgreSQL16.15 (Ubuntu package), shared_buffers=1GB, fsync=on, synchronous_commit=on, autovacuum on
MySQL8.0.46 (Ubuntu package), innodb_buffer_pool_size=1G, innodb_flush_log_at_trx_commit=1, binary log off (PostgreSQL has no equivalent second log, so I switched it off for parity)
Load generatorsysbench 1.0.20, 4 tables of 100,000 rows, 20 second runs
Other testsPHP 8.3.6 PDO, a 2,000,000-row table loaded identically into both (the sum(amount) checksums matched)

Both are close to default settings apart from memory. I did not tune either server for its workload. Versions matter here: MySQL 8.0 reached end of life on 30 April 2026, and the supported LTS lines are 8.4 and 9.7. PostgreSQL 16 is still supported (until November 2028), and 18 is the newest release I could confirm. Results on those newer versions may differ, so treat these as a snapshot of what Ubuntu 24.04 ships.

Result 1: OLTP throughput with sysbench

sysbench's standard transactional workloads. TPS is transactions per second, so higher is better. The 8-thread figures are the first run; I repeated some workloads and give the range.

WorkloadThreadsPostgreSQL TPSMySQL TPS95th percentile latency (PG / MySQL)
point_select123,55717,6460.06 / 0.08 ms
point_select8142,737 (range 120,940 to 142,737)85,080 (range 55,926 to 85,080)0.09 / 0.19 ms
read_only11,1468311.03 / 1.39 ms
read_only85,2502,7611.86 / 4.65 ms
read_write17104641.70 / 2.91 ms
read_write81,963 (range 1,694 to 1,963)780 (range 762 to 789)6.43 / 18.61 ms
write_only11,9591,2800.67 / 1.25 ms
write_only86,2462,2782.03 / 7.98 ms

PostgreSQL was ahead in every row. Three things to know about the numbers:

  • Run-to-run noise is real. The point-select results varied by 20 to 30 percent between runs, because the load generator shares two CPUs with the database. The ratio stayed between 1.7 and 2.2, but do not quote the absolute figures.
  • MySQL's performance_schema was not the reason. It is on by default and costs some throughput, so I restarted MySQL with it off and reran two workloads: 78,803 TPS for point_select and 789 for read_write, in line with the earlier runs.
  • PostgreSQL reported a few ignored errors under write load: 13 in the 8-thread read_write run and 99 in write_only, out of roughly 39,000 and 125,000 transactions. The server log showed duplicate-key errors from concurrent delete-and-reinsert of the same row, which sysbench ignores by default. MySQL reported none. They are a tiny fraction of transactions and do not change the picture, but a real application would have to retry them.

Result 2: reporting queries over 2 million rows

Same table and same data in both (events: id, user_id, kind, amount, created_at; plus a 50,000-row users table). I timed each query nine times inside one persistent PHP PDO connection and took the median. No secondary indexes on events.

QueryPostgreSQLMySQL
count(*)37 ms40 ms
GROUP BY kind with count, sum, avg120 ms616 ms
JOIN users GROUP BY region213 ms1,321 ms
ORDER BY id LIMIT 20 OFFSET 190000084 ms200 ms
Keyset: WHERE id > 1900000 ... LIMIT 200.14 ms0.72 ms
Primary key lookup0.12 ms0.06 ms

Part of PostgreSQL's lead on the heavy queries is parallel query. With set max_parallel_workers_per_gather=0 its GROUP BY went from 120 to 211 ms and the join from 213 to 378 ms, which is still well ahead of MySQL's single-threaded run. Two lessons from the pagination rows: a deep OFFSET is slow on both (so use keyset pagination, as in the Laravel pagination article), and MySQL is faster at the single primary-key lookup, though both are far below a millisecond.

Result 3: adding an index made MySQL's join slower

This one surprised me, and it is why you should check plans instead of assuming. I created an index on events(user_id) in both databases, ran ANALYZE, and repeated the join:

JOIN + GROUP BY regionBefore indexAfter index on user_id
PostgreSQL213 ms203 ms (same plan: hash join)
MySQL 8.0.461,321 ms2,094 ms

MySQL's plan flipped. Before the index it scanned events and looked up each row's user by primary key. After it, the optimizer chose to scan users and do one index lookup into events per user (50,000 lookups returning about 40 rows each), which its cost estimate rated cheaper and which was slower in practice. This is MySQL 8.0.46 only; I did not test 8.4 or 9.7. The index helped exactly the query it was made for: WHERE user_id = 123 took 0.08 ms in MySQL and 0.13 ms in PostgreSQL. Also in my measurements, MySQL's count(*) went from 40 to 124 ms after the index was added, and I did not investigate why.

Building the index took 421 ms in PostgreSQL and 1,922 ms in MySQL (single runs, wall time from the command-line clients).

Result 4: what repeated updates do to the table

PostgreSQL's design keeps old row versions in the table until VACUUM removes them. I took a 100,000-row table, disabled autovacuum for it to make the test deterministic, and ran update sbtest1 set k = k + 1 five times in both databases.

                          PostgreSQL                         MySQL (InnoDB)
before:                   30 MB table, 4.7 MB indexes        24.5 MB table, 1.5 MB indexes
after 5 full updates:     113 MB table, 11 MB indexes        24.5 MB table, 5.0 MB indexes
                          dead_tuples = 500000
after plain VACUUM:       113 MB table, dead_tuples = 0
5 more updates:           114 MB table (space reused)

The table in PostgreSQL grew to nearly four times its size, and a plain VACUUM made the space reusable but did not shrink the file (VACUUM FULL does, with a lock). In production autovacuum is on by default and normally keeps up, since I disabled it here to show the mechanism, but tables with heavy updates still need monitoring. In MySQL the table stayed the same size; the secondary index did grow, from 1.5 to 5.0 MB. The "before" sizes include some leftovers from the earlier benchmark runs.

Result 5: connections

new PDO connection (localhost, TCP):   PostgreSQL 1.62 ms    MySQL 0.16 ms
"select 1" on an open connection:      PostgreSQL 0.10 ms    MySQL 0.04 ms

Opening a PostgreSQL connection was about ten times more expensive than a MySQL one, which matters if your application opens a connection per request without pooling. Under heavy traffic, put a pooler such as PgBouncer in front of PostgreSQL. I did not test PgBouncer. The "select 1" gap is mostly a driver and protocol effect: sysbench uses prepared statements, where PostgreSQL was faster per query, while PHP's plain query() was faster on MySQL. Differences this small say more about the client than the database.

How to run this comparison on your own data

Benchmark your own queries on your own schema. The commands I used, with the connection details changed to yours:

# load the test tables (PostgreSQL shown; use --db-driver=mysql and --mysql-* options for MySQL)
sysbench oltp_read_write --db-driver=pgsql --pgsql-host=127.0.0.1 --pgsql-port=5432 \
  --pgsql-user=postgres --pgsql-db=sbtest --tables=4 --table-size=100000 prepare

# run it
sysbench oltp_read_write --db-driver=pgsql --pgsql-host=127.0.0.1 --pgsql-port=5432 \
  --pgsql-user=postgres --pgsql-db=sbtest --tables=4 --table-size=100000 \
  --threads=8 --time=20 run

Run each workload more than once, at more than one thread count, and read the "ignored errors" line. For your real queries, time them inside one open connection and read the plan:

-- PostgreSQL
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;

-- MySQL 8.0.18 and later
EXPLAIN ANALYZE SELECT ...;

-- watch for dead rows in PostgreSQL
SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;

Common mistakes when comparing databases

  • Comparing different settings. Match buffer memory and durability, or you are comparing configurations, not databases.
  • Mixing connection methods. My first timings used a unix socket for one and TCP for the other, and the results were wrong. Use the same transport and a persistent connection.
  • Timing the client startup. A new psql or mysql process per query measures process start, not the query.
  • Forgetting ANALYZE. Planners need fresh statistics after a bulk load.
  • Trusting one run. Repeat, and look at the spread.
  • Adding an index and not re-checking the plan. As Result 3 shows, an index can change a plan for the worse.
  • Benchmarking an end-of-life version. MySQL 8.0 is past end of life; test the version you would actually deploy.

Choosing: what the numbers do and do not say

  • For reporting, aggregates and joins over millions of rows, PostgreSQL was clearly faster here, partly through parallel query.
  • For many small, simple queries, both were fast, and MySQL was quicker on a single primary-key lookup and at opening connections.
  • For tables with heavy updates, PostgreSQL needs vacuum monitoring and MySQL does not have that particular concern.
  • Application fit usually matters more than these gaps: the team's experience, the tooling, the hosting options, and how close you already are to one of them. Laravel works with both. If you are already on MySQL 8.0, moving to 8.4 is the more urgent job (see the MySQL 8.0 to 8.4 upgrade guide), and most slow pages come from queries and indexes before they come from the database engine (see Laravel 13 performance optimization).

Tools: sysbench for load tests, EXPLAIN ANALYZE for plans, and PgBouncer for PostgreSQL connection pooling (not tested here).

Frequently asked questions

Is PostgreSQL faster than MySQL?

In this test, with near-default settings on a small machine, PostgreSQL 16 was faster on every sysbench workload and on aggregate and join queries. That is not a law. It depends on version, settings, hardware and workload, so test with your own queries.

Should I migrate from MySQL to PostgreSQL for speed?

Not on these numbers alone. Fix slow queries and missing indexes first, and weigh migration cost, tooling and team experience. If you are on MySQL 8.0, upgrading to a supported version is the priority.

Does Laravel work with both?

Yes, the framework supports both databases. Check the Laravel documentation for any feature that is specific to one of them.

What about MariaDB?

I did not test it, so I have no numbers for it.

Would you get the same results on bigger hardware?

I do not know. I ran on 2 vCPUs with the load generator on the same machine; multi-core servers, faster disks and separate client machines could change the ratios.

Free resources for this topic

Code snippets and fixes for this topic

Get one practical tip every week

New tutorials, fixes and free cheat sheets from DEV Talk, straight to your inbox. No spam, unsubscribe in one click.

Free download: Laravel interview questions (PDF)

Have a question about this post?

Ask the community: other developers and the author can answer.

Ask the community

Get notified about new articles

One click, no email needed. Turn it off anytime with the bell at the top.

Get new articles on your iPhone

Add DEV Talk to your Home Screen, open it, then tap the bell.

Rate this post
0.0
Share

Enjoying the content?

If this article helped you, you can support the author with a coffee. It keeps new posts coming.

Buy me a coffee (opens in a new tab)

Comments 0

Be kind. Comments with links are reviewed before they appear.

No comments yet

Be the first to share your thoughts.

Get new articles in your inbox

Practical tutorials and deep dives, about once a week. No spam, unsubscribe anytime.

Privacy policy

Add DEV Talk to your Home Screen

  1. 1 Tap the Share button in Safari's toolbar.
  2. 2 Scroll down and tap Add to Home Screen.
  3. 3 Tap Add. Open it from the Home Screen to read like an app and to turn on notifications with the bell.

We use cookies to improve your experience and analyze traffic. By continuing, you agree to our Privacy Policy.