MySQL vs PostgreSQL: Database Decision Guide for CTOs
Strategic comparison of MySQL and PostgreSQL. Performance, features, costs, team expertise, and when to choose each relational database.
Table of Contents
TL;DR: Decision Matrix
| Factor | MySQL | PostgreSQL | Winner |
|---|---|---|---|
| Simple key-lookup reads | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent | Too close to call — benchmark your own workload |
| Write concurrency | ⭐⭐⭐⭐ Strong | ⭐⭐⭐⭐ Strong | Depends on tuning, not engine |
| Complex queries | ⭐⭐⭐ Good | ⭐⭐⭐⭐⭐ Excellent | PostgreSQL |
| JSON Support | ⭐⭐⭐ Functional | ⭐⭐⭐⭐⭐ Native + indexable | PostgreSQL |
| Full-Text Search | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Better | PostgreSQL |
| Replication | ⭐⭐⭐⭐⭐ Mature | ⭐⭐⭐⭐ Good | MySQL |
| Ecosystem | ⭐⭐⭐⭐⭐ Massive | ⭐⭐⭐⭐⭐ Massive | Tie |
| Managed Services | ⭐⭐⭐⭐⭐ AWS RDS, GCP | ⭐⭐⭐⭐⭐ AWS RDS, GCP | Tie |
| Licensing | ⭐⭐⭐⭐ GPL (Oracle) | ⭐⭐⭐⭐⭐ PostgreSQL License | PostgreSQL |
| Community | ⭐⭐⭐⭐ Large | ⭐⭐⭐⭐⭐ Very active | PostgreSQL |
| Learning Curve | ⭐⭐⭐⭐⭐ Easy | ⭐⭐⭐⭐ Moderate | MySQL |
| Advanced Features | ⭐⭐⭐ Good | ⭐⭐⭐⭐⭐ Excellent | PostgreSQL |
Quick Recommendation:
- Simple CRUD app: MySQL (simplicity, smaller operational surface)
- Complex queries: PostgreSQL (advanced SQL, richer indexing)
- JSON data: PostgreSQL (native, indexable JSONB)
- Geospatial: PostgreSQL (PostGIS)
- Team knows MySQL: MySQL (unless you need Postgres features)
- Greenfield project: PostgreSQL (broadest feature set)
On performance: you will not find throughput numbers in this guide. Both engines are fast enough that the winner on any given workload is decided by your schema, indexes, hardware and configuration — not by the logo. Anyone quoting you a queries-per-second figure without publishing their hardware, dataset and config is quoting you folklore. See Performance: how to actually decide below.
The Real Question: Simplicity vs Features
Every CTO faces this trade-off:
MySQL = Simple, fast, battle-tested, smaller feature surface PostgreSQL = Feature-rich, standards-compliant, extensible, more to operate
Your choice depends on:
- Workload type (read-heavy vs write-heavy, simple vs complex queries)
- Data type (relational vs JSON/document hybrid)
- Team expertise (MySQL is easier to start with, Postgres has a steeper curve)
MySQL: The Simple, Fast Default
Why MySQL Still Dominates
Speed (Read-Heavy Workloads):
- Optimised for simple SELECT queries
- InnoDB (default since 5.5) is fast for primary-key lookups
- Clustered primary-key storage means a PK lookup reads the row directly from the index (InnoDB index types)
Simplicity:
-- MySQL: Straightforward
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
SELECT * FROM users WHERE id = 1;Ecosystem:
- Adoption: consistently one of the two most popular open-source relational databases in the DB-Engines ranking (a popularity score derived from job postings, search volume and social mentions — not a measure of installed base or market share)
- Hosting: Every shared host supports MySQL
- Tools: phpMyAdmin, MySQL Workbench, Adminer
- ORMs: Every framework (Rails, Django, Laravel, Spring) has mature MySQL support
Managed Services:
- AWS RDS for MySQL - Fully managed
- Google Cloud SQL for MySQL - Fully managed
- Azure Database for MySQL - Enterprise-grade
- PlanetScale - Vitess-based MySQL (serverless scaling)
Companies that have publicly documented running MySQL at scale:
- Uber — migrated to MySQL from Postgres in 2016 (Uber Engineering)
- GitHub — MySQL high availability at GitHub
- Facebook/Meta — MyRocks, Meta's MySQL storage engine
MySQL's Drawbacks
Narrower SQL surface:
-- Window functions and CTEs only from 8.0
-- No FULL OUTER JOIN
-- CHECK constraints only enforced from 8.0.16
-- No partial (filtered) indexesWindow functions and common table expressions both arrived in MySQL 8.0
(What Is New in MySQL 8.0) —
PostgreSQL had shipped both years earlier, so this is MySQL catching up rather
than a point of parity. The implementations are not equivalent either: MySQL's
recursive CTE disallows aggregates, window functions, GROUP BY, DISTINCT and
ORDER BY in the recursive SELECT
(docs).
CHECK constraints are only enforced from 8.0.16
(docs).
JSON is functional but less indexable:
-- MySQL: index a JSON path via a generated column
SELECT JSON_EXTRACT(data, '$.name') FROM users;
ALTER TABLE users ADD COLUMN name VARCHAR(255)
AS (JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')));
CREATE INDEX idx_name ON users(name);
-- vs PostgreSQL, where a GIN index covers the document directly(MySQL JSON type, indexing generated columns)
Replication has sharp edges:
- Statement-based replication can diverge on non-deterministic statements
- Row-based replication is safer but produces larger binlogs
- GTID adds safety at the cost of setup complexity (Replication formats)
Licensing Concerns:
- Owned by Oracle (some fear Oracle lock-in)
- GPL (can be restrictive for proprietary software)
- Commercial extensions (MySQL Enterprise) are expensive
When to Choose MySQL
✅ Read-heavy workload - Blog, CMS, e-commerce product catalog ✅ Simple queries - Key lookups, basic JOINs ✅ Team knows MySQL - Lower learning curve, faster onboarding ✅ Shared hosting - MySQL is universally available ✅ Legacy app - Already on MySQL, migration is expensive
❌ Don't choose MySQL if:
- Complex analytical queries (Postgres has the richer planner)
- JSON-heavy workload (Postgres JSONB indexes the document directly)
- Need advanced SQL features (FULL OUTER JOIN, partial indexes, exclusion constraints)
- Licensing concerns (Oracle-owned)
PostgreSQL: The Feature-Rich Powerhouse
Why PostgreSQL Is the "Most Advanced Open-Source Database"
SQL Compliance:
- Close adherence to the ANSI SQL standard (conformance)
- Window functions, CTEs, FULL OUTER JOIN
- Check constraints, exclusion constraints
- Partial indexes, expression indexes
JSONB (Native JSON):
-- PostgreSQL: Native JSONB (indexed, fast)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
data JSONB
);
INSERT INTO users (data) VALUES ('{"name": "Alice", "age": 30}');
SELECT * FROM users WHERE data->>'name' = 'Alice';
SELECT * FROM users WHERE data @> '{"age": 30}';
-- GIN index on JSONB
CREATE INDEX idx_data ON users USING GIN (data);Advanced Indexing:
- B-tree (default, general-purpose)
- Hash (equality lookups)
- GiST (geometric, full-text search)
- GIN (JSON, arrays, full-text)
- BRIN (large tables, time-series)
- Partial indexes (index subset of rows)
- Expression indexes (index computed values)
Extensions:
-- PostGIS (geospatial)
CREATE EXTENSION postgis;
SELECT ST_Distance(point1, point2) FROM locations;
-- pg_trgm (fuzzy text search)
CREATE EXTENSION pg_trgm;
SELECT * FROM users WHERE name % 'Alise'; -- Finds "Alice"
-- uuid-ossp (UUIDs)
CREATE EXTENSION "uuid-ossp";
INSERT INTO users (id) VALUES (uuid_generate_v4());
-- TimescaleDB (time-series)
CREATE EXTENSION timescaledb;
SELECT time_bucket('1 hour', timestamp), avg(value) FROM metrics GROUP BY 1;MVCC (Multi-Version Concurrency Control):
- Readers do not block writers and writers do not block readers
- Note that InnoDB also implements MVCC — this is not a point of difference between the two engines, but a point of difference in how each implements it. Postgres keeps old row versions in the table heap and reclaims them with VACUUM; InnoDB keeps them in a separate undo log and purges them in the background. That single design choice explains most of the operational differences below. (PostgreSQL MVCC, InnoDB multi-versioning)
Companies that have publicly documented running PostgreSQL at scale:
- Instagram — Sharding & IDs at Instagram
- Heroku Postgres, Neon, Supabase, Timescale — the managed-Postgres ecosystem is itself the clearest evidence of production maturity at scale
PostgreSQL's Drawbacks
Vacuum and bloat:
-- PostgreSQL reclaims dead tuples with VACUUM
VACUUM ANALYZE users;
-- Autovacuum runs automatically, but can cause I/O spikes
-- and needs tuning for high-write workloadsConnection model:
- A process per connection, which is heavier than a thread per connection. High connection counts usually need a pooler (PgBouncer, pgcat) in front. (Connections and authentication)
Replication:
- Streaming replication is solid; logical replication (10+) is newer
- HA typically needs Patroni or similar, where MySQL ships Group Replication
Learning Curve:
- More concepts to hold: VACUUM, autovacuum, bloat, transaction ID wraparound
- Advanced features require expertise
When to Choose PostgreSQL
✅ Complex queries - Analytical workloads, reporting ✅ JSON data - Hybrid relational + document store ✅ Advanced SQL - CTEs, window functions, FULL OUTER JOIN, exclusion constraints ✅ Geospatial - PostGIS is the industry standard ✅ Extensibility - Custom types, operators, index access methods
❌ Don't choose PostgreSQL if:
- Team lacks PostgreSQL expertise and the workload is simple
- Very high connection counts with no appetite for a pooler
- Legacy hosting (some shared hosts don't support Postgres)
Performance: How to Actually Decide
This guide deliberately publishes no throughput or latency figures for
either database, because any such number is meaningless without the hardware,
dataset, schema, index set, concurrency level, durability settings and version
that produced it. Change innodb_flush_log_at_trx_commit or
synchronous_commit alone and you can move "queries per second" by an order of
magnitude on either engine.
What you can rely on, because it follows from documented architecture rather than from a benchmark:
Where the engines genuinely differ
| Dimension | MySQL / InnoDB | PostgreSQL | Practical consequence |
|---|---|---|---|
| Old row versions | Undo log, purged in background (docs) | Kept in heap, reclaimed by VACUUM (docs) | Postgres needs vacuum tuning on high-churn tables; InnoDB needs undo/purge monitoring |
| Primary key storage | Clustered — row lives in the PK index (docs) | Heap + separate indexes (docs) | PK lookups have one less indirection on InnoDB; secondary indexes have one more |
| Connections | Thread per connection | Process per connection (docs) | Postgres usually wants a connection pooler sooner |
| Query planner | Narrower plan space | Richer plan space, more join/aggregate strategies (docs) | Postgres generally handles complex analytical SQL better |
| Index types | B-tree, R-tree (spatial), full-text, hash (MEMORY) | B-tree, Hash, GiST, SP-GiST, GIN, BRIN (docs) | Postgres can index JSON, arrays and trigram similarity directly |
| JSON | JSON type; index via generated columns (docs) | JSONB; GIN-indexable containment and existence (docs) | JSON-heavy filtering is materially simpler on Postgres |
How to get a number you can trust
- Take a copy of your schema and a representative slice of your data.
- Replay your query mix — not
SELECT 1, and not a generic sysbench profile unless a generic profile is genuinely what you run. - Fix the variables: same instance class, same storage, same durability settings, same client concurrency, warm cache.
- Measure p95 and p99, not the mean.
- Run it for long enough to cross a vacuum/purge cycle — throughput that looks great for ten minutes can look very different after an hour of churn.
If you want a documented, reproducible harness rather than rolling your own, sysbench and HammerDB both publish their methodology.
Feature Comparison
JSON Support
MySQL:
-- JSON type
CREATE TABLE users (data JSON);
-- Query JSON (path expression, not directly indexed)
SELECT * FROM users WHERE JSON_EXTRACT(data, '$.name') = 'Alice';
-- To index: generated column
ALTER TABLE users ADD COLUMN name VARCHAR(255) AS (JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')));
CREATE INDEX idx_name ON users(name);PostgreSQL:
-- JSONB type (binary, indexable)
CREATE TABLE users (data JSONB);
SELECT * FROM users WHERE data->>'name' = 'Alice';
CREATE INDEX idx_data ON users USING GIN (data);
-- Containment and key-existence operators
SELECT * FROM users WHERE data @> '{"age": 30}';
SELECT * FROM users WHERE data ? 'city';Winner: PostgreSQL — the document itself is indexable, with no generated-column step.
Full-Text Search
MySQL:
CREATE FULLTEXT INDEX idx_content ON posts(content);
SELECT * FROM posts WHERE MATCH(content) AGAINST('database' IN NATURAL LANGUAGE MODE);PostgreSQL:
ALTER TABLE posts ADD COLUMN search_vector tsvector;
UPDATE posts SET search_vector = to_tsvector('english', content);
CREATE INDEX idx_search ON posts USING GIN (search_vector);
SELECT * FROM posts WHERE search_vector @@ to_tsquery('database');
-- Fuzzy matching with pg_trgm
CREATE EXTENSION pg_trgm;
SELECT * FROM posts WHERE content % 'databse';Winner: PostgreSQL (configurable dictionaries and stemming, plus trigram similarity)
Geospatial
MySQL:
CREATE TABLE locations (point GEOMETRY);
INSERT INTO locations (point) VALUES (ST_GeomFromText('POINT(40.7 -74.0)'));
SELECT ST_Distance(point1, point2) FROM locations;PostgreSQL (PostGIS):
CREATE EXTENSION postgis;
CREATE TABLE locations (point GEOGRAPHY);
SELECT * FROM locations WHERE ST_DWithin(point, ST_MakePoint(-74.0, 40.7), 1000);
SELECT ST_Distance(point1, point2) FROM locations;(PostGIS)
Winner: PostgreSQL (PostGIS is the industry standard)
Transactions
Both engines default to READ COMMITTED and both implement MVCC, so plain reads do not take shared locks on either. The difference is in isolation options and in where old versions live.
-- MySQL
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- explicit lock when you want one
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- PostgreSQL
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- same explicit lock
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;PostgreSQL's SERIALIZABLE is a true serializable snapshot isolation implementation (docs); InnoDB's SERIALIZABLE is implemented by adding locks to reads (docs).
Winner: PostgreSQL, narrowly, on isolation guarantees.
Cost Comparison (Managed Services)
On the major managed platforms, the engine you pick does not change the bill. AWS RDS publishes the same instance-hour pricing for MySQL and for PostgreSQL, and Google Cloud SQL does the same:
Price your own instance class and storage on those pages — published rates move often enough that any figure quoted in an article is stale by the time you read it.
Specialized Services
MySQL-specific:
- PlanetScale - Serverless MySQL (Vitess)
- Aiven for MySQL - Multi-cloud
PostgreSQL-specific:
- Neon - Serverless Postgres
- Supabase - Postgres + backend services
- Timescale Cloud - Time-series Postgres
Cost Insight: Base infrastructure costs are the same. Choose on features and operational fit, not on the sticker price of the engine.
Migration Considerations
Moving FROM MySQL TO PostgreSQL
Why migrate:
- Need advanced SQL (exclusion constraints, partial indexes, FULL OUTER JOIN)
- Need indexable JSONB
- Need richer full-text search
- Need PostGIS (geospatial)
Migration tools:
- pgloader - Automated MySQL → PostgreSQL
- AWS Database Migration Service - Continuous replication
- Flyway, Liquibase - Schema migration tools
Challenges:
-- MySQL idioms that don't translate directly
AUTO_INCREMENT → GENERATED ... AS IDENTITY
UNSIGNED INT → INT or BIGINT with a CHECK constraint
ENUM → Custom enum type or CHECK constraint
'' vs NULL → MySQL's loose handling must be made explicitTimeline: 1-3 months (schema migration, testing, cutover)
Moving FROM PostgreSQL TO MySQL
Why migrate:
- Connection-count pressure without a pooler
- Write-amplification and replication characteristics at very large scale
- Team standardisation
This direction is less common, but it is not hypothetical — Uber documented exactly this migration and their reasoning in Why Uber Engineering Switched from Postgres to MySQL (2016). Note that the post describes Postgres 9.2-era behaviour; several of the specific complaints have been addressed in later releases, so read it as a case study in how to reason about a migration, not as a current scorecard.
The CTO's Checklist
Before choosing, answer these:
1. Workload Type
- Read-heavy or write-heavy?
- Simple queries or complex analytics? (Complex → PostgreSQL)
- OLTP or OLAP? (OLTP = either, OLAP = PostgreSQL)
2. Data Type
- Relational only? (Either works)
- JSON/document hybrid? (PostgreSQL JSONB)
- Geospatial? (PostgreSQL PostGIS)
- Time-series? (PostgreSQL TimescaleDB)
3. Team Expertise
- Team knows MySQL? (Stay with MySQL unless Postgres features are needed)
- Team knows PostgreSQL? (Stay with PostgreSQL)
- New team? (PostgreSQL for the broader feature set)
4. Infrastructure
- Shared hosting? (MySQL is more common)
- Cloud (AWS, GCP, Azure)? (Both supported equally)
- Expected peak connection count? (High → plan a pooler for Postgres)
5. Compliance
- Licensing concerns? (PostgreSQL license is more permissive)
- Enterprise support? (Both have commercial options)
Real-World Case Studies
Case 1: Uber (PostgreSQL → MySQL)
Initial: Postgres, behind a monolithic Python backend Problem: Write amplification, replication verbosity across data centres, and process-per-connection overhead at their connection counts Solution: Moved to Schemaless, a sharding layer built on MySQL/InnoDB Source: Why Uber Engineering Switched from Postgres to MySQL (July 2016)
Lesson: At extreme scale the deciding factors are storage-engine and replication mechanics, not SQL feature checklists. Note the direction of this migration — it is the opposite of what most "Postgres wins" comparisons assume.
Case 2: GitHub (MySQL)
Choice: MySQL, since launch Why: Read-heavy request mix, deep in-house operational expertise How they run it: Orchestrator-based failover, heavy use of read replicas Source: MySQL high availability at GitHub
Lesson: MySQL scales fine when you invest in the operational tooling around it.
Case 3: Instagram (PostgreSQL)
Choice: PostgreSQL Why: Relational core with application-level sharding How they run it: Logical shards mapped onto physical Postgres instances, with IDs generated to encode shard and timestamp Source: Sharding & IDs at Instagram
Lesson: Postgres scales horizontally when you do the sharding design work up front.
The Honest Recommendation
If you're a CTO choosing today:
- Greenfield project: PostgreSQL (broadest feature set, most headroom)
- Legacy MySQL app: Stay on MySQL (unless you need Postgres features)
- Simple CRUD app: Either — pick the one your team knows
- Complex analytics: PostgreSQL (richer planner, more SQL features)
- JSON-heavy: PostgreSQL (JSONB is indexable directly)
Red Flags:
- "Let's migrate from MySQL to PostgreSQL for fun" (expensive, risky)
- "PostgreSQL is always better" (Uber's engineering blog is the counterexample)
- "MySQL is dead" (it isn't — see the DB-Engines ranking)
- Any comparison quoting queries-per-second without publishing its methodology
Green Lights:
- "We need JSONB, exclusion constraints, PostGIS" (migrate to PostgreSQL)
- "We benchmarked our own workload and MySQL won" (that's the right way to decide)
- "We're starting fresh and have no strong team preference" (PostgreSQL)
Conclusion: Both Are Excellent
Poor decisions look like:
❌ Choosing on a benchmark table you found in a blog post ❌ Choosing MySQL for a data warehouse (PostgreSQL is better) ❌ Migrating MySQL → PostgreSQL without a clear reason (expensive)
Good decisions look like:
✅ Benchmarking your own schema and query mix ✅ Choosing PostgreSQL for complex SQL, JSON and geospatial ✅ Choosing the engine your team can operate at 3am
Both MySQL and PostgreSQL are world-class databases. Choose based on your workload, not hype.
Further Reading
- MySQL 8.4 Reference Manual - Official documentation
- PostgreSQL Documentation - Official documentation
- Postgres vs MongoDB - SQL vs NoSQL
- DB-Engines Ranking - Relative popularity over time
Decision Timeline:
- Prototype: 1 week with both databases
- Load testing: 2 weeks with your realistic workload
- Decision: Evaluate performance, complexity, cost
- Commitment: Don't switch for 3+ years (migration is expensive)