Database Normalization

Normalization is a set of design principles for organizing tables so the same piece of information isn't stored redundantly in multiple places. It sounds abstract, but the problems it prevents are very concrete - so let's start with a scenario that goes wrong before naming any rules.

A Motivating Example - The Giant Flat Table

Imagine, instead of StreamHub's actual separate users, movies, and reviews tables, someone had built a single giant flat reviews table that also stored the reviewing user's email and the movie's release year directly on every review row:

iduser_emailmovie_titlerelease_yearrating
1sam@example.comInception20105
2sam@example.comParasite20194
3sam@example.comThe Matrix19995

The problem: Sam's email address is now duplicated across every single review Sam has ever written. If Sam changes their email, updating it means finding and updating every one of those rows - potentially thousands of them for an active user - and if even one row gets missed, the data quietly drifts out of sync, with different rows disagreeing about Sam's own email address. Normalization exists specifically to prevent this kind of redundancy and the inconsistency it invites.

First Normal Form (1NF) - Atomic Columns

1NF requires that every column hold a single, atomic value - never a list crammed into one field. Consider a "bad" design where a movies table has a genre_names column storing something like 'Action,Sci-Fi,Thriller' as one comma-separated string:

idtitlegenre_names (violates 1NF)
7InceptionAction,Sci-Fi,Thriller

This looks convenient at first, but it makes even simple questions hard - "find every Sci-Fi movie" now needs fragile text-matching against a string instead of a clean, direct comparison, and there's no good way to enforce that each value in the list is actually a valid genre. StreamHub's real design, covered in Primary & Foreign Keys, avoids this entirely with the movie_genres junction table - each row pairs one movie_id with one genre_id, so every column holds exactly one atomic value. That's a 1NF-compliant way to model a many-to-many relationship.

Second Normal Form (2NF) - Depend on the Whole Key

2NF applies to tables with a composite primary key (a key made of more than one column), and it requires that every non-key column depend on the whole key, not just part of it. This is most relevant for a table like movie_genres, whose primary key is the combination of movie_id and genre_id together. If that table had, say, a movie_title column, that would violate 2NF - the title depends only on movie_id, not on the full (movie_id, genre_id) pair, so it doesn't belong there at all; it belongs on movies where it already lives.

Third Normal Form (3NF) - Depend Only on the Key

3NF goes a step further: every non-key column must depend only on the primary key, not on some other non-key column. The flat-reviews-table example from the start of this lesson is exactly a 3NF violation - in that table, user_email doesn't depend on the review's own id at all, it depends on which user wrote the review. Whenever a column's value is really a fact about something else referenced in the row (the user, in this case) rather than a fact about the row itself, it's a sign that column belongs on a different table, related back by a foreign key instead of copied in directly.

How the Normal Forms Build on Each Other

FormRequiresFixes
1NFEvery column holds one atomic valueComma-separated lists crammed into a single column
2NF1NF, plus every non-key column depends on the whole composite keyA column on a junction table that only depends on part of the key
3NF2NF, plus every non-key column depends only on the key, not another non-key columnA column that's really a fact about something else the row references

Each form assumes the one before it already holds - a table can't meaningfully satisfy 3NF without first satisfying 2NF and 1NF. In practice, designing tables around real-world entities the way StreamHub does - a users table for user facts, a movies table for movie facts, a reviews table for the review itself - tends to land you in 3NF almost automatically, without having to consciously check each rule one at a time.

Denormalization - A Deliberate, Practical Tradeoff

None of this means duplication is always wrong. Denormalization is the deliberate choice to duplicate some data anyway, usually for performance reasons in read-heavy systems. A realistic example: adding a cached avg_rating column directly on movies, kept up to date by a trigger whenever a review is inserted or changed - the same idea comes back in more detail in Triggers. This trades a small amount of duplicated, derived data for the ability to read a movie's rating instantly without recalculating an average across potentially thousands of reviews on every single page load.

Normalization is a genuine tradeoff between data integrity and query simplicity or performance - not an absolute rule to chase to its extreme in every situation. Most applications, including StreamHub, benefit from being properly normalized by default, and then denormalizing specific, measured hot spots only once there's a real performance reason to.

Next up: With the design principles behind good table structure covered, next we look at Indexes - how MySQL finds rows quickly once your tables are actually holding real, growing amounts of data.