DEV Community

Yasser Elgammal
Yasser Elgammal

Posted on Edited on

Database Normalization Explained: From 1NF to 3NF with a Simple Example

Normalization is the process of organizing data in a database to reduce redundancy, avoid data anomalies, and maintain data integrity.

Instead of keeping all related data in one large table, we divide the data into smaller, well-structured tables.

Each table should represent a specific entity or relationship, and foreign keys are used to connect these tables.

Let's understand normalization step by step using a simple example of users purchasing courses.


Before Normalization

Suppose we initially store users and their purchased courses like this:

user_id name email postal_code city course_ids course_names course_prices
1 Omar Hassan omar.hassan@example.com 34511 Damietta 1, 3 Intro to Python, Data Science 99.99, 199.99
2 Yusuf Ahmed yusuf.ahmed@example.com 35511 Mansoura 2, 1 Web Development, Intro to Python 149.99, 99.99
3 Ibrahim Khalid ibrahim.khalid@example.com 31512 Tanta 3, 2 Data Science, Web Development 199.99, 149.99

At first, this may look convenient.

However, there is an important problem.

Some columns contain multiple values inside a single cell.

For example:

course_ids = 1, 3
Enter fullscreen mode Exit fullscreen mode

and:

course_names = Intro to Python, Data Science
Enter fullscreen mode Exit fullscreen mode

A single database field should not contain a list of separate values.

This means our current table does not satisfy First Normal Form.


1. First Normal Form (1NF)

A table is in First Normal Form when:

  • Each column contains atomic values.
  • Each cell contains only one value.
  • There are no repeating groups.
  • Each row represents a single record.
  • Each row can be uniquely identified.

The main problem in our original table is that course information is stored as multiple values inside individual cells.

To fix this, we create a separate row for every user-course combination.

Table After 1NF

user_id name email postal_code city course_id course_name course_price purchase_price
1 Omar Hassan omar.hassan@example.com 34511 Damietta 1 Intro to Python 99.99 99.99
1 Omar Hassan omar.hassan@example.com 34511 Damietta 3 Data Science 199.99 199.99
2 Yusuf Ahmed yusuf.ahmed@example.com 35511 Mansoura 2 Web Development 149.99 149.99
2 Yusuf Ahmed yusuf.ahmed@example.com 35511 Mansoura 1 Intro to Python 99.99 99.99
3 Ibrahim Khalid ibrahim.khalid@example.com 31512 Tanta 3 Data Science 199.99 199.99
3 Ibrahim Khalid ibrahim.khalid@example.com 31512 Tanta 2 Web Development 149.99 149.99

Now every cell contains a single value.

For example:

course_id = 1
Enter fullscreen mode Exit fullscreen mode

instead of:

course_ids = 1, 3
Enter fullscreen mode Exit fullscreen mode

For this simplified example, we assume that a user purchases a specific course only once.

Therefore, we can use the following composite key to uniquely identify each row:

(user_id, course_id)
Enter fullscreen mode Exit fullscreen mode

The table now satisfies 1NF.

However, we still have a lot of duplicated data.

For example, Omar's name, email, postal code, and city appear once for every course he purchases.

Course information is also repeated every time another user purchases the same course.

This leads us to Second Normal Form.


2. Second Normal Form (2NF)

A table must first satisfy 1NF before it can satisfy 2NF.

2NF deals with partial dependencies.

When a table has a composite key, every non-key column should depend on the entire key, not only part of it.

Our composite key is:

(user_id, course_id)
Enter fullscreen mode Exit fullscreen mode

Now let's examine the dependencies.

User information:

user_id → name, email, postal_code, city
Enter fullscreen mode Exit fullscreen mode

These columns depend only on user_id, not on the entire composite key.

Course information:

course_id → course_name, course_price
Enter fullscreen mode Exit fullscreen mode

These columns depend only on course_id.

The purchase itself depends on the relationship between the user and course.

So we should separate these responsibilities into different tables.


users

id name email postal_code city
1 Omar Hassan omar.hassan@example.com 34511 Damietta
2 Yusuf Ahmed yusuf.ahmed@example.com 35511 Mansoura
3 Ibrahim Khalid ibrahim.khalid@example.com 31512 Tanta

User information is now stored only once.


courses

id name price
1 Intro to Python 99.99
2 Web Development 149.99
3 Data Science 199.99

Course information is also stored only once.


purchases

id user_id course_id purchase_price
1 1 1 99.99
2 1 3 199.99
3 2 2 149.99
4 2 1 99.99
5 3 3 199.99
6 3 2 149.99

The purchases table now represents the relationship between users and courses.

At this point:

  • User information belongs to users.
  • Course information belongs to courses.
  • Purchase information belongs to purchases.

Why Store purchase_price?

You may notice that we have:

courses.price
Enter fullscreen mode Exit fullscreen mode

and:

purchases.purchase_price
Enter fullscreen mode Exit fullscreen mode

This is intentional.

They represent two different pieces of information.

courses.price represents the current price of the course.

purchases.purchase_price represents the price paid by the user at the time of purchase.

For example, suppose:

Intro to Python = 99.99
Enter fullscreen mode Exit fullscreen mode

Omar purchases the course for:

99.99
Enter fullscreen mode Exit fullscreen mode

Later, the course price changes to:

129.99
Enter fullscreen mode Exit fullscreen mode

The current course record becomes:

courses.price = 129.99
Enter fullscreen mode Exit fullscreen mode

But Omar's old purchase should still contain:

purchases.purchase_price = 99.99
Enter fullscreen mode Exit fullscreen mode

Historical transaction data should not change just because the current product price changes.


3. Third Normal Form (3NF)

A table must first satisfy 2NF before it can satisfy 3NF.

3NF deals with transitive dependencies.

In simple terms:

A non-key column should not depend on another non-key column.

Consider our current users table:

id name email postal_code city
1 Omar Hassan omar.hassan@example.com 34511 Damietta
2 Yusuf Ahmed yusuf.ahmed@example.com 35511 Mansoura
3 Ibrahim Khalid ibrahim.khalid@example.com 31512 Tanta

For this example, we assume that every postal code identifies one city.

Therefore:

postal_code → city
Enter fullscreen mode Exit fullscreen mode

And we also have:

user_id → postal_code
Enter fullscreen mode Exit fullscreen mode

This gives us:

user_id → postal_code → city
Enter fullscreen mode Exit fullscreen mode

The city value indirectly depends on the user through postal_code.

This is a transitive dependency.

We can move this information into a separate table.


cities

id postal_code name
1 34511 Damietta
2 35511 Mansoura
3 31512 Tanta

Now the users table only needs to reference the appropriate city.


users After 3NF

id name email city_id
1 Omar Hassan omar.hassan@example.com 1
2 Yusuf Ahmed yusuf.ahmed@example.com 2
3 Ibrahim Khalid ibrahim.khalid@example.com 3

Now city-related information is stored in one place.

Instead of repeating:

34511 | Damietta
Enter fullscreen mode Exit fullscreen mode

for every user from Damietta, the user simply references:

city_id = 1
Enter fullscreen mode Exit fullscreen mode

Final Schema

After applying 1NF, 2NF, and 3NF, our final database contains four main tables.

users

users
-----
id
name
email
city_id
Enter fullscreen mode Exit fullscreen mode

cities

cities
------
id
postal_code
name
Enter fullscreen mode Exit fullscreen mode

courses

courses
-------
id
name
price
Enter fullscreen mode Exit fullscreen mode

purchases

purchases
---------
id
user_id
course_id
purchase_price
Enter fullscreen mode Exit fullscreen mode

Foreign Keys

The relationships between the tables are created using foreign keys.

users.city_id → cities.id

purchases.user_id → users.id

purchases.course_id → courses.id
Enter fullscreen mode Exit fullscreen mode

These foreign keys help maintain referential integrity.

For example, a purchase should not reference a user that does not exist.


Relationships

cities → users

cities
  |
  | 1
  |
  | *
users
Enter fullscreen mode Exit fullscreen mode

One city can have many users.


users → purchases

users
  |
  | 1
  |
  | *
purchases
Enter fullscreen mode Exit fullscreen mode

One user can have many purchases.


courses → purchases

courses
  |
  | 1
  |
  | *
purchases
Enter fullscreen mode Exit fullscreen mode

One course can appear in many purchases.


Complete Relationship

cities
  |
  | 1
  |
  | *
users
  |
  | 1
  |
  | *
purchases
  | *
  |
  | 1
courses
Enter fullscreen mode Exit fullscreen mode

We can also think of the purchases table as the bridge between users and courses:

users
  |
  | 1
  |
  | *
purchases
  | *
  |
  | 1
courses
Enter fullscreen mode Exit fullscreen mode

This means users and courses effectively have a many-to-many relationship through purchases.


Normalization Flow

The complete process looks like this:

Unnormalized Data
Multiple values inside cells
        |
        v
       1NF
One atomic value per cell
One user-course combination per row
        |
        v
       2NF
Remove partial dependencies
        |
        +---- users
        |
        +---- courses
        |
        +---- purchases
        |
        v
       3NF
Remove transitive dependencies
        |
        +---- cities
        |
        v
Final Normalized Schema
Enter fullscreen mode Exit fullscreen mode

What Did Each Normal Form Solve?

Before 1NF

We had multiple values inside a single cell:

course_ids = 1, 3
Enter fullscreen mode Exit fullscreen mode

This makes querying, updating, and maintaining the data difficult.


After 1NF

We converted those values into separate rows:

user_id | course_id
1       | 1
1       | 3
Enter fullscreen mode Exit fullscreen mode

Now every field contains one atomic value.


After 2NF

We removed partial dependencies.

Instead of repeating:

Omar Hassan
omar.hassan@example.com
Enter fullscreen mode Exit fullscreen mode

for every course Omar purchases, we store that information once in:

users
Enter fullscreen mode Exit fullscreen mode

Similarly, course information is stored once in:

courses
Enter fullscreen mode Exit fullscreen mode

And the relationship is stored in:

purchases
Enter fullscreen mode Exit fullscreen mode

After 3NF

We removed the transitive dependency:

user_id → postal_code → city
Enter fullscreen mode Exit fullscreen mode

City-related information is now stored separately in:

cities
Enter fullscreen mode Exit fullscreen mode

and users reference it using:

city_id
Enter fullscreen mode Exit fullscreen mode

Why Normalization Matters

Without normalization, databases can suffer from several common problems.

Update Anomaly

Suppose Data Science is stored in 100 purchase rows.

If its name changes, we may need to update 100 rows.

If one row is missed, the database becomes inconsistent.

With normalization, the course name exists once in courses.


Insert Anomaly

Suppose we want to add a new course before anyone purchases it.

If course information exists only inside a purchase table, we may not be able to store the course until someone buys it.

With a separate courses table, we can create the course independently.


Delete Anomaly

Suppose the last user who purchased a course deletes their purchase.

If course information is stored only in that purchase row, deleting the purchase may accidentally delete all information about the course.

With normalization, deleting a purchase does not delete the course itself.


Summary

Normalization helps structure database data in a way that reduces duplication and improves consistency.

In our example:

  • 1NF removed multiple values from individual cells.
  • 2NF removed partial dependencies by separating users, courses, and purchases.
  • 3NF removed the transitive dependency between postal codes and cities.

Our final schema contains:

users
cities
courses
purchases
Enter fullscreen mode Exit fullscreen mode

Each table has a clear responsibility:

  • users stores user information.
  • cities stores city and postal code information.
  • courses stores course information.
  • purchases stores user-course transactions.

The goal of normalization is not simply to create more tables.

The goal is to make sure each piece of information is stored in the correct place, reducing redundancy and preventing update, insert, and delete anomalies.

Top comments (0)