⌘ K
PARETO-OPTIMIZED · v1.0

The SQL syntax that cracks 90% of interviews

A focused reference covering the 20% of SQL that surfaces in 90% of interview questions — from foundational SELECT through window functions, recursive CTEs, and the classic patterns recruiters love to ask. Analogies where they help, diagrams where they clarify, runnable code everywhere it counts.

14
Topics
60+
Code Snippets
10
Interview Patterns
~90%
Coverage
01 / FOUNDATIONS

SELECT, WHERE, ORDER BY

The skeleton of every SQL query. Master the clause execution order first — most interview confusion comes from misunderstanding it.

Analogy

Think of a SQL query like filtering a spreadsheet: FROM opens the sheet, WHERE applies a filter, SELECT picks which columns to show, ORDER BY sorts, and LIMIT keeps only the first N rows.

FROM / JOIN WHERE GROUP BY HAVING SELECT DISTINCT ORDER BY LIMIT
Key Insight

SQL is evaluated in a different order than it's written. That's why you can't reference a column alias created in SELECT inside WHEREWHERE runs before SELECT.

Syntax
SELECT DISTINCT column1, column2 AS alias_name
FROM table_name
WHERE condition
ORDER BY column1 DESC, column2 ASC
LIMIT 10 OFFSET 20;

WHERE filters — operators you must know

OperatorMeaningExample
= <> > <ComparisonWHERE age > 25
AND OR NOTLogical combineWHERE dept='Eng' AND active=true
BETWEEN a AND bInclusive rangeWHERE age BETWEEN 25 AND 35
IN (a, b, c)Match any valueWHERE dept IN ('Eng','Sales')
LIKEPattern match (% any, _ one)WHERE name LIKE 'A%_n'
IS NULL / IS NOT NULLNULL check (≠ =)WHERE manager_id IS NULL
Example
-- Find active engineers over 30, ordered by hire date
SELECT id, name, hire_date, salary
FROM employees
WHERE dept = 'Engineering'
  AND age > 30
  AND manager_id IS NOT NULL
ORDER BY hire_date DESC, name ASC
LIMIT 20;
Gotcha

WHERE age = NULL always returns nothing. Use IS NULL. NULL behaves like "unknown" — it doesn't equal anything, including itself.

02 / JOINS

Combining Rows Across Tables

Joins are the most tested interview topic. Know each type's Venn diagram cold, and remember: JOINs combine rows horizontally; UNIONs combine them vertically.

Analogy

Imagine two contact lists: one with names and phone numbers, another with names and emails. A JOIN merges them by matching the name. An INNER JOIN keeps only people in both lists. A LEFT JOIN keeps everyone from the first list, leaving email blank if missing.

INNER JOIN
intersection only
LEFT JOIN
all left + matched
RIGHT JOIN
all right + matched
FULL OUTER
everything from both
A×B
CROSS JOIN
cartesian product
All join types
-- INNER: only matching rows
SELECT e.name, d.name AS dept
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id;

-- LEFT: every employee, dept name if it exists (else NULL)
SELECT e.name, d.name AS dept
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;

-- RIGHT: every department, employee if exists
SELECT e.name, d.name AS dept
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.id;

-- FULL OUTER: everything from both sides
SELECT e.name, d.name AS dept
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.id;

-- CROSS: every employee paired with every department
SELECT e.name, d.name AS dept
FROM employees e
CROSS JOIN departments d;

-- SELF JOIN: employee and their manager from same table
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Interview Tip

"Find customers who never placed an order" → use LEFT JOIN ... WHERE right.id IS NULL or NOT EXISTS. Both work; NOT EXISTS is often faster and handles NULLs safely.

Multi-table join
-- Orders → customers → employees (sales rep)
SELECT o.id, c.name AS customer, e.name AS sales_rep, o.amount
FROM orders o
JOIN customers c   ON o.customer_id = c.id
JOIN employees e   ON o.sales_rep_id = e.id
WHERE o.amount > 1000
ORDER BY o.amount DESC;
03 / AGGREGATION

GROUP BY & HAVING

Aggregation collapses many rows into summary rows. The pattern is rigid: SELECT columns must be either grouped or aggregated.

Analogy

Sorting a jar of M&Ms by color and counting each pile. GROUP BY color creates the piles. Aggregate functions like COUNT(*) count each pile. HAVING filters the piles (e.g., only piles with more than 5).

FunctionReturnsNULLs?
COUNT(*)All rows including NULLsCounts NULLs
COUNT(col)Non-NULL values in colSkips NULLs
COUNT(DISTINCT col)Unique non-NULL valuesSkips NULLs
SUM(col)Sum of numeric valuesSkips NULLs
AVG(col)Mean of non-NULL valuesSkips NULLs
MIN(col) / MAX(col)Smallest / largest valueSkips NULLs
Syntax
SELECT dept, COUNT(*) AS headcount, AVG(salary) AS avg_salary
FROM employees
WHERE active = true              -- filter rows BEFORE grouping
GROUP BY dept
HAVING COUNT(*) > 5              -- filter groups AFTER grouping
ORDER BY avg_salary DESC;
Common Mistake

Selecting a non-grouped, non-aggregated column:

SELECT dept, name, COUNT(*) FROM employees GROUP BY dept — invalid in strict SQL. name isn't grouped or aggregated. Either add it to GROUP BY or wrap it in an aggregate.

Multi-column grouping + HAVING
-- Average salary by department AND job title, only groups with 3+ people
SELECT dept, title, AVG(salary) AS avg_sal, COUNT(*) AS n
FROM employees
GROUP BY dept, title
HAVING COUNT(*) >= 3
ORDER BY dept, avg_sal DESC;

-- Find duplicate emails
SELECT email, COUNT(*) AS occurrences
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
Tip

WHERE filters rows before aggregation; HAVING filters groups after. Use HAVING with aggregate functions, WHERE with plain columns.

04 / SUBQUERIES

Queries Inside Queries

Subqueries let you build intermediate results inline. They appear in WHERE, FROM, and SELECT clauses — each with different rules.

TypeWhereReturnsUse case
ScalarSELECT / WHERE1 row, 1 colCompare to a single value
Multi-rowWHERE with IN / ANY / ALLMany rows, 1 colMatch against a list
CorrelatedWHERE / SELECTRe-runs per rowPer-row lookup
Derived tableFROMA result setPre-aggregate, then filter
EXISTSWHERETrue/FalseExistence check (NULL-safe)
All subquery patterns
-- 1. SCALAR subquery: employees earning more than company average
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

-- 2. IN subquery: employees in departments with budget over 1M
SELECT name
FROM employees
WHERE dept_id IN (
  SELECT id FROM departments WHERE budget > 1000000
);

-- 3. CORRELATED subquery: employees earning more than their dept average
SELECT e1.name, e1.salary, e1.dept
FROM employees e1
WHERE e1.salary > (
  SELECT AVG(e2.salary)
  FROM employees e2
  WHERE e2.dept = e1.dept        -- correlated to outer row
);

-- 4. DERIVED TABLE: pre-aggregate, then filter
SELECT dept, headcount
FROM (
  SELECT dept, COUNT(*) AS headcount
  FROM employees
  GROUP BY dept
) t
WHERE headcount > 10;

-- 5. EXISTS: customers who placed at least one order
SELECT c.name
FROM customers c
WHERE EXISTS (
  SELECT 1 FROM orders o
  WHERE o.customer_id = c.id
);
NULL Trap

NOT IN (subquery) breaks if the subquery returns any NULL. The whole result becomes empty because x NOT IN (NULL) is unknown, not true. Use NOT EXISTS instead — it's NULL-safe and usually faster.

05 / WINDOW FUNCTIONS

The Interview Differentiator

Window functions perform calculations across rows related to the current row — without collapsing them like GROUP BY does. This is the #1 topic that separates junior from senior candidates.

Analogy

Imagine a spreadsheet of students sorted by score. A window function is like adding a column that shows each student's rank — every original row stays, but you get an extra computed column based on neighboring rows. PARTITION BY is like restarting the rank counter for each class section.

Anatomy
FUNCTION_NAME() OVER (
  PARTITION BY column1, column2     -- optional: restart per group
  ORDER BY column3                  -- optional: defines row order
  ROWS BETWEEN ... AND ...          -- optional: frame definition
) AS alias

Ranking functions — know the differences

scoreROW_NUMBER()RANK()DENSE_RANK()
100111
100211
90332
80443
  • ROW_NUMBER() — always unique, no ties.
  • RANK() — ties get same rank, then skips (1,1,3,4).
  • DENSE_RANK() — ties get same rank, no skips (1,1,2,3). Best for "Nth highest" problems.
All window functions
-- Ranking: top 3 earners per department
SELECT dept, name, salary,
       DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
FROM employees;

-- ROW_NUMBER: assign sequential row ids per group
SELECT name, dept,
       ROW_NUMBER() OVER (PARTITION BY dept ORDER BY hire_date) AS seq
FROM employees;

-- LAG / LEAD: compare row to previous/next (e.g., MoM growth)
SELECT month, revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev_month,
       revenue - LAG(revenue) OVER (ORDER BY month) AS growth
FROM monthly_sales;

-- Running total (cumulative sum)
SELECT order_date, amount,
       SUM(amount) OVER (ORDER BY order_date) AS running_total
FROM orders;

-- Average within department (no ORDER BY = whole partition as frame)
SELECT name, dept, salary,
       AVG(salary) OVER (PARTITION BY dept) AS dept_avg
FROM employees;

-- NTILE: split into 4 quartiles by salary
SELECT name, salary,
       NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees;

-- FIRST_VALUE / LAST_VALUE in a partition
SELECT name, dept, salary,
       FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC) AS top_earner
FROM employees;

Frame clause — defining the "window"

Frame syntax
-- ROWS: physical row count. RANGE: logical value range.
SUM(amount) OVER (
  ORDER BY date
  ROWS BETWEEN 2 PRECEDING AND CURRENT ROW       -- 3-row rolling sum
)

SUM(amount) OVER (
  ORDER BY date
  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW  -- running total
)

SUM(amount) OVER (
  ORDER BY date
  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING  -- grand total per row
)
Why this matters

Window functions let you solve in one query what would otherwise need self-joins or multiple subqueries: running totals, ranks, top-N-per-group, gaps-islands, and time-series comparisons. They're almost always faster too.

06 / CTE

Common Table Expressions

CTEs are named temporary result sets that make complex queries readable. They're also recursive — enabling tree/hierarchy traversals that are otherwise painful.

Analogy

A CTE is like defining a helper variable in programming: WITH active_users AS (...) SELECT * FROM active_users. You build it once, name it, then reference it. Cleaner than nesting subqueries 4 levels deep.

Basic + multiple CTEs
WITH dept_stats AS (
  SELECT dept, AVG(salary) AS avg_sal, COUNT(*) AS headcount
  FROM employees
  GROUP BY dept
),
high_earners AS (
  SELECT name, dept, salary
  FROM employees
  WHERE salary > 100000
)
SELECT h.name, h.dept, h.salary, d.avg_sal, d.headcount
FROM high_earners h
JOIN dept_stats d ON h.dept = d.dept
ORDER BY h.salary DESC;

Recursive CTE — trees & hierarchies

Analogy

Think of a recursive CTE like following an org chart downward. The anchor picks the CEO. The recursive member says: "for each person found, find their direct reports." Keep going until no new people are found.

Employee hierarchy
WITH RECURSIVE org_tree AS (
  -- Anchor: top-level (no manager)
  SELECT id, name, manager_id, 1 AS depth,
         CAST(name AS VARCHAR(1000)) AS path
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive: find direct reports of previously found employees
  SELECT e.id, e.name, e.manager_id, o.depth + 1,
         CAST(o.path || ' > ' || e.name AS VARCHAR(1000))
  FROM employees e
  JOIN org_tree o ON e.manager_id = o.id
)
SELECT id, name, depth, path
FROM org_tree
ORDER BY path;

-- Generate a sequence of numbers 1..10
WITH RECURSIVE nums(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM nums WHERE n < 10
)
SELECT n FROM nums;
Watch out

Recursive CTEs without a proper termination condition can infinite-loop. Always include a WHERE clause in the recursive member that eventually becomes false.

07 / SET OPERATIONS

UNION, INTERSECT, EXCEPT

Set operations combine rows from two result sets vertically. Both queries must return the same number of columns with compatible types.

OperatorBehaviorDuplicates?
UNIONAll rows from bothRemoved
UNION ALLAll rows from bothKept (faster)
INTERSECTRows in bothRemoved
EXCEPT / MINUSRows in first onlyRemoved
Examples
-- UNION ALL: combine customer + employee names (keep duplicates)
SELECT name, email FROM customers
UNION ALL
SELECT name, email FROM employees;

-- INTERSECT: people who are both customers AND employees
SELECT name FROM customers
INTERSECT
SELECT name FROM employees;

-- EXCEPT: customers who are NOT employees
SELECT name FROM customers
EXCEPT
SELECT name FROM employees;
Performance

Use UNION ALL instead of UNION unless you specifically need duplicates removed. UNION triggers a sort/distinct that can be expensive on large datasets.

08 / CASE

Conditional Logic & NULL Helpers

CASE is SQL's if/then/else. Combined with aggregates, it powers pivots and conditional counts. Plus the trio of NULL helpers you must know.

CASE syntax
-- Searched CASE (more flexible, use this)
SELECT name, salary,
  CASE
    WHEN salary >= 150000 THEN 'Staff+'
    WHEN salary >= 100000 THEN 'Senior'
    WHEN salary >= 70000  THEN 'Mid'
    ELSE 'Junior'
  END AS level
FROM employees;

-- Simple CASE (compares single value for equality)
SELECT name,
  CASE dept
    WHEN 'Eng' THEN 'Engineering'
    WHEN 'Sales' THEN 'Sales'
    ELSE 'Other'
  END AS dept_name
FROM employees;

CASE inside aggregates — the pivot trick

Conditional aggregation
-- Count employees by dept in one row per department
SELECT
  COUNT(CASE WHEN dept = 'Eng'   THEN 1 END) AS eng_count,
  COUNT(CASE WHEN dept = 'Sales' THEN 1 END) AS sales_count,
  COUNT(CASE WHEN dept = 'Ops'   THEN 1 END) AS ops_count
FROM employees;

-- Pivot: revenue per quarter, one row per dept
SELECT dept,
  SUM(CASE WHEN quarter = 1 THEN revenue ELSE 0 END) AS q1,
  SUM(CASE WHEN quarter = 2 THEN revenue ELSE 0 END) AS q2,
  SUM(CASE WHEN quarter = 3 THEN revenue ELSE 0 END) AS q3,
  SUM(CASE WHEN quarter = 4 THEN revenue ELSE 0 END) AS q4
FROM quarterly_revenue
GROUP BY dept;

NULL helpers

FunctionWhat it doesExample
COALESCE(a, b, c)First non-NULL valueCOALESCE(phone, email, 'no contact')
NULLIF(a, b)NULL if a = b, else aNULLIF(denominator, 0) — avoids divide-by-zero
GREATEST(a, b, c)Largest of valuesGREATEST(q1, q2, q3, q4)
LEAST(a, b, c)Smallest of valuesLEAST(q1, q2, q3, q4)
Real-world combo
-- Safe percentage: divide but never by zero
SELECT
  dept,
  SUM(CASE WHEN active THEN 1 ELSE 0 END) AS active_count,
  COUNT(*) AS total,
  ROUND(
    SUM(CASE WHEN active THEN 1 ELSE 0 END) * 100.0 /
    NULLIF(COUNT(*), 0),
    2
  ) AS pct_active
FROM employees
GROUP BY dept;
09 / STRINGS

String Functions

Pattern matching, concatenation, slicing. Note: function names vary by database — know both flavors.

OperationPostgreSQL / StandardMySQL
ConcatenateCONCAT(a, b) or a || bCONCAT(a, b)
LengthLENGTH(s)CHAR_LENGTH(s)
SubstringSUBSTRING(s FROM 1 FOR 3)SUBSTRING(s, 1, 3) or SUBSTR()
Uppercase / LowercaseUPPER(s) / LOWER(s)same
TrimTRIM(s) / BTRIM / LTRIM / RTRIMsame
ReplaceREPLACE(s, 'a', 'b')same
PositionPOSITION('x' IN s)INSTR(s, 'x')
PadLPAD(s, n, '0')same
SplitSPLIT_PART(s, ',', 2)SUBSTRING_INDEX(s, ',', 2)
Group concatSTRING_AGG(s, ',' ORDER BY s)GROUP_CONCAT(s SEPARATOR ',')
Common patterns
-- Clean and normalize names
SELECT
  TRIM(name) AS clean_name,
  UPPER(TRIM(name)) AS upper_name,
  INITCAP(name) AS proper_name             -- PostgreSQL
FROM users;

-- Extract domain from email
SELECT email,
  SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain
FROM users;

-- Mask all but last 4 digits of SSN
SELECT
  CONCAT(REPEAT('•', LENGTH(ssn) - 4), RIGHT(ssn, 4)) AS masked
FROM users;

-- Combine rows into a comma-separated list
SELECT dept,
  STRING_AGG(name, ', ' ORDER BY name) AS all_names
FROM employees
GROUP BY dept;

-- Pattern matching with regex (PostgreSQL)
SELECT name FROM users
WHERE name ~ '^[A-Z][a-z]+$';              -- starts capital, rest lowercase
10 / DATES

Date & Time Functions

Date math shows up constantly in analytics interviews — retention, growth, time-between-events. Syntax varies a lot by dialect; learn the patterns.

OperationPostgreSQLMySQL
Now / todayNOW() / CURRENT_DATENOW() / CURDATE()
Add intervaldate + INTERVAL '7 days'DATE_ADD(date, INTERVAL 7 DAY)
Difference (days)date2 - date1DATEDIFF(date2, date1)
Extract partEXTRACT(YEAR FROM date)YEAR(date)
Truncate to monthDATE_TRUNC('month', date)DATE_FORMAT(date, '%Y-%m-01')
FormatTO_CHAR(date, 'YYYY-MM-DD')DATE_FORMAT(date, '%Y-%m-%d')
Cast string to datedate '2024-01-15' or CAST(s AS date)STR_TO_DATE(s, '%Y-%m-%d')
Common patterns
-- Users who signed up in the last 30 days
SELECT name, created_at
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days';

-- Orders grouped by month
SELECT
  DATE_TRUNC('month', order_date) AS month,
  COUNT(*) AS order_count,
  SUM(amount) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;

-- Average days between a user's first and second order
WITH order_seq AS (
  SELECT user_id, order_date,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS n
  FROM orders
)
SELECT AVG(second_date - first_date) AS avg_days_between
FROM (
  SELECT user_id,
    MAX(CASE WHEN n = 1 THEN order_date END) AS first_date,
    MAX(CASE WHEN n = 2 THEN order_date END) AS second_date
  FROM order_seq
  WHERE n <= 2
  GROUP BY user_id
) t
WHERE second_date IS NOT NULL;

-- Age from birthdate
SELECT name,
  EXTRACT(YEAR FROM AGE(CURRENT_DATE, birthdate))::int AS age
FROM users;
11 / DML

INSERT, UPDATE, DELETE, UPSERT

Data Manipulation Language — modifying rows. Less common in read-heavy interviews, but you'll need UPSERT and UPDATE-FROM-JOIN patterns.

INSERT
-- Single row
INSERT INTO employees (name, dept, salary)
VALUES ('Alice', 'Eng', 120000);

-- Multiple rows
INSERT INTO employees (name, dept, salary)
VALUES
  ('Bob',   'Eng',   95000),
  ('Carol', 'Sales', 85000),
  ('Dave',  'Eng',  110000);

-- INSERT from SELECT (load from another table)
INSERT INTO archive_employees (id, name, retired_on)
SELECT id, name, CURRENT_DATE
FROM employees
WHERE active = false;
UPDATE & DELETE
-- UPDATE with condition
UPDATE employees
SET salary = salary * 1.1, updated_at = NOW()
WHERE dept = 'Eng' AND performance = 'exceeds';

-- UPDATE from join (PostgreSQL syntax)
UPDATE employees e
SET salary = e.salary * 1.05
FROM departments d
WHERE e.dept_id = d.id AND d.budget > 1000000;

-- DELETE with condition
DELETE FROM employees
WHERE active = false AND last_login < CURRENT_DATE - INTERVAL '1 year';

-- DELETE using subquery
DELETE FROM orders
WHERE customer_id IN (
  SELECT id FROM customers WHERE deleted = true
);

-- DELETE duplicates keeping the lowest id
DELETE FROM users
WHERE id NOT IN (
  SELECT MIN(id) FROM users GROUP BY email
);

UPSERT (INSERT ... ON CONFLICT)

PostgreSQL UPSERT
-- Insert, or update on conflict
INSERT INTO employees (id, name, salary)
VALUES (1, 'Alice', 120000)
ON CONFLICT (id)
DO UPDATE SET salary = EXCLUDED.salary, name = EXCLUDED.name;

-- Insert, or do nothing
INSERT INTO subscriptions (user_id, plan)
VALUES (42, 'pro')
ON CONFLICT (user_id) DO NOTHING;
MySQL UPSERT
INSERT INTO employees (id, name, salary)
VALUES (1, 'Alice', 120000)
ON DUPLICATE KEY UPDATE salary = VALUES(salary);
12 / DDL

Schema, Constraints, Indexes

Data Definition Language — creating and modifying tables. Interviews rarely test DML/DDL directly, but you should know constraints, indexes, and views cold.

CREATE TABLE with all constraint types
CREATE TABLE employees (
  id           SERIAL PRIMARY KEY,                    -- auto-incrementing PK
  email        VARCHAR(255) NOT NULL UNIQUE,          -- no duplicates
  name         VARCHAR(100) NOT NULL,
  dept_id      INTEGER REFERENCES departments(id),    -- foreign key
  salary       NUMERIC(10, 2) CHECK (salary >= 0),    -- constraint
  hire_date    DATE DEFAULT CURRENT_DATE,             -- default value
  manager_id   INTEGER REFERENCES employees(id),      -- self-reference
  created_at   TIMESTAMP DEFAULT NOW(),

  -- Table-level constraints
  CONSTRAINT valid_salary CHECK (salary < 1000000),
  CONSTRAINT unique_dept_role UNIQUE (dept_id, email)
);

Common constraints

ConstraintWhat it enforces
PRIMARY KEYUnique + NOT NULL. One per table. Auto-indexed.
FOREIGN KEYValue must exist in referenced table's column. Prevents orphans.
UNIQUENo duplicate values. NULL allowed (usually multiple).
NOT NULLCannot be NULL.
CHECKCustom boolean condition must be true.
DEFAULTValue used when none provided on INSERT.
ALTER, DROP, TRUNCATE
-- Add / drop column
ALTER TABLE employees ADD COLUMN phone VARCHAR(20);
ALTER TABLE employees DROP COLUMN phone;

-- Add constraint
ALTER TABLE employees
  ADD CONSTRAINT positive_salary CHECK (salary >= 0);

-- Create index (for query speed)
CREATE INDEX idx_employees_dept     ON employees(dept_id);
CREATE UNIQUE INDEX idx_email       ON employees(email);
CREATE INDEX idx_orders_date_amount ON orders(order_date, amount);  -- composite

-- Drop & truncate (TRUNCATE is faster, can't be rolled back in some DBs)
DROP TABLE employees;          -- removes structure + data
TRUNCATE TABLE employees;      -- removes data, keeps structure

-- View: a saved query
CREATE VIEW active_employees AS
SELECT id, name, dept
FROM employees
WHERE active = true;
Indexing Essentials

Indexes speed up WHERE, JOIN ON, and ORDER BY — but slow down writes. Index foreign keys and columns you filter/sort on. Composite indexes work left-to-right: an index on (a, b, c) helps WHERE a=1 and WHERE a=1 AND b=2, but not WHERE b=2.

13 / PATTERNS

The 10 Classic Interview Patterns

These show up repeatedly across LeetCode, HackerRank, and live interviews. Memorize the patterns, not the answers — most variations are remixes of these.

01 Nth Highest Salary

Three approaches, from simplest to most portable. DENSE_RANK is preferred because it handles ties correctly.

Three solutions
-- Approach 1: LIMIT + OFFSET (MySQL/PostgreSQL, simplest)
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET (N - 1);            -- N=2 for 2nd highest

-- Approach 2: Subquery (works everywhere, but only for 2nd highest easily)
SELECT MAX(salary)
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

-- Approach 3: DENSE_RANK (modern, handles ties, scales to any N)
SELECT salary FROM (
  SELECT salary,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
  FROM employees
) t
WHERE rnk = N;
02 Find Duplicate Records

GROUP BY + HAVING is the canonical answer.

Solution
-- Duplicate emails
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

-- Duplicate (name, dept) pairs
SELECT name, dept, COUNT(*) AS cnt
FROM employees
GROUP BY name, dept
HAVING COUNT(*) > 1;
03 Delete Duplicates (Keep One)

Use ROW_NUMBER to mark all-but-one row per group, then delete the marked rows.

Solution
-- Using CTE + ROW_NUMBER
WITH dups 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 dups WHERE rn > 1);

-- Self-join approach (MySQL, older versions)
DELETE u1
FROM users u1
JOIN users u2
  ON u1.email = u2.email AND u1.id > u2.id;
04 Consecutive Numbers

The classic "find numbers appearing N times in a row." Uses ROW_NUMBER difference trick.

Solution
-- Find numbers appearing 3+ times consecutively
SELECT DISTINCT num
FROM (
  SELECT num,
    ROW_NUMBER() OVER (ORDER BY id) -
    ROW_NUMBER() OVER (PARTITION BY num ORDER BY id) AS diff
  FROM logs
) t
GROUP BY num, diff
HAVING COUNT(*) >= 3;

-- Alternative: self-join (more intuitive, less scalable)
SELECT DISTINCT a.num
FROM logs a
JOIN logs b ON a.id = b.id - 1 AND a.num = b.num
JOIN logs c ON b.id = c.id - 1 AND b.num = c.num;
05 Top N Per Group

The "Department Top 3" problem. Use DENSE_RANK if ties should be included, ROW_NUMBER if you need exactly N rows.

Solution
SELECT dept, name, salary
FROM (
  SELECT name, dept, salary,
    DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
  FROM employees
) t
WHERE rnk <= 3
ORDER BY dept, salary DESC;
06 Running Total / Cumulative Sum

Pure window function application.

Solution
-- Cumulative revenue over time
SELECT
  order_date,
  amount,
  SUM(amount) OVER (ORDER BY order_date
                    ROWS UNBOUNDED PRECEDING) AS running_total
FROM orders
ORDER BY order_date;

-- Per-customer running total
SELECT
  customer_id, order_date, amount,
  SUM(amount) OVER (PARTITION BY customer_id
                    ORDER BY order_date
                    ROWS UNBOUNDED PRECEDING) AS customer_total
FROM orders;
07 Year-over-Year / Month-over-Month Growth

LAG gives you the previous period's value in the same row, making growth calculations trivial.

Solution
SELECT
  year,
  revenue,
  LAG(revenue) OVER (ORDER BY year) AS prev_year,
  ROUND(
    (revenue - LAG(revenue) OVER (ORDER BY year)) * 100.0 /
    NULLIF(LAG(revenue) OVER (ORDER BY year), 0),
    2
  ) AS yoy_growth_pct
FROM yearly_revenue
ORDER BY year;
08 Tree / Hierarchy Traversal

Recursive CTE for org charts, category trees, conversation threads.

Solution
WITH RECURSIVE org_chain AS (
  SELECT id, name, manager_id, 1 AS level,
         CAST(name AS VARCHAR(1000)) AS path
  FROM employees
  WHERE manager_id IS NULL            -- start at the top

  UNION ALL

  SELECT e.id, e.name, e.manager_id, o.level + 1,
         CAST(o.path || ' > ' || e.name AS VARCHAR(1000))
  FROM employees e
  JOIN org_chain o ON e.manager_id = o.id
)
SELECT level, name, path FROM org_chain
ORDER BY path;
09 Pivot Rows to Columns

No PIVOT operator in most databases — use CASE + aggregate. The universal pattern.

Solution
-- Revenue per department, one column per quarter
SELECT
  dept,
  SUM(CASE WHEN quarter = 1 THEN revenue ELSE 0 END) AS q1,
  SUM(CASE WHEN quarter = 2 THEN revenue ELSE 0 END) AS q2,
  SUM(CASE WHEN quarter = 3 THEN revenue ELSE 0 END) AS q3,
  SUM(CASE WHEN quarter = 4 THEN revenue ELSE 0 END) AS q4,
  SUM(revenue) AS total
FROM quarterly_revenue
GROUP BY dept
ORDER BY total DESC;
10 Find Records With No Match

"Customers who never ordered." Use LEFT JOIN + IS NULL or NOT EXISTS.

Three solutions
-- LEFT JOIN approach (most intuitive)
SELECT c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL;

-- NOT EXISTS (NULL-safe, often fastest)
SELECT c.name
FROM customers c
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.customer_id = c.id
);

-- NOT IN (works, but breaks if orders.customer_id has NULLs)
SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);
14 / PERFORMANCE

Query Optimization Essentials

Senior interviews often include "how would you optimize this query?" Know these patterns cold.

Read the execution plan

EXPLAIN
-- View the query plan (PostgreSQL)
EXPLAIN SELECT * FROM employees WHERE dept_id = 5;

-- View plan + actually run it (shows real timing)
EXPLAIN ANALYZE SELECT * FROM employees WHERE dept_id = 5;

-- MySQL equivalent
EXPLAIN SELECT * FROM employees WHERE dept_id = 5;

Look for: Seq Scan (full table scan = bad on large tables), Index Scan (good), Bitmap Heap Scan (medium). Watch for nested loops with high row estimates.

Sargable predicates (Search ARGument ABLE)

Anti-patterns

These prevent index usage — the database must scan every row:

WHERE YEAR(date_col) = 2024 — function on column kills index. Use: WHERE date_col >= '2024-01-01' AND date_col < '2025-01-01'

WHERE LOWER(email) = 'alice@x.com' — use a case-insensitive collation or store lowercase.

WHERE col LIKE '%alice' — leading wildcard can't use index. LIKE 'alice%' can.

WHERE col + 1 = 10 — rewrite as col = 9.

Quick wins

  • Avoid SELECT * — fetch only needed columns. Saves I/O, network, memory.
  • Index foreign keys — most databases don't auto-index them; joins get slow.
  • Index columns in WHERE, JOIN ON, ORDER BY — but remember indexes cost write speed.
  • Use UNION ALL over UNION unless you need deduplication.
  • Use EXISTS over IN for large subquery result sets — short-circuits on first match.
  • Limit early — add LIMIT before complex joins if you only need a sample.
  • Filter in WHERE, not HAVING — WHERE runs before aggregation, cheaper.
  • Batch large deletesDELETE FROM logs WHERE date < ... LIMIT 10000 in a loop, not one massive DELETE.
Interview Framing

When asked "how would you optimize this query?" — walk through: (1) check the execution plan, (2) identify full table scans, (3) add indexes on filter/join columns, (4) rewrite non-sargable predicates, (5) consider denormalization or materialized views for read-heavy workloads.

No matches found. Try a different search term.