[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"$f0Oo4NqtEweeYYl5SiGRwYfY_MZFq7M3dnbF9Uh4e-Ek":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"draft":12,"readingMinutes":13,"html":14,"related":15},"mysql-vs-postgres","MySQL vs Postgres: What Actually Bites You in 2026","MySQL vs Postgres in November 2026: MySQL 9.7 LTS and calendar versions, Postgres 19 RC, and the collation and migration surprises that bite when you switch.","2026-11-04",[9,10,11],"mysql","postgres","databases",false,5,"\u003Cp>I have shipped far more MySQL than Postgres. Most of my PHP and Laravel work ran on it, and it never once let me down in a way I could blame on MySQL itself. These days I start new projects on Postgres anyway. Both of those are true, and the reasons are less about benchmarks than about a handful of behaviours that surprise people the first time they hit them.\u003C/p>\n\u003Cp>So this is not a feature matrix. Both databases will hold your users table just fine. It&#39;s the stuff that bites.\u003C/p>\n\u003Cp>Versions checked on November 4, 2026 against the \u003Ca href=\"https://www.postgresql.org/\">PostgreSQL home page\u003C/a> and the \u003Ca href=\"https://dev.mysql.com/doc/relnotes/mysql/9.7/en/\">MySQL 9.7 release notes\u003C/a>.\u003C/p>\n\u003Ch2>Where each one is right now\u003C/h2>\n\u003Cp>\u003Cstrong>Postgres\u003C/strong> 18 is the current stable line (18.6, released August 13, 2026). Postgres 19 is at RC 1 since October 15, so the final release is close. 18 was a big one for day-to-day work: asynchronous I/O for reads, \u003Ccode>uuidv7()\u003C/code> built in, and virtual generated columns as the default kind.\u003C/p>\n\u003Cp>\u003Cstrong>MySQL\u003C/strong> changed how it numbers releases, and if you haven&#39;t looked since 8.0 it is confusing. There are two tracks, LTS and Innovation. 8.4 is an LTS. \u003Cstrong>9.7 is an LTS too, and the last one with the old numbering\u003C/strong>: per the \u003Ca href=\"https://dev.mysql.com/doc/refman/9.7/en/mysql-releases.html\">MySQL release model page\u003C/a>, everything after it uses calendar versions in YY.M form, so 26.7 is the July 2026 release. An LTS gets 5 years of premier support plus 3 extended. An Innovation release is supported only until the next one ships, and you can&#39;t jump between Innovation majors directly, you go through an LTS. And 8.0, which half the internet still runs, reached end of life in April 2026.\u003C/p>\n\u003Cp>If you are on MySQL 8.0 today, that is your actual decision. Not &quot;MySQL or Postgres&quot; but &quot;8.4 LTS, 9.7 LTS, or move&quot;. The \u003Ca href=\"https://dev.mysql.com/doc/refman/8.4/en/upgrading.html\">8.0 to 8.4 upgrade notes\u003C/a> are worth reading before you pick, because 8.4 no longer loads the old \u003Ccode>mysql_native_password\u003C/code> plugin by default, and users created with it can&#39;t log in until you deal with that.\u003C/p>\n\u003Ch2>The collation thing\u003C/h2>\n\u003Cp>This is the one that catches Laravel developers moving to Postgres, every time.\u003C/p>\n\u003Cp>MySQL&#39;s default collation since 8.0 is \u003Ccode>utf8mb4_0900_ai_ci\u003C/code>. The \u003Ccode>ci\u003C/code> means case-insensitive, and the \u003Ccode>ai\u003C/code> means accent-insensitive. So in MySQL a unique index on \u003Ccode>email\u003C/code> treats \u003Ccode>ana@example.com\u003C/code> and \u003Ccode>Ana@Example.com\u003C/code> as the same value, and \u003Ccode>WHERE name = &#39;Jose&#39;\u003C/code> finds José. Your app has quietly depended on this for years, and you never wrote a line of code for it.\u003C/p>\n\u003Cp>Postgres compares strings exactly. Move the same schema over and a user who signed up as \u003Ccode>Ana@...\u003C/code> can&#39;t log in typing \u003Ccode>ana@...\u003C/code>, and worse, can sign up twice. You find out from a support email.\u003C/p>\n\u003Cimg src=\"/blog/mysql-vs-postgres/mysql-postgres-same-statements.webp\" alt=\"Four SQL statements on MySQL 9.7 and Postgres 18: a capitalised email, an accent-free search and a failed migration give different results\" width=\"1600\" height=\"900\" loading=\"lazy\">\u003Cp>The fixes on the Postgres side are boring but you have to actually do them: lowercase emails on write and keep a unique index on \u003Ccode>lower(email)\u003C/code>, or use the \u003Ccode>citext\u003C/code> extension, or a nondeterministic ICU collation. I go with the lowercase index. It&#39;s explicit, and anyone reading the schema can see what&#39;s going on.\u003C/p>\n\u003Ch2>Migrations that half-run\u003C/h2>\n\u003Cp>Postgres has transactional DDL. If a migration creates a table, adds two columns and then fails on the third, the whole thing rolls back and you fix it and run it again.\u003C/p>\n\u003Cp>MySQL commits implicitly on most DDL. A Laravel migration that dies halfway leaves the table created but no row in the \u003Ccode>migrations\u003C/code> table, so the next \u003Ccode>php artisan migrate\u003C/code> stops with:\u003C/p>\n\u003Cpre>\u003Ccode>SQLSTATE[42S01]: Base table or view already exists: 1050 Table &#39;orders&#39; already exists\n\u003C/code>\u003C/pre>\n\u003Cp>On a laptop that&#39;s a shrug. On a production deploy at night it&#39;s a manual cleanup with the site in maintenance mode. I&#39;ve learned to keep MySQL migrations small, one change each, for exactly this reason. Postgres lets me be lazier, and I&#39;ll take that.\u003C/p>\n\u003Ch2>What Postgres gives you that MySQL doesn&#39;t\u003C/h2>\n\u003Cp>Extensions are the real gap. \u003Ccode>pgvector\u003C/code> for embeddings (I wrote up \u003Ca href=\"/blog/pgvector\">how it behaves with filters\u003C/a>), PostGIS for maps, \u003Ccode>pg_trgm\u003C/code> for fuzzy search, \u003Ccode>pg_cron\u003C/code> for scheduled jobs inside the database. MySQL has vector types now too, but the ecosystem around Postgres is years ahead.\u003C/p>\n\u003Cp>Row level security is the other one. It&#39;s the whole reason Supabase works the way it does: the browser talks to the database through policies. MySQL has nothing like it, so with MySQL every rule lives in your API code. That&#39;s not worse, honestly. It&#39;s just a different shape of app.\u003C/p>\n\u003Cp>JSON works in both. Postgres \u003Ccode>jsonb\u003C/code> with a GIN index is nicer to query, MySQL&#39;s \u003Ccode>JSON\u003C/code> type is fine for storing a blob you mostly read whole.\u003C/p>\n\u003Ch2>What MySQL still does better\u003C/h2>\n\u003Cp>Simple replication and boring operations. A MySQL primary with a couple of replicas is something a lot of hosting people can run half asleep, and managed MySQL is available everywhere at the same price as Postgres. On AWS, RDS for MySQL and RDS for PostgreSQL cost the same per instance hour for the same class (see the \u003Ca href=\"https://aws.amazon.com/rds/pricing/\">RDS pricing page\u003C/a>), so cost is not a reason to pick either.\u003C/p>\n\u003Cp>Connection handling too. MySQL uses a thread per connection and copes with a few hundred open connections without drama. Postgres forks a process per connection, so once you have serverless functions or a big PHP-FPM pool you will want a pooler in front. I covered that in the \u003Ca href=\"/blog/pgbouncer\">PgBouncer post\u003C/a>. It&#39;s one more moving part.\u003C/p>\n\u003Cp>And if the existing app is on MySQL, works, and has a team that knows it, moving databases is a project with real risk and very little to show the customer at the end. I would not do it for taste.\u003C/p>\n\u003Ch2>What I&#39;d pick\u003C/h2>\n\u003Cp>New project, any stack: Postgres, mostly for the extensions and because Supabase, Neon and most hosted backends are built on it.\u003C/p>\n\u003Cp>Existing Laravel or WordPress app on MySQL 8.0: upgrade to 8.4 or 9.7 LTS, keep MySQL, and spend the saved weeks on features.\u003C/p>\n\u003Cp>Moving MySQL to Postgres anyway: lowercase-and-index your emails, grep for \u003Ccode>LIKE\u003C/code> queries that relied on case-insensitivity, check \u003Ccode>GROUP BY\u003C/code> queries, test every migration on a copy first. \u003Ca href=\"https://pgloader.io/\">pgloader\u003C/a> does the data move well, it&#39;s the app code that needs the work.\u003C/p>\n\u003Cp>If you&#39;re weighing a move like that on a production app and want a second pair of eyes on the schema first, \u003Ca href=\"https://calendly.com/director-norsky/meeting\">book a free call\u003C/a>.\u003C/p>\n",[16,20,24],{"slug":17,"title":18,"description":19},"pgbouncer","PgBouncer: What Transaction Mode Breaks, and the Fix","PgBouncer in transaction mode lets 1,000 clients share 20 Postgres connections. What it breaks (SET, LISTEN, advisory locks), and what 1.24 fixed.",{"slug":21,"title":22,"description":23},"supabase-cli","Supabase CLI: Migrations, Types and the Docker Catch","The Supabase CLI in late 2026: how to install it, why local dev needs a container runtime, and the migration commands that keep production in sync.",{"slug":25,"title":26,"description":27},"supabase-realtime","Supabase Realtime: What It Costs and Where It Breaks","Supabase Realtime in late 2026: how messages are counted and billed, the per-plan limits, the errors you hit first, and when to switch from Postgres Changes to Broadcast.",1794181974983]