SQL Basics & Query Fundamentals
SELECT, WHERE, ORDER BY, LIMIT
SELECT name AS employee_name, salary * 12 AS annual_salary FROM employees;
SELECT * FROM employees WHERE department = 'Engineering' AND salary > 80000;
SELECT * FROM employees WHERE salary BETWEEN 50000 AND 100000;
SELECT * FROM employees WHERE department IN ('Engineering', 'Product', 'Design');
SELECT * FROM employees WHERE name LIKE 'A%'; -- starts with A
SELECT * FROM employees WHERE name ILIKE 'ann%'; -- case-insensitive (Postgres)
SELECT name, department, salary FROM employees ORDER BY department ASC, salary DESC;
SELECT * FROM employees ORDER BY id LIMIT 10 OFFSET 10; -- pagination, page 2
DISTINCT
SELECT DISTINCT department FROM employees;
SELECT COUNT(DISTINCT department) FROM employees;
NULL Handling
SELECT * FROM employees WHERE manager_id IS NULL; -- never = NULL, always IS NULL
SELECT name, COALESCE(phone, email, 'no contact') AS contact FROM employees;
SELECT total_sales / NULLIF(total_orders, 0) AS avg_order_value FROM sales_summary; -- divide-by-zero guard
SELECT NULL + 5; -- NULL — any arithmetic with NULL is NULL
SELECT NULL = NULL; -- NULL, not TRUE — this is why `= NULL` in WHERE never matches
Why NULL = NULL isn't TRUE: NULL represents "unknown," and the comparison of two unknowns is itself unknown — not a special-cased equality. This is the root cause behind the classic NOT IN + NULL trap (see [[SQL JOINs]]'s anti-join section).
CASE WHEN
SELECT name, salary,
CASE
WHEN salary >= 100000 THEN 'Senior'
WHEN salary >= 60000 THEN 'Mid'
ELSE 'Junior'
END AS level
FROM employees;
-- CASE inside aggregation — conditional counting
SELECT
COUNT(CASE WHEN salary >= 100000 THEN 1 END) AS senior_count,
COUNT(CASE WHEN salary < 60000 THEN 1 END) AS junior_count
FROM employees;
Arithmetic — the Integer Division Gotcha
SELECT 7 / 2; -- 3 — integer division truncates (many DBs)
SELECT 7 / 2.0; -- 3.5 — force decimal division
SELECT 7::FLOAT / 2; -- 3.5
SELECT ROUND(3.14159, 2); -- 3.14
SELECT CEIL(3.1); -- 4
SELECT FLOOR(3.9); -- 3
SELECT GREATEST(10, 20, 15); -- 20 — row-level max of args, not an aggregate
The Relational Division Pattern
"Find all X such that X relates to EVERY Y in a list" — a genuinely distinct query shape from a normal filter.
-- Users who ordered ALL of a required set of products
SELECT user_id
FROM orders
WHERE product_id IN (SELECT id FROM required_products)
GROUP BY user_id
HAVING COUNT(DISTINCT product_id) = (SELECT COUNT(*) FROM required_products);
-- Students who passed ALL exams (no failures at all)
SELECT student_id FROM exam_results
GROUP BY student_id
HAVING SUM(CASE WHEN passed = FALSE THEN 1 ELSE 0 END) = 0;
The pattern: filter to the relevant rows, group, then compare a count against the total required count — matching means the group satisfies every required condition, not just at least one.
SQL Execution Order (Logical, Not Written)
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
WHERE runs before aggregation (can't reference an aggregate); HAVING runs after. SELECT aliases aren't visible in WHERE/GROUP BY because those clauses logically execute before SELECT assigns the alias.
Interview Drills
- Why does
WHERE COUNT(*) > 5fail with a syntax/semantic error? —WHEREexecutes beforeGROUP BY/aggregation in logical execution order — no aggregate value exists yet at that point in the pipeline. UseHAVING, which runs after aggregation. - A query does
SELECT AVG(score) FROM resultsand the score column has NULLs. What does the result reflect? —AVG(like all standard aggregates) silently skips NULLs — it computes the average of non-NULL values only, which is not the same as treating NULL as 0; useAVG(COALESCE(score, 0))if zero-treatment is actually intended.
Cross-links
[[SQL JOINs]] · [[SQL Aggregates & GROUP BY]] · [[SQL Subqueries & CTEs]]