SQL Interview Patterns
After you’ve solved enough SQL interview questions, you start noticing that the prompt wording changes, but the query shape stays the same. This class is a quick “pattern reminder” you can scan before interviews to refresh the handful of solution templates that cover most questions.
Below are 10 common patterns, each with a short “when you see it” description, a reliable boilerplate, and what to do next.
Give me all X, but only if Y
You’ll see: “Show users who…”, “Orders in the last 30 days…”, “Events matching…”
What to do when you see it: clarify the scope first (time window, status, definition of “active”), then filter early.
SQLSELECT
...
FROM x
WHERE
<row_filters>
AND <time_window_filter>;
Common considerations:
- Is “last X days” relative to today or a fixed date?
- Does the output need all columns or a count/sum?
Existence checks
You’ll see: “users who have at least one…”, “customers who never…”, “entities without…”
What to do when you see it: default to EXISTS / NOT EXISTS when you only care that a match exists (it’s naturally duplicate-safe).
SQL-- Has (at least one matching row)
SELECT
a.*
FROM a
WHERE EXISTS (
SELECT 1
FROM b
WHERE b.a_id = a.id
AND <b_filters>
);
-- Has-not (no matching rows)
SELECT
a.*
FROM a
WHERE NOT EXISTS (
SELECT 1
FROM b
WHERE b.a_id = a.id
AND <b_filters>
);
Filter on an aggregate
You’ll see: “at least 3 weeks…”, “>= 2 purchases…”, “avg spend above…”
What to do when you see it: decide whether the filter is on rows (WHERE) or groups (HAVING). If it’s a complex group metric, compute it first, then filter.
SQLSELECT
group_key,
COUNT(*) AS cnt
FROM t
GROUP BY group_key
HAVING COUNT(*) >= 3;
Two-step (cleaner for complex filters)
SQLWITH per_group AS (
SELECT
group_key,
<agg_expr> AS metric
FROM t
GROUP BY group_key
)
SELECT *
FROM per_group
WHERE metric >= <threshold>;
Pre-aggregating
You’ll see: “total paid per user”, “revenue by country”, “counts after joins”
What to do when you see it: identify the “many” side and pre-aggregate it down to the join key before joining.
SQLWITH b_agg AS (
SELECT
key,
SUM(metric) AS metric
FROM b
WHERE <filters>
GROUP BY key
)
SELECT
a.key,
COALESCE(SUM(b_agg.metric), 0) AS metric
FROM a
LEFT JOIN b_agg
ON b_agg.key = a.key
GROUP BY a.key;
If you join two “many” tables to the same base, totals will multiply unless you pre-agg.
Conditional aggregation
You’ll see: “count X and Y in one table”, “funnel by step”, “segment by device”
What to do when you see it: keep the grain stable, and use SUM(CASE WHEN ...) (or COUNT with NULLs) to compute multiple metrics in one pass.
SQLSELECT group_key, SUM(CASE WHEN condition_1 THEN 1 ELSE 0 END) AS metric_1, SUM(CASE WHEN condition_2 THEN 1 ELSE 0 END) AS metric_2 FROM t GROUP BY group_key;
Are we counting rows, or unique users? ← May need COUNT(DISTINCT user_id) or pre-agg.
Rates, shares, and NULL-safe division
You’ll see: “conversion rate”, “CTR”, “share of total”, “percent of users who…”
What to do when you see it: define numerator/denominator explicitly, force float math, and guard divide-by-zero with NULLIF.
SQLSELECT group_key,
numerator,
denominator,
100.0* numerator/NULLIF(denominator,0) AS pct
FROM (...);
- Rate based on “users” or “events”? ← Changes the denominator.
- Should empty groups show NULL, 0, or be excluded?
Ranking
You’ll see: “top 3”, “2nd highest”, “latest order”, “nth transaction”
What to do when you see it: use ROW_NUMBER() for exactly N rows per group; use RANK()/DENSE_RANK() if ties should be kept. Always add a deterministic tie-breaker.
SQLSELECT *
FROM (
SELECT
t.*,
ROW_NUMBER() OVER (
PARTITION BY group_key
ORDER BY sort_key DESC, tie_breaker DESC
) AS rn
FROM t
) AS x
WHERE rn <= N;
“Top 3” with ties ← do they want exactly 3 rows or all ties at the cutoff?
Rolling metrics
You’ll see: “7-day rolling average”, “trailing 28 days”, “cumulative revenue”
What to do when you see it: first compute the metric at the correct time grain (day/week/month), then apply a window frame.
SQLSELECT dt, metric, AVG(metric) OVER ( ORDER BY dt ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) AS rolling_7 FROM daily_metrics;
If dates are missing, “7 rows” ≠ “7 days”. Here, a date spine might be necessary.
Previous vs current
You’ll see: “week-over-week change”, “difference from previous”, “growth rate”
What to do when you see it: decide if “previous” means previous row or previous period (calendar-aligned), then use LAG().
SQLSELECT
key,
dt,
metric,
metric - LAG(metric) OVER (
PARTITION BY key
ORDER BY dt
) AS delta
FROM t;
For period-over-period (e.g., “previous month”), ensure your dt is already bucketed to month.
Self-joins
You’ll see: “pairs of items”, “before/after events”, “employee-manager”, “referrer relationships”
What to do when you see it: alias the same table twice, and write a rule to avoid duplicates or self-pairs (a.id < b.id is a common trick).
SQLSELECT
a.id AS left_id,
b.id AS right_id
FROM t AS a
JOIN t AS b
ON <pair_condition>
WHERE a.id < b.id;
Pair explosions are real ← filter the dataset first (time window, paid only, etc.).
Syntax cheat sheet
The following section is intentionally syntax-only: short, copyable snippets you can reuse while practicing. It’s not meant to teach concepts, just to reduce “blank page” time when you recognize a pattern.
Use it as a quick reference for common SQL building blocks (filters, windows, bucketing, NULL-safety, ranking, existence checks).
