This episode covers the foundations of relational database design: Entity Relationship Diagrams, Primary Keys and Foreign Keys, data normalization principles from 1NF to BCNF, as well as hands-on practice with Data Definition Language (DDL) for creating, altering, and dropping databases and tables.

Welcome to episode 2 of the Learn SQL with PostgreSQL series! In episode 1, we understood the history of the relational model and the reasons for choosing PostgreSQL. Now it's time to jump into the most important technical foundation: how to design a good database. Most real-world database problems — duplicate data, inconsistency, slowing queries — actually stem from poor design, not the wrong engine.
In this episode, we'll cover relational data modeling with Entity Relationship Diagrams (ERD), the role of Primary Keys, Foreign Keys, and Unique Constraints, the data normalization principles from 1NF to BCNF, and then hands-on practice with Data Definition Language (DDL).
Before writing CREATE TABLE, a good database designer first draws the big picture. Data modeling is the process of turning real-world business needs into an executable database structure.
An Entity Relationship Diagram (ERD) is a diagram that describes entities, attributes, and the relationships between entities. In the context of a relational database:
users, products, orders.email, price, status.users 1 ────── n orders n ────── 1 products
(id) (id, user_id) (id)
(email) (product_id) (name)
(quantity) (price)How to read the diagram above: one users has many orders, and each orders references one products. This kind of relationship is known as one-to-many, and it's the most common pattern in relational database design. There are also one-to-one and many-to-many relationships, which are usually broken up with a junction table.
Tip
A rule of thumb: if one entity can have many of another entity (e.g. one user has many orders), then the "many" entity stores the foreign key to the "one" entity. So the orders table stores user_id — not the other way around. This pattern is almost always correct.
These three concepts are the backbone of relational database integrity:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
total NUMERIC(10,2) NOT NULL
);Notice: the user_id column in the orders table is a foreign key referencing id in the users table. Thanks to this FK, the database will reject an order whose user_id doesn't exist in the users table — integrity is maintained automatically by the engine.
Normalization is the process of designing table structures to minimize data duplication and prevent anomalies. Repeated data isn't just a waste of storage — it's a time bomb of inconsistency: if one copy is updated but the others aren't, which one is correct?
Normalization works through levels called Normal Forms (NF). The higher the level, the cleaner the structure:
| Normal Form | Main rule | Problem it prevents |
|---|---|---|
| 1NF | Each column stores atomic values; no arrays or comma-separated lists | Multi-valued columns that are hard to query |
| 2NF | Satisfies 1NF + all non-key columns depend fully on the primary key | Data duplication on composite keys |
| 3NF | Satisfies 2NF + no transitive dependencies (non-key depending on another non-key) | Update anomalies on derived data |
| BCNF | A strict version of 3NF: every determiner must be a candidate key | Residual anomalies not caught by 3NF |
Let's look at a practical example. The following table violates 1NF because the tags column stores multiple values in one column:
CREATE TABLE products_bad (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
tags TEXT NOT NULL
);The 1NF fix is to move tags into a separate table that stores one tag per row. And so on: each normalization level eliminates one class of data anomaly.
It's important to note: normalization isn't a dogma without exceptions. In the real world, denormalized tables (deliberately adding duplication) are often used for analytical performance — for example storing an aggregated total_revenue column, or using JSONB for flexible attributes. We'll look at when denormalization is justified in episode 20 (the e-commerce case study).
Note
The golden rule: normalize up to 3NF first, then denormalize consciously and with documentation if there's a strong performance reason.
Once the design and normalization are solid, it's time to express them in SQL. Data Definition Language (DDL) is the set of commands for defining and managing database structure — databases, tables, columns, and constraints.
CREATE DATABASE shop;
DROP DATABASE shop;Important note: DROP DATABASE can't be run while you're still connected to the database in question. Switch to another one first, e.g. the postgres database, before dropping shop.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
full_name TEXT NOT NULL,
age INTEGER CHECK (age >= 17),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);Notice the elements: SERIAL PRIMARY KEY for an auto-increment id, NOT NULL for required columns, UNIQUE for email, CHECK for value validation, and DEFAULT now() for automatic timestamps. We'll break down these data types and constraints more deeply in episode 3.
Table structure rarely stays completely unchanged after the initial creation. ALTER TABLE is the command for structural evolution:
ALTER TABLE users ADD COLUMN phone TEXT;
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
ALTER TABLE users DROP COLUMN phone;
ALTER TABLE users RENAME TO members;Then there are two commands whose meanings are often mixed up:
TRUNCATE TABLE: quickly removes ALL rows, but the table and its structure remain. Can't be used with WHERE.DROP TABLE: permanently removes the table along with its structure and all its contents.TRUNCATE TABLE orders;
DROP TABLE orders;Danger
DROP TABLE and TRUNCATE TABLE are operations that cannot be undone. When you drop a table on a production database, there's no "recycle bin". Always verify the table name before executing, or turn off autocommit and run it inside a transaction so you can ROLLBACK (we'll learn this in episode 11).
All the DDL above can be executed directly in psql. To verify the results, use the meta-commands we learned in episode 0:
\dt
\d users\dt lists the tables, while \d users shows the columns, types, and constraints of the users table.
| # | Mistake | Symptom | Solution |
|---|---|---|---|
| 1 | Forgetting the unique constraint on email | Users can register with duplicate emails | Add UNIQUE during CREATE TABLE |
| 2 | Putting the FK on the wrong table | Reversed relationships and weird JOIN queries | Remember the rule: the "many" table stores the FK |
| 3 | DROP DATABASE while connected to that database | Error database is being accessed by other users | Switch to another database first |
| 4 | TRUNCATE without understanding its effect | All data gone without warning | Make sure this is really what you want |
In this episode 2 we've built a solid design foundation: modeling data with Entity Relationship Diagrams, understanding the roles of Primary Keys, Foreign Keys, and Unique Constraints, mastering the normalization principles from 1NF to BCNF, and practicing DDL to create, alter, and drop databases and tables.
Key takeaways:
CREATE/ALTER/DROP/TRUNCATE is the DDL toolkit you must master.In episode 3, we'll dive deeper into the raw materials of a database: Data Types in PostgreSQL and constraint management — from numeric, string, date-time, boolean, and UUID types, to various constraints like NOT NULL, FOREIGN KEY with referential actions, and CHECK. Your skill set will shift from "can create a table" to "creating correct tables".