Advanced SQL & Internals

query optimization · window functions · indexing · ACID

1

Window Functions Deep-Dive

A window function computes across a set of rows related to the current row, without collapsing them like GROUP BY does. Every window function uses OVER (...).

Ranking functions

ROW_NUMBER()

Unique sequential number, no ties — always 1,2,3,4...

SELECT name, salary,
  ROW_NUMBER() OVER (
    ORDER BY salary DESC) AS rn
FROM employees;

RANK()

Ties share a rank; next rank skips (1,2,2,4).

SELECT name, salary,
  RANK() OVER (
    ORDER BY salary DESC) AS rnk
FROM employees;

DENSE_RANK()

Ties share a rank; next rank does not skip (1,2,2,3).

SELECT name, salary,
  DENSE_RANK() OVER (
    ORDER BY salary DESC) AS drnk
FROM employees;
salary: 90k 90k 80k 70k ROW_NUMBER 1 2 3 4 RANK 1 1 3 4 DENSE_RANK 1 1 2 3

Same two tied 90k salaries (highlighted) — watch how each function handles the tie differently.

Value functions

LAG()

Value from a previous row in the ordered window.

SELECT order_date, revenue,
  LAG(revenue, 1) OVER (
    ORDER BY order_date) AS prev_day
FROM daily_sales;

LEAD()

Value from a following row in the ordered window.

SELECT order_date, revenue,
  LEAD(revenue, 1) OVER (
    ORDER BY order_date) AS next_day
FROM daily_sales;

FIRST_VALUE() / LAST_VALUE()

First / last value in the frame — remember to set the frame explicitly for LAST_VALUE (see below).

SELECT name, salary,
  FIRST_VALUE(name) OVER (
    ORDER BY salary DESC) AS top_earner
FROM employees;

Aggregate window functions

Any aggregate (SUM, AVG, COUNT, MIN, MAX) can run as a window function by adding OVER (...) — rows stay ungrouped.

SELECT department, name, salary,
  AVG(salary) OVER (PARTITION BY department) AS dept_avg,
  SUM(salary) OVER (PARTITION BY department) AS dept_total
FROM employees;

Frame specifications

PARTITION BY groups rows (like GROUP BY, but no collapsing). ORDER BY inside OVER sets row order. The frame defines exactly which rows around the current row are included.

-- running total: every row up to and including the current one
SELECT order_date, revenue,
  SUM(revenue) OVER (
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM daily_sales;

-- 3-row moving average: current row + 2 before it
SELECT order_date, revenue,
  AVG(revenue) OVER (
    ORDER BY order_date
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) AS moving_avg_3d
FROM daily_sales;
ROWS vs RANGE: ROWS counts physical rows. RANGE counts by value — rows with the same ORDER BY value are treated as one peer group, useful when ties should move together.
2

CTEs vs Subqueries vs Views vs Materialized Views

ConstructPersists?Best for
WITH ... AS (CTE)No — scoped to one queryReadability, breaking a complex query into named steps, recursive queries.
SubqueryNo — inline, nestedOne-off, simple filters/lookups; gets unreadable when nested deeply.
ViewDefinition only (no data stored)Reusable logic shared across many queries; always reflects live data.
Materialized ViewYes — result set stored on diskExpensive aggregations queried often; needs manual/scheduled REFRESH.
-- CTE: readable, named step
WITH high_value AS (
  SELECT customer_id, SUM(amount) AS total
  FROM orders GROUP BY customer_id
)
SELECT * FROM high_value WHERE total > 10000;

-- View: saved, reusable definition
CREATE VIEW vw_high_value_customers AS
SELECT customer_id, SUM(amount) AS total
FROM orders GROUP BY customer_id;

-- Materialized View: stores the result, needs refresh
CREATE MATERIALIZED VIEW mv_daily_revenue AS
SELECT order_date, SUM(amount) AS revenue
FROM orders GROUP BY order_date;

REFRESH MATERIALIZED VIEW mv_daily_revenue;
Interview line: "A view is a saved query; a materialized view is a saved result." Views trade freshness for zero storage; materialized views trade storage/staleness for speed.
3

Database Indexing

An index is a separate, sorted lookup structure that lets the engine find rows without scanning the whole table.

50 20 80

B-Tree: balanced, sorted tree. Leaf nodes (yellow) are linked, so it handles equality and range queries (BETWEEN, >, ORDER BY) in O(log n).

key: "abc" hash() 0x7F bucket → row ptr

Hash index: key hashed to a bucket, O(1) average lookup — but only for exact-match (=); no ordering, so ranges and sorting can't use it.

Clustered vs Non-Clustered

ClusteredNon-Clustered
Data storageTable rows are physically sorted in index orderSeparate structure with pointers back to the row
How many per tableOne (it is the table)Many allowed
Lookup costDirect — data lives at the leafExtra hop ("bookmark lookup") to fetch the full row
Typical usePrimary keyFrequently filtered/sorted columns that aren't the PK

Composite indexes

An index on multiple columns, e.g. (last_name, first_name). Order matters — it only helps queries that filter on a left-to-right prefix of the columns.

CREATE INDEX idx_name ON employees(last_name, first_name);

-- ✅ uses the index (prefix match)
SELECT * FROM employees WHERE last_name = 'Shah';

-- ❌ does NOT use the index efficiently (skips the prefix)
SELECT * FROM employees WHERE first_name = 'Ana';
4

Query Optimization & Performance Tuning

Reading an execution plan

EXPLAIN ANALYZE
SELECT customer_id, SUM(amount)
FROM orders
WHERE order_date >= '2026-01-01'
GROUP BY customer_id;

EXPLAIN shows the planned steps and estimated cost; EXPLAIN ANALYZE actually runs the query and shows real timing too. Read it bottom-up / inside-out — the innermost operations run first and feed the ones above.

Index Scan

Uses an index to jump straight to matching rows. Fast, cost scales with result size, not table size.

Table / Seq Scan

Reads every row in the table. Fine for small tables or when most rows match — but a warning sign on a large filtered query.

Common tuning wins

Trap: OR conditions across different columns often prevent index use — consider rewriting as UNION of two indexed queries.
5

Database Transactions & ACID

A transaction is a group of operations that succeed or fail together.

Atomicity

All-or-nothing. If one step fails, the whole transaction rolls back.

Consistency

A transaction moves the database from one valid state to another — constraints always hold.

Isolation

Concurrent transactions don't see each other's half-finished work.

Durability

Once committed, changes survive — even a crash right after.

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;  -- both updates succeed together, or neither does
Everyday analogy: a bank transfer. Atomicity = money can't vanish mid-transfer. Isolation = another query can't see the sender debited but receiver not yet credited.
6

Production Query: CTEs + Window + Aggregation

A realistic multi-table query — monthly revenue per customer, their rank within each month, and a running total over time.

WITH monthly_customer_revenue AS (
  SELECT
    c.customer_id,
    c.customer_name,
    DATE_TRUNC('month', o.order_date) AS month,
    SUM(oi.quantity * oi.unit_price) AS revenue
  FROM customers c
  JOIN orders o       ON o.customer_id = c.customer_id
  JOIN order_items oi ON oi.order_id   = o.order_id
  GROUP BY c.customer_id, c.customer_name,
           DATE_TRUNC('month', o.order_date)
),
ranked AS (
  SELECT *,
    RANK() OVER (
      PARTITION BY month ORDER BY revenue DESC
    ) AS rank_in_month,
    SUM(revenue) OVER (
      PARTITION BY customer_id ORDER BY month
      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total_revenue
  FROM monthly_customer_revenue
)
SELECT customer_name, month, revenue, rank_in_month, running_total_revenue
FROM ranked
WHERE rank_in_month <= 3
ORDER BY month, rank_in_month;

Reads as: build the monthly revenue base in the first CTE → rank and accumulate in the second CTE → filter to top 3 per month in the final SELECT. Note the filter on rank_in_month happens in the outer query, since window function results can't be used in WHERE/HAVING directly.

7

SQL Interview Tricks & Traps — Cheat Sheet

Duplicate handling:
-- find duplicate rows by a key
SELECT email, COUNT(*)
FROM users GROUP BY email HAVING COUNT(*) > 1;

-- delete duplicates, keep the lowest id
DELETE FROM users
WHERE id NOT IN (
  SELECT MIN(id) FROM users GROUP BY email
);
Running totals & moving averages: use SUM()/AVG() OVER (ORDER BY ... ROWS BETWEEN ...) — see Section 1's frame specs. Forgetting the frame clause means the default frame is used, which can silently give the wrong answer for LAST_VALUE.
DENSE_RANK vs RANK — pick top-N per group without gaps:
-- gaps in RANK can skip valid "3rd place" ties
SELECT *, DENSE_RANK() OVER (
  PARTITION BY department ORDER BY salary DESC) AS drnk
FROM employees
QUALIFY drnk <= 3;  -- (Snowflake/BigQuery); else wrap in a subquery
Second-highest value without LIMIT/OFFSET:
SELECT MAX(salary) FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
NULLs break naive equality joins/filters: WHERE col != value silently drops NULL rows. Use WHERE col IS DISTINCT FROM value (Postgres) or explicit OR col IS NULL.
COUNT(*) vs COUNT(column): COUNT(*) counts all rows; COUNT(column) skips NULLs in that column — easy to misreport a metric if you don't notice the difference.
Window functions can't be filtered directly in WHERE/HAVING — they run after those clauses in logical order. Wrap in a CTE or subquery and filter in the outer query (as in Section 6's production example).
GROUP BY with an unaggregated column not in the GROUP BY list is an error in strict-mode engines (Postgres, SQL Server) — MySQL may silently allow it and pick an arbitrary value. Don't rely on that behavior.