SQL Fundamentals
Querying with SELECT
SELECT name, city, marks -- π which columns
JrCodex·7 min read
Jr Codex DBMS Notes
Level: Beginner Prerequisites: Chapter 1: DDL — Defining Schema Time to complete: ~20 minutes
Table of Contents
- The Shape of a Query
- Filtering with WHERE
- Pattern Matching and Ranges
- Handling NULL
- Expressions and CASE
- Sorting and Limiting
- Summary & Next Steps
1. The Shape of a Query
SELECT name, city, marks -- π which columns
FROM students -- which relation
WHERE marks > 80 -- σ which rows
ORDER BY marks DESC -- presentation only
LIMIT 3; -- presentation onlyTwo Habits to Form Now
─────────────────────────────────────────
NAME YOUR COLUMNS. SELECT * is fine when
exploring and a liability in code — it breaks
when a column is added, fetches data you do not
need, and prevents index-only scans (Module 5).
ORDER BY IS NOT OPTIONAL when order matters.
Relations are unordered (Module 2, Chapter 1);
without ORDER BY the database may return rows in
any order, and that order can change when the
plan changes.
─────────────────────────────────────────
2. Filtering with WHERE
SELECT name, marks FROM students WHERE city = 'Pune';
SELECT name, marks FROM students WHERE marks >= 80;
SELECT name FROM students WHERE city = 'Pune' AND marks > 85;
SELECT name FROM students WHERE city = 'Delhi' OR city = 'Mumbai';
SELECT name FROM students WHERE NOT city = 'Pune';Operator Precedence
─────────────────────────────────────────
NOT binds tighter than AND, which binds tighter
than OR.
So this:
WHERE city = 'Pune' AND marks > 85 OR marks > 90
Means this:
WHERE (city = 'Pune' AND marks > 85) OR marks > 90
Which probably is not what you meant. PARENTHESISE
whenever AND and OR appear together — always, not
only when unsure.
─────────────────────────────────────────
-- Comparison operators
= <> != < > <= >=
-- Set membership: IN is cleaner than a chain of ORs
SELECT name FROM students WHERE city IN ('Delhi','Mumbai');
SELECT name FROM students WHERE city NOT IN ('Pune');3. Pattern Matching and Ranges
-- LIKE: % matches any sequence, _ matches exactly one character
SELECT name FROM students WHERE name LIKE 'A%'; -- starts with A
SELECT name FROM students WHERE name LIKE '%a'; -- ends with a
SELECT name FROM students WHERE name LIKE '%ee%'; -- contains 'ee'
SELECT name FROM students WHERE name LIKE '_a%'; -- 'a' as 2nd character
-- BETWEEN is inclusive at BOTH ends
SELECT name, marks FROM students WHERE marks BETWEEN 70 AND 85;
-- identical to: marks >= 70 AND marks <= 85The LIKE Performance Trap
─────────────────────────────────────────
LIKE 'A%' CAN use an index — the prefix is
known, so the B+ tree can seek to
it (Module 5, Chapter 2).
LIKE '%son' CANNOT. A leading wildcard means
every row must be examined.
For substring search at scale you need a
full-text index or a trigram index — not LIKE.
This single fact explains a lot of slow search
boxes.
─────────────────────────────────────────
BETWEEN and Dates
─────────────────────────────────────────
BETWEEN '2026-01-01' AND '2026-01-31' on a
TIMESTAMP silently excludes almost all of the
31st — because '2026-01-31' means midnight at
its start.
Use a half-open range instead:
WHERE ts >= '2026-01-01' AND ts < '2026-02-01'
Half-open ranges also compose cleanly across
month boundaries, which BETWEEN does not.
─────────────────────────────────────────
4. Handling NULL
Module 2, Chapter 1 covered the theory. This is the practice.
SELECT name FROM students WHERE marks IS NULL; -- Sam
SELECT name FROM students WHERE marks IS NOT NULL; -- the other five
SELECT name FROM students WHERE marks = NULL; -- ALWAYS empty. Never write this.The Exclusion Trap
─────────────────────────────────────────
SELECT name FROM students WHERE marks < 80;
Returns Ravi and Karan — but NOT Sam, whose marks
are NULL. Sam is neither below 80 nor at-or-above
it; the comparison is UNKNOWN and the row is
dropped.
If "not yet assessed" should appear in a
low-scorers report, you must ask for it:
WHERE marks < 80 OR marks IS NULL
─────────────────────────────────────────
-- COALESCE: first non-null argument
SELECT name, COALESCE(marks, 0) AS marks_or_zero FROM students;
SELECT name, COALESCE(city, 'Unknown') AS city FROM students;
-- NULLIF: NULL when the two arguments are equal — useful to avoid divide-by-zero
SELECT total / NULLIF(count_of_items, 0) AS average FROM sales;Displaying vs Filtering
─────────────────────────────────────────
COALESCE is for DISPLAY — turning NULL into
something readable in the output.
It is not a substitute for handling NULL in
WHERE. Wrapping a column in COALESCE inside a
WHERE clause also prevents the index on that
column from being used (Module 5, Chapter 4).
─────────────────────────────────────────
5. Expressions and CASE
-- Arithmetic and derived columns
SELECT name, marks, marks * 1.1 AS boosted FROM students;
SELECT name || ' (' || COALESCE(city,'?') || ')' AS label FROM students;
-- CASE: SQL's if/else
SELECT name,
marks,
CASE
WHEN marks IS NULL THEN 'Not assessed'
WHEN marks >= 85 THEN 'Distinction'
WHEN marks >= 70 THEN 'Pass'
ELSE 'Needs work'
END AS band
FROM students
ORDER BY marks DESC NULLS LAST;CASE Evaluates Top-Down
─────────────────────────────────────────
The FIRST matching WHEN wins, so order matters.
Putting `WHEN marks >= 70` before
`WHEN marks >= 85` would band everyone above 85
as 'Pass', because the first condition already
matched.
Also: put the NULL check FIRST. `marks >= 85`
is UNKNOWN for NULL, so it falls through to
ELSE and Sam would be labelled 'Needs work'
rather than 'Not assessed'.
─────────────────────────────────────────
6. Sorting and Limiting
SELECT name, marks FROM students ORDER BY marks DESC;
SELECT name, city, marks FROM students ORDER BY city ASC, marks DESC; -- two keys
-- NULL placement is explicit in standard SQL
SELECT name, marks FROM students ORDER BY marks DESC NULLS LAST;
-- Paging
SELECT name, marks FROM students ORDER BY marks DESC LIMIT 3;
SELECT name, marks FROM students ORDER BY marks DESC LIMIT 3 OFFSET 3; -- page 2Ties Make Paging Unstable
─────────────────────────────────────────
If several rows share the sort value, their
relative order is UNDEFINED and may differ
between runs.
Page 1 and page 2 can then both contain the same
row, and another row appears on neither.
FIX: always add a unique tie-breaker.
ORDER BY marks DESC, id ASC
This is a real, common, hard-to-reproduce bug in
paginated APIs.
─────────────────────────────────────────
OFFSET Does Not Scale
─────────────────────────────────────────
LIMIT 20 OFFSET 100000 makes the database
produce 100,020 rows and discard 100,000 of them.
Deep pages get linearly slower.
KEYSET PAGINATION instead — remember the last row
and seek past it:
SELECT ... WHERE (marks, id) < (:last_marks, :last_id)
ORDER BY marks DESC, id DESC LIMIT 20;
Constant time at any depth, because the index
seeks directly (Module 5).
─────────────────────────────────────────
7. Summary & Next Steps
Key Takeaways
- Name your columns rather than using
SELECT *, and never rely on row order without an explicitORDER BY. - Parenthesise whenever
ANDandORappear together —NOTbinds tightest, thenAND, thenOR. - A leading wildcard in
LIKEmakes an index unusable, andBETWEENon timestamps silently loses the last day; use half-open ranges. - Comparisons never match NULL, so exclusion filters silently drop those rows; add
OR col IS NULLwhen they belong in the result.
Concept Check
WHERE marks < 80omits a student. Which one, why, and what is the fix?- Why does
LIKE '%son'force a full scan whileLIKE 'A%'does not? - A paginated API occasionally shows the same row on two consecutive pages. What is the cause and the one-line fix?
Next Chapter
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DBMS Index