Last 30 Days
No notifications
A database is an organized collection of data that can be efficiently accessed, managed, and updated.
| Feature | SQL (Relational) | NoSQL (Non-Relational) | |||
| Structure | Tables with rows/columns | Documents, key-value, graphs | |||
| Schema | Fixed schema (strict) | Flexible schema (dynamic) | |||
| Relationships | JOINs, foreign keys | Embedded docs, references | |||
| Scaling | Vertical (bigger server) | Horizontal (more servers) | |||
| ACID | β Full support | Varies by implementation | |||
| Examples | PostgreSQL, MySQL, SQLite | MongoDB, Redis, DynamoDB | When to Use What? | Use SQL When | Use NoSQL When |
| Complex relationships | Flexible/evolving schema | ||||
| Financial data (ACID needed) | High write throughput | ||||
| Structured data | Hierarchical/nested data | ||||
| Complex queries with JOINs | Rapid prototyping | ||||
| Strong consistency needed | Horizontal scaling needed | ACID Properties | Property | Meaning | Example |
| Atomicity | All or nothing | Bank transfer: both debit & credit succeed or neither | |||
| Consistency | Data always valid | Balance never goes negative | |||
| Isolation | Transactions don't interfere | Two transfers run independently | |||
| Durability | Committed = permanent | Server crash doesn't lose data |
A database is just a program whose entire job is to store data safely and let many people query it at once β fast, consistent, and without losing anything when the power goes out. Files in a folder cannot do that. Once your app has more than one user or more than one process, you need a database.
Three promises a real database makes that a JSON file does not:
1. Durability β once it says "saved", the data survives a crash. 2. Concurrency β 100 users editing at the same time will not corrupt each other's writes. 3. Querying β ask "give me users in India who signed up last week" and get an answer in milliseconds, not minutes.
Everything else (joins, indexes, transactions, replication) is built on top of these three.
| Family | Examples | Mental model | Best for |
| Relational (SQL) | PostgreSQL, MySQL, SQLite | Spreadsheets with strict columns + foreign keys | Anything with relationships, finance, most apps |
| Document | MongoDB, Firestore | Folders of JSON files | Flexible shapes, nested data, fast iteration |
| Keyβvalue | Redis, DynamoDB | A giant Map | Caching, sessions, rate limits, queues |
| Graph | Neo4j, Memgraph | Nodes + edges | Social graphs, recommendations, fraud rings |
90% of new web apps live happily on PostgreSQL + Redis. Everything else is a special tool for a special problem.
SQL is not "old" and NoSQL is not "web scale". SQL gives you schemas + joins + transactions β invaluable when data is interconnected. NoSQL (document) gives you flexible shapes + horizontal scale β invaluable when records are self-contained and you do not know your schema yet. Both exist in 2026 because they solve different problems. Most production systems use both.
1. "NoSQL is faster." Only for the queries it was designed for. A bad schema in MongoDB is slower than a good one in PostgreSQL.
2. Storing files (PDFs, images, videos) in the database. Use object storage (S3, R2, Cloudinary). Keep only the URL in the DB.
3. One database per microservice from day one. Premature. Start with one DB; split when scaling forces it.
4. Hardcoding the connection string. Always read from process.env, never commit credentials.
5. Closing the connection after every query. Connections are expensive. Use a pool.
6. No backups. "It is in the cloud" is not a backup. Automated daily snapshots + monthly restore drills.
Normalised (SQL default) β each fact lives in exactly one place. No duplication. Slightly slower reads (joins), much safer writes.
users(id, name, email)
posts(id, title, user_id)
comments(id, text, post_id, user_id)Denormalised (document default) β store the data the way you read it. Fewer queries, but updates may need to touch many copies.
{
"_id": "u1",
"name": "Alice",
"posts": [
{ "title": "Hi", "comments": [{ "text": "Cool", "author": "Bob" }] }
]
}Rule of thumb: embed when read together, reference when shared or unbounded.
id SERIAL in SQL, _id ObjectId in Mongo, UUID anywhere).Without an index the DB scans every row (O(n)). With an index it walks a tree (O(log n)). The cost: extra storage and slightly slower writes (the tree must be updated).
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author_created ON posts(author_id, created_at DESC);Rule of thumb: index columns used in WHERE, JOIN, ORDER BY. Don't index every column β each one slows writes.
Opening a TCP connection + auth takes ~30β100ms. Doing that per request kills your throughput. A pool keeps N connections open and hands them out:
const pool = new Pool({ max: 20, idleTimeoutMillis: 30_000 });
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);Size the pool to roughly (CPUs * 2) + 1 per app instance. Way more than that hurts the DB without helping you.
A transaction = a group of statements that succeed or fail together. The classic example: transfer money.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- or ROLLBACK if anything failedACID = Atomic, Consistent, Isolated, Durable. You get this for free in SQL. NoSQL document stores have it too now (MongoDB since 4.0), but the cost / scope differs.
When you scale to multiple machines, you can pick at most two of: Consistency, Availability, Partition tolerance. Networks fail, so P is mandatory β the real choice is CP (refuse writes during a split, e.g. MongoDB primary) or AP (accept writes everywhere, reconcile later, e.g. Cassandra).
NoSQL often uses BASE instead of ACID: Basically Available, Soft state, Eventually consistent.
Production schemas change. Never edit them manually. Use migration tools (Prisma Migrate, Knex, Flyway, Liquibase) so every environment ends up identical and changes are reviewable in PRs.
Golden rule: expand, then contract. Add the new column β dual-write β backfill β switch reads β drop the old column. Never break the old code in one step.
1. Spin up Postgres in Docker, create a users and posts table with a foreign key, insert a few rows.
2. Connect from Node with pg using a pool, run a parameterised query.
3. Add an index on posts.created_at, compare EXPLAIN ANALYZE before and after.
4. Wrap a two-statement update in BEGIN/COMMIT and intentionally throw to see the rollback.