📝 SQL Query Patterns¶
The query shapes interviewers ask again and again (LeetCode SQL 50 style). Each has a worked query + what it's testing. Theory behind them: SQL Theory Q&A. Drill: Query Practice Set.
Sample schema used below:
Employee(id, name, salary, dept_id, manager_id)
Department(id, name)
Sales(id, emp_id, amount, sold_at)
Joins (inner / outer / self)¶
-- Employees with their department name (INNER: only matched rows)
SELECT e.name, d.name AS dept
FROM Employee e
JOIN Department d ON e.dept_id = d.id;
-- All employees, dept name NULL if unassigned (LEFT OUTER)
SELECT e.name, d.name AS dept
FROM Employee e
LEFT JOIN Department d ON e.dept_id = d.id;
-- Self-join: each employee with their manager's name
SELECT e.name AS employee, m.name AS manager
FROM Employee e
LEFT JOIN Employee m ON e.manager_id = m.id;
GROUP BY + HAVING + aggregates¶
-- Departments with average salary > 50k (HAVING filters groups, WHERE filters rows)
SELECT d.name, COUNT(*) AS headcount, AVG(e.salary) AS avg_sal
FROM Employee e
JOIN Department d ON e.dept_id = d.id
WHERE e.salary IS NOT NULL -- row filter (before grouping)
GROUP BY d.name
HAVING AVG(e.salary) > 50000; -- group filter (after aggregation)
Subqueries vs CTEs¶
-- Employees earning above their department's average (correlated subquery)
SELECT e.name, e.salary
FROM Employee e
WHERE e.salary > (
SELECT AVG(e2.salary) FROM Employee e2 WHERE e2.dept_id = e.dept_id
);
-- Same idea, readable with a CTE
WITH dept_avg AS (
SELECT dept_id, AVG(salary) AS avg_sal FROM Employee GROUP BY dept_id
)
SELECT e.name, e.salary
FROM Employee e
JOIN dept_avg da ON e.dept_id = da.dept_id
WHERE e.salary > da.avg_sal;
Window functions (the mid-level differentiator)¶
-- Rank employees by salary within each department
SELECT name, dept_id, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drnk
FROM Employee;
-- Running total of sales over time
SELECT sold_at, amount,
SUM(amount) OVER (ORDER BY sold_at
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM Sales;
-- Compare each sale to the previous one (LAG)
SELECT sold_at, amount,
amount - LAG(amount) OVER (ORDER BY sold_at) AS delta_vs_prev
FROM Sales;
Nth-highest value¶
-- 2nd-highest salary (window approach - handles ties predictably)
WITH ranked AS (
SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM Employee
)
SELECT DISTINCT salary FROM ranked WHERE rnk = 2;
LIMIT 1 OFFSET 1), and how ties change the answer.
Finding & removing duplicates¶
-- Find duplicate emails
SELECT email, COUNT(*) FROM Users GROUP BY email HAVING COUNT(*) > 1;
-- Keep one row per duplicate group, delete the rest (by lowest id)
WITH dupes AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM Users
)
DELETE FROM Users WHERE id IN (SELECT id FROM dupes WHERE rn > 1);
Pivot (rows → columns)¶
-- Sales per employee split into columns by year (conditional aggregation)
SELECT emp_id,
SUM(CASE WHEN EXTRACT(YEAR FROM sold_at) = 2024 THEN amount ELSE 0 END) AS y2024,
SUM(CASE WHEN EXTRACT(YEAR FROM sold_at) = 2025 THEN amount ELSE 0 END) AS y2025
FROM Sales
GROUP BY emp_id;
CASE inside an aggregate - the portable way (vs vendor PIVOT).
Date bucketing¶
-- Daily sales totals
SELECT DATE_TRUNC('day', sold_at) AS day, SUM(amount) AS total
FROM Sales
GROUP BY DATE_TRUNC('day', sold_at)
ORDER BY day;
Common interview traps¶
- Putting an aggregate in
WHERE(useHAVING). - Forgetting
LEFT JOIN+WHERE right.col = xsilently turns it back into an inner join (filter in theONinstead). - Assuming
ROW_NUMBERandRANKare the same - they differ on ties. COUNT(*)vsCOUNT(col)- the latter skips NULLs.- Selecting non-aggregated columns not in
GROUP BY(invalid in standard SQL).
Related¶
- Theory behind these → SQL Theory Q&A
- Timed practice → Query Practice Set
- Q&A → Databases Q&A