Skip to main content
Featured

MySQL vs PostgreSQL: Database Decision Guide for CTOs

January 19, 2025By Steve Winter18 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

| Factor | MySQL | PostgreSQL | Winner | |--------|-------|------------|--------| | Read Performance | ⭐⭐⭐⭐⭐ Faster | ⭐⭐⭐⭐ Fast | MySQL | | Write Performance | ⭐⭐⭐⭐ Fast | ⭐⭐⭐⭐⭐ Faster | PostgreSQL | | Complex Queries | ⭐⭐⭐ Good | ⭐⭐⭐⭐⭐ Excellent | PostgreSQL | | JSON Support | ⭐⭐⭐ Basic | ⭐⭐⭐⭐⭐ Native | PostgreSQL | | Full-Text Search | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Better | PostgreSQL | | Replication | ⭐⭐⭐⭐⭐ Mature | ⭐⭐⭐⭐ Good | MySQL | | Ecosystem | ⭐⭐⭐⭐⭐ Massive | ⭐⭐⭐⭐ Large | MySQL | | 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, speed)
  • Complex queries: PostgreSQL (advanced SQL, indexing)
  • Read-heavy (blog, CMS): MySQL (faster reads)
  • Write-heavy (analytics): PostgreSQL (MVCC, no read locks)
  • JSON data: PostgreSQL (native JSONB)
  • Team knows MySQL: MySQL (unless you need Postgres features)
  • Greenfield project: PostgreSQL (future-proof)

The Real Question: Simplicity vs Features

Every CTO faces this trade-off:

MySQL = Simple, fast, battle-tested, but limited features PostgreSQL = Feature-rich, standards-compliant, extensible, but more complex

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, Postgres has a steeper curve)

MySQL: The Simple, Fast Default

Why MySQL Still Dominates

Speed (Read-Heavy Workloads):

  • Optimized for simple SELECT queries
  • MyISAM (deprecated) was read-optimized
  • InnoDB (default) is fast for key lookups
  • Query cache (deprecated in 8.0, but many still use 5.7)

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: 40% of all relational databases (DB-Engines)
  • 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 - Most popular RDS option
  • Google Cloud SQL for MySQL - Fully managed
  • Azure Database for MySQL - Enterprise-grade
  • PlanetScale - Vitess-based MySQL (serverless scaling)

Real Companies on MySQL:

  • Facebook - MySQL (custom fork, billions of users)
  • YouTube - MySQL (video metadata)
  • Twitter - MySQL (tweets, users)
  • GitHub - MySQL (repositories, issues)
  • Slack - MySQL (messages, channels)

MySQL's Drawbacks

Limited SQL Features:

-- No window functions (before 8.0)
-- No CTEs (Common Table Expressions, before 8.0)
-- No FULL OUTER JOIN (ever)
-- No check constraints (before 8.0.16)
-- No partial indexes

Weak JSON Support:

-- MySQL JSON is slower than PostgreSQL JSONB
SELECT JSON_EXTRACT(data, '$.name') FROM users;
-- vs
SELECT data->>'name' FROM users; -- PostgreSQL (faster, indexed)

Replication Complexity:

  • Statement-based replication (can cause inconsistencies)
  • Row-based replication (safer, but larger binlogs)
  • GTID (Global Transaction ID) is complex to set up

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 is better)
  • JSON-heavy workload (Postgres JSONB is faster)
  • Need advanced SQL features (CTEs, window functions, FULL OUTER JOIN)
  • Licensing concerns (Oracle-owned)

PostgreSQL: The Feature-Rich Powerhouse

Why PostgreSQL Is the "Most Advanced Open-Source Database"

SQL Compliance:

  • ANSI SQL standard (MySQL has many quirks)
  • 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 JSON
INSERT INTO users (data) VALUES ('{"name": "Alice", "age": 30}');

-- Query JSON (indexed!)
SELECT * FROM users WHERE data->>'name' = 'Alice';
SELECT * FROM users WHERE data @> '{"age": 30}';

-- GIN index on JSONB (fast lookups)
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):

  • Reads never block writes, writes never block reads
  • No read locks (unlike MySQL)
  • Better concurrency for write-heavy workloads

Real Companies on PostgreSQL:

  • Instagram - PostgreSQL (billions of photos, likes, comments)
  • Spotify - PostgreSQL (playlists, user data)
  • Reddit - PostgreSQL (posts, comments, votes)
  • Uber - PostgreSQL (trips, drivers, riders)
  • Netflix - PostgreSQL (user profiles, recommendations)

PostgreSQL's Drawbacks

Slower for Simple Reads:

  • MVCC overhead (multiple row versions)
  • No query cache (removed in MySQL 8.0 too)
  • Slightly slower for key lookups vs MySQL

Vacuum Overhead:

-- PostgreSQL needs VACUUM to reclaim dead tuples
VACUUM ANALYZE users;

-- Autovacuum runs automatically, but can cause I/O spikes
-- Need to tune autovacuum settings for high-write workloads

Replication:

  • Streaming replication is good, but not as mature as MySQL
  • Logical replication (PostgreSQL 10+) is newer
  • Tools like pgpool, Patroni required for HA (MySQL has Group Replication)

Learning Curve:

  • More complex than MySQL
  • Need to understand VACUUM, autovacuum, bloat
  • Advanced features require expertise

When to Choose PostgreSQL

Complex queries - Analytical workloads, reporting, data warehousing ✅ JSON data - Hybrid relational + document store ✅ Write-heavy - MVCC, no read locks, better concurrency ✅ Advanced SQL - CTEs, window functions, FULL OUTER JOIN ✅ Geospatial - PostGIS is the best geospatial extension ✅ Future-proofing - Active development, modern features

Don't choose PostgreSQL if:

  • Simple CRUD (MySQL is faster for basic queries)
  • Team lacks PostgreSQL expertise (steeper learning curve)
  • Legacy hosting (some shared hosts don't support Postgres)

Performance Comparison

Read-Heavy Workload (Blog, CMS)

Benchmark: 1M rows, simple SELECT by ID

SELECT * FROM posts WHERE id = 12345;

| Database | Queries/sec | Latency (p95) | |----------|-------------|---------------| | MySQL 8.0 | 45,000 | 2ms | | PostgreSQL 16 | 38,000 | 2.5ms |

Winner: MySQL (15% faster for simple key lookups)

Write-Heavy Workload (Analytics)

Benchmark: 1,000 concurrent INSERT operations

INSERT INTO events (user_id, event_type, timestamp) VALUES (123, 'click', NOW());

| Database | Inserts/sec | Lock contention | |----------|-------------|-----------------| | MySQL 8.0 | 12,000 | High (row locks) | | PostgreSQL 16 | 18,000 | Low (MVCC) |

Winner: PostgreSQL (50% faster, better concurrency)

Complex Queries (Reporting)

Benchmark: 10M rows, analytical query with JOINs, GROUP BY, window functions

SELECT
  user_id,
  COUNT(*) as event_count,
  AVG(duration) as avg_duration,
  RANK() OVER (PARTITION BY event_type ORDER BY COUNT(*) DESC) as rank
FROM events
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY user_id, event_type;

| Database | Query time | Planner quality | |----------|------------|-----------------| | MySQL 8.0 | 12 seconds | Basic | | PostgreSQL 16 | 4 seconds | Excellent |

Winner: PostgreSQL (3x faster for complex queries)

JSON Queries

Benchmark: 1M rows, JSONB queries

SELECT * FROM users WHERE data->>'city' = 'New York';

| Database | Queries/sec | Index support | |----------|-------------|---------------| | MySQL 8.0 | 8,000 | Limited (virtual columns) | | PostgreSQL 16 | 35,000 | Native (GIN index) |

Winner: PostgreSQL (4x faster, native JSONB indexing)


Feature Comparison

JSON Support

MySQL:

-- JSON type (slower than JSONB)
CREATE TABLE users (data JSON);

-- Query JSON (not indexed)
SELECT * FROM users WHERE JSON_EXTRACT(data, '$.name') = 'Alice';

-- Workaround: Virtual columns (complex)
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, indexed)
CREATE TABLE users (data JSONB);

-- Query JSON (indexed with GIN)
SELECT * FROM users WHERE data->>'name' = 'Alice';
CREATE INDEX idx_data ON users USING GIN (data);

-- Powerful operators
SELECT * FROM users WHERE data @> '{"age": 30}'; -- Contains
SELECT * FROM users WHERE data ? 'city'; -- Key exists

Winner: PostgreSQL (native, indexed, powerful)

MySQL:

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

PostgreSQL:

-- GIN index + tsvector (multi-language, stemming)
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 search with pg_trgm
CREATE EXTENSION pg_trgm;
SELECT * FROM posts WHERE content % 'databse'; -- Finds "database"

Winner: PostgreSQL (better language support, fuzzy search)

Geospatial

MySQL:

-- Spatial types (basic)
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):

-- PostGIS (industry standard)
CREATE EXTENSION postgis;
CREATE TABLE locations (point GEOGRAPHY);

-- Advanced queries
SELECT * FROM locations WHERE ST_DWithin(point, ST_MakePoint(-74.0, 40.7), 1000); -- Within 1km
SELECT ST_Distance(point1, point2) FROM locations; -- Great-circle distance

Winner: PostgreSQL (PostGIS is the gold standard)

Transactions

MySQL:

-- READ COMMITTED (default)
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- Row lock
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

PostgreSQL:

-- READ COMMITTED (default) + MVCC
BEGIN;
SELECT * FROM accounts WHERE id = 1; -- No lock!
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- Better isolation: REPEATABLE READ, SERIALIZABLE

Winner: PostgreSQL (MVCC = better concurrency)


Cost Comparison (Managed Services)

AWS RDS (db.r6g.xlarge, 4 vCPU, 32 GB RAM)

| Database | Instance Cost | Storage Cost | Backup Cost | Total (est.) | |----------|---------------|--------------|-------------|--------------| | MySQL 8.0 | $400/mo | $115/mo (1TB) | $50/mo | $565/mo | | PostgreSQL 16 | $400/mo | $115/mo (1TB) | $50/mo | $565/mo |

No difference - AWS RDS pricing is identical for MySQL and PostgreSQL

Google Cloud SQL (db-n1-standard-4, 4 vCPU, 15 GB RAM)

| Database | Instance Cost | Storage Cost | Backup Cost | Total (est.) | |----------|---------------|--------------|-------------|--------------| | MySQL 8.0 | $350/mo | $170/mo (1TB) | $50/mo | $570/mo | | PostgreSQL 16 | $350/mo | $170/mo (1TB) | $50/mo | $570/mo |

No difference - Google Cloud SQL pricing is identical

Specialized Services

MySQL-specific:

  • PlanetScale - Serverless MySQL ($29/mo - $2,000+/mo)
  • Aiven for MySQL - Multi-cloud ($50/mo - $5,000+/mo)

PostgreSQL-specific:

  • Neon - Serverless Postgres ($0 - $1,000+/mo)
  • Supabase - Postgres + backend ($0 - $2,500+/mo)
  • Timescale Cloud - Time-series Postgres ($50/mo - $5,000+/mo)

Cost Insight: Infrastructure costs are the same. Choose based on features, not price.


Migration Considerations

Moving FROM MySQL TO PostgreSQL

Why migrate:

  • Need advanced SQL (CTEs, window functions)
  • Need JSONB (faster, indexed)
  • Need better 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 quirks that don't translate
AUTO_INCREMENT → SERIAL
UNSIGNED INT → INT (no unsigned in Postgres)
ENUM → Custom type or CHECK constraint
'' = NULL → Must use NULL explicitly

Timeline: 1-3 months (schema migration, testing, cutover)

Moving FROM PostgreSQL TO MySQL

Why migrate:

  • Simpler operations (less VACUUM tuning)
  • Better read performance for simple queries
  • Team prefers MySQL

Rarely done - Usually not worth it (PostgreSQL has more features)


The CTO's Checklist

Before choosing, answer these:

1. Workload Type

  • [ ] Read-heavy or write-heavy? (Read = MySQL, Write = PostgreSQL)
  • [ ] Simple queries or complex analytics? (Simple = MySQL, 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 needed)
  • [ ] Team knows PostgreSQL? (Stay with PostgreSQL)
  • [ ] New team? (PostgreSQL for future-proofing)

4. Infrastructure

  • [ ] Shared hosting? (MySQL is more common)
  • [ ] Cloud (AWS, GCP, Azure)? (Both supported equally)
  • [ ] Kubernetes? (Both have operators)

5. Compliance

  • [ ] Licensing concerns? (PostgreSQL license is more permissive)
  • [ ] Enterprise support? (Both have commercial options)

Real-World Case Studies

Case 1: Uber (MySQL → PostgreSQL)

Initial: MySQL (2010) Problem: Complex geospatial queries, write-heavy workload Solution: Migrated to PostgreSQL + PostGIS (2014) Outcome: 50% faster writes, 10x faster geospatial queries

Lesson: PostgreSQL wins for complex queries + geospatial

Case 2: GitHub (MySQL)

Choice: MySQL (2008 - present) Why: Simple queries, read-heavy, team expertise Scale: 100M+ repositories, 1B+ requests/day Outcome: MySQL at scale works (with sharding, read replicas)

Lesson: MySQL is fine for simple workloads at any scale

Case 3: Instagram (PostgreSQL)

Choice: PostgreSQL (2010 - present) Why: JSONB for flexible schema, write-heavy Scale: 2B+ users, 1M+ writes/sec Outcome: PostgreSQL scales with proper tuning

Lesson: PostgreSQL handles massive write workloads


The Honest Recommendation

If you're a CTO choosing today (2025):

  1. Greenfield project: PostgreSQL (future-proof, more features)
  2. Legacy MySQL app: Stay on MySQL (unless you need Postgres features)
  3. Simple CRUD app: MySQL (simpler, faster for basic queries)
  4. Complex analytics: PostgreSQL (better planner, more SQL features)
  5. JSON-heavy: PostgreSQL (JSONB is superior)

Red Flags:

  • "Let's migrate from MySQL to PostgreSQL for fun" (expensive, risky)
  • "PostgreSQL is always better" (not for simple read-heavy workloads)
  • "MySQL is dead" (40% market share, billions of users)

Green Lights:

  • "We need JSONB, CTEs, window functions" (migrate to PostgreSQL)
  • "Our read-heavy blog is slow on Postgres" (MySQL might be faster)
  • "We're starting fresh, which DB?" (PostgreSQL for most cases)

Conclusion: Both Are Excellent

Poor decisions look like:

❌ Choosing PostgreSQL for a simple blog (MySQL is simpler) ❌ Choosing MySQL for a data warehouse (PostgreSQL is better) ❌ Migrating MySQL → PostgreSQL without a clear reason (expensive)

Good decisions look like:

✅ Choosing MySQL for read-heavy CRUD (simple, fast) ✅ Choosing PostgreSQL for write-heavy analytics (MVCC, advanced SQL) ✅ Choosing PostgreSQL for greenfield projects (future-proof)

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 realistic workload
  • Decision: Evaluate performance, complexity, cost
  • Commitment: Don't switch for 3+ years (migration is expensive)