Verdict up front: Pick Postgres for new SaaS projects. JSONB, window functions, CTEs, and partial indexes solve problems you don't know you have yet. Pick MySQL if you're integrating with the LAMP ecosystem (WordPress, Magento, Drupal) or you have a specific read-heavy workload where MySQL's simpler buffer pool wins. Both engines are mature and either will scale you to 100M rows.
The pricing reality on AWS RDS
For a typical SaaS database (db.t4g.medium, 100GB storage, multi-AZ):
- RDS Postgres: $98/month compute + $23/month storage = $121/month
- RDS MySQL: $98/month compute + $23/month storage = $121/month
- Aurora Postgres: $148/month compute + $24/month storage = $172/month
- Aurora MySQL: $148/month compute + $24/month storage = $172/month
Identical. Stop choosing on infrastructure cost. The differences are in feature set and operational characteristics.
JSONB: Postgres's killer feature
Postgres JSONB stores JSON as a binary tree with B-tree, GIN, and GIN-trigram indexes. You can query like:
SELECT * FROM events
WHERE payload->>'user_id' = '123'
AND payload->'metadata'->>'source' = 'webhook'
AND payload @> '{"tags": ["urgent"]}';
With a GIN index on payload, this is millisecond-fast at 10M rows. The same query in MySQL 8 works (JSON_EXTRACT) but is slower and requires generated columns + secondary indexes for performance.
If you're storing webhook payloads, event logs, audit trails, or any schema-flexible data, Postgres saves you a separate document store. We've seen teams add MongoDB alongside MySQL specifically because MySQL JSON wasn't enough — Postgres JSONB would have removed the need.
Full-text search
Postgres has tsvector + GIN indexes — full-text search with stemming, ranking, and language-specific dictionaries built in. Good enough for product catalogs up to ~1M items.
MySQL has FULLTEXT indexes on InnoDB. Functional, faster for boolean mode searches, but no stemming, no language-aware tokenisation. For English-only product names, MySQL is fine. For multilingual content or fuzzy matching, you need ElasticSearch alongside.
Window functions and CTEs
Both engines support window functions and recursive CTEs in current versions. Postgres has had them since 2009; MySQL added them in 8.0 (2018). Feature parity here is roughly equal in 2026.
One difference: Postgres CTEs were not inlined by the planner pre-12 (they acted as optimisation fences). Post-12, they inline automatically when used once. MySQL inlines CTEs by default. Performance: identical in modern versions.
Partition pruning
Both engines support range, list, and hash partitioning. Postgres declarative partitioning (10+) supports partition pruning at planning time and execution time. MySQL 8 has similar support.
Practical edge: Postgres has better support for sub-partitioning and constraint exclusion. For time-series data with 50M+ rows partitioned by month, Postgres pruning is more reliable. We've debugged MySQL partition pruning failures that Postgres handled cleanly.
Replication and HA
Postgres: streaming replication is rock solid, logical replication (10+) lets you replicate selective tables. Tooling: Patroni for HA orchestration, PgBouncer for connection pooling.
MySQL: row-based replication is mature, group replication enables multi-primary, ProxySQL for connection pooling. InnoDB Cluster combines all three.
Both can do 99.99% uptime with the right setup. MySQL's read-replica scaling is slightly easier to set up; Postgres's logical replication is more flexible.
Side-by-side
| Feature | PostgreSQL 16 | MySQL 8.4 LTS |
|---|---|---|
| RDS price (db.t4g.medium multi-AZ) | $121/month | $121/month |
| JSON support | JSONB (indexed) | JSON (limited indexing) |
| Full-text search | tsvector + GIN, multilingual | FULLTEXT, English-friendly |
| Window functions | Yes (since 2009) | Yes (since 8.0, 2018) |
| Recursive CTEs | Yes | Yes |
| Partial indexes | Yes | No (use generated columns) |
| GIN / GIST indexes | Yes | No |
| Array types | Yes | No |
| Materialized views | Yes | No (use cron + table) |
| Logical replication | Yes (built-in 10+) | Yes (group replication) |
| Connection pooling default | PgBouncer (external) | Built-in connection limit |
| Memory footprint | Higher (per-connection process) | Lower (threaded) |
| Default isolation level | Read Committed | Repeatable Read |
| Storage engines | Single (heap) | InnoDB / MyISAM / Memory |
| Stored procedures language | PL/pgSQL, Python, JS, etc | SQL only |
| Spatial / GIS | PostGIS (best-in-class) | Built-in (basic) |
When to choose Postgres
- Greenfield SaaS — pick it as the default
- You store JSON or schema-flexible data
- You need full-text search across multiple languages
- You use GIS / geospatial data (PostGIS is the gold standard)
- You'll write complex analytical queries (window functions, CTEs, lateral joins)
- Multi-tenant SaaS with row-level security
When to choose MySQL
- Integrating with LAMP ecosystem (WordPress, Magento, Drupal, Joomla)
- Read-heavy workload with simple queries (catalog, content site)
- Memory-constrained hosting (MySQL uses ~30% less RAM per connection)
- Your DBA team has 10 years of MySQL ops experience and zero Postgres
- You need Vitess sharding for horizontal scale (YouTube/Slack pattern)
Migrating between Postgres and MySQL?
We've moved 4 production databases between Postgres and MySQL (both directions). We know which features won't survive and which need rewriting.
Book a discovery call