Prompt
Why is my PostgreSQL app slow after switching from MySQL?
Latest observation
There are a few very common reasons an app feels slower after moving from MySQL to PostgreSQL. Usually it’s not that PostgreSQL is “slow,” but that the workload, queries, or settings don’t match it yet.
Common causes
1. Different query plans
PostgreSQL and MySQL optimize queries differently. A query that was fast in MySQL may now:
- use a different index
- choose a different join order
- do a sequential scan instead of an index scan
What to do:
Run EXPLAIN (ANALYZE, BUFFERS) on slow queries and compare expected vs actual row counts.
2. Missing or wrong indexes
PostgreSQL often benefits from more deliberate indexing, especially for:
- joins
WHEREfiltersORDER BY- partial-match / functional queries
Examples:
LOWER(email)queries may need a functional indexWHERE deleted_at IS NULLmay benefit from a partial index- composite indexes need the right column order
What to do:
Check slow queries and verify the indexes match their predicates exactly.
3. Autovacuum / table bloat
Postgres uses MVCC, so updates/deletes leave dead tuples behind until vacuumed. If autovacuum isn’t keeping up:
- tables and indexes bloat
- scans get slower
- caches become less effective
What to do:
Check:
- autovacuum activity
- table bloat
- dead tuple counts
- whether large update/delete workloads exist
4. Too many random I/O reads
PostgreSQL is often more sensitive to disk latency than MySQL when:
- data doesn’t fit in memory
- queries do lots of random access
- indexes are missing or poorly selective
What to do:
Look at buffer hits vs reads in EXPLAIN (ANALYZE, BUFFERS). If you see lots of reads, memory or indexing may be the issue.
5. Default settings may be too conservative
Postgres defaults are safe, not always performance-tuned for your machine. Things that often matter:
shared_bufferseffective_cache_sizework_memmaintenance_work_memrandom_page_costcheckpoint_timeoutmax_wal_size
What to do:
Tune based on RAM, storage type, and workload. Don’t blindly copy settings from MySQL assumptions.
6. Connection management overhead
If your app opens many short-lived connections, Postgres may suffer more than expected because each connection is relatively expensive.
What to do:
Use a pooler like:
- PgBouncer
- application connection pooling
7. Query patterns that behave differently
Some MySQL-friendly patterns are inefficient in Postgres:
- relying on implicit type casting
LIKE '%term%'without trigram/full-text indexing- large
OFFSETpagination - correlated subqueries where joins would be better
SELECT *on wide tables
What to do:
Review the SQL patterns, not just the schema.
8. Migration/import issues
Sometimes the schema was migrated, but not optimized for Postgres:
- indexes didn’t carry over correctly
- column types are suboptimal
- sequences/defaults were not set up properly
- constraints/statistics are missing
What to do:
Audit the migrated schema carefully.
Best first steps
- Identify the slowest queries.
- Run
EXPLAIN (ANALYZE, BUFFERS)on them. - Check indexes and row estimates.
- Verify autovacuum is healthy.
- Confirm the app uses connection pooling.
- Review Postgres config for your workload.
If you want a fast diagnosis
Send me:
- one slow query
- its
EXPLAIN (ANALYZE, BUFFERS)output - table schemas and indexes
- approximate row counts
- your PostgreSQL version and server specs
and I can help pinpoint the cause.
Brands