Backend

SQL vs NoSQL: I've Shipped Both, Here's How I Actually Decide

· 10 min read · Backend
Cover illustration for this SQL vs NoSQL guide: the words "SQL vs NoSQL" hand-lettered inside a sketched frame.

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:

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

AspectSQL (MySQL, PostgreSQL)NoSQL (MongoDB)
Where the schema livesIn the databaseIn your application code
RelationshipsJOINs, done by the databaseEmbed the data, or join it yourself
Unknown future queriesEasy — just write the queryHard — you modelled for known reads
Changing the shapeA migration, once, on the tableFree today, an if in your code forever
TransactionsMature, boring, reliableSupported, but usually a design smell
Scaling outHarder (sharding is real work)Built for it
What breaks firstYour migration on deploy dayYour 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…

Reach for NoSQL when…

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

Key Takeaways

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.

Keep reading