Skip to content
PRSINDIA

Engineering

Postgres or MySQL in 2026? An honest answer for Indian startups

Both will carry you further than your business will. Here is the short list of things that genuinely differ — and the connection-pooling trap that catches every Laravel team.

PRS Admin 8 min read

We get asked this at the start of most projects, usually with more urgency than the question deserves. So, the honest answer first:

For the overwhelming majority of Indian startups, either database will comfortably carry you past ₹100 crore in revenue. Neither choice will be why you fail. Pick on what your team knows, then read the tiebreakers below to check you are not walking into the one case where it does matter.

Now the tiebreakers, because there are a handful of places where the difference is real and one of them will cost you a weekend if you do not know about it.

Where Postgres genuinely wins

1. JSON that you actually query

Both store JSON. Only Postgres indexes it in a way that scales. A jsonb column with a GIN index gives you fast containment queries on arbitrary keys:

CREATE INDEX idx_attrs ON products USING GIN (attributes jsonb_path_ops);

-- fast, index-backed, on an arbitrary nested key
SELECT * FROM products
WHERE attributes @> '{"specs": {"voltage": "240V"}}';

MySQL 8 has functional indexes on generated columns, which means you must declare in advance every JSON path you will ever query. That is fine when you have three. It is not a schema-flexible design; it is a schema with extra steps.

If your product has variable attributes — a marketplace catalogue, a form builder, an EAV-shaped domain — this alone is close to decisive.

2. Partial and expression indexes

-- Index only the rows that matter. On a 50M-row table where
-- 200k orders are open, this index is 250x smaller and stays in RAM.
CREATE INDEX idx_open_orders ON orders (created_at)
WHERE status IN ('pending', 'processing');

-- Case-insensitive uniqueness, enforced by the database
CREATE UNIQUE INDEX idx_email_ci ON users (lower(email));

MySQL has neither. The second one in particular is a correctness feature, not a performance one, and working around it in application code means someone will eventually register as Rahul@x.com and rahul@x.com.

3. The extension ecosystem

PostGIS for anything geospatial is not merely better than the MySQL equivalent, it is a different category of software — and if you are doing delivery, logistics or hyperlocal anything, you will need it. pgvector means your RAG embeddings live in the database you already run, filtered by the same WHERE clause as everything else, instead of in a second system you have to operate and keep in sync. TimescaleDB if you have time-series.

4. Expressive SQL that removes application code

Filtered aggregates, lateral joins, real CTEs, DISTINCT ON, window functions that were there fifteen years before MySQL got them:

SELECT
  date_trunc('month', created_at) AS month,
  count(*)                                        AS orders,
  count(*) FILTER (WHERE status = 'delivered')    AS delivered,
  count(*) FILTER (WHERE payment_mode = 'upi')    AS upi_orders,
  sum(total) FILTER (WHERE status = 'delivered')  AS revenue
FROM orders
GROUP BY 1 ORDER BY 1;

MySQL 8 can express this with SUM(CASE WHEN ...). It is uglier and it is not a real difference. But the accumulation of small expressiveness wins is what people mean when they say Postgres "feels" better, and it does show up as less application code.

5. Transactional DDL

Wrap a migration in a transaction. If step four fails, steps one through three roll back. MySQL commits each DDL statement implicitly, so a failed migration leaves your schema in a partial state and you get to fix it by hand, at 2 a.m., on production. Anyone who has done this once has an opinion about it.

6. Constraints the database actually enforces

CHECK constraints, exclusion constraints, domains, real ENUM types, row-level security. MySQL 8 does support CHECK now, which closed the most embarrassing gap.

Where MySQL genuinely wins

1. The connection model — and this is a big one

Postgres forks a process per connection. Each one costs roughly 5–10 MB. MySQL uses a thread per connection, which is dramatically lighter.

Why this matters more than it sounds, and specifically why it bites Laravel teams:

PHP-FPM holds a database connection per worker process. Three application servers with 200 FPM workers each is 600 connections. A db.r6g.large Postgres on RDS defaults to a max_connections in the low hundreds. You will exhaust it, and the failure is not graceful — it is FATAL: sorry, too many clients already during your first traffic spike, and by definition that is the moment it matters.

The fix is well understood and it is not optional: PgBouncer (or RDS Proxy) in transaction pooling mode, sitting between your app and the database. But it is another component to run, and transaction pooling quietly forbids a few things your ORM might do — session-level advisory locks, some prepared-statement patterns, SET that expects to persist across statements. Laravel works fine with it once configured, and "once configured" is a real afternoon of someone's life plus a thing that can break in production if a developer later reaches for a session-scoped feature.

MySQL under the same load simply does not have this problem. If your team is small and has no dedicated infra person, that is a legitimate, unglamorous argument.

2. Operational familiarity

Every ops person in India has restored a MySQL backup. The replication story is boring and well-trodden. There are more people who can fix it at 3 a.m., and that has a value even if it is not an engineering value.

3. Ecosystem gravity in the PHP world

WordPress, Magento, and a very large body of Laravel packages assume MySQL. Most of them work on Postgres. "Most" is doing real work in that sentence, and you will be the one who finds the exception, in a package that does raw SQL_CALC_FOUND_ROWS or relies on MySQL's forgiving GROUP BY.

4. Clustered primary key layout

InnoDB stores rows in primary-key order. Point lookups by PK and range scans on the PK are genuinely fast, and if your workload is overwhelmingly "fetch this row by ID", it is a real, if modest, advantage.

Things that are no longer differentiators

To be fair to MySQL, several standard arguments against it are simply out of date. MySQL 8 has CTEs, window functions, CHECK constraints, atomic DDL (not transactional, but no longer leaving corrupt metadata), decent JSON functions and instant ADD COLUMN. Anyone still arguing from a 2015 comparison is arguing about a database that no longer exists.

And Postgres has genuine operational sharp edges MySQL does not: autovacuum, which you will eventually have to tune and which will eventually surprise you; transaction ID wraparound, which is unlikely to bite you but is catastrophic when it does; and a replication story that is more powerful and less forgiving.

The migration question

You have a working MySQL application. Should you move?

Almost certainly not. Not "for correctness". Not because Postgres is nicer. A migration is weeks of work, a data-integrity risk, a query-rewrite exercise, and a period during which your team is worse at operating their own database.

Migrate when you need a capability: you are adding geospatial and need PostGIS; you are adding semantic search and want pgvector next to your relational data; your product's core entity has genuinely dynamic attributes you must query. Those are reasons. "Postgres is better" is not a reason, it is a preference, and it will not survive a conversation with your CFO.

Practical notes for India

  • ap-south-1 (Mumbai) has everything. RDS for both, Aurora for both. Aurora is roughly 20–25% more expensive and buys faster failover and better read-replica scaling. For most startups, plain RDS Multi-AZ is the right call until it demonstrably is not.
  • DPDP Act: encryption at rest (KMS), encryption in transit (enforce TLS — rds.force_ssl on Postgres, require_secure_transport on MySQL), and an audit trail of who accessed personal data. Postgres row-level security is a genuinely useful tool here for multi-tenant products.
  • Test your restores. Not your backups — your restores. Quarterly. A backup you have never restored is a hypothesis, and this is entirely database-agnostic advice that half the companies we audit are failing.

Our default in 2026

  • New product, greenfield, team is comfortable: Postgres. The JSON, the partial indexes, pgvector and transactional DDL compound over the life of a product, and we would rather set up PgBouncer once on day one than migrate in year three.
  • Laravel + Filament admin, standard CRUD, small team, no infra specialist: MySQL, without embarrassment. It will not be the constraint. You will spend the saved week on the product.
  • Anything geospatial, or anything with an LLM/retrieval component: Postgres. Not close.
  • An existing MySQL system that works: leave it alone and go build a feature.

The database you can operate confidently at 3 a.m. beats the database that is 12% better on a benchmark you will never run.

Written by

PRS Admin

Building software at PRS India.

Keep reading

All articles

Engineering

Server Components changed how we structure Next.js apps — our current default

Read it

Engineering

Laravel queues in production: four failure modes nobody warns you about

Read it

Engineering

Offline-first isn't a feature, it's an architecture — a driver app post-mortem

Read it