Search "SQL vs NoSQL" and you'll get four hundred versions of the same comparison chart. Structured vs unstructured. Vertical vs horizontal scaling. ACID vs BASE. All technically correct, and none of it helps you at 11 PM when you're staring at an empty project folder trying to decide what to actually use.
I've now shipped projects on both MySQL and MongoDB. The chart didn't help me. It's the same species of question as library vs framework — the answer isn't sitting in a feature table, it's in what you're actually building. What helped was getting a few decisions wrong and having to live with them. So this post isn't a chart — it's the decision process I use now, why I use it, and the flowchart I wish someone had given me two years ago.
The One-Sentence Version
Here's the cheat code:
- SQL: You decide the shape of your data up front, and the database enforces it forever.
- NoSQL: You decide the shape of your data later, and you enforce it forever.
Read that second line again, because it's the part the comparison charts leave out. "Schemaless" does not mean there is no schema. Your data always has a shape — the question is only whether that shape lives in the database, where it's checked on every write, or in your application code, where nothing checks it and a typo ships to production.
The Real Difference Isn't Scale
Most beginners think this is a scaling decision. It usually isn't. Unless you're handling millions of writes, both databases will happily do everything you ask and neither will break a sweat. At the scale of a side project, a client site, or an internal tool, "which one is faster" is almost never the deciding factor.
The decision that actually matters is this:
How many different ways will I need to read this data later?
That's it. That's the whole question.
If you write data one way and read it back that same way, a document store is a great fit — the document is the answer, you fetch it and you're done. If you'll need to slice the same data twenty different ways later ("all users who signed up last month", "reports grouped by language", "who hasn't logged in since March"), a relational database is going to save you an enormous amount of pain, because you don't have to know those twenty questions in advance. SQL lets you ask questions you hadn't thought of yet. That's the actual superpower, and nobody puts it on the chart.
A Side-by-Side Comparison
| Aspect | SQL (MySQL, PostgreSQL) | NoSQL (MongoDB) |
|---|---|---|
| Where the schema lives | In the database | In your application code |
| Relationships | JOINs, done by the database | Embed the data, or join it yourself |
| Unknown future queries | Easy — just write the query | Hard — you modelled for known reads |
| Changing the shape | A migration, once, on the table | Free today, an if in your code forever |
| Transactions | Mature, boring, reliable | Supported, but usually a design smell |
| Scaling out | Harder (sharding is real work) | Built for it |
| What breaks first | Your migration on deploy day | Your assumption about old documents |
How This Played Out in My Own Projects
Relational: the app with users and history
In AI Explains Repo, users log in, analyse a GitHub repository, and get a report back. Their past reports stay in their history. Write that data out as sentences and the shape gives itself away: a user has many sessions, a session has one report. That word "has many" is the tell. Every time you catch yourself saying it, you're describing a relationship, and relationships are what relational databases were literally built for.
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE reports (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
repo_url VARCHAR(500) NOT NULL,
body JSON NOT NULL, -- the AI output, shape varies
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
Two things worth noticing here, because this is the part I got wrong the first time.
First, ON DELETE CASCADE. When a user deletes their account, their reports go with them — enforced by the database, not by me remembering to write the cleanup code. In MongoDB, forgetting that line is how you end up with orphaned documents nobody notices for six months.
Second, look at body JSON. The AI's output doesn't have a fixed shape — some reports have security findings, some don't, the nesting varies by repo. That's genuinely unstructured, so I stopped fighting it and stored it as JSON. And this is the thing that took me too long to learn: modern SQL databases have JSON columns. "But my data is unstructured" hasn't been a reason to leave SQL since about 2016. You can keep the structured parts structured and let the messy part be messy, in the same table, in the same query.
Document store: the data that has no fixed shape
The Health-Aware Recipe Modifier is the opposite situation. It generates a recipe tailored to a medical condition — ingredients, substitutions, warnings, nutrition notes — and the shape genuinely differs from one recipe to the next. One has three substitutions, one has none, one has a nested list of warnings per ingredient.
Model that relationally and you get a table of recipes, a table of ingredients, a table of substitutions, a table of warnings, and a five-way JOIN to reassemble something you only ever read as one whole object. Model it as a document and it's just… the object:
await db.collection("recipes").insertOne({
userId: "u_123",
title: "Low-Sodium Chicken Curry",
condition: "hypertension",
ingredients: [
{ name: "chicken", qty: "500g" },
{ name: "salt", qty: "1 tsp", flagged: true,
reason: "high sodium", substitute: "lemon juice + herbs" }
],
warnings: ["Avoid stock cubes — hidden sodium"],
createdAt: new Date()
});
No JOINs, no migration when the AI starts returning a new field, and the read is a single lookup. Here the document store isn't a compromise — it's genuinely the better model, because I write this data one way and read it back that exact same way.
And sometimes: no database at all
My Online Python Code Editor runs user code on the server and streams the output back. I spent real time planning the database for it before I noticed there was nothing to store. Nobody logs in, nothing persists, the interesting problems were all about sandboxing and process isolation instead.
Worth saying out loud, because "which database" is such a reflex question: the right answer is sometimes "none". A database you don't need is just a deploy step that can fail.
Four Things That Changed My Mind
1. "MongoDB is faster" is usually about the index, not the database. Most speed comparisons you'll read online are between a well-indexed collection and an unindexed table. Add the index and the gap mostly disappears. Benchmark your own query before you switch databases over a number you read on Reddit.
2. You will do joins in NoSQL anyway — just badly. The moment you need "this user's reports plus the user's plan", you're either using $lookup (a join with extra steps) or you're fetching in a loop and doing it in JavaScript. That loop is an N+1 query, it works fine on your ten test documents, and it falls over on real data. Avoiding joins by choosing MongoDB doesn't avoid joins — it just moves them somewhere slower and less tested.
3. Migrations don't disappear, they hide. Adding a field to MongoDB is free on day one. But now half your documents have the field and half don't, and every piece of code that touches them needs if (doc.version === 1) — forever, in every function, in every service. A SQL migration hurts once, on deploy day, and then it's genuinely over. NoSQL doesn't remove that cost. It spreads it thin enough that you stop noticing you're paying it.
4. "I'll figure out the schema later" means "I'll figure it out under pressure, with production data in it." Being forced to define a schema up front felt like friction when I was starting out. It's actually the database asking you a question you should be able to answer, at the cheapest possible moment to answer it.
When Each One Genuinely Wins
Reach for SQL when…
- Your data has relationships — users, orders, comments, anything you describe with "has many" or "belongs to".
- Anything involves money, counts, or inventory. Transactions exist for a reason.
- You'll need reporting or analytics later.
GROUP BYbeats writing an aggregation pipeline every time. - The data outlives the application. Schemas are documentation that can't go stale.
- You don't know yet how you'll query it. This is the strongest reason on the list.
Reach for NoSQL when…
- Each record is self-contained and read as a whole — documents, logs, events, AI outputs.
- The shape genuinely varies per record and you're not just avoiding the design work.
- It's cache-like, session-like, or disposable.
- You're writing far more than you're reading, at volume, and horizontal scale is a real requirement rather than an aspiration.
And be honest about this one
"It's faster to prototype" is a real advantage, and it's also a loan. You'll repay it the first time a product decision requires querying your data a way you never modelled for. Sometimes taking that loan is correct — a hackathon, a proof of concept, something you'll throw away. Just know you're taking it.
The Flowchart I Actually Use
Do I need to store anything at all?
|
no ---> use no database
|
yes
|
Does my data have relationships?
("a user HAS MANY reports")
|
yes ---> SQL
|
no
|
Money, counts, or inventory involved?
|
yes ---> SQL
|
no
|
Do I know every way I'll query this?
|
no ---> SQL
|
yes
|
Do I read each record as one whole object?
|
no ---> SQL
|
yes
|
Does the shape truly vary per record?
|
no ---> SQL (with a JSON column)
|
yes ---> NoSQL
Yes, most paths lead to SQL. That's not a bias, that's the base rate: most applications have relationships and unknown future queries. NoSQL is the specialist tool, and it's excellent when the shoe fits. The mistake is reaching for it by default because it felt easier in the tutorial.
Common Misconceptions
- "NoSQL is schemaless." → The schema moved into your code. It's still there, it's just unenforced and undocumented.
- "SQL can't handle unstructured data." → PostgreSQL's
JSONBand MySQL'sJSONcolumns have handled it for years, and you can index inside them. - "NoSQL scales, SQL doesn't." → SQL scales fine to a size you are statistically unlikely to reach on a side project. Solve the problem you have.
- "Pick one and use it for everything." → Real systems mix them constantly: Postgres for the core data, Redis for sessions, Mongo for a document-shaped subsystem. "Which database" is a per-dataset question, not a per-project identity.
- "The choice is permanent." → Migrating is painful but survivable. Choosing nothing and building nothing is worse.
Key Takeaways
- The schema always exists. You're only choosing whether the database enforces it or you do.
- The deciding question is "how many ways will I read this?", not "how much data will I have?".
- Relationships and unknown future queries → SQL. Self-contained, whole-object records → NoSQL.
- JSON columns mean "my data is messy" is no longer a reason to leave SQL.
- NoSQL doesn't delete the cost of migrations and joins — it relocates them into your application code.
- Sometimes the correct database is no database.
Bottom Line: Don't pick a database by reading a comparison chart, and definitely don't pick one because it's what the tutorial used. Write down how you'll read your data, follow the flowchart, and pick the boring option when it's close. The best database for your project is usually the one you can debug at 2 AM — and you'll be doing that eventually, whichever one you choose.