PostgreSQL vs MySQL for SaaS: I've Used Both, Here's What I Pick
Let me preface this: MySQL is not bad. I used it happily for years. It powers Facebook, Wikipedia, and thousands of excellent applications. If you're using MySQL and it's working for you, you don't ne
PostgreSQL vs MySQL for SaaS: I've Used Both, Here's What I Pick
Let me preface this: MySQL is not bad. I used it happily for years. It powers Facebook, Wikipedia, and thousands of excellent applications. If you're using MySQL and it's working for you, you don't need to panic and migrate.
But if you're starting a new SaaS product in 2026, I'd pick PostgreSQL every time. Here's why.
The Short Version
PostgreSQL gives you more tools for modelling complex data, stronger data integrity by default, and better performance characteristics for the kinds of queries SaaS applications tend to need. The ecosystem has matured to the point where there's no practical downside to choosing it.
JSON/JSONB: This Changes Everything
MySQL has JSON columns. PostgreSQL has JSONB columns. They sound similar. They are not.
PostgreSQL's JSONB (Binary JSON) is stored in a decomposed binary format. This means:
-- PostgreSQL: index into JSON, query it efficiently
CREATE INDEX idx_settings_theme ON users USING GIN (settings);
SELECT * FROM users
WHERE settings @> '{"theme": "dark"}';
-- This uses the GIN index. It's fast even at millions of rows.
In Laravel, this works beautifully:
// Query JSON columns naturally
User::where('settings->theme', 'dark')->get();
// Store structured data without a separate table
$user->update([
'settings' => [
'theme' => 'dark',
'notifications' => [
'email' => true,
'push' => false,
],
'timezone' => 'Europe/London',
],
]);
Why this matters for SaaS: user preferences, feature flags, integration configurations, webhook payloads, audit logs — all of these are semi-structured data that doesn't justify a dedicated table. JSONB lets you store them efficiently and query them properly.
MySQL's JSON type works for basic storage, but the querying capabilities are significantly weaker. You can't create a GIN index on a MySQL JSON column. Complex JSON queries are slower and more awkward to write.
Indexing That Doesn't Make You Cry
PostgreSQL's indexing is significantly more versatile:
Partial indexes — index only the rows you care about:
-- Only index active subscriptions
CREATE INDEX idx_active_subs ON subscriptions (user_id, plan_id)
WHERE status = 'active';
This index is smaller (only active rows), faster to query, and faster to maintain. In a SaaS app where 80% of your queries are about active records, partial indexes are transformative.
Expression indexes — index computed values:
-- Index the lowercase email for case-insensitive lookups
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
GIN indexes — for full-text search, JSONB, and array columns:
-- Full-text search without Elasticsearch
CREATE INDEX idx_articles_search ON articles
USING GIN (to_tsvector('english', title || ' ' || body));
SELECT * FROM articles
WHERE to_tsvector('english', title || ' ' || body)
@@ plainto_tsquery('laravel deployment');
That last one is genuinely useful. For many SaaS applications, PostgreSQL's built-in full-text search is good enough. You don't need Elasticsearch, Meilisearch, or Algolia until you hit serious scale. One fewer dependency is one fewer thing to maintain.
CTEs: Write Readable Queries for Once
Common Table Expressions let you break complex queries into named steps:
WITH monthly_revenue AS (
SELECT
date_trunc('month', created_at) AS month,
SUM(amount) AS revenue
FROM payments
WHERE status = 'completed'
GROUP BY date_trunc('month', created_at)
),
monthly_growth AS (
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month) * 100,
2
) AS growth_pct
FROM monthly_revenue
)
SELECT * FROM monthly_growth
WHERE month >= NOW() - INTERVAL '12 months'
ORDER BY month;
MySQL 8 supports CTEs too now, to be fair. But PostgreSQL also supports recursive CTEs (for hierarchical data like org charts or category trees), and WITH clauses in UPDATE and DELETE statements.
In Laravel, you can use CTEs with the query builder:
DB::query()
->withExpression('monthly_revenue', function ($query) {
$query->from('payments')
->selectRaw("date_trunc('month', created_at) AS month")
->selectRaw('SUM(amount) AS revenue')
->where('status', 'completed')
->groupByRaw("date_trunc('month', created_at)");
})
->from('monthly_revenue')
->get();
Array Types: Surprisingly Useful
PostgreSQL supports native array columns:
// Migration
$table->text('tags')->nullable(); // In PostgreSQL, this can be an array
// Or using raw SQL for proper array type
DB::statement('ALTER TABLE articles ADD COLUMN tags TEXT[] DEFAULT \'{}\'');
Array columns are perfect for: tags, permissions lists, feature flags, categories — anything that's a simple list of values that doesn't justify a many-to-many relationship with a pivot table.
Instead of articles, tags, and article_tag tables, you have one column. For simple tagging use cases, this is dramatically simpler.
Data Integrity: Postgres Is Stricter (That's Good)
MySQL has historically been loose with data integrity. It would silently truncate strings, convert invalid dates to 0000-00-00, and coerce types in ways that could corrupt your data without you noticing.
MySQL 8 in strict mode is much better. But PostgreSQL has always been strict by default. If you try to insert a string into an integer column, it tells you to piss off. If your string is too long for the column, it errors instead of truncating.
For a SaaS application handling money, user data, and business-critical information, I want the database to tell me when something's wrong. Not silently "fix" it and let me discover the corruption three months later.
Transactions and Concurrency
PostgreSQL uses MVCC (Multi-Version Concurrency Control) with true serialisable isolation levels. This means concurrent operations don't block each other, and you get genuine ACID compliance even under heavy load.
For SaaS applications where multiple users might be modifying the same data simultaneously (think: collaborative editing, shared dashboards, team management), PostgreSQL's concurrency model is more robust.
Where MySQL Still Makes Sense
I'm not a zealot. MySQL is the right choice when:
- You're using a managed service that only supports MySQL (some shared hosting, some PaaS platforms)
- Your team knows MySQL deeply and doesn't know PostgreSQL (productivity > theoretical advantages)
- The application is simple CRUD without complex queries, JSON storage, or full-text search
- You're integrating with legacy systems that are built around MySQL
- You're using a framework or CMS that's optimised for MySQL (WordPress, obviously)
For a small Laravel app that does basic CRUD with Eloquent, MySQL and PostgreSQL will perform identically and the choice doesn't matter. It's when complexity increases that PostgreSQL starts pulling ahead.
The Laravel Perspective
Switching between MySQL and PostgreSQL in Laravel is straightforward:
// config/database.php
'default' => env('DB_CONNECTION', 'pgsql'),
'connections' => [
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
],
],
Most Eloquent code works identically on both databases. The differences show up when you:
- Use JSON column queries (Postgres is better)
- Use raw queries (syntax differs slightly)
- Use database-specific features (full-text search, arrays)
If you're starting a new project, set DB_CONNECTION=pgsql from day one. Herd supports PostgreSQL. Docker makes it trivial to run locally. There's genuinely no barrier to entry any more.
Performance
This is going to be unsatisfying: for most applications, the performance difference between MySQL and PostgreSQL is negligible. Both are fast. Both handle millions of rows. Both scale with proper indexing.
Where PostgreSQL edges ahead:
- Complex queries with multiple joins and subqueries
- JSONB operations
- Full-text search
- Write-heavy workloads with complex transactions
Where MySQL edges ahead:
- Simple read-heavy workloads
- Point queries on primary keys
- MyRocks storage engine for specific use cases
For a typical SaaS application with a mix of reads and writes, complex queries for reporting, and JSON data for flexible storage, PostgreSQL is the better fit. Not because MySQL can't handle it — it can — but because PostgreSQL makes it easier and more efficient.
My Recommendation
For new SaaS projects: PostgreSQL. No contest.
For new simple websites and apps: either. Pick what your team knows.
For existing MySQL projects: don't migrate unless you're hitting real limitations. The migration cost is almost never worth it for running applications.
PostgreSQL isn't just a database — it's a toolkit. JSON storage, full-text search, array handling, powerful indexing, and strict data integrity. For SaaS applications that need to evolve rapidly and handle complex data, it's the right foundation.
Building a SaaS product? I can help with architecture, payments, and the hard bits.
Get the Friday email
What I shipped this week, what I learned, one useful thing.
No spam. Unsubscribe anytime. Privacy policy.