diff --git a/src/content/docs/guides/_map.json b/src/content/docs/guides/_map.json
index 8c67c0f9..aa69f6b8 100644
--- a/src/content/docs/guides/_map.json
+++ b/src/content/docs/guides/_map.json
@@ -22,5 +22,12 @@
["mysql-local-setup", "Local setup of MySQL"],
["seeding-with-partially-exposed-tables", "Seeding Partially Exposed Tables with Foreign Key"],
["seeding-using-with-option", "Seeding using 'with' option"],
- ["full-text-search-with-generated-columns", "Full-text search with Generated Columns"]
+ ["full-text-search-with-generated-columns", "Full-text search with Generated Columns"],
+ ["first-normal-form", "First Normal Form (1NF)"],
+ ["second-normal-form", "Second Normal Form (2NF)"],
+ ["third-normal-form", "Third Normal Form (3NF)"],
+ ["boyce-codd-normal-form", "Boyce-Codd Normal Form (BCNF)"],
+ ["elementary-key-normal-form", "Elementary Key Normal Form (EKNF)"],
+ ["fourth-normal-form", "Fourth Normal Form (4NF)"],
+ ["fifth-normal-form", "Fifth Normal Form (5NF)"]
]
diff --git a/src/content/docs/guides/boyce-codd-normal-form.mdx b/src/content/docs/guides/boyce-codd-normal-form.mdx
new file mode 100644
index 00000000..013ab05a
--- /dev/null
+++ b/src/content/docs/guides/boyce-codd-normal-form.mdx
@@ -0,0 +1,201 @@
+---
+title: Boyce-Codd Normal Form (BCNF)
+slug: boyce-codd-normal-form
+---
+
+import Section from "@mdx/Section.astro";
+import CodeTabs from '@mdx/CodeTabs.astro';
+import CodeTab from '@mdx/CodeTab.astro';
+import Prerequisites from "@mdx/Prerequisites.astro";
+
+
+- You should be familiar with [3NF](/docs/guides/third-normal-form)
+- You should be familiar with [EKNF](/docs/guides/elementary-key-normal-form)
+
+
+**Boyce-Codd Normal Form (BCNF)** is a stricter form of normalization than **Third Normal Form**. While **3NF** focuses on the relationship between non-prime attributes and candidate keys, **BCNF** goes a step further by addressing potential redundancies even when **3NF** is achieved. The goal is to eliminate all redundancy that can be detected through functional dependencies.
+
+In practice, you barely see tables in **3NF** that violate **BCNF**. However, it's important to understand the concept of **BCNF**.
+
+## The BCNF Rule
+
+To achieve **BCNF**, table should already be in **3NF** and must satisfy the following condition:
+
+For every non-trivial functional dependency X -> Y, the determinant (X) must be a super key. In simpler terms, the left-hand side of any functional dependency must uniquely determine the entire row in the table.
+
+## Example
+
+We have a table `room_reservations` with the following schema:
+
+
+
+ ```ts
+ import { integer, pgTable, primaryKey, unique, varchar } from "drizzle-orm/pg-core";
+
+ export const roomReservations = pgTable("room_reservations", {
+ room: integer("room").notNull(),
+ startTime: varchar("start_time", { length: 5 }).notNull(),
+ endTime: varchar("end_time", { length: 5 }).notNull(),
+ rateType: varchar("rate_type", { length: 20 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.room, t.startTime] }),
+ unique().on(t.room, t.endTime),
+ unique().on(t.rateType, t.startTime),
+ unique().on(t.rateType, t.endTime),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "room_reservations" (
+ "room" integer NOT NULL,
+ "start_time" varchar(5) NOT NULL,
+ "end_time" varchar(5) NOT NULL,
+ "rate_type" varchar(20) NOT NULL,
+ CONSTRAINT "room_reservations_room_start_time_pk" PRIMARY KEY("room","start_time"),
+ CONSTRAINT "room_reservations_room_end_time_unique" UNIQUE("room","end_time"),
+ CONSTRAINT "room_reservations_rate_type_start_time_unique" UNIQUE("rate_type","start_time"),
+ CONSTRAINT "room_reservations_rate_type_end_time_unique" UNIQUE("rate_type","end_time")
+ );
+ ```
+
+
+ ```plaintext
+ +------------------------------------------------+
+ | room_reservations |
+ +------------+------------+----------+-----------+
+ | room_number| start_time | end_time | rate_type |
+ +------------+------------+----------+-----------+
+ | 101 | 08:00 | 09:00 | SAVER |
+ | 101 | 09:30 | 10:30 | SAVER |
+ | 101 | 12:00 | 13:30 | STANDARD |
+ | 101 | 14:00 | 15:30 | STANDARD |
+ | 201 | 10:00 | 11:30 | DELUXE-B |
+ | 201 | 11:30 | 13:30 | DELUXE-B |
+ | 201 | 15:00 | 16:30 | DELUXE-A |
+ +------------+------------+----------+-----------+
+ ```
+
+
+
+### Business rules
+
+1. Each row in this table represents a hotel room reservation. For simplicity, assume the hotel has two rooms: `Room 101` (a standard room) and `Room 201` (a deluxe suite).
+2. A booking is defined by the `room` and the time period (`start_time` to `end_time`) for which that room is reserved. No room can have overlapping bookings (only one reservation per room at a given time).
+3. Additionally, each booking has a `rate_type`. There are four distinct rate types in this example, each implying a specific guest status (membership level):
+- `SAVER` - for `Room 101` bookings made by loyalty members (member discount rate)
+- `STANDARD` - for `Room 101` bookings made by non-members (regular rate)
+- `DELUXE-A` - for `Room 201` bookings made by members (premium room, member rate)
+- `DELUXE-B` - for `Room 201` bookings made by non-members (premium room, standard/non-member rate)
+
+### Functional Dependencies
+
+1. `room, start_time -> end_time, rate_type`
+2. `room, end_time -> start_time, rate_type`
+3. `rate_type, start_time -> room, end_time`
+4. `rate_type, end_time -> room, start_time`
+5. `rate_type -> room`. Each rate type is associated with exactly one room. From the business rules, we see that a given rate code implies a specific room (e.g. any `SAVER` rate booking is always for `Room 101`, and any `DELUXE-B` booking is always for `Room 201`). Formally, this means rate type functionally determines room.
+
+### Candidate keys
+
+The candidate keys for this table are:
+1. `room, start_time`
+2. `room, end_time`
+3. `rate_type, start_time`
+4. `rate_type, end_time`
+
+Even though in the sample data each `start_time` and `end_time` happens to be unique across all bookings, we cannot treat `start_time` or `end_time` alone as a key because on another day two different rooms could have bookings that begin or end at the same time. That is why a combination of attributes is needed to uniquely identify a booking.
+
+### 3NF Analysis
+
+Since every attribute in this table is part of at least one candidate key, there are no non-prime attributes. This means no transitive dependencies exist, so the table is in **3NF**.
+
+### BCNF Analysis
+
+The relation is not in **BCNF** because of this functional dependency:
+
+`rate_type -> room`
+
+The left-hand side (`rate_type`) is not a super key. Both `rate_type` and `room` are prime attributes because they are part of candidate keys. This dependency violates the **BCNF** rule.
+
+This leads to:
+1. Information duplication.
+2. Update anomalies. If the hotel changes which room a given rate code applies to, multiple rows would need updating.
+3. Deletion anomalies. If a `DELUXE-A` booking is deleted, the information about which room that rate code applies to would be lost.
+
+### BCNF Decomposition
+
+We need to split the table to eliminate the problematic dependency. We create two tables:
+
+
+
+ ```ts
+ import { boolean, foreignKey, integer, pgTable, primaryKey, unique, varchar } from "drizzle-orm/pg-core";
+
+ export const rateTypes = pgTable("rate_types", {
+ rateType: varchar("rate_type", { length: 20 }).notNull().primaryKey(),
+ room: integer("room").notNull(),
+ memberFlag: boolean("member_flag").notNull(),
+ }, (t) => [
+ unique().on(t.room, t.memberFlag),
+ ]);
+
+ export const reservations = pgTable("reservations", {
+ room: integer("room").notNull(),
+ startTime: varchar("start_time", { length: 5 }).notNull(),
+ endTime: varchar("end_time", { length: 5 }).notNull(),
+ memberFlag: boolean("member_flag").notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.room, t.startTime] }),
+ foreignKey({
+ columns: [t.room, t.memberFlag],
+ foreignColumns: [rateTypes.room, rateTypes.memberFlag],
+ }).onDelete("cascade").onUpdate("cascade"),
+ unique().on(t.room, t.endTime),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "rate_types" (
+ "rate_type" varchar(20) PRIMARY KEY NOT NULL,
+ "room" integer NOT NULL,
+ "member_flag" boolean NOT NULL,
+ CONSTRAINT "rate_types_room_member_flag_unique" UNIQUE("room","member_flag")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "reservations" (
+ "room" integer NOT NULL,
+ "start_time" varchar(5) NOT NULL,
+ "end_time" varchar(5) NOT NULL,
+ "member_flag" boolean NOT NULL,
+ CONSTRAINT "reservations_room_start_time_pk" PRIMARY KEY("room","start_time"),
+ CONSTRAINT "reservations_room_end_time_unique" UNIQUE("room","end_time")
+ );
+ --> statement-breakpoint
+ ALTER TABLE "reservations" ADD CONSTRAINT "reservations_room_member_flag_rate_types_room_member_flag_fk" FOREIGN KEY ("room","member_flag") REFERENCES "public"."rate_types"("room","member_flag") ON DELETE cascade ON UPDATE cascade;
+ ```
+
+
+ ```plaintext
+ +-----------------------------------+ +---------------------------------------------+
+ | rate_types | | reservations |
+ +-------------+------+--------------+ +------+------------+----------+--------------+
+ | rate_type | room | member_flag | | room | start_time | end_time | member_flag |
+ +-------------+------+--------------+ +------+------------+----------+--------------+
+ | SAVER | 101 | true | | 101 | 08:00 | 09:00 | true |
+ | STANDARD | 101 | false | | 101 | 09:30 | 10:30 | true |
+ | DELUXE-A | 201 | true | | 101 | 12:00 | 13:30 | false |
+ | DELUXE-B | 201 | false | | 101 | 14:00 | 15:30 | false |
+ +-------------+------+--------------+ | 201 | 10:00 | 11:30 | false |
+ | 201 | 11:30 | 13:30 | false |
+ | 201 | 15:00 | 16:30 | true |
+ +------+------------+----------+--------------+
+ ```
+
+
+
+The dependency `rate_type -> room` is fully enforced in this table (and is no longer a problem, because `rate_type` is a candidate key here). The table also has another candidate key `room, member_flag`, since each combination of room and membership status determines a unique rate type.
+`rate_type` is no longer stored in `reservations` table, so the redundancy is gone. Instead, the combination of `room` and `member_flag` for a reservation can be used to lookup the `rate_type` from the `rate_types` table when needed.
+
+Both tables are now in **BCNF**. For every functional dependency in each table, the left-hand side is a super key.
diff --git a/src/content/docs/guides/elementary-key-normal-form.mdx b/src/content/docs/guides/elementary-key-normal-form.mdx
new file mode 100644
index 00000000..bdc00942
--- /dev/null
+++ b/src/content/docs/guides/elementary-key-normal-form.mdx
@@ -0,0 +1,267 @@
+---
+title: Elementary Key Normal Form (EKNF)
+slug: elementary-key-normal-form
+---
+
+import CodeTabs from '@mdx/CodeTabs.astro';
+import CodeTab from '@mdx/CodeTab.astro';
+import Prerequisites from "@mdx/Prerequisites.astro";
+
+
+- You should be familiar with [3NF](/docs/guides/third-normal-form)
+- You should be familiar with [BCNF](/docs/guides/boyce-codd-normal-form)
+
+
+**Elementary key normal form (EKNF)** is a subtle enhancement on **Third Normal Form**. It aims to eliminate certain types of redundancy and update anomalies that can still exist in **3NF** when a table has multiple overlapping candidate keys. **EKNF** is stricter than **3NF**, but more flexible than **BCNF**. It is useful in situations where enforcing **BCNF** would lead to the loss of some original dependencies that are important for the business logic of the application.
+
+## Key concepts
+
+1. Multiple overlapping candidate keys means that a table has more than one candidate key, and those keys share some of the same attributes.
+2. Elementary attribute is an attribute that is part of a candidate key but not a whole candidate key on its own.
+
+## The EKNF Rule
+
+To achieve **EKNF**, table should already be in **3NF** and must satisfy the following condition:
+
+Every non-trivial functional dependency X → A in the relation satisfies at least one of the following:
+
+- `X` is a candidate key.
+- `A` is an elementary attribute of a candidate key.
+
+## Example
+
+When a table has already achieved **3NF**, the next step is often to normalize it to **BCNF** to eliminate remaining anomalies.
+However, decomposing to **BCNF** could discard some original functional dependencies, potentially making it impossible to enforce certain business rules.
+
+For example, we have a table `classroom_assignment` with the following schema:
+
+
+
+ ```ts
+ import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const classroomAssignment = pgTable("classroom_assignment", {
+ professor: varchar("professor", { length: 100 }).notNull(),
+ course: varchar("course", { length: 100 }).notNull(),
+ room: varchar("room", { length: 10 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.professor, t.course] }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "classroom_assignment" (
+ "professor" varchar(100) NOT NULL,
+ "course" varchar(100) NOT NULL,
+ "room" varchar(10) NOT NULL,
+ CONSTRAINT "classroom_assignment_professor_course_pk" PRIMARY KEY("professor","course")
+ );
+ ```
+
+
+ ```plaintext
+ +----------------------------------------------+
+ | classroom_assignment |
+ +--------------+-------------------+-----------+
+ | professor | course | room |
+ +--------------+-------------------+-----------+
+ | Prof. Smith | Cybersecurity | Room 101 |
+ | Prof. Smith | Web Development | Room 102 |
+ | Prof. Brown | Database Systems | Room 103 |
+ | Prof. Brown | Web Development | Room 104 |
+ | Prof. Brown | Cybersecurity | Room 101 |
+ +--------------+-------------------+-----------+
+ ```
+
+
+
+### Business rules
+
+1. Each room is specialized to a particular course.
+2. Professors can teach multiple courses.
+3. A professor can be assigned to multiple rooms for different courses.
+
+### Functional Dependencies
+
+1. `professor, course -> room`. A given professor teaching a given course is assigned to a specific room.
+2. `room -> course`. Each room is dedicated to a single course. So knowing the room alone is enough to know the course.
+
+### Candidate keys
+
+The table has two candidate keys: `professor, course` and `professor, room`. Each of these can uniquely determine all attributes in the table. These keys overlap on the `professor` attribute.
+
+### 3NF Analysis
+
+Since every attribute in this table is part of at least one candidate key, there are no non-prime attributes. This means no transitive dependencies exist, so the table is in **3NF**.
+
+### BCNF Analysis
+
+The relation is not in **BCNF** because of this functional dependency:
+
+`room -> course`
+
+The left-hand side (`room`) is not a super key. Both `room` and `course` are prime attributes because they are part of candidate keys. This dependency violates the **BCNF** rule.
+
+This leads to:
+1. Information duplication.
+2. Update anomalies. For example, suppose someone mistakenly updates one of `Prof. Brown` records to assign a different course to `Room 101` (`Prof. Brown - Operating Systems - Room 101`). Now `Room 101` appears in two rows: one with course `Cybersecurity` (for `Prof. Smith`) and one with `Operating Systems` (for `Prof. Brown`). This contradicts the rule that `Room 101` should have only one course.
+3. Delete anomalies. If we delete the row `Prof. Smith - Web Development - Room 102`, we lose information that `Room 102` is assigned to `Web Development`. If we later want to assign `Room 102` to another professor, we won't know which course it is dedicated to.
+
+### BCNF Decomposition
+
+We need to split the table to eliminate the problematic dependency. We create two tables:
+
+
+
+ ```ts
+ import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const rooms = pgTable("rooms", {
+ room: varchar("room", { length: 10 }).notNull().primaryKey(),
+ course: varchar("course", { length: 100 }).notNull(),
+});
+
+ export const professorRooms = pgTable("professor_rooms", {
+ professor: varchar("professor", { length: 100 }).notNull(),
+ room: varchar("room", { length: 10 }).notNull().references(() => rooms.room, { onDelete: 'cascade', onUpdate: 'cascade' }),
+ }, (t) => [
+ primaryKey({ columns: [t.professor, t.room] })
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "professor_rooms" (
+ "professor" varchar(100) NOT NULL,
+ "room" varchar(10) NOT NULL,
+ CONSTRAINT "professor_rooms_professor_room_pk" PRIMARY KEY("professor","room")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "rooms" (
+ "room" varchar(10) PRIMARY KEY NOT NULL,
+ "course" varchar(100) NOT NULL
+ );
+ --> statement-breakpoint
+ ALTER TABLE "professor_rooms" ADD CONSTRAINT "professor_rooms_room_rooms_room_fk" FOREIGN KEY ("room") REFERENCES "public"."rooms"("room") ON DELETE cascade ON UPDATE cascade;
+ ```
+
+
+ ```plaintext
+ +------------------------------+ +--------------------------+
+ | rooms | | professor_rooms |
+ +-----------+------------------+ +--------------+-----------+
+ | room | course | | professor | room |
+ +-----------+------------------+ +--------------+-----------+
+ | Room 101 | Cybersecurity | | Prof. Smith | Room 101 |
+ | Room 102 | Web Development | | Prof. Smith | Room 102 |
+ | Room 103 | Database Systems | | Prof. Brown | Room 101 |
+ | Room 104 | Web Development | | Prof. Brown | Room 103 |
+ +-----------+------------------+ | Prof. Brown | Room 104 |
+ +--------------+-----------+
+ ```
+
+
+
+We have achieved **BCNF** by splitting the table into two tables: `rooms` and `professor_rooms`. Now, the tables are both in **BCNF**. But does this **BCNF** design preserve our original FDs?
+
+### Dependency Preservation Check
+
+1. `room -> course` is preserved directly in the `rooms` table.
+2. `professor, course -> room`. In the original table, `professor, course` was a candidate key, so it always mapped to a single room and as it was a primary key, it prevents professor teaching the same course twice.
+
+In the new design, we can still derive that relationship by joining the two tables, but it is not enforced as a single functional dependency in either table. In fact, a professor could be assigned to the same course in two different rooms without any single table catching it.
+
+In the `professor_rooms` table, the combination `professor, room` is unique, but nothing stops a professor from occupying two different rooms. Meanwhile, the `rooms` table allows the same course in multiple rooms (since its primary key is `room`, not `course`). Together, this means a professor could have two entries in `professor_rooms` (two rooms) that, via the `rooms` table, both correspond to the same course. So, we lost the original functional dependency.
+
+In summary, the fully normalized **BCNF** design eliminates the room/course redundancy but fails to preserve the `professor, course -> room` dependency, allowing a new kind of inconsistency.
+
+In such situations, we can use **EKNF**. It allows overlapping candidate keys and preserves all functional dependencies, while still avoiding anomalies, at the cost of not being completely **BCNF**.
+
+## EKNF Decomposition
+
+The idea is to allow a certain dependency (like `room -> course`) to remain, but control it through constraints and a schema design rather than pure decomposition. In our example, we want to maintain both rules:
+
+1. `room -> course`
+2. `professor, course -> room`
+
+The **EKNF** design will look like a hybrid of the original and the **BCNF** approach:
+
+- Keep a relation that is like the original table (to directly enforce `professor, course -> room` via a candidate key).
+- Also include a separate relation for the `room -> course` mapping (to have a single source-of-truth for each room's course).
+
+
+
+ ```ts
+ import { foreignKey, pgTable, primaryKey, unique, varchar } from "drizzle-orm/pg-core";
+
+ export const rooms = pgTable("rooms", {
+ room: varchar("room", { length: 10 }).notNull().primaryKey(),
+ course: varchar("course", { length: 100 }).notNull(),
+ }, (t) => [
+ unique().on(t.room, t.course), // We need this unique constraint to set composite foreign key in classroom_assignment table
+ ]);
+
+ export const classroomAssignment = pgTable("classroom_assignment", {
+ professor: varchar("professor", { length: 100 }).notNull(),
+ course: varchar("course", { length: 100 }).notNull(),
+ room: varchar("room", { length: 10 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.professor, t.course] }),
+ foreignKey({
+ columns: [t.room, t.course],
+ foreignColumns: [rooms.room, rooms.course],
+
+ }).onDelete('cascade').onUpdate('cascade'),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "classroom_assignment" (
+ "professor" varchar(100) NOT NULL,
+ "course" varchar(100) NOT NULL,
+ "room" varchar(10) NOT NULL,
+ CONSTRAINT "classroom_assignment_professor_course_pk" PRIMARY KEY("professor","course")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "rooms" (
+ "room" varchar(10) PRIMARY KEY NOT NULL,
+ "course" varchar(100) NOT NULL,
+ CONSTRAINT "rooms_room_course_unique" UNIQUE("room","course")
+ );
+ --> statement-breakpoint
+ ALTER TABLE "classroom_assignment" ADD CONSTRAINT "classroom_assignment_room_course_rooms_room_course_fk" FOREIGN KEY ("room","course") REFERENCES "public"."rooms"("room","course") ON DELETE cascade ON UPDATE cascade;
+ ```
+
+
+ ```plaintext
+ +------------------------------+
+ | rooms |
+ +-----------+------------------+
+ | room | course |
+ +-----------+------------------+
+ | Room 101 | Cybersecurity |
+ | Room 102 | Web Development |
+ | Room 103 | Database Systems |
+ | Room 104 | Web Development |
+ +-----------+------------------+
+
+ +----------------------------------------------+
+ | classroom_assignment |
+ +--------------+-------------------+-----------+
+ | professor | course | room |
+ +--------------+-------------------+-----------+
+ | Prof. Smith | Cybersecurity | Room 101 |
+ | Prof. Smith | Web Development | Room 102 |
+ | Prof. Brown | Database Systems | Room 103 |
+ | Prof. Brown | Web Development | Room 104 |
+ | Prof. Brown | Cybersecurity | Room 101 |
+ +--------------+-------------------+-----------+
+ ```
+
+
+
+Crucially, we add a referential integrity constraint. The pair `room, course` in `classroom_assignment` must match an entry in the `rooms` table. So, this **EKNF** design preserves both original FDs as enforceable constraints while avoiding anomalies.
+
+It's worth noting that **EKNF** is not commonly listed in practical normalization steps (**BCNF** is usually the next step after **3NF**). However, this example demonstrates its value for cases where **BCNF** is not dependency-preserving, but we need to maintain certain dependencies for business logic.
diff --git a/src/content/docs/guides/fifth-normal-form.mdx b/src/content/docs/guides/fifth-normal-form.mdx
new file mode 100644
index 00000000..e44ef7fd
--- /dev/null
+++ b/src/content/docs/guides/fifth-normal-form.mdx
@@ -0,0 +1,242 @@
+---
+title: Fifth Normal Form (5NF)
+slug: fifth-normal-form
+---
+
+import Section from "@mdx/Section.astro";
+import CodeTabs from '@mdx/CodeTabs.astro';
+import CodeTab from '@mdx/CodeTab.astro';
+import Prerequisites from "@mdx/Prerequisites.astro";
+import Callout from '@mdx/Callout.astro';
+
+
+- You should be familiar with [4NF](/docs/guides/fourth-normal-form)
+
+
+**The Fifth Normal Form (5NF)**, also known as **Project-Join Normal Form (PJ/NF)** is a level of database normalization where a table is already in **4NF**, and every non-trivial join dependency in that table is implied by the candidate keys. The goal is to eliminate redundancy caused by join dependencies and to ensure that relations are decomposed into smaller components without any loss of data.
+
+In practice, **5NF** is rarely used in database design.
+
+## Key concepts
+
+1. A `Join Dependency` (JD) is a constraint that specifies that table `R` can be split into several smaller tables `R1, R2,..., Rk` and by performing a natural join on these tables, the original table `R` can be reconstructed without any loss of information and no false information (spurious rows) is created during the process.
+2. A `Lossless/Non-Loss Decomposition` is a decomposition when all the sub-relations do the natural join and the obtained table is equal to the original table.
+3. A `Natural Join` is a join operation that is used to combine two relations based on all common attributes.
+4. A JD denoted as `*(R1, R2, ..., Rk)` on a relation `R` is considered trivial if at least one of the components `Ri` (where `i` is between `1` and `k`) is equal to the set of all attributes of the original relation `R`. In simple terms: A join dependency is trivial if one of the parts you're joining already is the whole original table.
+
+## Example
+
+We have a table `agent_inventory` with the following schema:
+
+
+
+ ```ts
+ import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const agentInventory = pgTable("agent_inventory", {
+ agent: varchar("agent", { length: 255 }).notNull(),
+ supplier: varchar("supplier", { length: 255 }).notNull(),
+ itemType: varchar("item_type", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.agent, t.supplier, t.itemType] }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "agent_inventory" (
+ "agent" varchar(255) NOT NULL,
+ "supplier" varchar(255) NOT NULL,
+ "item_type" varchar(255) NOT NULL,
+ CONSTRAINT "agent_inventory_agent_supplier_item_type_pk" PRIMARY KEY("agent","supplier","item_type")
+ );
+ ```
+
+
+ ```plaintext
+ +------------------------------------------+
+ | agent_inventory |
+ +-------------+-------------+--------------+
+ | agent | supplier | item_type |
+ +-------------+-------------+--------------+
+ | Anna Bell | GearUp | Scanner |
+ | Anna Bell | Innovate | Scanner |
+ | Anna Bell | Innovate | Projector |
+ | Chris Day | Innovate | Projector |
+ | Chris Day | ProServe | Projector |
+ | Chris Day | ProServe | Webcam |
+ | Chris Day | ProServe | Dock |
+ | Helen Fox | GearUp | Scanner |
+ | Helen Fox | GearUp | Webcam |
+ | Helen Fox | ProServe | Webcam |
+ | Helen Fox | ProServe | Dock |
+ +-------------+-------------+--------------+
+ ```
+
+
+
+1. An `Agent` can be authorized to source products from multiple `Suppliers`.
+2. An `Agent` can be authorized to offer multiple `Item types`.
+3. A `Supplier` can manufacture multiple `Item types`.
+4. The core business constraint: An `Agent` must offer a specific `Item type` from a specific `Supplier` if and only if the following three conditions are all true:
+ - The `Agent` is authorized to source products from that `Supplier`.
+ - The `Agent` is authorized to offer that `Item type`.
+ - The `Supplier` is known to manufacture that `Item type`.
+
+**Illustrative Consequence**: An `Agent` has certain `Suppliers` and certain `Item types` in their repertoire. If supplier `S1` and supplier `S2` are in their repertoire, and item type `I` is in their repertoire, then (assuming supplier `S1` and supplier `S2` both manufacture item type `I`), the agent must offer items of item type `I` those manufactured by supplier `S1` and those manufactured by supplier `S2`.
+You cannot have all the component relationships true without the corresponding combined record existing in the `agent_inventory` table. It's this enforced combination based on the component parts that leads to the Join Dependency.
+
+### Candidate Keys
+
+`agent, supplier, item_type` is the only candidate key. All three attributes are required to uniquely identify a specific assignment row.
+
+### Join Dependency
+
+`*( {agent, supplier}, {agent, item_type}, {supplier, item_type} )` is a non-trivial join dependency. This means that the table can be decomposed into smaller tables without losing any information.
+
+### 4NF Analysis
+
+The table is in **4NF** because there are no non-trivial multivalued dependencies. For example, `Helen Fox` sources from `GearUp` and `ProServe`, and she offers `Webcam`, `Scanners` and `Docks`. However, she doesn't offer `GearUp` `Docks`, nor does she offer `ProServe` `Scanners`. The allowed combinations are specific and don't show the independence required for MVDs based on single attributes.
+
+### 5NF Analysis
+
+The table is not in **5NF** because it contains non-trivial join dependency. This join dependency is not implied by the candidate key `agent, supplier, item_type` because none of the components in the JD (`{agent, supplier}`, `{agent, item_type}`, `{supplier, item_type}`) are superkeys of the original `agent_inventory` table.
+
+This leads to:
+1. Redundancy.
+2. Deletion Anomalies: Changing a single underlying fact (e.g., a supplier stops making an item type) may require updating multiple rows in the `agent_inventory` table. For instance, if `ProServe` stopped making `Docks`, rows for both `Chris Day` and `Helen Fox` would need deletion.
+3. Insertion Anomalies: If underlying facts change such that the rule dictates a new row should exist (e.g., if `Chris Day` starts handling `GearUp` suppliers, and `GearUp` makes `Scanners`, which `Chris Day` already handles, the rule implies (`Chris Day`, `GearUp`, `Scanner`) must be inserted), the single-table structure doesn't automatically enforce this insertion based on the component facts.
+
+### 5NF Decomposition
+
+We need to split the table into three tables: `agent_suppliers`, `agent_item_types`, and `supplier_item_types`:
+
+
+
+ ```ts
+ import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const agentSuppliers = pgTable("agent_suppliers", {
+ agent: varchar("agent", { length: 255 }).notNull(),
+ supplier: varchar("supplier", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.agent, t.supplier] }),
+ ]);
+
+ export const agentItemTypes = pgTable("agent_item_types", {
+ agent: varchar("agent", { length: 255 }).notNull(),
+ itemType: varchar("item_type", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.agent, t.itemType] }),
+ ]);
+
+ export const supplierItemTypes = pgTable("supplier_item_types", {
+ supplier: varchar("supplier", { length: 255 }).notNull(),
+ itemType: varchar("item_type", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.supplier, t.itemType] }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "agent_item_types" (
+ "agent" varchar(255) NOT NULL,
+ "item_type" varchar(255) NOT NULL,
+ CONSTRAINT "agent_item_types_agent_item_type_pk" PRIMARY KEY("agent","item_type")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "agent_suppliers" (
+ "agent" varchar(255) NOT NULL,
+ "supplier" varchar(255) NOT NULL,
+ CONSTRAINT "agent_suppliers_agent_supplier_pk" PRIMARY KEY("agent","supplier")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "supplier_item_types" (
+ "supplier" varchar(255) NOT NULL,
+ "item_type" varchar(255) NOT NULL,
+ CONSTRAINT "supplier_item_types_supplier_item_type_pk" PRIMARY KEY("supplier","item_type")
+ );
+ ```
+
+
+ ```plaintext
+ +----------------------+ +-----------------------+
+ | agent_suppliers | | agent_item_types |
+ +-----------+----------+ +-----------+-----------+
+ | agent | supplier | | agent | item_type |
+ +-----------+----------+ +-----------+-----------+
+ | Anna Bell | GearUp | | Anna Bell | Scanner |
+ | Anna Bell | Innovate | | Anna Bell | Projector |
+ | Chris Day | Innovate | | Chris Day | Projector |
+ | Chris Day | ProServe | | Chris Day | Webcam |
+ | Helen Fox | GearUp | | Chris Day | Dock |
+ | Helen Fox | ProServe | | Helen Fox | Scanner |
+ +-----------+----------+ | Helen Fox | Webcam |
+ | Helen Fox | Dock |
+ +-----------+-----------+
+ +----------------------+
+ | supplier_item_types |
+ +----------+-----------+
+ | supplier | item_type |
+ +----------+-----------+
+ | GearUp | Scanner |
+ | GearUp | Webcam |
+ | Innovate | Scanner |
+ | Innovate | Projector |
+ | ProServe | Projector |
+ | ProServe | Webcam |
+ | ProServe | Dock |
+ +----------+-----------+
+ ```
+
+
+
+
+You should also add foreign key constraints on the relevant columns in the `agent_item_types`, `agent_suppliers`, and `supplier_item_types` tables. These constraints should reference the primary key columns in the respective tables (or equivalent tables defining agents, suppliers, and item types).
+
+
+To retrieve the data in a format equivalent to the original `agent_inventory` table we can execute this query:
+
+
+```ts
+const db = drizzle(...);
+
+const results = await db
+ .select({
+ agent: agentSuppliers.agent,
+ supplier: agentSuppliers.supplier,
+ itemType: agentItemTypes.itemType,
+ })
+ .from(agentSuppliers)
+ .innerJoin(
+ agentItemTypes,
+ eq(agentSuppliers.agent, agentItemTypes.agent)
+ )
+ .innerJoin(
+ supplierItemTypes,
+ and(
+ eq(agentSuppliers.supplier, supplierItemTypes.supplier),
+ eq(agentItemTypes.itemType, supplierItemTypes.itemType)
+ )
+ );
+```
+
+```sql
+SELECT
+ "agent_suppliers"."agent",
+ "agent_suppliers"."supplier",
+ "agent_item_types"."item_type"
+FROM
+ "agent_suppliers"
+INNER JOIN
+ "agent_item_types"
+ ON "agent_suppliers"."agent" = "agent_item_types"."agent"
+INNER JOIN
+ "supplier_item_types"
+ ON "agent_suppliers"."supplier" = "supplier_item_types"."supplier"
+ AND "agent_item_types"."item_type" = "supplier_item_types"."item_type";
+```
+
+
+Decomposing the table into `agent_suppliers`, `agent_item_types`, and `supplier_item_types` achieves **5NF**. Each table now represents a single fundamental relationship from the core business rule. This eliminates the specific join dependency that caused redundancy and update anomalies in the original table.
diff --git a/src/content/docs/guides/first-normal-form.mdx b/src/content/docs/guides/first-normal-form.mdx
new file mode 100644
index 00000000..8294c986
--- /dev/null
+++ b/src/content/docs/guides/first-normal-form.mdx
@@ -0,0 +1,293 @@
+---
+title: First Normal Form (1NF)
+slug: first-normal-form
+---
+
+import CodeTabs from '@mdx/CodeTabs.astro';
+import CodeTab from '@mdx/CodeTab.astro';
+
+**The First Normal Form (1NF)** is the first step in the normalization process of a database. It ensures that every column in a table contains only atomic values, and that the table has unique column names and no repeating groups.
+
+## Key Concepts
+
+1. Atomic Values Only: Each column should contain indivisible values — no lists, arrays, or nested records.
+2. Consistent Data Types: Every value in a column must be of the same type (e.g., all integers or all strings).
+3. Unique Rows: Every row should be uniquely identifiable, typically using a primary key.
+4. Distinct Column Names: No duplicate column names, each column represents a single attribute.
+5. Order Independence: The order of rows or columns does not affect the table's meaning or function.
+6. No Repeating Groups: Avoid multiple columns storing the same type of data (e.g., `skill1`, `skill2`, `skill3`).
+
+## The 1NF Rule
+
+To achieve **1NF**, a table must satisfy the following conditions:
+
+- Every column contains only atomic values.
+- All entries in a column are of the same type.
+- Columns have unique names.
+- Rows are uniquely identifiable.
+- There's no meaningful dependence on the order of data.
+- There are no repeating groups.
+
+## Example with Atomicity Violation
+
+The following table violates **1NF** because the `courses` column contains multiple values in a single field (e.g., "Math, Physics"). This breaks the atomicity rule.
+
+
+
+ ```ts
+ import { integer, pgTable, varchar, text } from "drizzle-orm/pg-core";
+
+ export const students = pgTable("students", {
+ id: integer("id").notNull().primaryKey(),
+ name: varchar("name", { length: 255 }).notNull(),
+ courses: text("courses").notNull(),
+ });
+ ```
+
+
+ ```sql
+ CREATE TABLE "students" (
+ "id" integer PRIMARY KEY NOT NULL,
+ "name" varchar(255) NOT NULL,
+ "courses" text NOT NULL
+ );
+ ```
+
+
+ ```plaintext
+ +---------------------------------------------+
+ | students |
+ +---------+-----------+-----------------------+
+ | id | name | courses |
+ +---------+-----------+-----------------------+
+ | 1 | Alice | Math, Physics |
+ | 2 | Bob | Literature |
+ | 3 | Charlie | Math, Chemistry, Art |
+ | 4 | Andrew | Chemistry, Literature |
+ +---------+-----------+-----------------------+
+ ```
+
+
+
+To bring the table into **1NF**, we separate `courses` into a standalone table and use a join table (`enrollments`) to represent the many-to-many relationship between `students` and `courses`.
+
+
+
+ ```ts
+ import { integer, pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const students = pgTable("students", {
+ id: integer("id").notNull().primaryKey(),
+ name: varchar("name", { length: 255 }).notNull(),
+ });
+
+ export const courses = pgTable("courses", {
+ id: integer("id").notNull().primaryKey(),
+ name: varchar("name", { length: 255 }).notNull().unique(),
+ });
+
+ export const enrollments = pgTable("enrollments", {
+ studentId: integer("student_id")
+ .notNull()
+ .references(() => students.id, { onDelete: "cascade" }),
+ courseId: integer("course_id")
+ .notNull()
+ .references(() => courses.id, { onDelete: "cascade" }),
+ }, (t) => [
+ primaryKey({ columns: [t.studentId, t.courseId] }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "courses" (
+ "id" integer PRIMARY KEY NOT NULL,
+ "name" varchar(255) NOT NULL,
+ CONSTRAINT "courses_name_unique" UNIQUE("name")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "enrollments" (
+ "student_id" integer NOT NULL,
+ "course_id" integer NOT NULL,
+ CONSTRAINT "enrollments_student_id_course_id_pk" PRIMARY KEY("student_id","course_id")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "students" (
+ "id" integer PRIMARY KEY NOT NULL,
+ "name" varchar(255) NOT NULL
+ );
+ --> statement-breakpoint
+ ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_student_id_students_id_fk" FOREIGN KEY ("student_id") REFERENCES "public"."students"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_course_id_courses_id_fk" FOREIGN KEY ("course_id") REFERENCES "public"."courses"("id") ON DELETE cascade ON UPDATE no action;
+ ```
+
+
+ ```plaintext
+ +---------------------+ +-----------------------+
+ | students | | courses |
+ +---------+-----------+ +---------+-------------+
+ | id | name | | id | name |
+ +---------+-----------+ +---------+-------------+
+ | 1 | Alice | | 1 | Math |
+ | 2 | Bob | | 2 | Physics |
+ | 3 | Charlie | | 3 | Literature |
+ | 4 | Andrew | | 4 | Chemistry |
+ +---------+-----------+ | 5 | Art |
+ +---------+-------------+
+
+
+ +-------------------------+
+ | enrollments |
+ +------------+------------+
+ | student_id | course_id |
+ +------------+------------+
+ | 1 | 1 |
+ | 1 | 2 |
+ | 2 | 3 |
+ | 3 | 1 |
+ | 3 | 4 |
+ | 3 | 5 |
+ | 4 | 4 |
+ | 4 | 3 |
+ +------------+------------+
+ ```
+
+
+
+With this structure:
+- Each course is stored once in the `courses` table.
+- Relationships are clearly defined in the `enrollments` table.
+- All values are atomic, satisfying the **1NF** requirement.
+
+## Example with Repeating Groups Violation
+
+This structure violates **1NF** because it uses repeating groups — multiple columns (`skill1`, `skill2`, `skill3`) to store the same kind of data: skills.
+
+
+
+ ```ts
+ import { integer, pgTable, varchar } from "drizzle-orm/pg-core";
+
+ export const employees = pgTable("employees", {
+ id: integer("id").notNull().primaryKey(),
+ name: varchar("name", { length: 255 }).notNull(),
+ skill1: varchar("skill1", { length: 255 }),
+ skill2: varchar("skill2", { length: 255 }),
+ skill3: varchar("skill3", { length: 255 })
+ });
+ ```
+
+
+ ```sql
+ CREATE TABLE "employees" (
+ "id" integer PRIMARY KEY NOT NULL,
+ "name" varchar(255) NOT NULL,
+ "skill1" varchar(255),
+ "skill2" varchar(255),
+ "skill3" varchar(255)
+ );
+ ```
+
+
+ ```plaintext
+ +---------------------------------------------------------+
+ | employees |
+ +---------+-----------+-----------+-----------+-----------+
+ | id | name | skill1 | skill2 | skill3 |
+ +---------+-----------+-----------+-----------+-----------+
+ | 1 | Alice | SQL | Python | NULL |
+ | 2 | Bob | Java | NULL | NULL |
+ | 3 | Charlie | Excel | HTML | SQL |
+ +---------+-----------+-----------+-----------+-----------+
+ ```
+
+
+
+We need to eliminate the repeating skill columns and store each skill as a separate record, using a new table to link `employees` to their `skills`.
+
+
+
+ ```ts
+ import { integer, pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const employees = pgTable("employees", {
+ id: integer("id").notNull().primaryKey(),
+ name: varchar("name", { length: 255 }).notNull(),
+ });
+
+ export const skills = pgTable("skills", {
+ id: integer("id").notNull().primaryKey(),
+ name: varchar("name", { length: 255 }).notNull().unique(),
+ });
+
+ export const employeeSkills = pgTable("employee_skills", {
+ employeeId: integer("employee_id")
+ .notNull()
+ .references(() => employees.id, { onDelete: "cascade" }),
+ skillId: integer("skill_id")
+ .notNull()
+ .references(() => skills.id, { onDelete: "cascade" }),
+ }, (table) => [
+ primaryKey({ columns: [table.employeeId, table.skillId] }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "employee_skills" (
+ "employee_id" integer NOT NULL,
+ "skill_id" integer NOT NULL,
+ CONSTRAINT "employee_skills_employee_id_skill_id_pk" PRIMARY KEY("employee_id","skill_id")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "employees" (
+ "id" integer PRIMARY KEY NOT NULL,
+ "name" varchar(255) NOT NULL
+ );
+ --> statement-breakpoint
+ CREATE TABLE "skills" (
+ "id" integer PRIMARY KEY NOT NULL,
+ "name" varchar(255) NOT NULL,
+ CONSTRAINT "skills_name_unique" UNIQUE("name")
+ );
+ --> statement-breakpoint
+ ALTER TABLE "employee_skills" ADD CONSTRAINT "employee_skills_employee_id_employees_id_fk" FOREIGN KEY ("employee_id") REFERENCES "public"."employees"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ ALTER TABLE "employee_skills" ADD CONSTRAINT "employee_skills_skill_id_skills_id_fk" FOREIGN KEY ("skill_id") REFERENCES "public"."skills"("id") ON DELETE cascade ON UPDATE no action;
+ ```
+
+
+ ```plaintext
+ +---------------------+ +-----------------+
+ | employees | | skills |
+ +---------+-----------+ +----+------------+
+ | id | name | | id | name |
+ +---------+-----------+ +----+------------+
+ | 1 | Alice | | 1 | SQL |
+ | 2 | Bob | | 2 | Python |
+ | 3 | Charlie | | 3 | Java |
+ +---------+-----------+ | 4 | Excel |
+ | 5 | Power BI |
+ +----+------------+
+
+ +-------------------------+
+ | employee_skills |
+ +-------------+-----------+
+ | employee_id | skill_id |
+ +-------------+-----------+
+ | 1 | 1 |
+ | 1 | 2 |
+ | 2 | 3 |
+ | 3 | 4 |
+ | 3 | 5 |
+ | 3 | 1 |
+ +-------------+-----------+
+ ```
+
+
+
+With this structure:
+- Each skill is stored once in the `skills` table.
+- Relationships between `employees` and `skills` are clearly defined in the `employee_skills` table.
+- Repeating groups are eliminated, and each column represents a single attribute - satisfying the **1NF** requirement.
+
+In practice, most **1NF** rules - like having unique column names, consistent data types, unique rows (via primary keys), and order independence are either enforced automatically by the database system or are built into standard SQL table design. Because of that, we don't show examples of those rules in this guide.
diff --git a/src/content/docs/guides/fourth-normal-form.mdx b/src/content/docs/guides/fourth-normal-form.mdx
new file mode 100644
index 00000000..807fa168
--- /dev/null
+++ b/src/content/docs/guides/fourth-normal-form.mdx
@@ -0,0 +1,160 @@
+---
+title: Fourth Normal Form (4NF)
+slug: fourth-normal-form
+---
+
+import Section from "@mdx/Section.astro";
+import CodeTabs from '@mdx/CodeTabs.astro';
+import CodeTab from '@mdx/CodeTab.astro';
+import Prerequisites from "@mdx/Prerequisites.astro";
+import Callout from '@mdx/Callout.astro';
+
+
+- You should be familiar with [BCNF](/docs/guides/boyce-codd-normal-form)
+
+
+**The Fourth Normal Form (4NF)** is a level of database normalization where a table is already in **BCNF**, and for every non-trivial multivalued dependency, the determinant must be a super key. The goal is to eliminate redundant data and maintain data consistency.
+
+## Key concepts
+
+A `Multi-valued Dependency` (MVD) is a type of dependency that exists in a relation with at least three attributes A, B, and C if, for each value of A, the set of possible values of B associated with A and the set of possible values of C associated with A are independent of each other. This means that the values of B associated with A are not determined by the values of C associated with A, and vice versa. Essentially, for a given A, you can combine any value from the set of B values with any value from the set of C values.
+
+`Multi-valued Dependency` `X ->> Y` in a relation `R` is considered trivial if `Y` is a subset of `X` or `X` and `Y` together make up all the attributes of the relation.
+
+## Example
+
+We have a table `courses_instructors_textbooks` with the following schema:
+
+
+
+ ```ts
+ import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const coursesInstructorsTextbooks = pgTable("courses_instructors_textbooks", {
+ courseName: varchar("course_name", { length: 255 }).notNull(),
+ instructor: varchar("instructor", { length: 255 }).notNull(),
+ textbook: varchar("textbook", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.courseName, t.instructor, t.textbook],
+ name: "pk_courses_instructors_textbooks",
+ }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "courses_instructors_textbooks" (
+ "course_name" varchar(255) NOT NULL,
+ "instructor" varchar(255) NOT NULL,
+ "textbook" varchar(255) NOT NULL,
+ CONSTRAINT "pk_courses_instructors_textbooks" PRIMARY KEY("course_name","instructor","textbook")
+ );
+ ```
+
+
+ ```plaintext
+ +-------------------------------------------------+
+ | courses_instructors_textbooks |
+ +-------------+----------------+------------------+
+ | course_name | instructor | textbook |
+ +-------------+----------------+------------------+
+ | CS101 | Dr. Smith | Database Design |
+ | CS101 | Dr. Smith | Algorithms |
+ | CS101 | Dr. Smith | Data Structures |
+ | CS101 | Dr. Johnson | Database Design |
+ | CS101 | Dr. Johnson | Algorithms |
+ | CS101 | Dr. Johnson | Data Structures |
+ | CS102 | Dr. Williams | Machine Learning |
+ | CS102 | Dr. Williams | AI Concepts |
+ +-----------+------------------+------------------+
+ ```
+
+
+
+1. Course can be taught by multiple instructors.
+2. Each course can have multiple textbooks.
+3. The set of instructors assigned to teach a specific course is independent of the set of textbooks required for that same course. Knowing an instructor for a course tells you nothing specific about which textbook is used (beyond the list of all possible textbooks for that course), and vice-versa.
+
+### Multi-valued Dependencies
+
+1. `course_name ->> instructor`
+2. `course_name ->> textbook`
+
+### Candidate keys
+
+`course_name, instructor, textbook` is the only candidate key. We need all three to uniquely identify a row because a course can have multiple instructors and textbooks.
+
+### BCNF Analysis
+
+The table is in **BCNF** because there are no non-trivial FDs where the determinant is not a superkey.
+
+### 4NF Analysis
+
+Table is not in **4NF** because the multi-valued dependencies are not trivial. The determinant `course_name` is not a super key.
+
+This leads to:
+1. Information duplication.
+2. Insertion anomalies. If a new instructor or textbook needs to be added for a course, we would need to insert multiple rows to account for all the combinations of instructors and textbooks for that course.
+3. Deletion anomalies. If an instructor stops teaching a course, multiple rows (one for each textbook associated with the course) must be deleted.
+
+### 4NF Decomposition
+
+We need to split the table into two tables: `courses_instructors` and `courses_textbooks`:
+
+
+
+ ```ts
+ import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const coursesInstructors = pgTable("courses_instructors", {
+ courseName: varchar("course_name", { length: 255 }).notNull(),
+ instructor: varchar("instructor", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.courseName, t.instructor] }),
+ ]);
+
+ export const coursesTextbooks = pgTable("courses_textbooks", {
+ courseName: varchar("course_name", { length: 255 }).notNull(),
+ textbook: varchar("textbook", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.courseName, t.textbook] }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "courses_instructors" (
+ "course_name" varchar(255) NOT NULL,
+ "instructor" varchar(255) NOT NULL,
+ CONSTRAINT "courses_instructors_course_name_instructor_pk" PRIMARY KEY("course_name","instructor")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "courses_textbooks" (
+ "course_name" varchar(255) NOT NULL,
+ "textbook" varchar(255) NOT NULL,
+ CONSTRAINT "courses_textbooks_course_name_textbook_pk" PRIMARY KEY("course_name","textbook")
+ );
+ ```
+
+
+ ```plaintext
+ +----------------------------------------+ +----------------------------------+
+ | courses_instructors | | courses_textbooks |
+ +--------------+-------------------------+ +--------------+-------------------+
+ | course_name | instructor | | course_name | textbook |
+ +--------------+-------------------------+ +--------------+-------------------+
+ | CS101 | Dr. Smith | | CS101 | Database Design |
+ | CS101 | Dr. Johnson | | CS101 | Algorithms |
+ | CS102 | Dr. Williams | | CS101 | Data Structures |
+ +--------------+-------------------------+ | CS102 | Machine Learning |
+ | CS102 | AI Concepts |
+ +--------------+-------------------+
+ ```
+
+
+
+
+You should also add foreign key constraints on the `course_name` column in both the `courses_instructors` and `courses_textbooks` tables. These constraints should reference the `course_name` primary key column in a `courses` table (or equivalent table defining courses).
+
+
+Now, the tables are free from the non-trivial multi-valued dependencies that violated **4NF** in the original table. We have achieved **4NF** by storing each independent relationship separately.
diff --git a/src/content/docs/guides/second-normal-form.mdx b/src/content/docs/guides/second-normal-form.mdx
new file mode 100644
index 00000000..94b01567
--- /dev/null
+++ b/src/content/docs/guides/second-normal-form.mdx
@@ -0,0 +1,146 @@
+---
+title: Second Normal Form (2NF)
+slug: second-normal-form
+---
+
+import CodeTabs from '@mdx/CodeTabs.astro';
+import CodeTab from '@mdx/CodeTab.astro';
+import Prerequisites from "@mdx/Prerequisites.astro";
+
+
+- You should be familiar with [1NF](/docs/guides/first-normal-form)
+
+
+**The Second Normal Form (2NF)** is a database normalization form that builds on the **First Normal Form**. The primary goal of **2NF** is to eliminate partial dependencies.
+
+## Key Concepts
+
+1. A `Candidate key` is a minimal set of attributes that can uniquely identify each row in a table. There can be multiple candidate keys in a table.
+2. A `Non-prime` attribute is a column that isn't part of any candidate key.
+3. A `Functional dependency` is a relationship between two sets of attributes in a table, where the value of one set uniquely determines the value of another.
+4. A `partial dependency` happens when a non-prime attribute depends on only a part of a candidate key, rather than on the entire candidate key.
+
+## The 2NF Rule
+
+To achieve **2NF**, table should already be in **1NF** and must satisfy the following condition:
+
+Table does not have any non-prime attribute that is functionally dependent on any proper subset of any candidate key of the relation. In simpler terms, all non-prime attributes must depend on the entire candidate key, not just a part of it.
+
+## Example
+
+We have a table `enrollments` with the following schema:
+
+
+
+ ```ts
+ import { integer, pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const enrollments = pgTable("enrollments", {
+ studentId: integer("student_id").notNull(),
+ courseId: integer("course_id").notNull(),
+ courseName: varchar("course_name", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.studentId, t.courseId] }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "enrollments" (
+ "student_id" integer NOT NULL,
+ "course_id" integer NOT NULL,
+ "course_name" varchar(255) NOT NULL,
+ CONSTRAINT "enrollments_student_id_course_id_pk" PRIMARY KEY("student_id","course_id")
+ );
+ ```
+
+
+ ```plaintext
+ +---------------------------------------+
+ | enrollments |
+ +-------------+-----------+-------------+
+ | student_id | course_id | course_name |
+ +-------------+-----------+-------------+
+ | 1 | 101 | Math |
+ | 2 | 102 | Chemistry |
+ | 1 | 102 | Chemistry |
+ | 3 | 103 | Literature |
+ +-------------+-----------+-------------+
+ ```
+
+
+
+### Functional Dependencies
+
+1. `student_id, course_id -> course_name`.
+2. `course_id -> course_name`.
+
+### Candidate keys
+
+Only the pair of attributes `student_id` and `course_id` can uniquely identify each row in the table. Therefore, the candidate key is `student_id, course_id`.
+
+### 1NF Analysis
+
+The table is in **1NF** because all attributes contain atomic values, and there are no repeating groups or arrays.
+
+### 2NF Analysis
+
+`course_name` is a non-prime attribute because it is not part of the candidate key. Moreover, it is partially dependent on `course_id`, which is a part of the candidate key `student_id, course_id`. This violates the **2NF** rule.
+
+This leads to redundancy and potential update anomalies. For example, if the course name for `course_id - 101` changes, we would need to update multiple rows.
+
+### 2NF Decomposition
+
+To eliminate the partial dependency, we can decompose the table into two separate tables:
+
+
+
+ ```ts
+ import { integer, pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const courses = pgTable("courses", {
+ id: integer("id").notNull().primaryKey(),
+ courseName: varchar("course_name", { length: 255 }).notNull(),
+ });
+
+ export const enrollments = pgTable("enrollments", {
+ studentId: integer("student_id").notNull(),
+ courseId: integer("course_id").notNull().references(() => courses.id, { onDelete: "cascade", onUpdate: "cascade" }),
+ }, (t) => [
+ primaryKey({ columns: [t.studentId, t.courseId] }),
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "courses" (
+ "id" integer PRIMARY KEY NOT NULL,
+ "course_name" varchar(255) NOT NULL
+ );
+ --> statement-breakpoint
+ CREATE TABLE "enrollments" (
+ "student_id" integer NOT NULL,
+ "course_id" integer NOT NULL,
+ CONSTRAINT "enrollments_student_id_course_id_pk" PRIMARY KEY("student_id","course_id")
+ );
+ --> statement-breakpoint
+ ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_course_id_courses_id_fk" FOREIGN KEY ("course_id") REFERENCES "public"."courses"("id") ON DELETE cascade ON UPDATE cascade;
+ ```
+
+
+ ```plaintext
+ +-------------------+ +-------------------------+
+ | courses | | enrollments |
+ +-----+-------------+ +-------------+-----------+
+ | id | course_name | | student_id | course_id |
+ +-----+-------------+ +-------------+-----------+
+ | 101 | Math | | 1 | 101 |
+ | 102 | Chemistry | | 2 | 102 |
+ | 103 | Literature | | 1 | 102 |
+ +-----+-------------+ | 3 | 103 |
+ +-------------+-----------+
+ ```
+
+
+
+With this decomposition `course_name` is no longer repeated in the `enrollments` table and each table is free from partial dependencies. We have achieved **2NF**.
diff --git a/src/content/docs/guides/third-normal-form.mdx b/src/content/docs/guides/third-normal-form.mdx
new file mode 100644
index 00000000..e65c591c
--- /dev/null
+++ b/src/content/docs/guides/third-normal-form.mdx
@@ -0,0 +1,154 @@
+---
+title: Third Normal Form (3NF)
+slug: third-normal-form
+---
+
+import CodeTabs from '@mdx/CodeTabs.astro';
+import CodeTab from '@mdx/CodeTab.astro';
+import Prerequisites from "@mdx/Prerequisites.astro";
+
+
+- You should be familiar with [2NF](/docs/guides/second-normal-form)
+
+
+**The Third Normal Form (3NF)** is a database normalization form that builds on the **Second Normal Form**. The primary goal of **3NF** is to eliminate transitive dependencies.
+
+## Key Concepts
+
+1. A `Transitive dependency` occurs when one attribute in a database indirectly relies on another through a third attribute, causing redundancy. For example, if A depends on B (`A -> B`) and B depends on C (`B -> C`), then A is transitively dependent on C (`A -> C`).
+2. A `Functional dependency` (`X -> Y`) in a relation `R` is considered trivial if the set of attributes `Y` is a subset of (or equal to) the set of attributes `X`.
+3. A `Super key` is a candidate key or a superset of a candidate key.
+
+## The 3NF Rule
+
+A relation is in **3NF** if it is in **2NF** and at least one of the following conditions holds in every non-trivial function dependency `X -> Y`:
+
+- `X` is a super key.
+- `Y` is a prime attribute.
+
+## Example
+
+We have a table `course_instructors` with the following schema:
+
+
+
+ ```ts
+ import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const courseInstructors = pgTable("course_instructors", {
+ course: varchar("course", { length: 255 }).notNull(),
+ semester: varchar("semester", { length: 255 }).notNull(),
+ instructor: varchar("instructor", { length: 255 }).notNull(),
+ instructorEmail: varchar("instructor_email", { length: 255 }).notNull(),
+ }, (t) => [
+ primaryKey({ columns: [t.course, t.semester] })
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "course_instructors" (
+ "course" varchar(255) NOT NULL,
+ "semester" varchar(255) NOT NULL,
+ "instructor" varchar(255) NOT NULL,
+ "instructor_email" varchar(255) NOT NULL,
+ CONSTRAINT "course_instructors_course_semester_pk" PRIMARY KEY("course","semester")
+ );
+ ```
+
+
+ ```plaintext
+ +---------------------------------------------------------------------------------+
+ | course_instructors |
+ +--------------------+--------------+----------------+----------------------------+
+ | course | semester | instructor | instructor_email |
+ +--------------------+--------------+----------------+----------------------------+
+ | Data Structures | Fall 2022 | Jane Robinson | jane.robinson@example.com |
+ | Algorithms | Spring 2023 | Mike Green | mike.green@example.com |
+ | Operating Systems | Spring 2023 | Jane Robinson | jane.robinson@example.com |
+ | Data Structures | Spring 2023 | Lisa White | lisa.white@example.com |
+ +--------------------+--------------+----------------+----------------------------+
+ ```
+
+
+
+### Functional Dependencies
+
+1. `course, semester -> instructor`.
+2. `instructor -> instructor_email`.
+3. `course, semester -> instructor_email`.
+
+### Candidate keys
+
+Only the pair of attributes `course` and `semester` can uniquely identify each row in the table. Therefore, the candidate key is `course, semester`.
+
+### 2NF Analysis
+
+The table is in **2NF** because all non-key attributes are fully functionally dependent on the entire candidate key (`course, semester`).
+
+### 3NF Analysis
+
+The table is **not in 3NF** because of the transitive dependency `instructor -> instructor_email`. The attribute `instructor_email` is dependent on `instructor`, which is not a super key.
+
+This leads to:
+
+1. Redundancy: as the same instructor's email can appear multiple times for different courses and semesters.
+2. Update anomalies: as changing an instructor's email would require updating multiple rows.
+
+### 3NF Decomposition
+
+To eliminate the transitive dependency, we can decompose the table into two separate tables:
+
+
+
+ ```ts
+ import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
+
+ export const instructors = pgTable("instructors", {
+ instructor: varchar("instructor", { length: 255 }).notNull().primaryKey(),
+ instructorEmail: varchar("instructor_email", { length: 255 }).notNull(),
+ });
+
+ export const courseInstructors = pgTable("course_instructors", {
+ course: varchar("course", { length: 255 }).notNull(),
+ semester: varchar("semester", { length: 255 }).notNull(),
+ instructor: varchar("instructor", { length: 255 }).notNull().references(() => instructors.instructor, { onDelete: "cascade", onUpdate: "cascade" }),
+ }, (t) => [
+ primaryKey({ columns: [t.course, t.semester] })
+ ]);
+ ```
+
+
+ ```sql
+ CREATE TABLE "course_instructors" (
+ "course" varchar(255) NOT NULL,
+ "semester" varchar(255) NOT NULL,
+ "instructor" varchar(255) NOT NULL,
+ CONSTRAINT "course_instructors_course_semester_pk" PRIMARY KEY("course","semester")
+ );
+ --> statement-breakpoint
+ CREATE TABLE "instructors" (
+ "instructor" varchar(255) PRIMARY KEY NOT NULL,
+ "instructor_email" varchar(255) NOT NULL
+ );
+ --> statement-breakpoint
+ ALTER TABLE "course_instructors" ADD CONSTRAINT "course_instructors_instructor_instructors_instructor_fk" FOREIGN KEY ("instructor") REFERENCES "public"."instructors"("instructor") ON DELETE cascade ON UPDATE cascade;
+ ```
+
+
+ ```plaintext
+ +----------------------------------------------------+ +------------------------------------------------+
+ | course_instructors | | instructors |
+ +--------------------+--------------+----------------+ +------------------+-----------------------------+
+ | Course | Semester | Instructor | | Instructor | Email |
+ +--------------------+--------------+----------------+ +------------------+-----------------------------+
+ | Data Structures | Fall 2022 | Jane Robinson | | Jane Robinson | jane.robinson@example.com |
+ | Algorithms | Spring 2023 | Mike Green | | Mike Green | mike.green@example.com |
+ | Operating Systems | Spring 2023 | Jane Robinson | | Lisa White | lisa.white@example.com |
+ | Data Structures | Spring 2023 | Lisa White | +------------------+-----------------------------+
+ +--------------------+--------------+----------------+
+ ```
+
+
+
+With this decomposition, we have eliminated the transitive dependency. The `instructor_email` is now stored in a separate table, and we have achieved **3NF**.