CodeOath
← All posts
System Design52 min total · 14 parts

System Design Fundamentals for Interviews: Scalability, Trade-offs, and the Framework Interviewers Actually Grade

Part 10 of 14 · ~3 min

Storage and Indexing at Scale

SQL vs. NoSQL: The Trade-offs That Actually Matter

"NoSQL is faster" is the wrong way to frame this comparison — a well-indexed relational query and a well-designed NoSQL lookup can both be extremely fast; the real differences are about what each is structurally built to guarantee.

Relational (SQL)NoSQL (document / key-value / wide-column)
SchemaFixed, enforced by the databaseFlexible — fields can vary per record, enforced (if at all) by the application
RelationshipsFirst-class — joins across tablesGenerally avoided — data is denormalized so related data lives together
TransactionsStrong ACID guarantees, often across multiple tablesUsually strong only within a single document/row; weaker across records
Horizontal scalingHarder — joins and cross-row transactions don't shard cleanlyEasier — many NoSQL stores are built from the ground up to shard
Best fitData with genuine relational structure and a need for multi-row transactional integrityHigh write throughput, flexible/evolving schemas, data that's naturally accessed as a whole document

The honest reason a NoSQL store is often faster for a specific access pattern isn't "NoSQL is inherently faster" — it's that a document store returning one whole document in one lookup is doing structurally less work than a relational query joining five normalized tables to reconstruct the equivalent object. That's a consequence of the data being modeled differently (denormalized, next section) for that specific access pattern — not a property of the storage engine itself. A relational database with the same access pattern and comparable indexing can be just as fast for that one query; it's joins across many tables and horizontal write scaling where the structural difference actually bites.

Indexing, Briefly

An index (most commonly backed by a B-tree, or a hash index for pure equality lookups) trades write cost and storage space for read speed: it maintains a separate, sorted structure pointing at where each indexed value actually lives, so a lookup on that column becomes a fast tree traversal instead of scanning every row in the table. The trade-off is real, not just theoretical — every index has to be updated on every write to the indexed column, so a table with ten indexes pays that update cost ten times over on every insert, which is exactly why indexing every column "just in case" is a real anti-pattern, not a free win.

When Denormalization Is the Right Call

Normalization (structuring data to eliminate redundancy — each fact stored exactly once, referenced by foreign key elsewhere) is the default a relational schema starts from, and for good reason: it keeps data consistent, since there's only one place to update a fact when it changes. Denormalization deliberately reintroduces redundancy — duplicating data across records — to avoid the cost of reconstructing it via joins on every read.

Denormalization is the right call when a specific read path is both extremely frequent and expensive to compute from a normalized schema — precomputing and storing a product's average rating alongside the product itself, for instance, rather than joining and averaging its reviews on every single page view. It's the wrong call applied indiscriminately, because every duplicated fact is now a fact that has to be kept in sync everywhere it's duplicated — update a user's display name in a normalized schema and one row changes; do the same in a schema that denormalized that name into every comment they've ever posted, and now every one of those rows needs an update (or a background job to eventually reconcile them) to avoid showing stale, inconsistent data next to their name.

Common mistake: Denormalizing a field "for performance" before ever measuring whether the join it's avoiding was actually a bottleneck. Denormalization is a targeted fix for a specific, measured hot path — not a default schema style — because every instance of it is a small, permanent tax on write consistency.