1. The Relational Model: Tables, Rows & Keys
A relational database stores data in tables — a fixed set of named, typed columns, with each row holding one record. Every table should have a primary key, a column (or set of columns) that uniquely identifies each row.
users
+----+----------+---------------------+
| id | name | email |
+----+----------+---------------------+
| 1 | Asha | asha@example.com |
| 2 | Ravi | ravi@example.com |
+----+----------+---------------------+
"Relational" refers to how tables relate to each other — an orders
table doesn't repeat a customer's name and email on every order; it stores a
user_id that references the users table's primary key.
This — storing each fact exactly once, and referencing it elsewhere — is the core
idea Week 5's normalization builds a full framework around.
2. Installing PostgreSQL & Connecting with psql
brew install postgresql@16
brew services start postgresql@16
# Windows: download the installer from postgresql.org/download
# Ubuntu/Debian:
sudo apt install postgresql postgresql-contrib
psql -U postgres
-- once connected:
CREATE DATABASE learn_sql;
\c learn_sql
psql commands starting with a backslash (\c,
\dt, \d tablename) are client-side meta-commands, not
SQL — \dt lists tables, \d orders describes a specific
table's columns, both genuinely useful while learning. A GUI client (TablePlus,
DBeaver, or pgAdmin) is a fine alternative once the command line gets tedious, but
this course uses psql throughout since it's universal and scriptable.
3. SELECT, WHERE & ORDER BY
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
age INT
);
INSERT INTO users (name, email, age) VALUES
('Asha', 'asha@example.com', 30),
('Ravi', 'ravi@example.com', 25),
('Priya', 'priya@example.com', 28);
SELECT name, email FROM users; -- specific columns
SELECT * FROM users; -- every column
SELECT * FROM users WHERE age > 26; -- filtering rows
SELECT * FROM users WHERE name = 'Asha';
SELECT * FROM users ORDER BY age DESC; -- sorting, newest/oldest first
SELECT * FROM users ORDER BY age ASC LIMIT 1; -- the single youngest user
SQL clauses have a fixed order (SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT)
but a different execution order — FROM and
WHERE effectively run first to determine which rows are in play,
SELECT picks which columns to return from those rows, and
ORDER BY/LIMIT apply last. This distinction matters more
once GROUP BY and aggregates enter next week.
4. Data Types & NULL
| Type | Use for |
|---|---|
INT / BIGINT | Whole numbers — IDs, counts |
TEXT / VARCHAR(n) | Strings — TEXT is unlimited length and the usual default in Postgres |
BOOLEAN | True/false flags |
DATE / TIMESTAMP | Dates, and dates with a time component |
NUMERIC(p, s) | Exact decimal numbers — money, never FLOAT for currency |
NULL means "unknown" or "not applicable" — it is not zero, not an
empty string, and not equal to itself in a comparison.
SELECT * FROM users WHERE age = NULL; -- returns nothing, always — wrong
SELECT * FROM users WHERE age IS NULL; -- the correct way to check for NULL
age = NULL never evaluates to true for any row, because comparing
anything to "unknown" is itself unknown — this is the single most common beginner
mistake in SQL, and IS NULL/IS NOT NULL are the only
correct way to test for it.
5. Hands-on Exercise
Build and query a small library database
Create a table, insert real data, and write queries exercising everything from this week.
Requirements:
- Create a
bookstable with columns forid(primary key),title,author,published_year(INT), andavailable(BOOLEAN). - Insert at least 8 rows, including at least one book with a
NULLpublished_year(representing an unknown publication date). - Write a query returning only
titleandauthorfor books published after 2010, ordered bypublished_yeardescending. - Write a query finding all books with an unknown publication year, using
IS NULLcorrectly. - Write a query returning the 3 most recently published books using
ORDER BYandLIMIT.
If a query filtering for published_year = NULL mysteriously returns zero rows even though you know NULL rows exist, that's not a bug — it's exactly the NULL comparison trap from this week's last section. Rewrite it as published_year IS NULL.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does a well-designed orders table store a customer's user_id rather than repeating their name and email on every order row?
Why does a well-designed orders table store a customer's user_id rather than repeating their name and email on every order row?
Repeating the same customer data on every order duplicates it — if the customer's email changes, every single order row would need updating, and any row updated inconsistently creates contradictory data. Storing a user_id that references the users table keeps each fact in exactly one place, referenced everywhere it's needed — the foundational idea Week 5's normalization builds on directly.
Q2
What is the actual execution order of SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT, and why does it matter?
What is the actual execution order of SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT, and why does it matter?
Despite SELECT being written first, PostgreSQL conceptually evaluates FROM/WHERE first (determining which rows are in play), then SELECT (which columns to return), then ORDER BY and LIMIT last. This matters because a column referenced in WHERE doesn't need to appear in SELECT's output list — the filter operates on the underlying rows, not the final projected columns.
Q3
Why does WHERE age = NULL return zero rows even for a table where several rows genuinely have a NULL age?
Why does WHERE age = NULL return zero rows even for a table where several rows genuinely have a NULL age?
NULL represents an unknown value, and comparing anything — including another NULL — to an unknown value produces an unknown result, not true. SQL's = operator can never evaluate to true against NULL, no matter what's being compared, which is exactly why a dedicated IS NULL operator exists as the only correct way to test for it.
Q4
Why should currency amounts use NUMERIC rather than a floating-point type?
Why should currency amounts use NUMERIC rather than a floating-point type?
Floating-point types store an approximation of most decimal values in binary, which can introduce tiny rounding errors that accumulate over many calculations — unacceptable for money, where every cent needs to be exact. NUMERIC(p, s) stores an exact decimal value with a defined precision and scale, with no approximation involved.