Comparison · Rails Databases
Postgres vs MySQL for Rails
Both are excellent at general OLTP work for a Rails app. Postgres pulls ahead on JSONB, advanced indexing, MVCC, CTEs, and extensions. MySQL wins on simplicity, replication maturity, and some read-heavy patterns. Six axes and the cases where each is the right call.
Where this comparison comes from
Rails ships with both Postgres and MySQL adapters and treats neither as the default. SQLite became the default for new Rails 8 apps, but for production, almost every Rails team picks Postgres or MySQL. The reason both have stuck around: each is the right answer in different circumstances, and the gap between them in the OLTP middle ground has narrowed steadily over a decade.
The decision happens once per project and is hard to reverse. Migration between the two is possible but takes weeks for a non-trivial app: vendor-specific column types, query syntax, index types, and operational tooling all have to be reworked. The senior call is making the decision deliberately rather than picking by familiarity.
Both work. The question is which one fits the data, the team, and the operations you actually have. The rest of this lesson walks through the six axes that decide it.
Axis 1: data model flexibility
Postgres ships with the broadest set of column types in mainstream databases. JSONB is the headline: a binary-encoded JSON type with first-class indexing (GIN), containment operators, and Active Record support since Rails 4.2. Storing user preferences, audit log payloads, third-party integration responses: all easy in Postgres. Arrays, range types, enums, UUIDs, hstore, and PostGIS for geometry round out the catalog.
MySQL 8 added a JSON type that is functionally close to Postgres's JSONB but with quirks: indexing requires generated columns, the query syntax is more verbose, and the Active Record integration is thinner. For light JSON use, MySQL is fine. For an app that stores significant schema-less data, Postgres is a meaningfully better fit.
Verdict. If your data model includes JSONB, arrays, or extensions like PostGIS, Postgres. If you are storing tabular data with the occasional JSON blob, both work.
Axis 2: indexing options
Postgres has six index types: btree, hash, GiST, SP-GiST, GIN, and BRIN. The wide selection is genuinely useful. Indexing beyond btree walks through partial indexes, expression indexes, GIN for JSONB and full-text, BRIN for time-series, and INCLUDE for covering indexes. Each solves a problem shape that a btree-only database has to work around.
MySQL has btree (the default for InnoDB), hash (for memory tables and an option for InnoDB in 8+), and full-text indexes. Partial indexes and expression indexes are not first-class; the workaround is to create a generated column and index it. The result is more migrations, more schema drift, and indexes that are harder to reason about.
Verdict. Postgres for any workload where you expect to reach for non-btree indexes (JSONB, full-text, time-series). For straightforward OLTP, MySQL's narrower set is enough.
Axis 3: concurrency and MVCC
Both databases use MVCC (Multi-Version Concurrency Control) so reads do not block writes and vice versa. The implementations differ.
Postgres stores all row versions in the table itself; old versions become "dead tuples" and are cleaned up by autovacuum. The trade-off is potential bloat on high-write tables (covered in MVCC, vacuum, and bloat) and the operational discipline of tuning autovacuum.
MySQL with InnoDB stores old versions in a separate undo log. Bloat is less of an issue, but the undo log can grow during long-running transactions and cause its own operational problems. The pattern of issues is different rather than smaller.
Verdict. Both are battle-tested under heavy OLTP load. The team you have is more important than the engine you pick.
Axis 4: replication and high availability
MySQL's replication is older, more mature, and has a richer ecosystem. Statement-based and row-based replication, GTIDs, group replication, semi-sync replication, and a wider set of tooling (Orchestrator, ProxySQL, Vitess for horizontal scaling). MySQL has been running large-scale production at YouTube, Facebook, Booking.com, and Shopify for over a decade, and the operational playbooks are well-documented.
Postgres's streaming replication has been production-ready since 9.0 (2010), logical replication since 10 (2017), and the gap with MySQL on replication-specific features has closed substantially. Tools like Patroni handle automatic failover; pglogical and the built-in logical replication cover most cross-cluster use cases. The Postgres community has caught up in the last five years, though it has not fully matched MySQL's depth on the most exotic patterns.
Verdict. Both handle "primary plus read replica" trivially. For the high end (multi-region active-active, sharding, complex failover topologies), MySQL still has more pre-built solutions. Postgres is fine for the typical "one primary, one or two replicas" pattern Rails apps run.
Axis 5: query expressiveness
Both support window functions, CTEs, and recursive queries. Postgres has had them longer and the implementation is more complete. Lateral joins, anonymous code blocks (DO), procedural languages (PL/pgSQL, PL/Python), and rich PostGIS geometry queries all live in Postgres.
MySQL 8 added CTEs, window functions, and recursive queries, which closes most of the gap. Lateral joins and CTEs as updatable expressions (UPDATE inside a CTE) are still Postgres-only. For most Rails app queries, both engines compose the SQL you need.
Verdict. Postgres for analytical SQL, complex reporting, or anything that benefits from PostGIS. MySQL for straightforward OLTP queries.
Axis 6: hosted vendors and ops familiarity
Both are first-class on AWS RDS, Google Cloud SQL, Azure Database, Aiven, Crunchy, Supabase, PlanetScale (MySQL-compatible), and every PaaS Rails teams typically use. The vendor matrix is roughly the same.
Heroku Postgres is more mature and feature-rich than its MySQL alternatives on the platform. Render's Postgres offering is also stronger. For teams that prefer managed services with deep platform integration, Postgres has a slight edge in the Rails-adjacent hosting ecosystem.
The bigger factor is the team. A team with 10 years of MySQL ops experience should run MySQL. A team that knows Postgres should run Postgres. The mid-incident skill of "I know what to look at when the database is on fire" is more valuable than any architectural feature.
Verdict. Roughly even on hosting. Tilt toward Postgres if you are starting fresh; tilt toward MySQL if your team's expertise is there.
The decision rule
Default to Postgres if any of:
- The app stores JSONB, arrays, or geometric data.
- Search or filtering requires GIN-style indexes (full-text, JSONB containment).
- The team has no strong prior preference.
- You expect to lean on advanced SQL (CTEs, window functions, lateral joins).
- You will deploy on Heroku, Render, or Supabase, which favor Postgres.
Default to MySQL if any of:
- The team has deep MySQL ops experience and limited Postgres experience.
- The app needs the operational maturity around large-scale replication (Vitess, sharding, multi-region active-active).
- You are integrating with systems already on MySQL and want to avoid two database engines.
- You will deploy on PlanetScale and want its branching workflow.
For a new Rails app in 2026 with no strong team preference and a typical SaaS shape, Postgres is the safer default. The wider feature set, the JSONB story, the better fit with Rails 8's Solid Suite (Solid Queue, Solid Cache, Solid Cable all default to the primary database), and the hosting ecosystem alignment all push the same direction.
The principle at play
Database choice is a long-term decision. The cost of being wrong is measured in months, not days. The cost of choosing well is invisible because the database stops being a topic and becomes a substrate. The senior call is to pick deliberately and then commit, not to keep relitigating the choice every two years.
Both Postgres and MySQL are excellent. Neither will be the reason your product fails. The team's familiarity with the engine they pick matters more than the marginal advantages of either. Default to Postgres for a fresh Rails 8 app; default to whatever your team knows for everything else.