On this page
- The short answer
- How I tested
- Result 1: OLTP throughput with sysbench
- Result 2: reporting queries over 2 million rows
- Result 3: adding an index made MySQL's join slower
- Result 4: what repeated updates do to the table
- Result 5: connections
- How to run this comparison on your own data
- Common mistakes when comparing databases
- Choosing: what the numbers do and do not say
- Related Dev Talk articles and tools
- Frequently asked questions
- Is PostgreSQL faster than MySQL?
- Should I migrate from MySQL to PostgreSQL for speed?
- Does Laravel work with both?
- What about MariaDB?
- 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
| Workload | What I measured | Result in this test |
|---|---|---|
| Simple point reads | sysbench, 8 connections | PostgreSQL about 1.7 to 2.2 times the throughput |
| Mixed read/write transactions | sysbench, 8 connections | PostgreSQL about 2.1 to 2.5 times the throughput |
| Aggregates and joins over 2M rows | GROUP BY, JOIN | PostgreSQL 5 to 6 times faster |
| Table size after heavy updates | 5 full-table updates | PostgreSQL grew 30 to 113 MB; MySQL stayed at 24.5 MB |
| Single primary-key lookup from PHP | one connection, sequential | MySQL about 2 times faster (0.06 vs 0.12 ms) |
| Cost of opening a connection | PDO connect, localhost | MySQL 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
| Item | Setting |
|---|---|
| Hardware | 2 vCPU, 8 GB RAM Linux container, client and servers on the same machine |
| PostgreSQL | 16.15 (Ubuntu package), shared_buffers=1GB, fsync=on, synchronous_commit=on, autovacuum on |
| MySQL | 8.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 generator | sysbench 1.0.20, 4 tables of 100,000 rows, 20 second runs |
| Other tests | PHP 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.
| Workload | Threads | PostgreSQL TPS | MySQL TPS | 95th percentile latency (PG / MySQL) |
|---|---|---|---|---|
| point_select | 1 | 23,557 | 17,646 | 0.06 / 0.08 ms |
| point_select | 8 | 142,737 (range 120,940 to 142,737) | 85,080 (range 55,926 to 85,080) | 0.09 / 0.19 ms |
| read_only | 1 | 1,146 | 831 | 1.03 / 1.39 ms |
| read_only | 8 | 5,250 | 2,761 | 1.86 / 4.65 ms |
| read_write | 1 | 710 | 464 | 1.70 / 2.91 ms |
| read_write | 8 | 1,963 (range 1,694 to 1,963) | 780 (range 762 to 789) | 6.43 / 18.61 ms |
| write_only | 1 | 1,959 | 1,280 | 0.67 / 1.25 ms |
| write_only | 8 | 6,246 | 2,278 | 2.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_schemawas 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.
| Query | PostgreSQL | MySQL |
|---|---|---|
count(*) | 37 ms | 40 ms |
GROUP BY kind with count, sum, avg | 120 ms | 616 ms |
JOIN users GROUP BY region | 213 ms | 1,321 ms |
ORDER BY id LIMIT 20 OFFSET 1900000 | 84 ms | 200 ms |
Keyset: WHERE id > 1900000 ... LIMIT 20 | 0.14 ms | 0.72 ms |
| Primary key lookup | 0.12 ms | 0.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 region | Before index | After index on user_id |
|---|---|---|
| PostgreSQL | 213 ms | 203 ms (same plan: hash join) |
| MySQL 8.0.46 | 1,321 ms | 2,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 msOpening 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 runRun 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
psqlormysqlprocess 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).
Related Dev Talk articles and tools
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.
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.
Enjoying the content?
If this article helped you, you can support the author with a coffee. It keeps new posts coming.
Comments 0
No comments yet
Be the first to share your thoughts.