Skip to main content
Featured

MySQL vs PostgreSQL: Database Decision Guide for CTOs

January 19, 2025•By The CTO•20 min read•
...
•comparisons

Strategic comparison of MySQL and PostgreSQL. Performance, features, costs, team expertise, and when to choose each relational database.

TL;DR: Decision Matrix

FactorMySQLPostgreSQLWinner
Simple key-lookup reads⭐⭐⭐⭐⭐ Excellent⭐⭐⭐⭐⭐ ExcellentToo close to call — benchmark your own workload
Write concurrency⭐⭐⭐⭐ Strong⭐⭐⭐⭐ StrongDepends on tuning, not engine
Complex queries⭐⭐⭐ Good⭐⭐⭐⭐⭐ ExcellentPostgreSQL
JSON Support⭐⭐⭐ Functional⭐⭐⭐⭐⭐ Native + indexablePostgreSQL
Full-Text Search⭐⭐⭐ Good⭐⭐⭐⭐ BetterPostgreSQL
Replication⭐⭐⭐⭐⭐ Mature⭐⭐⭐⭐ GoodMySQL
Ecosystem⭐⭐⭐⭐⭐ Massive⭐⭐⭐⭐⭐ MassiveTie
Managed Services⭐⭐⭐⭐⭐ AWS RDS, GCP⭐⭐⭐⭐⭐ AWS RDS, GCPTie
Licensing⭐⭐⭐⭐ GPL (Oracle)⭐⭐⭐⭐⭐ PostgreSQL LicensePostgreSQL
Community⭐⭐⭐⭐ Large⭐⭐⭐⭐⭐ Very activePostgreSQL
Learning Curve⭐⭐⭐⭐⭐ Easy⭐⭐⭐⭐ ModerateMySQL
Advanced Features⭐⭐⭐ Good⭐⭐⭐⭐⭐ ExcellentPostgreSQL

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:

  1. Workload type (read-heavy vs write-heavy, simple vs complex queries)
  2. Data type (relational vs JSON/document hybrid)
  3. 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:

sql
-- 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:

MySQL's Drawbacks

Narrower SQL surface:

sql
-- Window functions and CTEs only from 8.0
-- No FULL OUTER JOIN
-- CHECK constraints only enforced from 8.0.16
-- No partial (filtered) indexes

Window 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:

sql
-- 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):

sql
-- 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);

(JSON types)

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)

(Index types)

Extensions:

sql
-- 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:

sql
-- PostgreSQL reclaims dead tuples with VACUUM
VACUUM ANALYZE users;

-- Autovacuum runs automatically, but can cause I/O spikes
-- and needs tuning for high-write workloads

(Routine vacuuming)

Connection 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

DimensionMySQL / InnoDBPostgreSQLPractical consequence
Old row versionsUndo 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 storageClustered — row lives in the PK index (docs)Heap + separate indexes (docs)PK lookups have one less indirection on InnoDB; secondary indexes have one more
ConnectionsThread per connectionProcess per connection (docs)Postgres usually wants a connection pooler sooner
Query plannerNarrower plan spaceRicher plan space, more join/aggregate strategies (docs)Postgres generally handles complex analytical SQL better
Index typesB-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
JSONJSON 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

  1. Take a copy of your schema and a representative slice of your data.
  2. Replay your query mix — not SELECT 1, and not a generic sysbench profile unless a generic profile is genuinely what you run.
  3. Fix the variables: same instance class, same storage, same durability settings, same client concurrency, warm cache.
  4. Measure p95 and p99, not the mean.
  5. 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:

sql
-- 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:

sql
-- 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.

MySQL:

sql
CREATE FULLTEXT INDEX idx_content ON posts(content);
SELECT * FROM posts WHERE MATCH(content) AGAINST('database' IN NATURAL LANGUAGE MODE);

(MySQL full-text search)

PostgreSQL:

sql
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';

(PostgreSQL text search)

Winner: PostgreSQL (configurable dictionaries and stemming, plus trigram similarity)

Geospatial

MySQL:

sql
CREATE TABLE locations (point GEOMETRY);
INSERT INTO locations (point) VALUES (ST_GeomFromText('POINT(40.7 -74.0)'));
SELECT ST_Distance(point1, point2) FROM locations;

(MySQL spatial types)

PostgreSQL (PostGIS):

sql
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.

sql
-- 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:

sql
-- 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 explicit

Timeline: 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:

  1. Greenfield project: PostgreSQL (broadest feature set, most headroom)
  2. Legacy MySQL app: Stay on MySQL (unless you need Postgres features)
  3. Simple CRUD app: Either — pick the one your team knows
  4. Complex analytics: PostgreSQL (richer planner, more SQL features)
  5. 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


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)

Accounts are opening soon

Save your tool results, track your scores over time, and get your invite before the public launch. One email, nothing else.

No spam. We only email you about your invite.