Welcome back. In our previous lesson, you mastered the fundamental skill of retrieving data from a single table using the SELECT and WHERE clauses. We established that SQL is a declarative language, and you learned to precisely define the characteristics of the data you wish to retrieve.
This lesson moves from retrieving data to designing the very structure that holds it. Our learning outcome is to design a normalized schema for storing narrative text (books, chapters, chunks) using appropriate data types, primary keys, and foreign keys. This is a foundational architectural task. A well-designed schema is the bedrock of a robust and scalable application; it prevents data corruption, ensures logical consistency, and simplifies querying. A poor schema, conversely, leads to bugs, performance issues, and maintenance nightmares. For your goal of building a production-grade system, mastering schema design is non-negotiable.
1. The Rationale for Normalization: Avoiding Anomalies
Before diving into the "how" of schema design, we must first understand the "why." What happens when a database schema is poorly designed? Imagine we tried to store all our project's data in one giant, flat table:
| book_title | author_name | author_dob | chapter_number | chapter_title | chunk_text |
|---|---|---|---|---|---|
| Dune | Frank Herbert | 1920-10-08 | 1 | A Beginning | "In the week before their departure..." |
| Dune | Frank Herbert | 1920-10-08 | 1 | A Beginning | "It is a planet of awesome vistas..." |
| Dune | Frank Herbert | 1920-10-08 | 2 | Muad'Dib | "Paul’s eyes opened to the dim..." |
This design is rife with problems stemming from data redundancy. The author's name and date of birth are repeated for every single chunk of the book. This leads to three classes of problems, known as anomalies:
- Update Anomaly: If we discover Frank Herbert's date of birth was recorded incorrectly, we would have to update thousands of rows. If we miss even one, the database becomes internally inconsistent, violating data integrity.
- Insertion Anomaly: How do we add a new author to our database before we have any of their books? In this schema, we can't. We need a book, chapter, and chunk to create a row, so we cannot store information about an author independently.
- Deletion Anomaly: If we delete the last remaining chunk of a particular book, we might inadvertently delete the only record of that book's existence or its author's details from our database.
Database normalization is the formal process of organizing columns and tables to minimize data redundancy and eliminate these anomalies. The following video provides an excellent, practical introduction to why normalization is essential and the problems it solves.
Learn Database Normalization - 1NF, 2NF, 3NF, 4NF, 5NF
Please watch the introduction of the video "Learn Database Normalization - 1NF, 2NF, 3NF, 4NF, 5NF" from the Decomplexify channel.
Watch from the beginning up to the summary of benefits. Pay close attention to the examples of data integrity failure and the definitions of the three anomalies.
2. The Normal Forms: A Step-by-Step Guide
Normalization is achieved by following a set of rules called Normal Forms. Each normal form represents a progressively stricter set of conditions. For most practical purposes, achieving the Third Normal Form (3NF) is the goal.
A helpful mnemonic for 3NF, which we'll unpack, is that every non-key attribute "must depend on the key, the whole key, and nothing but the key."
Let's continue with the "Learn Database Normalization" video, which brilliantly explains these forms.
Learn Database Normalization - 1NF, 2NF, 3NF, 4NF, 5NF
This segment of the video breaks down the most important normal forms using clear, step-by-step examples.
First, watch the section on First Normal Form (1NF) from this timestamp. Focus on the four rules, especially the prohibition of "repeating groups." Think about how storing a list of chapter titles in a single chapters column within a books table would violate this. Next, watch the explanation of Second Normal Form (2NF) from this part. The key concept here is eliminating "part-key dependencies," which arise when you have a composite primary key. Finally, watch the section on Third Normal Form (3NF) and Boyce-Codd Normal Form (BCNF) from this segment. This is the most critical part. Internalize the idea of removing "transitive dependencies"—where a non-key attribute depends on another non-key attribute. The mantra about "the key, the whole key, and nothing but the key" is your primary design heuristic.
For a more formal, academic perspective that aligns with your preference for university materials, you can review the following lecture notes from CMU. They formally define the properties of a normalized schema, such as lossless joins and dependency preservation.
[PDF] 15-445/645 Database Systems (Fall 2017) - 05 Normal Forms
These notes from CMU's database course offer a concise, theoretical summary of normal forms.
Read the first page, from the beginning to the end of the section on BCNF. Compare these formal definitions to the practical explanations in the video. Also, note the final section on NoSQL, which mentions denormalization for performance. This is a critical tradeoff you will document in a future Architecture Decision Record.
3. Designing the Schema for the Narrative Intelligence Engine
Now, let's apply these principles to your project. We need to store books, which are composed of chapters, which are in turn composed of chunks of text. This is a natural hierarchy.
An Entity-Relationship Diagram (ERD) helps visualize the tables and their relationships.

Our goal is to create a similar structure for our narrative data.
Approach A: The Static, Multi-Table Schema
The most straightforward normalized design is to create a separate table for each entity in our hierarchy:
-
authorstable: To store information about authors.id:BIGINT PRIMARY KEY(a surrogate key)name:VARCHAR(255)birth_year:INT- ...other author-specific data
-
bookstable: To store information about books.id:BIGINT PRIMARY KEYauthor_id:BIGINT, aFOREIGN KEYreferencingauthors(id).title:TEXTpublication_year:INT
-
chapterstable:id:BIGINT PRIMARY KEYbook_id:BIGINT, aFOREIGN KEYreferencingbooks(id).chapter_number:INTtitle:TEXT
-
chunkstable: The core text units for your RAG system.id:BIGINT PRIMARY KEYchapter_id:BIGINT, aFOREIGN KEYreferencingchapters(id).content:TEXTchunk_index_in_chapter:INT(to maintain order)
This design is fully normalized to 3NF. There is no redundant data. We can add an author without a book, and deleting the last chunk of a chapter doesn't delete the chapter itself.
However, this design is rigid. What if a book has "Parts" that contain chapters? What about prefaces or appendices? What if a text has no chapters at all? We would need to add more tables or make columns nullable, which can complicate logic.
Approach B: The Dynamic, Adjacency List Model
A more flexible and powerful approach for hierarchical data is the Adjacency List model. Here, we use a single table with a self-referencing foreign key to model the hierarchy.
Imagine a single table, documents:
id:BIGINT PRIMARY KEYparent_id:BIGINT, aFOREIGN KEYreferencingdocuments(id). This is the key to the model.document_type:VARCHAR(50)(e.g., 'book', 'chapter', 'chunk', 'part')title:TEXTcontent:TEXTposition:INT(to order siblings, e.g., chapter 1, chapter 2)
In this model:
- A book is a row where
parent_idisNULL. - A chapter of that book is a row whose
parent_idis theidof the book row. - A chunk of that chapter is a row whose
parent_idis theidof the chapter row.
This design is incredibly flexible. It can represent any depth of hierarchy without schema changes. It treats all narrative components as a generic "document," which aligns beautifully with future plans for building a knowledge graph.
The following resources provide an excellent breakdown of how to model hierarchical data in PostgreSQL, focusing on the adjacency list model.
Modeling Hierarchical Tree Data in PostgreSQL
This article is essential for your long-term project architecture. It contrasts the static approach with the dynamic adjacency list model.
First, quickly read through Approach #1 to understand the limitations of the multi-table design we just discussed. Then, focus deeply on Approach #2. This explains the adjacency list model with a self-referencing parent ID. This is the core concept you should aim to implement. Pay attention to how it allows for "polymorphism" where every entry is a generic type.
To see this self-referencing foreign key in action, the following short video provides a very clear visual explanation.
How to Model & Query Hierarchical Data in PostgreSQL (CTEs & Recursion Explained)
This video, "How to Model & Query Hierarchical Data in PostgreSQL", quickly demonstrates the adjacency list concept with a simple employees table.
Watch from the ER diagram explanation to see how a table can have a foreign key that points to itself, forming a tree structure. This will solidify the concept from the article.
Your Task: Design and Implement the Initial Schema
Your task is to write the CREATE TABLE SQL statements for the initial version of your project's database. I recommend you use the Adjacency List model (Approach B) as it provides the long-term flexibility your project requires. However, starting with the static multi-table model (Approach A) is also acceptable if you want to begin with a simpler structure, as long as you document the trade-offs.
Deliverable:
In your project repository, create a new SQL file (e.g., schema.sql). In this file, write the CREATE TABLE statements for your chosen design.
For each table, you must:
- Define a primary key. Use
BIGINTandGENERATED ALWAYS AS IDENTITYfor auto-incrementing surrogate keys. - Define foreign keys with
REFERENCESto enforce relationships. - Choose appropriate data types for each column (
TEXT,VARCHAR,INT,TIMESTAMPTZfor created/updated timestamps, etc.). - Add
NOT NULLconstraints where appropriate.
In your engineering journal, for each table and column, write a brief justification for your choice of data type and constraints. For example: "I chose TEXT for the title column instead of VARCHAR(255) because some literary works have very long titles, and in modern PostgreSQL, the performance difference is negligible for our use case, while the flexibility is valuable."
Conclusion
In this lesson, you've moved from a user of data to an architect of data. You've learned that normalization is a critical process for ensuring data integrity by eliminating redundancy and its associated update, insertion, and deletion anomalies. You explored the progressive rules of the normal forms, internalizing the 3NF heuristic: "depend on the key, the whole key, and nothing but the key."
Most importantly, you applied these principles to the core challenge of your capstone project: modeling hierarchical narrative data. You compared a rigid, static schema with a flexible, dynamic adjacency list model, a design choice that will have long-term implications for your system's evolution.
Key Takeaways:
- Normalization prevents data anomalies by reducing redundancy.
- Achieving 3NF is the standard for robust relational schema design.
- Hierarchical data, like
book -> chapter -> chunk, can be modeled using separate tables or, more flexibly, with a self-referencing adjacency list. - Choosing the right data types (
BIGINT,TEXT,TIMESTAMPTZ) and constraints (PRIMARY KEY,FOREIGN KEY,NOT NULL) is a fundamental part of schema implementation.
In the next lesson, we will explore how to retrieve information from our newly designed, multi-table schema. Now that you have decomposed your data into related tables, you will learn to use JOINs to put it back together in meaningful ways, a direct consequence of the "lossless join" property that good normalization guarantees.