Week 1: SQL Fundamentals — SELECT, WHERE, ORDER BY & the Relational Model

Almost every backend, from a two-person startup to a bank's core ledger, ultimately stores its real data in a relational database. This week installs PostgreSQL, connects to it for the first time, and covers the handful of clauses — SELECT, WHERE, ORDER BY — that make up the vast majority of queries anyone actually writes.

Module 1 of 10 Week 1 of 10 ~2–3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain what a relational database is, in terms of tables, rows, columns and keys
  • Install PostgreSQL and connect to it with psql
  • Write a SELECT query with WHERE, ORDER BY and LIMIT
  • Choose the right PostgreSQL data type for a column, and explain what NULL means

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.

a simple table, conceptually
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

terminal — macOS (Homebrew)
brew install postgresql@16
brew services start postgresql@16
terminal — Windows/Linux
# Windows: download the installer from postgresql.org/download
# Ubuntu/Debian:
sudo apt install postgresql postgresql-contrib
connecting with psql, PostgreSQL's command-line client
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

setting up a table to query
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);
basic queries
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

TypeUse for
INT / BIGINTWhole numbers — IDs, counts
TEXT / VARCHAR(n)Strings — TEXT is unlimited length and the usual default in Postgres
BOOLEANTrue/false flags
DATE / TIMESTAMPDates, 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.

the NULL trap
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

Hands-on

Build and query a small library database

Create a table, insert real data, and write queries exercising everything from this week.

Requirements:

  1. Create a books table with columns for id (primary key), title, author, published_year (INT), and available (BOOLEAN).
  2. Insert at least 8 rows, including at least one book with a NULL published_year (representing an unknown publication date).
  3. Write a query returning only title and author for books published after 2010, ordered by published_year descending.
  4. Write a query finding all books with an unknown publication year, using IS NULL correctly.
  5. Write a query returning the 3 most recently published books using ORDER BY and LIMIT.
Hint

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?

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?

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?

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?

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.