PostgreSQL vs MySQL: The 2023 Comparison
When to choose PostgreSQL for your next project and when MySQL is still the better choice for legacy systems.
SQL is everywhere and most devs write it badly.
You don't need to be a database administrator to write good SQL. But if you're a backend developer, you are writing SQL every day. And if you're like most of us, you're probably getting away with bad habits that will eventually bite you in production.
We've seen production databases slow to a crawl because of a single missing index. We've seen data corruption because a transaction wasn't wrapped. We've seen teams lose weeks of work because they relied on an ORM to design their schema.
Here are the seven most common SQL mistakes developers make, and the concrete steps to fix them.
It's easy. It's fast to type. But it's a performance and security nightmare. Selecting every column from a table returns more data than your application needs, increasing network latency and memory usage. Worse, it exposes columns you might not want to return to the client (like password hashes or internal flags).
❌ Bad:
SELECT * FROM users WHERE id = 1;
✅ Good:
SELECT id, email, first_name FROM users WHERE id = 1;
When you join two tables, the database has to look up rows in the second table for every row in the first. If the column you're joining on isn't indexed, this becomes a full table scan. If you have a high-traffic table with a foreign key relationship, you need an index immediately.
❌ Slow:
SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
✅ Fast:
CREATE INDEX idx_orders_user_id ON orders(user_id);
This is the classic Laravel/ActiveRecord mistake. You fetch a list of 50 posts, then loop through them to get the author's name for each one. That's 1 query to get posts, plus 50 queries to get authors. If your API scales to 10,000 requests, you just killed your database.
❌ N+1 Problem:
posts.forEach(post => post.author);
(1 query for posts, 50 queries for authors)
✅ Eager Loading:
Post::with('author')->get();
(2 queries total)
If you're updating multiple tables or performing a complex operation that depends on data consistency, you must wrap it in a transaction. Without one, if the second query fails, the first one remains committed, leaving your data in an inconsistent state.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
Storing naive timestamps (without timezone info) is a recipe for bugs. If your server is in New York and your database is in London, naive timestamps will shift by 5 hours when you query them. Always use TIMESTAMPTZ (PostgreSQL) or WITH TIME ZONE (MySQL) to store the actual moment in time.
Developers often trust their intuition about how a query will run. But the database optimizer is smarter than you. Run EXPLAIN ANALYZE on your slow queries. It will tell you exactly how many rows were scanned, if indexes were used, and where the bottleneck is.
ORMs are great for rapid development, but they often default to a normalized schema that is terrible for performance. They might create a separate table for every single relationship. If you care about speed, you need to understand the trade-offs between normalization and denormalization, and sometimes you need to write raw SQL migrations to fix it.
When to choose PostgreSQL for your next project and when MySQL is still the better choice for legacy systems.
A deep dive into B-Trees, Hash indexes, and covering indexes. How to make your queries fly.
Step-by-step guide to building a scalable backend API from scratch using modern tooling.