InterviewsVector

SQL Interview Knowledge & Query Lab

Master SQL through 102 original questions and challenges: define the result grain, write the query, explain the plan, handle edge cases, and make the Senior or Staff production call.

102
canonical items
16
Query Labs
26
performance + internals
29
Senior / Staff

Trace one interview answer end to end

  1. 01 · BUSINESS INTENT

    Latest paid order for every account, including accounts with no paid order.

  2. 02 · OUTPUT GRAIN

    One row per account

    Illustrative expected row
    accountorderpaid_at
    429012026-02-11
  3. 03 · SQL
    ROW_NUMBER() OVER (
      PARTITION BY account_id
      ORDER BY paid_at DESC,
               order_id DESC
    ) AS recency
  4. 04 · CONCEPTUAL PLAN
    1. Index range scan
    2. Window + row filter
    3. LEFT JOIN accounts
  5. 05 · VERIFY
    • Actual vs estimated rows
    • Buffers and sort spill
    • Representative account skew
    • p95 / p99 after rollout

18 topic areas · 12 layered cornerstone answers · updated

What should I study for a SQL interview?

Prepare in layers: first master result semantics—NULL, joins, grouping, and logical query order. Then practice defining output grain and writing joins, CTEs, and window functions from realistic schemas. Next learn how indexes, statistics, and physical plans shape runtime work. Senior interviews add transaction invariants, isolation, locks, migrations, incidents, capacity, and trade-offs that must be verified with evidence.

Portable reasoning. Explicit engine boundaries.

The hub teaches shared relational and optimizer models, then labels where PostgreSQL, MySQL, or SQL Server syntax and behavior diverge.

  1. 01

    Lead with portable semantics

    Grain, NULL behavior, join preservation, isolation invariants, and plan evidence transfer across engines.

  2. 02

    Label vendor syntax

    PostgreSQL examples mark DATE_TRUNC, FILTER, arrays, INTERVAL, extended statistics, and EXPLAIN options.

  3. 03

    Name behavior differences

    Defaults, isolation, MVCC cleanup, online DDL, plan caches, included columns, and optimizer features vary by engine and version.

Build from result semantics to Staff judgment

Do not jump from syntax drills to sharding slogans. Each stage produces an interview capability the next stage depends on.

  1. 0119 items

    Foundation

    Relational query semantics

    Build precise mental models for filtering, NULL, joins, grouping, and SQL's logical processing order.

    Predict query results before relying on trial and error.

  2. 0218 items

    Query craft

    Composable analytical SQL

    Use subqueries, CTEs, windows, and advanced patterns to solve realistic data requirements.

    Define output grain, write a correct query, and explain every transformation.

  3. 0310 items

    Data design

    Models, constraints & integrity

    Design keys, relationships, constraints, and schemas that make invalid states difficult to represent.

    Defend normalization and denormalization from workload and invariant needs.

  4. 0427 items

    Under the hood

    Indexes, optimizers & plans

    Connect SQL text to cardinality estimates, access paths, join algorithms, memory, sorting, and I/O.

    Read a conceptual plan and investigate why the optimizer chose it.

  5. 0518 items

    Production

    Transactions, concurrency & incidents

    Reason about isolation, locks, MVCC, pools, replicas, migrations, and failure under load.

    Move from symptom to evidence, mitigation, and verified recovery.

  6. 0610 items

    Staff judgment

    Database platform decisions

    Balance consistency, performance, ownership, capacity, cost, governance, and reversibility across teams.

    Make defensible decisions when no single database technique is universally correct.

Start with the Query Lab

Eighteen connected interview domains

Jump to filters →

Define the grain before you write the query

Sixteen original challenges move from joins and grouping through windows, recursion, cohorts, performance debugging, and plan interpretation. All schemas and rows are synthetic fixtures, and every solution stays in the server-rendered page.

  1. 01Read schema
  2. 02Name output grain
  3. 03Predict edge cases
  4. 04Verify the plan
01
FundamentalQueryPortable SQL

Regional order totals with zero-order regions

Return every region, its paid-order count, and paid revenue for January 2026—even when the region has no paid orders.

Output grain

One row per region.

Schema

CREATE TABLE regions (region_id INT PRIMARY KEY, name VARCHAR(40));
CREATE TABLE orders (
  order_id INT PRIMARY KEY,
  region_id INT REFERENCES regions(region_id),
  status VARCHAR(20),
  ordered_at TIMESTAMP,
  total_amount DECIMAL(12,2)
);

Think first

  • Which table contains the full reporting population?
  • Would a WHERE predicate remove the NULL-extended region rows?

Synthetic sample data

regions
region_idname
1North
2South
3West
orders
order_idregion_idstatusordered_attotal_amount
1011paid2026-01-08120
1021cancelled2026-01-0960
1032paid2026-01-1280
1042paid2026-02-0240

Expected output

Validate values and row grain before opening the solution.

result
regionpaid_orderspaid_revenue
North1120
South180
West00

Reasoning path

  1. 01

    Preserve regions

    Drive from regions and LEFT JOIN matching orders.

  2. 02

    Filter matches

    Put status and half-open date bounds in the ON clause.

  3. 03

    Aggregate

    COUNT the nullable order key and coalesce SUM for empty groups.

Reveal solution and explanationOPEN +
SELECT r.name AS region,
       COUNT(o.order_id) AS paid_orders,
       COALESCE(SUM(o.total_amount), 0) AS paid_revenue
FROM regions AS r
LEFT JOIN orders AS o
  ON o.region_id = r.region_id
 AND o.status = 'paid'
 AND o.ordered_at >= TIMESTAMP '2026-01-01 00:00:00'
 AND o.ordered_at <  TIMESTAMP '2026-02-01 00:00:00'
GROUP BY r.region_id, r.name
ORDER BY r.name;
How it works
  • The LEFT JOIN produces one NULL-extended row for West.
  • COUNT(o.order_id) ignores that NULL while SUM returns NULL, so COALESCE supplies zero.

Performance perspective

For a large orders table, index by the selective lookup path—often (region_id, status, ordered_at)—and verify with the real workload; low-cardinality status alone is rarely useful.

Edge cases

  • Refunded orders need an explicit business rule.
  • Use the reporting timezone when deriving month boundaries.

Interviewer follow-up: How would you add each region's percentage of company revenue without changing the output grain?

02
FundamentalQueryPortable SQL

Customers without a successful payment

Find active customers who have never had a successful payment, including customers with no payment attempts.

Output grain

One row per qualifying customer.

Schema

CREATE TABLE customers (customer_id INT PRIMARY KEY, name VARCHAR(80), active BOOLEAN);
CREATE TABLE payments (
  payment_id INT PRIMARY KEY,
  customer_id INT REFERENCES customers(customer_id),
  status VARCHAR(20),
  amount DECIMAL(12,2)
);

Think first

  • Is the requested output a list of payments or customers?
  • Can a payment status be NULL?

Synthetic sample data

customers
customer_idnameactive
1Ashatrue
2Bentrue
3Chentrue
4Dinafalse
payments
payment_idcustomer_idstatusamount
111succeeded40
122failed55
134succeeded25

Expected output

Validate values and row grain before opening the solution.

result
customer_idname
2Ben
3Chen

Reasoning path

  1. 01

    Filter the population

    Begin with active customers.

  2. 02

    Express absence

    Reject a customer only when a succeeded payment exists.

  3. 03

    Keep the grain

    Do not join and deduplicate when no payment columns are needed.

Reveal solution and explanationOPEN +
SELECT c.customer_id, c.name
FROM customers AS c
WHERE c.active = TRUE
  AND NOT EXISTS (
    SELECT 1
    FROM payments AS p
    WHERE p.customer_id = c.customer_id
      AND p.status = 'succeeded'
  )
ORDER BY c.customer_id;
How it works
  • The correlated subquery answers a yes/no question per customer.
  • NOT EXISTS is unaffected by unrelated NULL values in payments.
Alternative · LEFT anti-join
SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN payments p
  ON p.customer_id = c.customer_id AND p.status = 'succeeded'
WHERE c.active = TRUE AND p.payment_id IS NULL;

Correct when the NULL test uses a non-nullable matched key, but NOT EXISTS states the intent more directly.

Dialect note: BOOLEAN and TRUE are standard SQL and supported by PostgreSQL/MySQL; SQL Server commonly models the flag with BIT and compares it with 1.

Performance perspective

An index on payments(customer_id, status) supports the existence probe; compare plans because optimizers may rewrite both forms to an anti-join.

Edge cases

  • Decide whether reversed or refunded payments count as successful.
  • Never replace this blindly with NOT IN if the subquery can return NULL.

Interviewer follow-up: Return customers whose most recent payment attempt failed.

03
IntermediateQueryPostgreSQL

Months above each customer's own average spend

For each customer, return months whose paid spend is above that customer's average monthly paid spend.

Output grain

One row per customer-month above that customer's monthly average.

Schema

CREATE TABLE orders (
  order_id BIGINT PRIMARY KEY,
  customer_id BIGINT NOT NULL,
  paid_at DATE,
  status TEXT NOT NULL,
  amount NUMERIC(12,2) NOT NULL
);

Think first

  • What is the comparison grain?
  • Should months with no orders participate in the average?

Synthetic sample data

orders
order_idcustomer_idpaid_atstatusamount
1102026-01-04paid100
2102026-02-07paid300
3102026-02-18paid100
4202026-01-11paid50
5202026-02-11paid50

Expected output

Validate values and row grain before opening the solution.

result
customer_idmonthmonthly_spendavg_monthly_spend
102026-02-01400250

Reasoning path

  1. 01

    Build months

    Collapse paid orders to one row per customer-month.

  2. 02

    Add baseline

    Compute AVG(monthly_spend) within each customer partition.

  3. 03

    Filter outside

    Window results are not available to the same SELECT's WHERE clause.

Reveal solution and explanationOPEN +
WITH monthly AS (
  SELECT customer_id,
         DATE_TRUNC('month', paid_at)::date AS month,
         SUM(amount) AS monthly_spend
  FROM orders
  WHERE status = 'paid'
  GROUP BY customer_id, DATE_TRUNC('month', paid_at)::date
), scored AS (
  SELECT monthly.*,
         AVG(monthly_spend) OVER (PARTITION BY customer_id) AS avg_monthly_spend
  FROM monthly
)
SELECT customer_id, month, monthly_spend, avg_monthly_spend
FROM scored
WHERE monthly_spend > avg_monthly_spend
ORDER BY customer_id, month;
How it works
  • The first CTE changes grain from order to customer-month.
  • The window average preserves that monthly grain while adding the customer baseline.

Dialect note: DATE_TRUNC and ::date are PostgreSQL syntax; use the platform's month-bucketing expression elsewhere.

Performance perspective

Filter paid rows early. For repeated reporting, consider a maintained monthly aggregate; measure freshness and write amplification before materializing.

Edge cases

  • This version excludes zero-spend months.
  • Choose business timezone before truncating timestamps.

Interviewer follow-up: Include calendar months with zero spend in the average.

04
AdvancedQueryPortable SQL

Top products per category, including ties

Return the two highest-revenue products in each category for Q1 2026, including ties at second place.

Output grain

One row per qualifying category-product.

Schema

CREATE TABLE products (product_id INT PRIMARY KEY, category_id INT, name VARCHAR(80));
CREATE TABLE order_items (order_id INT, product_id INT, sold_at DATE, quantity INT, unit_price DECIMAL(12,2));

Think first

  • Should equal revenue consume one rank or two row positions?
  • Must line items be aggregated before ranking?

Synthetic sample data

products
product_idcategory_idname
110Pen
210Pad
310Clip
420Mug
520Cup
order_items
order_idproduct_idsold_atquantityunit_price
112026-01-051010
222026-01-06810
332026-02-01420
442026-03-01350
552026-03-03250

Expected output

Validate values and row grain before opening the solution.

result
category_idproductrevenuerevenue_rank
10Pen1001
10Pad802
10Clip802
20Mug1501
20Cup1002

Reasoning path

  1. 01

    Calculate product revenue

    Group line items at category-product grain.

  2. 02

    Rank within category

    DENSE_RANK keeps ties and produces no rank gaps.

  3. 03

    Keep ranks 1–2

    Filter the derived rank in an outer query.

Reveal solution and explanationOPEN +
WITH product_revenue AS (
  SELECT p.category_id, p.product_id, p.name,
         SUM(oi.quantity * oi.unit_price) AS revenue
  FROM products p
  JOIN order_items oi ON oi.product_id = p.product_id
  WHERE oi.sold_at >= DATE '2026-01-01'
    AND oi.sold_at <  DATE '2026-04-01'
  GROUP BY p.category_id, p.product_id, p.name
), ranked AS (
  SELECT product_revenue.*,
         DENSE_RANK() OVER (
           PARTITION BY category_id ORDER BY revenue DESC
         ) AS revenue_rank
  FROM product_revenue
)
SELECT category_id, name AS product, revenue, revenue_rank
FROM ranked
WHERE revenue_rank <= 2
ORDER BY category_id, revenue_rank, product_id;
How it works
  • Revenue becomes a stable value per product before ranking.
  • DENSE_RANK includes every product tied at the second distinct revenue.

Performance perspective

A date-partitioned or date-leading access path can reduce scanned line items, but join and aggregation cost still depend on quarter selectivity and physical design.

Edge cases

  • Returns fewer than two rows when a category has fewer products.
  • Define whether returns reduce revenue.

Interviewer follow-up: How would ROW_NUMBER and RANK change the result?

05
IntermediateQueryPortable SQL

Latest subscription state per account

Return each account's latest subscription state, breaking timestamp ties deterministically.

Output grain

One row per account with at least one state event.

Schema

CREATE TABLE subscription_events (
  event_id BIGINT PRIMARY KEY,
  account_id BIGINT NOT NULL,
  state VARCHAR(20) NOT NULL,
  changed_at TIMESTAMP NOT NULL
);

Think first

  • What makes the ordering total rather than partial?
  • Should accounts without events appear?

Synthetic sample data

subscription_events
event_idaccount_idstatechanged_at
110trial2026-01-01 09:00
210active2026-01-03 10:00
320active2026-01-02 08:00
420past_due2026-01-04 11:00
520cancelled2026-01-04 11:00

Expected output

Validate values and row grain before opening the solution.

result
account_idstatechanged_at
10active2026-01-03 10:00
20cancelled2026-01-04 11:00

Reasoning path

  1. 01

    Partition

    Restart numbering for every account.

  2. 02

    Order deterministically

    Sort by changed_at and event_id descending.

  3. 03

    Select one

    Keep rn = 1 after the window is evaluated.

Reveal solution and explanationOPEN +
WITH ranked AS (
  SELECT event_id, account_id, state, changed_at,
         ROW_NUMBER() OVER (
           PARTITION BY account_id
           ORDER BY changed_at DESC, event_id DESC
         ) AS rn
  FROM subscription_events
)
SELECT account_id, state, changed_at
FROM ranked
WHERE rn = 1
ORDER BY account_id;
How it works
  • ROW_NUMBER produces exactly one winner per account.
  • event_id resolves events sharing a timestamp instead of leaving the result nondeterministic.

Performance perspective

An index on (account_id, changed_at DESC, event_id DESC) can support account-scoped lookups; a full snapshot still processes all accounts unless maintained separately.

Edge cases

  • A surrogate ID is only a valid tie-breaker if it reflects the intended event order.
  • Late-arriving events may require an effective time distinct from ingestion time.

Interviewer follow-up: Return the latest state as of a supplied historical timestamp.

06
AdvancedQueryPostgreSQL

Rolling seven-day signup conversion

For each calendar day, calculate the seven-day rolling percentage of signups that purchased within 24 hours.

Output grain

One row per calendar day in the requested range.

Schema

CREATE TABLE signups (user_id BIGINT PRIMARY KEY, signed_up_at TIMESTAMPTZ NOT NULL);
CREATE TABLE purchases (purchase_id BIGINT PRIMARY KEY, user_id BIGINT, purchased_at TIMESTAMPTZ NOT NULL);

Think first

  • Can multiple purchases multiply a signup?
  • Are missing calendar days allowed to shrink a ROWS frame?

Synthetic sample data

signups
user_idsigned_up_at
12026-01-01 09:00Z
22026-01-01 10:00Z
32026-01-02 11:00Z
42026-01-03 12:00Z
purchases
purchase_iduser_idpurchased_at
1012026-01-01 15:00Z
1122026-01-03 12:00Z
1232026-01-03 08:00Z

Expected output

Validate values and row grain before opening the solution.

result (through Jan 3)
dayrolling_signupsrolling_convertedconversion_pct
2026-01-012150
2026-01-023266.67
2026-01-034250

Reasoning path

  1. 01

    Classify signups

    Use EXISTS to create one conversion flag per signup.

  2. 02

    Densify days

    Join to a calendar series so one row always means one day.

  3. 03

    Window totals

    Sum daily counts over the current day and six preceding rows.

Reveal solution and explanationOPEN +
WITH days AS (
  SELECT d::date AS day
  FROM GENERATE_SERIES(DATE '2026-01-01', DATE '2026-01-03', INTERVAL '1 day') d
), classified AS (
  SELECT s.user_id, s.signed_up_at::date AS day,
         CASE WHEN EXISTS (
           SELECT 1 FROM purchases p
           WHERE p.user_id = s.user_id
             AND p.purchased_at >= s.signed_up_at
             AND p.purchased_at < s.signed_up_at + INTERVAL '24 hours'
         ) THEN 1 ELSE 0 END AS converted
  FROM signups s
), daily AS (
  SELECT d.day, COUNT(c.user_id) AS signups,
         COALESCE(SUM(c.converted), 0) AS converted
  FROM days d LEFT JOIN classified c ON c.day = d.day
  GROUP BY d.day
), rolling AS (
  SELECT day,
    SUM(signups) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS rolling_signups,
    SUM(converted) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS rolling_converted
  FROM daily
)
SELECT *, ROUND(100.0 * rolling_converted / NULLIF(rolling_signups, 0), 2) AS conversion_pct
FROM rolling ORDER BY day;
How it works
  • EXISTS prevents multiple purchases from duplicating a signup.
  • A dense day series makes a seven-row frame mean seven calendar days.

Performance perspective

Index purchases(user_id, purchased_at). For a long dashboard range, persist daily classifications or aggregates and define how late events are corrected.

Edge cases

  • Specify reporting timezone for day boundaries.
  • Decide whether exactly 24 hours is included; this uses a half-open interval.

Interviewer follow-up: Compute the same metric for a configurable 1-, 7-, and 28-day attribution window.

07
AdvancedQueryPostgreSQL

Longest consecutive active-day streak

Return each user's longest streak of distinct consecutive active calendar days.

Output grain

One row per user.

Schema

CREATE TABLE user_activity (user_id BIGINT, occurred_at TIMESTAMPTZ NOT NULL);

Think first

  • Can multiple events occur on one day?
  • How will equal-length streaks be resolved?

Synthetic sample data

user_activity
user_idoccurred_at
12026-01-01 09:00Z
12026-01-01 17:00Z
12026-01-02 10:00Z
12026-01-04 10:00Z
22026-02-05 10:00Z
22026-02-06 10:00Z
22026-02-07 10:00Z

Expected output

Validate values and row grain before opening the solution.

result
user_idstreak_startstreak_endstreak_days
12026-01-012026-01-022
22026-02-052026-02-073

Reasoning path

  1. 01

    Deduplicate days

    Reduce many events to one row per user-date.

  2. 02

    Form islands

    Consecutive dates share date minus row-number days.

  3. 03

    Rank streaks

    Count islands and select each user's longest deterministic winner.

Reveal solution and explanationOPEN +
WITH active_days AS (
  SELECT DISTINCT user_id, occurred_at::date AS active_day
  FROM user_activity
), numbered AS (
  SELECT user_id, active_day,
         ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY active_day) AS rn
  FROM active_days
), streaks AS (
  SELECT user_id, MIN(active_day) AS streak_start,
         MAX(active_day) AS streak_end, COUNT(*) AS streak_days
  FROM numbered
  GROUP BY user_id, active_day - (rn::int)
), ranked AS (
  SELECT streaks.*,
         ROW_NUMBER() OVER (
           PARTITION BY user_id
           ORDER BY streak_days DESC, streak_end DESC
         ) AS choice
  FROM streaks
)
SELECT user_id, streak_start, streak_end, streak_days
FROM ranked WHERE choice = 1 ORDER BY user_id;
How it works
  • Subtracting an increasing sequence from consecutive dates produces a constant island key.
  • The final ROW_NUMBER chooses the most recent streak when lengths tie.

Dialect note: Date and row-number subtraction shown here is PostgreSQL-specific.

Performance perspective

An expression index on reporting-date may help, but timezone-dependent expressions need care. A daily activity fact table avoids repeated raw-event deduplication.

Edge cases

  • Convert timestamps in the user's business timezone before casting.
  • Clarify whether an account's creation day can begin a streak.

Interviewer follow-up: Return every streak of at least seven days instead of only the longest.

08
AdvancedQueryPostgreSQL

Sessionize product events after 30 minutes of inactivity

Assign each event to a user session, starting a new session after more than 30 minutes of inactivity.

Output grain

One row per event with a user-local session number.

Schema

CREATE TABLE events (event_id BIGINT PRIMARY KEY, user_id BIGINT, occurred_at TIMESTAMPTZ, event_name TEXT);

Think first

  • Is exactly 30 minutes a new session?
  • What resolves equal timestamps?

Synthetic sample data

events
event_iduser_idoccurred_atevent_name
172026-01-01 10:00Zopen
272026-01-01 10:20Zsearch
372026-01-01 10:51Zview
472026-01-01 11:00Zbuy

Expected output

Validate values and row grain before opening the solution.

result
event_iduser_idsession_number
171
271
372
472

Reasoning path

  1. 01

    Read previous time

    LAG within each user finds the prior event.

  2. 02

    Mark boundaries

    First events and gaps over 30 minutes receive 1.

  3. 03

    Number sessions

    A running sum converts boundary markers into session numbers.

Reveal solution and explanationOPEN +
WITH sequenced AS (
  SELECT e.*,
         LAG(occurred_at) OVER (
           PARTITION BY user_id ORDER BY occurred_at, event_id
         ) AS previous_at
  FROM events e
), marked AS (
  SELECT sequenced.*,
         CASE WHEN previous_at IS NULL
                    OR occurred_at > previous_at + INTERVAL '30 minutes'
              THEN 1 ELSE 0 END AS starts_session
  FROM sequenced
)
SELECT event_id, user_id,
       SUM(starts_session) OVER (
         PARTITION BY user_id ORDER BY occurred_at, event_id
         ROWS UNBOUNDED PRECEDING
       ) AS session_number
FROM marked
ORDER BY user_id, occurred_at, event_id;
How it works
  • LAG observes the preceding event without changing row count.
  • The cumulative boundary count is constant inside a session and increases at each break.

Performance perspective

An index on (user_id, occurred_at, event_id) aligns with partition ordering; high-volume pipelines often sessionize incrementally but must handle late events.

Edge cases

  • This definition keeps exactly 30 minutes in the same session.
  • Late events can change historical session boundaries.

Interviewer follow-up: Aggregate each session's start, end, duration, and event count.

09
AdvancedQueryPortable SQL

Deduplicate retried payment callbacks

Keep one authoritative callback per provider event, preferring the latest received row and a deterministic tie-breaker.

Output grain

One row per provider-event ID.

Schema

CREATE TABLE payment_callbacks (
  callback_id BIGINT PRIMARY KEY,
  provider_event_id VARCHAR(80) NOT NULL,
  status VARCHAR(20) NOT NULL,
  received_at TIMESTAMP NOT NULL
);

Think first

  • What is the real duplicate key?
  • What makes the survivor deterministic?

Synthetic sample data

payment_callbacks
callback_idprovider_event_idstatusreceived_at
1evt_apending2026-01-01 10:00
2evt_asucceeded2026-01-01 10:02
3evt_bfailed2026-01-01 11:00
4evt_bfailed2026-01-01 11:00

Expected output

Validate values and row grain before opening the solution.

result
provider_event_idstatuscallback_id
evt_asucceeded2
evt_bfailed4

Reasoning path

  1. 01

    Name the key

    provider_event_id identifies repeated delivery of the same event.

  2. 02

    Rank candidates

    Order newest-first, then use callback_id to resolve equal receipt times.

  3. 03

    Select the survivor

    Keep only row one for each business key.

Reveal solution and explanationOPEN +
WITH ranked AS (
  SELECT payment_callbacks.*,
         ROW_NUMBER() OVER (
           PARTITION BY provider_event_id
           ORDER BY received_at DESC, callback_id DESC
         ) AS rn
  FROM payment_callbacks
)
SELECT provider_event_id, status, callback_id
FROM ranked
WHERE rn = 1
ORDER BY provider_event_id;
How it works
  • The window preserves every candidate while assigning exactly one winner.
  • The outer filter applies the documented survivorship rule.

Performance perspective

Prevent duplicates at ingestion with a unique provider-event key when possible. Query-time cleanup needs sorting or an aligned index and does not make side effects idempotent.

Edge cases

  • A later callback is not necessarily a later provider state unless the contract guarantees it.
  • Store the raw payload for audit and replay.

Interviewer follow-up: How would you remove stored duplicates safely while foreign keys reference callback_id?

10
AdvancedQueryPostgreSQL

Expand an organization hierarchy with depth and path

Starting at manager 1, return every report with depth and an inspectable path while guarding against cycles.

Output grain

One row per reachable employee.

Schema

CREATE TABLE employees (
  employee_id INT PRIMARY KEY,
  manager_id INT REFERENCES employees(employee_id),
  name TEXT NOT NULL
);

Think first

  • Does the root belong in the result?
  • What should happen if corrupt data contains a cycle?

Synthetic sample data

employees
employee_idmanager_idname
1NULLMira
21Noah
31Omar
42Priya
54Quinn

Expected output

Validate values and row grain before opening the solution.

result
employee_idnamedepthpath
1Mira01
2Noah11>2
3Omar11>3
4Priya21>2>4
5Quinn31>2>4>5

Reasoning path

  1. 01

    Anchor

    Seed the CTE with employee 1 at depth zero.

  2. 02

    Walk children

    Join employee.manager_id to the current employee.

  3. 03

    Carry safety state

    Append IDs to an array and refuse IDs already in the path.

Reveal solution and explanationOPEN +
WITH RECURSIVE org AS (
  SELECT employee_id, manager_id, name, 0 AS depth,
         ARRAY[employee_id] AS path_ids
  FROM employees WHERE employee_id = 1
  UNION ALL
  SELECT e.employee_id, e.manager_id, e.name, o.depth + 1,
         o.path_ids || e.employee_id
  FROM employees e
  JOIN org o ON e.manager_id = o.employee_id
  WHERE NOT e.employee_id = ANY(o.path_ids)
)
SELECT employee_id, name, depth,
       ARRAY_TO_STRING(path_ids, '>') AS path
FROM org
ORDER BY path_ids;
How it works
  • The anchor establishes the root; the recursive branch adds one hierarchy level per iteration.
  • Path state both explains the route and prevents infinite recursion.

Dialect note: Array path tracking is PostgreSQL syntax; other engines offer different recursive-CTE cycle techniques.

Performance perspective

Index employees(manager_id). For frequently queried large trees, compare adjacency lists with closure tables or materialized paths and include write costs.

Edge cases

  • Orphans are unreachable from the chosen root.
  • Add a maximum depth if the engine or data contract does not provide one.

Interviewer follow-up: Return aggregate payroll under each manager without double-counting.

11
AdvancedQueryPostgreSQL

Monthly cohort retention matrix

Compute month-zero and month-one retention counts by each user's signup month.

Output grain

One row per signup cohort month.

Schema

CREATE TABLE users (user_id BIGINT PRIMARY KEY, signed_up_at DATE NOT NULL);
CREATE TABLE activity (user_id BIGINT, active_at DATE NOT NULL);

Think first

  • Is retention an event count or a distinct-user count?
  • Does signup itself imply month-zero activity?

Synthetic sample data

users
user_idsigned_up_at
12026-01-03
22026-01-20
32026-02-02
activity
user_idactive_at
12026-01-04
12026-02-07
22026-01-21
32026-02-03
32026-03-01

Expected output

Validate values and row grain before opening the solution.

result
cohort_monthcohort_sizemonth_0month_1
2026-01-01221
2026-02-01111

Reasoning path

  1. 01

    Assign cohorts

    Derive each user's signup month exactly once.

  2. 02

    Deduplicate activity

    Reduce repeated events to one user-month.

  3. 03

    Calculate offsets

    Convert year and month differences into an integer period.

Reveal solution and explanationOPEN +
WITH cohorts AS (
  SELECT user_id, DATE_TRUNC('month', signed_up_at)::date AS cohort_month
  FROM users
), user_months AS (
  SELECT DISTINCT user_id, DATE_TRUNC('month', active_at)::date AS active_month
  FROM activity
), periods AS (
  SELECT c.cohort_month, c.user_id,
         12 * (EXTRACT(YEAR FROM u.active_month) - EXTRACT(YEAR FROM c.cohort_month))
         + EXTRACT(MONTH FROM u.active_month) - EXTRACT(MONTH FROM c.cohort_month) AS period
  FROM cohorts c LEFT JOIN user_months u ON u.user_id = c.user_id
)
SELECT cohort_month, COUNT(DISTINCT user_id) AS cohort_size,
       COUNT(DISTINCT user_id) FILTER (WHERE period = 0) AS month_0,
       COUNT(DISTINCT user_id) FILTER (WHERE period = 1) AS month_1
FROM periods GROUP BY cohort_month ORDER BY cohort_month;
How it works
  • The cohort table fixes the denominator at user grain.
  • Distinct user-month activity ensures multiple events cannot inflate retention.

Performance perspective

Large event stores usually maintain a user-day or user-month activity fact. Partition pruning by active_at helps only when the query bounds activity dates.

Edge cases

  • This treats calendar months, not 30-day windows.
  • Define whether pre-signup activity or deleted users are excluded.

Interviewer follow-up: Return retention percentages through month 12 without hard-coding twelve columns.

12
IntermediateQueryPortable SQL

Available inventory after live reservations

Return every SKU's available quantity after subtracting only unexpired active reservations at a supplied instant.

Output grain

One row per SKU.

Schema

CREATE TABLE inventory (sku VARCHAR(30) PRIMARY KEY, on_hand INT NOT NULL);
CREATE TABLE reservations (
  reservation_id BIGINT PRIMARY KEY, sku VARCHAR(30), quantity INT,
  status VARCHAR(20), expires_at TIMESTAMP
);

Think first

  • Which timestamp defines now for the whole statement?
  • Should expired reservation rows remove SKUs?

Synthetic sample data

inventory
skuon_hand
A10
B5
C3
reservations
reservation_idskuquantitystatusexpires_at
1A3active2026-01-01 13:00
2A2cancelled2026-01-01 14:00
3B5active2026-01-01 11:00

Expected output

Validate values and row grain before opening the solution.

result at 2026-01-01 12:00
skuon_handreservedavailable
A1037
B505
C303

Reasoning path

  1. 01

    Preserve SKUs

    Start from inventory and left-join live reservations.

  2. 02

    Sum live quantity

    Filter active, unexpired rows in the join.

  3. 03

    Expose the invariant

    Return both reserved and available so negative stock is visible.

Reveal solution and explanationOPEN +
SELECT i.sku, i.on_hand,
       COALESCE(SUM(r.quantity), 0) AS reserved,
       i.on_hand - COALESCE(SUM(r.quantity), 0) AS available
FROM inventory i
LEFT JOIN reservations r
  ON r.sku = i.sku
 AND r.status = 'active'
 AND r.expires_at > TIMESTAMP '2026-01-01 12:00:00'
GROUP BY i.sku, i.on_hand
ORDER BY i.sku;
How it works
  • Filtering inside ON keeps SKUs with no live reservation.
  • The same aggregate feeds both the auditable reserved value and the available calculation.

Performance perspective

Index live-reservation lookups by sku and expiry, but correctness requires transactional reservation writes—this read query alone cannot prevent overselling.

Edge cases

  • Specify whether expiry at exactly now is live; this query says no.
  • A negative result should trigger an invariant alert, not be silently clamped.

Interviewer follow-up: Design the transaction that reserves stock safely under contention.

13
SeniorPerformancePortable SQL

Stable keyset pagination for a mutable feed

Fetch the next 20 published posts after a cursor ordered by published_at descending, then post_id descending.

Output grain

One row per published post in the next page.

Schema

CREATE TABLE posts (
  post_id BIGINT PRIMARY KEY,
  status VARCHAR(20) NOT NULL,
  published_at TIMESTAMP NOT NULL,
  title VARCHAR(200) NOT NULL
);

Think first

  • Does the sort key uniquely order every row?
  • Must the cursor include every ORDER BY column?

Synthetic sample data

posts
post_idstatuspublished_attitle
105published2026-02-02 10:00Five
104published2026-02-02 09:00Four
103draft2026-02-02 08:00Three
102published2026-02-01 12:00Two
101published2026-02-01 12:00One

Expected output

Validate values and row grain before opening the solution.

result after cursor (2026-02-02 09:00, 104), limit 2
post_idpublished_attitle
1022026-02-01 12:00Two
1012026-02-01 12:00One

Reasoning path

  1. 01

    Define total order

    post_id breaks equal published_at values.

  2. 02

    Seek after cursor

    Use less-than comparisons that mirror descending order.

  3. 03

    Align the index

    Lead with the equality filter, then both ordering columns.

Reveal solution and explanationOPEN +
SELECT post_id, published_at, title
FROM posts
WHERE status = 'published'
  AND (
    published_at < TIMESTAMP '2026-02-02 09:00:00'
    OR (published_at = TIMESTAMP '2026-02-02 09:00:00' AND post_id < 104)
  )
ORDER BY published_at DESC, post_id DESC
FETCH FIRST 20 ROWS ONLY;

CREATE INDEX idx_posts_feed
  ON posts (status, published_at DESC, post_id DESC);
How it works
  • The predicate begins strictly after the last visible composite key.
  • The index can seek into the published range and emit rows in requested order.

Performance perspective

Keyset cost stays close to page size instead of growing with page depth. It trades away arbitrary page numbers and needs cursor versioning plus consistent filter semantics.

Edge cases

  • Updates to published_at can move rows between pages.
  • Encode cursor values and sort version; do not accept arbitrary SQL fragments.

Interviewer follow-up: How would you support backwards pagination and nullable sort keys?

14
IntermediateDebuggingPortable SQL

Debug a row-multiplying revenue join

A report overstates both item revenue and payment totals when orders have multiple items and payments. Find and repair the grain mismatch.

Output grain

One row per order.

Schema

CREATE TABLE orders (order_id INT PRIMARY KEY);
CREATE TABLE order_items (order_id INT, quantity INT, unit_price DECIMAL(12,2));
CREATE TABLE payments (payment_id INT PRIMARY KEY, order_id INT, amount DECIMAL(12,2));

Think first

  • How many rows exist per order in each input?
  • What row count appears after joining both children?

Synthetic sample data

orders
order_id
1
order_items
order_idquantityunit_price
1130
1170
payments
payment_idorder_idamount
10160
11140

Expected output

Validate values and row grain before opening the solution.

result
order_iditem_totalpaid_total
1100100

Reasoning path

  1. 01

    State each grain

    Items and payments are independently one-to-many from orders.

  2. 02

    Observe fanout

    Joining both raw children produces 2 × 2 rows for order 1.

  3. 03

    Restore one-to-one

    Aggregate each child to order_id before joining.

Reveal solution and explanationOPEN +
WITH item_totals AS (
  SELECT order_id, SUM(quantity * unit_price) AS item_total
  FROM order_items GROUP BY order_id
), payment_totals AS (
  SELECT order_id, SUM(amount) AS paid_total
  FROM payments GROUP BY order_id
)
SELECT o.order_id,
       COALESCE(i.item_total, 0) AS item_total,
       COALESCE(p.paid_total, 0) AS paid_total
FROM orders o
LEFT JOIN item_totals i ON i.order_id = o.order_id
LEFT JOIN payment_totals p ON p.order_id = o.order_id
ORDER BY o.order_id;
How it works
  • Each CTE produces at most one row per order.
  • The final joins are therefore one-to-one at the reporting grain.

Performance perspective

Pre-aggregation reduces intermediate rows and prevents incorrect sums. Confirm with row counts at each plan node, not only final latency.

Edge cases

  • Payment refunds may require signed amounts or separate status rules.
  • Do not mask an unknown item total with zero unless that is the business meaning.

Interviewer follow-up: What database constraints or semantic-layer tests would catch this class of bug?

15
SeniorPerformancePostgreSQL

Optimize a non-sargable daily order query

Rewrite a query that filters DATE(created_at) and lower(email), then propose an index only after stating the workload assumptions.

Output grain

One row per matching order, newest first.

Schema

CREATE TABLE orders (
  order_id BIGINT PRIMARY KEY,
  customer_email TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL,
  total_amount NUMERIC(12,2) NOT NULL
);

Think first

  • Which timezone defines the business day?
  • Is email comparison truly case-insensitive in this product?

Synthetic sample data

orders
order_idcustomer_emailcreated_atstatustotal_amount
1Asha@Example.com2026-03-01 08:00Zpaid90
2asha@example.com2026-03-01 15:00Zpaid40
3asha@example.com2026-03-02 01:00Zpaid20

Expected output

Validate values and row grain before opening the solution.

result for UTC 2026-03-01
order_idcreated_attotal_amount
22026-03-01 15:00Z40
12026-03-01 08:00Z90

Reasoning path

  1. 01

    Rewrite time

    Use a half-open timestamp range so created_at remains directly searchable.

  2. 02

    Normalize identity

    Use an explicit normalized email expression or stored canonical column.

  3. 03

    Index the access path

    Put email equality before the timestamp range and verify ordering needs.

Reveal solution and explanationOPEN +
SELECT order_id, created_at, total_amount
FROM orders
WHERE LOWER(customer_email) = LOWER('ASHA@example.com')
  AND created_at >= TIMESTAMPTZ '2026-03-01 00:00:00+00'
  AND created_at <  TIMESTAMPTZ '2026-03-02 00:00:00+00'
ORDER BY created_at DESC;

CREATE INDEX idx_orders_email_created
  ON orders (LOWER(customer_email), created_at DESC);
How it works
  • The range avoids applying a date function to every stored timestamp.
  • The expression index matches the remaining email expression exactly.
Alternative · Canonical stored email
ALTER TABLE orders ADD COLUMN customer_email_key TEXT;
-- Backfill and enforce application/database normalization, then index:
CREATE INDEX idx_orders_email_key_created ON orders (customer_email_key, created_at DESC);

Makes semantics explicit across queries but adds migration, write-path, and consistency work.

Performance perspective

Use EXPLAIN with representative parameters. If one customer owns a large share of rows, estimates, clustering, limits, and heap fetches may dominate despite the index.

Edge cases

  • Unicode email normalization is a product rule, not merely LOWER().
  • Local-day bounds can cross daylight-saving transitions.

Interviewer follow-up: What changes when the query also filters status and returns only the latest 25 rows?

16
SeniorDatabase InternalsPostgreSQL

Read a plan with a bad cardinality estimate

Given a conceptual plan whose filter estimate is 100 rows but actual output is 120,000, explain the join failure and the evidence-led next actions.

Output grain

A diagnosis and verification plan, not a rewritten result set.

Schema

orders: 40M rows; customers: 2M rows
Predicate: orders.status = 'pending' AND orders.region = 'west'
Indexes: orders(customer_id), customers(customer_id)
Statistics: status and region tracked independently

Think first

  • Where does estimated row count first diverge from actual?
  • Are loops multiplying per-loop time and rows?

Synthetic sample data

conceptual plan
nodeestimated rowsactual rowsloops
Filter orders1001200001
Index lookup customers11120000
Nested loop1001200001

Expected output

Validate values and row grain before opening the solution.

diagnosis
observationimplication
Filter estimate is 1,200× lowJoin cost was priced against the wrong outer size
Inner lookup loops 120,000 timesRandom access can dominate runtime
status and region are correlatedIndependent statistics may miss the joint distribution

Reasoning path

  1. 01

    Find first divergence

    Start at leaf nodes; the filter is wrong before the join is chosen.

  2. 02

    Explain propagation

    The optimizer expects 100 outer rows and prices a nested loop accordingly.

  3. 03

    Test causes

    Inspect stale or insufficient statistics, correlated predicates, skew, and parameters.

  4. 04

    Verify remedies

    Refresh or extend statistics, retest representative binds, then consider indexes or query changes.

Reveal solution and explanationOPEN +
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT c.customer_id, c.name, o.order_id
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.status = 'pending' AND o.region = 'west';

-- Evidence-led PostgreSQL follow-up, not an automatic fix:
CREATE STATISTICS orders_status_region_stats (dependencies, mcv)
  ON status, region FROM orders;
ANALYZE orders;
How it works
  • ANALYZE exposes actual rows and loops; BUFFERS helps distinguish CPU from I/O work.
  • Extended statistics can teach PostgreSQL about cross-column dependency or common value combinations.

Dialect note: Extended statistics and EXPLAIN options shown are PostgreSQL-specific; the diagnosis pattern applies across cost-based optimizers.

Performance perspective

Do not force a hash join first. Repair the information or access path that made the estimate wrong, test realistic parameter distributions, and compare total work plus tail latency.

Edge cases

  • EXPLAIN ANALYZE executes the statement; use caution with writes and production load.
  • Prepared statements may use a generic plan that behaves differently across parameter skew.

Interviewer follow-up: If estimates become accurate but the plan stays slow, which buffer, spill, and concurrency signals do you inspect next?

Sketch the mechanism, then explain the trade-off

These diagrams are intentionally compact: enough structure to reconstruct an answer without turning a database into a false cartoon.

Logical query order

Reason about which relation each clause receives. Physical execution may differ.

  1. 1FROM / JOIN
  2. 2WHERE
  3. 3GROUP + aggregate
  4. 4HAVING
  5. 5window
  6. 6SELECT
  7. 7DISTINCT
  8. 8ORDER
  9. 9LIMIT
Open layered answer →

B-tree access path

Descend to the first key, scan a leaf range, then fetch rows unless the index covers the query.

Open layered answer →

SQL-to-runtime pipeline

The optimizer explores alternatives using estimates; the executor produces evidence.

  1. 01Parse
  2. 02Bind
  3. 03Logical rewrite
  4. 04Estimate
  5. 05Choose physical plan
  6. 06Execute + measure
estimate100 rows
actual120,000 rows
Open layered answer →

MVCC visibility timeline

Readers choose visible versions; writers still coordinate with other writers.

Open layered answer →

Move from symptom to evidence to verified recovery

A strong incident answer names the first safe action, the evidence that changes the diagnosis, the durable decision, and proof that the system recovered.

  1. 01

    A query regresses after data growth

    p99 rises from 90 ms to 4.8 s after a tenant import; CPU and reads climb.

    Evidence
    Compare plan hash, estimated/actual rows, statistics age, skew, buffer reads, spills, and representative binds.
    Decision
    Repair estimates or access path only after locating the first excess-work node; do not start with a hint.
    Verify
    Replay small and whale tenants, compare work and tail latency, then watch plan and data-shape drift.
    Open canonical answer
  2. 02

    Two write paths deadlock

    Order finalization and inventory release intermittently choose a transaction victim.

    Evidence
    Capture the deadlock graph, locked keys/indexes, statements, call paths, and acquisition order.
    Decision
    Break the cycle with consistent order, shorter scope, narrower predicates, and bounded victim retry.
    Verify
    Force the same interleaving under load and monitor deadlock recurrence rather than only request success.
    Open canonical answer
  3. 03

    The connection pool saturates

    Application requests queue while the database shows many idle or long-held sessions.

    Evidence
    Measure pool wait, checkout duration, active DB work, transaction age, per-instance pools, and server limits.
    Decision
    Fix leaks and long holds, enforce deadlines, and size global concurrency to database capacity—not pod count.
    Verify
    Load-test autoscaling and failure recovery; alert on wait time and transaction age before hard exhaustion.
    Open canonical answer
  4. 04

    A read replica serves stale state

    A user updates a profile, then the next request displays the old value.

    Evidence
    Identify the read route, write position, replica replay position, lag distribution, and operation consistency need.
    Decision
    Use writer stickiness, version tokens, bounded waits, or writer reads for the affected operation—not universal consistency.
    Verify
    Inject lag and failover; prove the user-visible guarantee while monitoring writer load and fallback rate.
    Open canonical answer
  5. 05

    A migration partially fails

    New binaries expect a column that is absent on one shard after a deployment interruption.

    Evidence
    Inventory schema state, binary versions, traffic, locks, replicas, migration ledger, and forward/backward compatibility.
    Decision
    Stop the rollout, restore one compatible state, then resume from idempotent checkpoints with explicit ownership.
    Verify
    Run compatibility probes across every shard and version before contracting old code or declaring recovery.
    Open canonical answer
  6. 06

    One tenant creates a noisy-neighbor event

    A shared PostgreSQL cluster misses latency SLOs during one customer's bulk workload.

    Evidence
    Attribute CPU, I/O, locks, temp spill, connections, rows, and query shapes to tenant and workload class.
    Decision
    Apply admission control and workload limits first; isolate or repartition only with a measured capacity model.
    Verify
    Replay the burst, confirm fairness and throughput, and define the threshold that triggers tenant isolation.
    Open canonical answer

What changes as the interview level rises

  1. 01

    Fundamental

    Predict filtering, NULL, joins, and grouping results; write small correct queries.

    Practice this level →
  2. 02

    Intermediate

    Define grain and compose CTEs, subqueries, windows, models, and application boundaries.

    Practice this level →
  3. 03

    Advanced

    Explain B-trees, estimates, plans, isolation, MVCC, replicas, and non-trivial query patterns.

    Practice this level →
  4. 04

    Senior

    Diagnose slow queries, deadlocks, pool pressure, stale reads, and migration risk with evidence.

    Practice this level →
  5. 05

    Staff / Principal

    Sequence reversible platform decisions across scale, tenancy, governance, capacity, and ownership.

    Practice this level →

Search by question, concept, or production symptom

Alias-aware search connects phrases such as anti-join, fanout, seek pagination, EXPLAIN ANALYZE, and MVCC to the canonical answer.

0 studied102 total
Next: Logical query order

Showing 102 of 102 questions

SQL Fundamentals & Query Semantics

Filtering, ordering, expressions, grouping, logical query order, and the semantics behind familiar syntax.

Interview focus: Predict behavior precisely instead of reciting clause definitions.

  • IntermediateConcept8 min

    What is SQL's logical query-processing order?

    Separate the written clause order from the logical relation each clause receives.

    SELECTFROMWHERE
    30-second interview answer

    A useful logical model is FROM and JOINs first, then WHERE, GROUP BY, aggregate calculation, HAVING, window functions, SELECT, DISTINCT, ORDER BY, and finally row limiting. Engines can physically reorder work when equivalence is preserved. The model explains scope rules—for example, a SELECT alias usually does not exist when WHERE is evaluated—and helps predict where rows disappear or multiply.

  • FundamentalConcept3 min

    What is the difference between WHERE and HAVING?

    Filter input rows with WHERE and completed groups with HAVING.

    WHEREHAVINGGROUP BY
    30-second interview answer

    WHERE removes input rows before grouping; HAVING removes groups after aggregate values exist. Put nonaggregate predicates in WHERE when semantics allow so less data reaches grouping. HAVING is required for predicates such as HAVING SUM(amount) > 1000. Moving a predicate between them can change the answer, so this is a semantics decision before it is an optimization.

    Concise answer
  • FundamentalConcept3 min

    How do COUNT(*), COUNT(column), and COUNT(DISTINCT column) differ?

    Know exactly which rows and non-NULL values each count includes.

    COUNTNULLDISTINCT
    30-second interview answer

    COUNT(*) counts rows. COUNT(column) counts rows where that expression is not NULL. COUNT(DISTINCT column) counts distinct non-NULL values. Performance depends on plan and storage engine, not a universal COUNT(1) trick. Choose the form that matches the business question and watch for join multiplication before counting.

    Concise answer
  • IntermediateConcept3 min

    Why can a SELECT alias usually not be referenced in WHERE?

    Use logical query order to explain why some clauses can see an alias and others cannot.

    aliasesWHEREORDER BY
    30-second interview answer

    WHERE is logically evaluated before SELECT creates the output alias, so the alias is normally not in scope. ORDER BY is later and commonly can use it. Repeat the expression, or place it in a derived table or CTE when reuse improves clarity. Exact alias rules vary by engine, so avoid relying on a permissive dialect in portable interview SQL.

    Concise answer

JOINs & Relationship Reasoning

Inner, outer, self, semi, and anti joins; row multiplication; predicate placement; and realistic relationships.

Interview focus: State the required row preservation and output grain before choosing syntax.

  • FundamentalConcept3 min

    How do INNER JOIN and LEFT JOIN differ?

    Choose the join from which side's rows the result must preserve.

    INNER JOINLEFT JOINrow preservation
    30-second interview answer

    INNER JOIN returns matching combinations only. LEFT JOIN preserves every left row and fills right-side columns with NULL when no match exists. Start by stating the required output grain and preservation rule. Performance is workload- and plan-dependent; rewriting a LEFT JOIN as INNER JOIN is valid only when unmatched left rows are not part of the required result.

    Concise answer
  • IntermediateQuery8 min

    How can WHERE accidentally turn a LEFT JOIN into an INNER JOIN?

    A right-side WHERE predicate rejects the NULL-extended rows created for nonmatches.

    LEFT JOINONWHERE
    30-second interview answer

    After a LEFT JOIN, unmatched rows have NULLs on the right. A WHERE condition such as WHERE payments.status = 'settled' evaluates UNKNOWN for those rows and removes them, leaving only matches. Put the predicate in ON when it defines which right rows qualify while all left rows must remain. Keep it in WHERE when unmatched rows should be removed.

  • IntermediateDebugging3 min

    Why do duplicate-looking rows appear after a JOIN?

    Treat row multiplication as relationship cardinality, not a DISTINCT problem.

    JOINcardinalityDISTINCT
    30-second interview answer

    A join returns every matching pair. A one-to-many relationship therefore repeats the one-side columns, and incomplete join keys can create unintended many-to-many multiplication. Define the desired grain, inspect key uniqueness on both inputs, and aggregate or select one child row deliberately. Adding DISTINCT can hide the symptom while preserving wrong totals and unnecessary work.

    Concise answer
  • IntermediateConcept3 min

    When should you use EXISTS instead of a JOIN?

    Use EXISTS when the requirement asks whether a related row exists, not for its columns.

    EXISTSJOINsemi join
    30-second interview answer

    EXISTS expresses a semi-join: return an outer row if at least one related row qualifies, without multiplying it by the number of matches. JOIN is appropriate when columns or multiplicity from both relations are needed. Modern optimizers can transform equivalent forms, so choose the clearest semantics and inspect the plan rather than claiming EXISTS is always faster.

    Concise answer
  • IntermediateConcept3 min

    When should you use CROSS, SELF, RIGHT, or FULL OUTER JOIN?

    Use less-common join forms when they express a deliberate product, self-relationship, or two-sided preservation rule.

    CROSS JOINSELF JOINRIGHT JOIN
    30-second interview answer

    CROSS JOIN deliberately produces every pair and fits combinations, calendars, and small parameter grids. A self join relates rows in one table through aliases, such as employees to managers. RIGHT JOIN is the mirrored preservation rule of LEFT JOIN and is often rewritten as LEFT JOIN for reading flow. FULL OUTER JOIN preserves unmatched rows from both sides and is useful for reconciliation; MySQL has no native FULL OUTER JOIN, so emulate it carefully with left/right anti-matched branches and UNION ALL. In every case, state the output grain and expected cardinality before choosing syntax.

    Concise answer
  • FundamentalQueryQuery Lab12 min

    Find active customers who have never had a successful payment, including customers with no payment attempts.

    Use NOT EXISTS when the business question is about the absence of a matching row.

    NOT EXISTSanti-joinNULL
    30-second interview answer

    One row per qualifying customer. Use NOT EXISTS when the business question is about the absence of a matching row. Begin with active customers. Reject a customer only when a succeeded payment exists. Do not join and deduplicate when no payment columns are needed.

    Open Query Lab →
  • IntermediateDebuggingQuery Lab12 min

    A report overstates both item revenue and payment totals when orders have multiple items and payments. Find and repair the grain mismatch.

    Two one-to-many joins create an item-by-payment cross product; aggregate each child to order grain first.

    join fanoutpre-aggregationgrain
    30-second interview answer

    One row per order. Two one-to-many joins create an item-by-payment cross product; aggregate each child to order grain first. Items and payments are independently one-to-many from orders. Joining both raw children produces 2 × 2 rows for order 1. Aggregate each child to order_id before joining.

    Open Query Lab →

Aggregation & Grouping

Aggregate semantics, grouping levels, HAVING, conditional aggregation, and distinct counts.

Interview focus: Keep group grain, NULL behavior, and duplicate effects explicit.

  • IntermediateQuery3 min

    What is conditional aggregation, and when is it useful?

    Compute several filtered metrics at one grouping grain with CASE inside aggregates.

    CASESUMCOUNT
    30-second interview answer

    Conditional aggregation places a CASE expression inside SUM, COUNT, or another aggregate—for example SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END). It can produce multiple metrics per group in one query. Be explicit about ELSE and NULL behavior, avoid mixing incompatible grains, and verify whether one wider aggregation is actually cheaper than separate selective queries.

    Concise answer
  • AdvancedQuery3 min

    How do you combine monthly and customer-level aggregates correctly?

    Materialize the first grain conceptually before aggregating to a coarser grain.

    GROUP BYCTEgrain
    30-second interview answer

    Compute the finer grain first—such as one row per customer and month—in a CTE or derived table, then aggregate or compare those rows at customer grain. Joining raw orders directly to a customer-level average can weight busy months incorrectly. State each intermediate grain aloud; most aggregate-of-aggregate mistakes are really grain mistakes.

    Concise answer
  • AdvancedPerformance3 min

    What should you consider before using COUNT(DISTINCT ...)?

    Distinct aggregates may require hashing or sorting and can expose an upstream grain problem.

    COUNT DISTINCThash aggregatesort
    30-second interview answer

    COUNT(DISTINCT value) is semantically correct when unique non-NULL values are the metric. It may require a large hash set or sort, especially per group. First check whether an earlier join multiplied rows unnecessarily or whether the model can supply an already-unique relation. Then inspect memory, spill behavior, cardinality, and engine-specific approximate options only when the product tolerates approximation.

    Concise answer
  • FundamentalQueryQuery Lab12 min

    Return every region, its paid-order count, and paid revenue for January 2026—even when the region has no paid orders.

    Start from the complete region set, constrain orders in the join, and aggregate once per region.

    LEFT JOINGROUP BYCOALESCE
    30-second interview answer

    One row per region. Start from the complete region set, constrain orders in the join, and aggregate once per region. Drive from regions and LEFT JOIN matching orders. Put status and half-open date bounds in the ON clause. COUNT the nullable order key and coalesce SUM for empty groups.

    Open Query Lab →
  • AdvancedQueryQuery Lab12 min

    Compute month-zero and month-one retention counts by each user's signup month.

    Assign one cohort per user, reduce activity to user-month, calculate month offsets, then pivot counts.

    cohortconditional aggregationdistinct users
    30-second interview answer

    One row per signup cohort month. Assign one cohort per user, reduce activity to user-month, calculate month offsets, then pivot counts. Derive each user's signup month exactly once. Reduce repeated events to one user-month. Convert year and month differences into an integer period.

    Open Query Lab →

Subqueries, EXISTS & CTEs

Scalar and correlated subqueries, semi/anti joins, readable decomposition, and recursive traversal.

Interview focus: Choose semantics and maintainability without repeating optimizer myths.

  • AdvancedConcept3 min

    How do correlated subqueries and JOINs compare?

    Choose the form that expresses the relationship and verify whether the optimizer decorrelates it.

    correlated subqueryJOINdecorrelation
    30-second interview answer

    A correlated subquery references the current outer row; a join combines relations directly. Correlation is not automatically slow—optimizers can decorrelate many EXISTS and aggregate subqueries—but some shapes execute repeated work. Use the clearest correct form, inspect the physical plan and row counts, and rewrite only when evidence shows repeated scans or poor estimates.

    Concise answer
  • IntermediatePerformance3 min

    Does EXISTS always perform better than IN?

    Equivalent IN and EXISTS predicates often become the same semi-join plan.

    INEXISTSsemi join
    30-second interview answer

    No. For equivalent non-NULL semantics, modern optimizers often transform IN and EXISTS into the same semi-join. Choose IN for membership and EXISTS for correlated existence when that reads more clearly. The important exception is negative membership: NOT IN can become UNKNOWN when the subquery contains NULL, while NOT EXISTS usually expresses the intended anti-join safely.

    Concise answer
  • IntermediateConcept3 min

    What is the practical difference between a CTE and a subquery?

    A CTE names a statement-scoped relation; performance behavior varies by engine and version.

    CTEsubqueryWITH
    30-second interview answer

    Both can express an intermediate relation. A CTE often improves naming and decomposition and can be referenced more clearly; a derived table is local to one FROM position. Neither is universally faster. Engines may inline, materialize, or otherwise transform a CTE depending on version, reuse, recursion, and hints, so separate maintainability value from runtime evidence.

    Concise answer
  • AdvancedQuery3 min

    How does a recursive CTE work, and where is it useful?

    Build a result iteratively from an anchor row set and a recursive expansion step.

    recursive CTEhierarchyUNION ALL
    30-second interview answer

    A recursive CTE has an anchor query and a recursive member that reads the rows produced so far to find the next level. It is useful for org charts, categories, dependencies, and bounded graph traversal. Define termination, maximum depth, path representation, duplicate policy, and cycle handling; exact recursion limits and cycle syntax differ by engine.

    Concise answer
  • IntermediateQueryQuery Lab12 min

    For each customer, return months whose paid spend is above that customer's average monthly paid spend.

    Aggregate raw orders to the monthly grain before comparing each month with a customer-level window average.

    CTEAVGwindow function
    30-second interview answer

    One row per customer-month above that customer's monthly average. Aggregate raw orders to the monthly grain before comparing each month with a customer-level window average. Collapse paid orders to one row per customer-month. Compute AVG(monthly_spend) within each customer partition. Window results are not available to the same SELECT's WHERE clause.

    Open Query Lab →
  • AdvancedQueryQuery Lab12 min

    Starting at manager 1, return every report with depth and an inspectable path while guarding against cycles.

    Anchor at the requested employee, recursively join children, and carry state needed for depth and cycle detection.

    recursive CTEhierarchycycle guard
    30-second interview answer

    One row per reachable employee. Anchor at the requested employee, recursively join children, and carry state needed for depth and cycle detection. Seed the CTE with employee 1 at depth zero. Join employee.manager_id to the current employee. Append IDs to an array and refuse IDs already in the path.

    Open Query Lab →

Window Functions

Ranking, navigation, running aggregates, frames, top-N per group, and time-series reasoning.

Interview focus: Explain partition, order, frame, ties, and determinism separately.

  • IntermediateQuery8 min

    How do ROW_NUMBER(), RANK(), and DENSE_RANK() differ?

    Choose whether ties need unique row positions, rank gaps, or dense value ranks.

    ROW_NUMBERRANKDENSE_RANK
    30-second interview answer

    ROW_NUMBER assigns every row a unique sequence, so include a stable tiebreaker when the chosen row matters. RANK gives ties the same rank and leaves gaps; DENSE_RANK gives ties the same rank without gaps. Use ROW_NUMBER to select exactly one row, RANK for competition-style placement, and DENSE_RANK to rank distinct value groups.

  • IntermediateConcept3 min

    How do GROUP BY and window functions differ?

    GROUP BY changes result grain; a window computes across related rows while retaining them.

    GROUP BYOVERPARTITION BY
    30-second interview answer

    GROUP BY collapses input rows to one row per group. A window function computes over a partition and ordering but preserves each row, which supports comparisons to group totals, running values, rankings, and previous-row logic. If a window result must be filtered, place it in a CTE or derived table unless the engine supports a suitable later-stage clause.

    Concise answer
  • AdvancedQuery3 min

    Why do window frames matter for running totals and LAST_VALUE()?

    The partition is not always the frame; defaults can stop at the current peer group.

    ROWSRANGEwindow frame
    30-second interview answer

    PARTITION BY defines eligible rows, ORDER BY defines sequence, and the frame defines which ordered rows contribute to the current calculation. Defaults vary by function and engine context and can include peer rows or stop at the current row, surprising LAST_VALUE and running totals. State ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW—or the full partition—when correctness depends on it.

    Concise answer
  • IntermediateQuery3 min

    When should you use LAG() or LEAD()?

    Compare a row with its previous or next ordered peer without a self-join.

    LAGLEADPARTITION BY
    30-second interview answer

    LAG reads a previous row's value and LEAD reads a following row within the window order. They are useful for deltas, state transitions, time gaps, churn, and sequence detection. Partition by the entity, order by a deterministic event key, and define the first/last-row default and duplicate timestamp policy explicitly.

    Concise answer
  • AdvancedQueryQuery Lab12 min

    Return the two highest-revenue products in each category for Q1 2026, including ties at second place.

    Aggregate revenue first, then rank products inside each category with DENSE_RANK.

    DENSE_RANKPARTITION BYtop-N
    30-second interview answer

    One row per qualifying category-product. Aggregate revenue first, then rank products inside each category with DENSE_RANK. Group line items at category-product grain. DENSE_RANK keeps ties and produces no rank gaps. Filter the derived rank in an outer query.

    Open Query Lab →
  • IntermediateQueryQuery Lab12 min

    Return each account's latest subscription state, breaking timestamp ties deterministically.

    Rank state events newest-first with a stable secondary key, then keep row one.

    ROW_NUMBERdeterministic orderinghistory table
    30-second interview answer

    One row per account with at least one state event. Rank state events newest-first with a stable secondary key, then keep row one. Restart numbering for every account. Sort by changed_at and event_id descending. Keep rn = 1 after the window is evaluated.

    Open Query Lab →
  • AdvancedQueryQuery Lab12 min

    For each calendar day, calculate the seven-day rolling percentage of signups that purchased within 24 hours.

    Classify each signup once, aggregate to a dense daily series, then window the numerator and denominator.

    window frameconditional aggregationfunnel
    30-second interview answer

    One row per calendar day in the requested range. Classify each signup once, aggregate to a dense daily series, then window the numerator and denominator. Use EXISTS to create one conversion flag per signup. Join to a calendar series so one row always means one day. Sum daily counts over the current day and six preceding rows.

    Open Query Lab →

Advanced Query Patterns

Gaps and islands, sessionization, deduplication, cohorts, hierarchies, relational division, and latest-row patterns.

Interview focus: Derive intermediate relations and edge-case policy before writing the final query.

  • AdvancedQuery3 min

    How should you reason about a gaps-and-islands query?

    Define what counts as adjacent, then derive a stable identifier for each consecutive run.

    gaps and islandsROW_NUMBERLAG
    30-second interview answer

    First define the entity grain and what 'consecutive' means—calendar days, business days, sequence numbers, or a time threshold. Then either subtract ROW_NUMBER from an ordered value to create a constant island key, or use LAG to mark breaks and a running sum to number islands. Deduplicate input and define timestamp ties before grouping.

    Concise answer
  • IntermediateQuery3 min

    What is a reliable pattern for the latest row per entity?

    Rank rows inside each entity and use a unique tiebreaker to choose exactly one.

    ROW_NUMBERtop N per groupdeduplication
    30-second interview answer

    Use ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY event_time DESC, event_id DESC), then keep row number one. The unique tiebreaker makes the result deterministic when timestamps tie. Engine-specific shortcuts can be useful, but the window pattern is portable and explicit. For high-frequency reads, consider whether the model should maintain current state separately.

    Concise answer
  • AdvancedQueryQuery Lab12 min

    Return each user's longest streak of distinct consecutive active calendar days.

    Deduplicate activity days, derive an island key from date minus row number, then rank streaks.

    gaps and islandsROW_NUMBERdate arithmetic
    30-second interview answer

    One row per user. Deduplicate activity days, derive an island key from date minus row number, then rank streaks. Reduce many events to one row per user-date. Consecutive dates share date minus row-number days. Count islands and select each user's longest deterministic winner.

    Open Query Lab →
  • AdvancedQueryQuery Lab12 min

    Assign each event to a user session, starting a new session after more than 30 minutes of inactivity.

    Detect session boundaries with LAG, then cumulatively sum the boundary flags per user.

    LAGrunning SUMevents
    30-second interview answer

    One row per event with a user-local session number. Detect session boundaries with LAG, then cumulatively sum the boundary flags per user. LAG within each user finds the prior event. First events and gaps over 30 minutes receive 1. A running sum converts boundary markers into session numbers.

    Open Query Lab →
  • AdvancedQueryQuery Lab12 min

    Keep one authoritative callback per provider event, preferring the latest received row and a deterministic tie-breaker.

    Partition by the provider's idempotency key and explicitly encode which duplicate survives.

    ROW_NUMBERdeduplicationidempotency
    30-second interview answer

    One row per provider-event ID. Partition by the provider's idempotency key and explicitly encode which duplicate survives. provider_event_id identifies repeated delivery of the same event. Order newest-first, then use callback_id to resolve equal receipt times. Keep only row one for each business key.

    Open Query Lab →

NULL & Three-Valued Logic

Unknown values in comparisons, aggregates, joins, anti-joins, constraints, and ordering.

Interview focus: Trace TRUE, FALSE, and UNKNOWN through every predicate.

  • FundamentalConcept3 min

    Why does NULL = NULL not evaluate to TRUE?

    NULL represents unknown or absent information, so ordinary comparisons produce UNKNOWN.

    NULLUNKNOWNIS NULL
    30-second interview answer

    NULL is not an ordinary value, so comparing it with = or <> yields UNKNOWN, even against another NULL. WHERE keeps only TRUE, which removes UNKNOWN rows. Use IS NULL and IS NOT NULL for null tests, and use an engine's null-safe equality only when that dialect-specific semantic is intended.

    Concise answer
  • IntermediateQuery3 min

    Why can NOT IN return no rows when the subquery contains NULL?

    One NULL in the membership set makes the negative comparison unknown for nonmatching values.

    NOT INNULLNOT EXISTS
    30-second interview answer

    x NOT IN (subquery) is effectively a conjunction of x <> each returned value. If one value is NULL, that comparison is UNKNOWN, so the conjunction is not TRUE and WHERE removes the row. Use NOT EXISTS with a correlated equality for the usual anti-join intent, or prove and enforce that the subquery column cannot be NULL.

    Concise answer
  • IntermediateConcept3 min

    How does NULL affect aggregates and JOINs?

    Most aggregates ignore NULL, and ordinary equality joins do not match NULL keys.

    NULLaggregatesJOIN
    30-second interview answer

    COUNT(column), SUM, AVG, MIN, and MAX ignore NULL inputs, while COUNT(*) counts rows; an aggregate over no non-NULL inputs is often NULL, not zero. Equality joins do not match NULL to NULL. Apply COALESCE only when the business meaning of missing is truly the chosen replacement—turning unknown amount into zero can change semantics.

    Concise answer

Data Modeling & Normalization

Keys, relationships, junction tables, dependencies, normalization, denormalization, and schema evolution.

Interview focus: Protect invariants while naming read, write, and operational trade-offs.

  • IntermediateArchitecture3 min

    When should you use a natural key or a surrogate key?

    Separate stable row identity from mutable business identifiers and still enforce business uniqueness.

    primary keynatural keysurrogate key
    30-second interview answer

    A natural key comes from the domain and is valuable when it is compact, stable, and truly unique. A surrogate key gives the row an implementation-controlled identity and avoids propagating a wide or mutable business key. Many designs use both: a surrogate primary key plus a UNIQUE constraint on the real business identifier. The constraint matters; adding an id must not permit duplicate domain entities.

    Concise answer
  • IntermediateArchitecture3 min

    When is a composite primary key appropriate?

    Use multiple columns when their combination is the stable identity, while accounting for propagation cost.

    composite keyjunction tableforeign key
    30-second interview answer

    A composite key fits a relationship such as (order_id, product_id) when each pair may occur once and that combination is the row's identity. It enforces the invariant directly. Costs include wider foreign keys and indexes, awkward references, and difficulty when the business later permits multiple lines for the same pair. Choose from domain identity and evolution, not a universal single-column rule.

    Concise answer
  • FundamentalArchitecture3 min

    How do you model one-to-one, one-to-many, and many-to-many relationships?

    Express cardinality with foreign keys and uniqueness rather than application convention.

    foreign keyjunction tablecardinality
    30-second interview answer

    Put the foreign key on the many side for one-to-many. A one-to-one is usually a foreign key with a UNIQUE constraint, often used to split optional or security-sensitive data. A many-to-many needs a junction table with foreign keys to both parents and a key or uniqueness rule for the relationship. Add relationship attributes to that junction row, not to either parent.

    Concise answer
  • SeniorArchitecture3 min

    When should a schema be normalized or denormalized?

    Normalize authoritative write models; denormalize deliberately for measured read paths with an ownership plan.

    3NFdenormalizationfunctional dependency
    30-second interview answer

    Normalization stores each fact under a clear key and reduces update, insert, and delete anomalies. Denormalization can reduce joins or precompute a high-value read path, but creates duplicate state, refresh logic, storage, and repair work. Start normalized for correctness; denormalize when a measured workload needs it and document the source of truth, update mechanism, staleness budget, and reconciliation path.

    Concise answer
  • IntermediateScenario3 min

    How would you model users, orders, products, and order items?

    Design a small transactional schema and justify identity, historical facts, and constraints.

    ordersorder itemsconstraints
    30-second interview answer

    Use users, products, orders, and order_items. The junction row owns quantity and the price/currency captured at purchase time so product-price changes do not rewrite history. Orders reference the customer and carry lifecycle fields; order_items reference both order and product with positive-quantity checks. Decide whether repeated product lines are allowed, enforce that rule, and keep payment records separate from order state.

    Concise answer

Indexes & Access Paths

B-tree mental models, composite and covering indexes, selectivity, expression/partial indexes, and write cost.

Interview focus: Design from recurring query shapes, then verify the chosen access path.

  • IntermediateDatabase Internals8 min

    How does a B-tree index speed up a query?

    Use ordered keys to narrow the pages and rows the engine must examine.

    B-treeindex scanordered keys
    30-second interview answer

    A B-tree keeps key values ordered in a balanced search structure. Equality predicates descend to a narrow key range; range and ordered queries can then scan adjacent leaf entries instead of the whole table. Entries identify rows or, in some engines, contain the clustered row. The benefit depends on selectivity and avoided work; base-row lookups, random I/O, and maintenance are part of the cost.

  • IntermediatePerformance3 min

    Why should you not index every column?

    Every index consumes storage and must be maintained through writes, cleanup, and schema operations.

    write amplificationstorageindex maintenance
    30-second interview answer

    An index is another data structure to store, cache, update, log, replicate, back up, and maintain. Extra indexes slow inserts and relevant updates, increase storage and memory pressure, lengthen maintenance, and can overlap without serving a distinct workload. Design the smallest useful set from recurring query patterns and observe usage across a representative business cycle before removal.

    Concise answer
  • AdvancedPerformance9 min

    How should you choose column order in a composite index?

    Order keys from the workload's equality, range, ordering, grouping, and coverage needs—not one slogan.

    composite indexcolumn orderrange predicate
    30-second interview answer

    Start with the recurring query shape. Equality predicates often precede the first range predicate; following keys may support ORDER BY, GROUP BY, or coverage. 'Most selective first' is not universal, and newer engines may use skip-scan behavior in some cases. Compare important workload queries, data distribution, write cost, and the actual plan before replacing useful single-column indexes.

  • AdvancedPerformance3 min

    What is a covering index, and when can it hurt?

    An index can supply all required columns, but wider indexes increase storage and write work.

    covering indexindex-only scanINCLUDE
    30-second interview answer

    A covering index contains the search keys and every column the query needs, allowing an index-only access path when engine visibility and storage rules permit. It can remove many base-table lookups for a hot selective query. Wider entries reduce fan-out, use more cache and storage, and amplify writes, so coverage should serve a proven workload rather than every SELECT list.

    Concise answer
  • IntermediatePerformance3 min

    Why can a low-cardinality index be ineffective?

    If a predicate matches a large share of rows, scanning can cost less than index traversal plus lookups.

    selectivitycardinalityscan
    30-second interview answer

    An index on a value with few distinct options—such as a balanced boolean—may still point to half the table. Traversing the index and fetching many scattered rows can cost more than a sequential scan. Distribution matters more than the raw distinct count: a partial index for a rare status or a composite index matching a selective workload can still be valuable.

    Concise answer
  • AdvancedDebugging8 min

    Why might a database ignore an available index?

    The cost model may correctly prefer a scan—or be making a poor choice from bad estimates.

    statisticsselectivitysargability
    30-second interview answer

    The table may be small, the predicate may return many rows, the index prefix may not match, an expression or implicit cast may block a search condition, or base-row lookups may be expensive. Statistics, skew, parameters, and stale metadata can also mislead the optimizer. Inspect estimated and actual rows, bytes/pages read, and the predicate before forcing an access path.

  • SeniorPerformance3 min

    When are partial, filtered, or expression indexes useful?

    Index a valuable subset or repeatable expression when ordinary keys do not match the workload.

    partial indexfiltered indexexpression index
    30-second interview answer

    A partial or filtered index targets rows satisfying a stable predicate, such as open orders; an expression index stores a computed search key, such as normalized email. They can be smaller and more selective, but predicate matching, expression syntax, determinism rules, and maintenance differ by engine. Use them for a proven recurring shape and document the dialect boundary.

    Concise answer

Query Execution & Optimizers

Parsing, logical and physical plans, statistics, estimates, scans, joins, sorts, aggregation, and plan evidence.

Interview focus: Reason from estimated versus actual work rather than memorizing plan-node names.

  • AdvancedDatabase Internals9 min

    How does a database turn SQL text into a result?

    Follow SQL through parsing and binding, logical transformation, physical planning, execution, and result delivery.

    parserlogical planoptimizer
    30-second interview answer

    The engine parses SQL, resolves names and types, builds a logical relational expression, rewrites equivalent forms, estimates candidate physical plans, and chooses access paths, join algorithms, ordering, and aggregation operators. The executor runs that plan and streams or materializes results. Caches, statistics, parameters, memory, and concurrency influence the chosen and observed behavior.

  • AdvancedDatabase Internals3 min

    How does a cost-based optimizer use statistics?

    Estimate rows and resource work for possible plans from table and column distributions.

    statisticscardinality estimationcost model
    30-second interview answer

    The optimizer uses table sizes, distinct counts, histograms or similar distributions, null fractions, indexes, and correlation assumptions to estimate rows flowing through each operator. Those estimates drive scan, join, order, memory, and parallelism choices. Statistics are summaries, not truth; skew, correlated columns, stale data, and parameter values can create large estimate errors.

    Concise answer
  • IntermediatePerformance3 min

    When is a sequential scan better than an index scan?

    A scan can be optimal for small tables or queries that need a large share of rows.

    sequential scanindex scanselectivity
    30-second interview answer

    A sequential scan reads the table in storage order and can be cheaper when the table is small, most rows qualify, or index hits would trigger many scattered lookups. An index scan wins when it can avoid enough work or provide useful ordering. Do not treat every scan as a bug; compare actual rows, pages/bytes, cache state, and concurrency impact.

    Concise answer
  • AdvancedDatabase Internals3 min

    How do nested-loop, hash, and merge joins differ?

    Match the physical algorithm to input size, predicate, ordering, memory, and available access paths.

    nested loophash joinmerge join
    30-second interview answer

    Nested loops work well when the outer input is small and the inner side has an efficient lookup. Hash joins build a hash table for equality joins and suit larger unsorted inputs if memory is adequate. Merge joins advance through compatible ordered inputs and can exploit existing order. Estimates, spills, parallelism, and engine implementation matter more than declaring one algorithm best.

    Concise answer
  • SeniorDebugging3 min

    What should you compare in an estimated and actual execution plan?

    Compare estimated and observed rows, loops, time, I/O, memory, and spills without treating cost as elapsed time.

    EXPLAINactual rowsestimates
    30-second interview answer

    Look for the first large estimate error, repeated loops, rows removed, scans, join shape, sorts, spills, memory, and logical/physical I/O. Estimated cost is an internal comparison unit, not milliseconds. Actual-plan facilities may execute the statement, so use care with writes and production load. The most visually expensive node is not automatically the root cause.

  • SeniorScenario3 min

    Why might the same query use an index in staging but scan in production?

    Plans depend on data volume and distribution, statistics, parameters, configuration, and runtime resources.

    statisticsdata skewparameters
    30-second interview answer

    Production may have different row counts, value skew, statistics freshness, indexes, parameter distributions, cached plans, engine versions, memory, parallelism settings, or storage costs. Capture the exact SQL and bind values plus both plans and metadata. Reproducing only the schema is not environment parity; representative distribution is usually the missing evidence.

    Concise answer
  • SeniorDatabase InternalsQuery Lab12 min

    Given a conceptual plan whose filter estimate is 100 rows but actual output is 120,000, explain the join failure and the evidence-led next actions.

    A large early estimate error compounds upward and can make a nested loop that looked cheap execute an inner lookup 120,000 times.

    EXPLAIN ANALYZEcardinalitystatistics
    30-second interview answer

    A diagnosis and verification plan, not a rewritten result set. A large early estimate error compounds upward and can make a nested loop that looked cheap execute an inner lookup 120,000 times. Start at leaf nodes; the filter is wrong before the join is chosen. The optimizer expects 100 outer rows and prices a nested loop accordingly. Inspect stale or insufficient statistics, correlated predicates, skew, and parameters. Refresh or extend statistics, retest representative binds, then consider indexes or query changes.

    Open Query Lab →

SQL Performance Optimization

Sargability, excessive scans and sorts, pagination, query shape, N+1 behavior, statistics, and workload testing.

Interview focus: Start with a baseline and plan evidence; change one cause and remeasure.

  • SeniorScenario3 min

    A query grew from 100 ms to 12 seconds as a table reached 100 million rows. What do you investigate?

    Measure how rows, I/O, memory, sorting, and concurrency changed before proposing an index.

    data growthexecution planstatistics
    30-second interview answer

    Capture the current plan and runtime metrics, compare the old plan if available, and quantify returned versus examined rows, pages/bytes, sort or hash spills, lock waits, cache state, and parameter values. Check statistics and index alignment with the current query shape. Then change one cause, test representative values and concurrency, canary, and verify latency plus write overhead.

    Concise answer
  • IntermediatePerformance3 min

    What makes a predicate sargable?

    Write a predicate the access method can use to narrow keys rather than transform every row.

    sargabilityindexpredicate
    30-second interview answer

    A sargable predicate exposes a searchable key or range, such as ordered_at >= start AND ordered_at < end, rather than applying a function to the indexed column for every row. Implicit casts, arithmetic, leading wildcards, and collation mismatches can also block useful access. Expression indexes may help a stable transformed lookup, with dialect-specific rules.

    Concise answer
  • IntermediateDebugging3 min

    Why is DISTINCT sometimes a sign of a query bug?

    DISTINCT can hide an incorrect join and adds hash or sort work to remove the symptom.

    DISTINCTJOINsort
    30-second interview answer

    DISTINCT is correct when the result truly asks for a set of unique projected values. It is suspicious when added after a join to remove duplicates created by an incomplete key or one-to-many relationship. Define the output grain, repair the join or pre-aggregate/select the child row, then use DISTINCT only if unique projection is still the requirement.

    Concise answer
  • AdvancedPerformance3 min

    How do OFFSET and keyset pagination differ?

    OFFSET is simple but can scan and discard earlier rows; keyset continues from a stable ordered key.

    OFFSETkeyset paginationORDER BY
    30-second interview answer

    OFFSET/LIMIT supports arbitrary page numbers but the engine may still process and discard the preceding rows, and concurrent changes can create duplicates or gaps. Keyset pagination filters after the last composite sort key, making deep pages proportional to page size when an index matches. It needs a total deterministic order and cannot jump to arbitrary pages cheaply.

    Concise answer
  • IntermediatePerformance3 min

    Why can SELECT * be expensive in production code?

    Unneeded columns increase bytes, memory, serialization, and coupling and can block a covering access path.

    SELECT *row widthcovering index
    30-second interview answer

    SELECT * can read, transfer, allocate, and serialize columns the caller does not use; wide values make this especially costly. It also couples consumers to schema order and additions and may prevent an index-only plan. It is fine for ad hoc inspection, but stable application queries should name the contract and still measure the actual bottleneck.

    Concise answer
  • SeniorDebugging3 min

    How can parameter values cause a query-plan regression?

    One reusable plan can be excellent for common values and poor for rare or highly skewed values.

    parametersplan cachedata skew
    30-second interview answer

    If the engine compiles or caches a plan using one parameter distribution, that access path may not fit another. Capture slow and fast bind values, their cardinalities and plans, and the cache/compile behavior. Remedies differ by engine and may include better statistics, query reshaping, selective recompilation, or separate paths; forcing one plan can simply move the regression.

    Concise answer
  • SeniorPerformanceQuery Lab12 min

    Fetch the next 20 published posts after a cursor ordered by published_at descending, then post_id descending.

    Turn the last row's complete ordering key into a range predicate instead of rescanning and discarding OFFSET rows.

    keyset paginationcomposite indexcursor
    30-second interview answer

    One row per published post in the next page. Turn the last row's complete ordering key into a range predicate instead of rescanning and discarding OFFSET rows. post_id breaks equal published_at values. Use less-than comparisons that mirror descending order. Lead with the equality filter, then both ordering columns.

    Open Query Lab →
  • SeniorPerformanceQuery Lab12 min

    Rewrite a query that filters DATE(created_at) and lower(email), then propose an index only after stating the workload assumptions.

    Move time transformation to constants, define case-insensitive identity deliberately, and align a narrow index with the equality-plus-range access path.

    sargabilityrange predicateindex design
    30-second interview answer

    One row per matching order, newest first. Move time transformation to constants, define case-insensitive identity deliberately, and align a narrow index with the equality-plus-range access path. Use a half-open timestamp range so created_at remains directly searchable. Use an explicit normalized email expression or stored canonical column. Put email equality before the timestamp range and verify ordering needs.

    Open Query Lab →

Transactions, ACID & Isolation

Atomicity, boundaries, isolation anomalies, optimistic concurrency, retries, and long-running transactions.

Interview focus: Name the business invariant and the anomaly it must survive.

  • IntermediateConcept3 min

    What do ACID properties mean in a production database?

    Connect textbook properties to commit boundaries, invariants, concurrency, and failure recovery.

    ACIDtransactiondurability
    30-second interview answer

    Atomicity commits all operations or none; consistency means a transaction preserves declared invariants; isolation controls what concurrent transactions can observe; durability means committed effects survive the database's promised failures. ACID does not define every business rule or make distributed workflows atomic. The application must choose boundaries, constraints, isolation, retries, and external side-effect handling deliberately.

    Concise answer
  • IntermediateScenario3 min

    An API updates an account balance and inserts a ledger row. What if the second operation fails?

    Keep dependent database changes in one transaction so failure cannot expose partial state.

    transaction boundaryrollbackledger
    30-second interview answer

    Both changes belong in one local transaction if balance and ledger must agree. If the insert fails, roll back the balance update. Enforce supporting constraints and use a stable operation/idempotency key for retries. A remote notification or payment call should not be held inside the database transaction; use an outbox or another explicit delivery boundary.

    Concise answer
  • AdvancedConcept10 min

    How do transaction isolation levels differ?

    Explain isolation through prevented anomalies, then name the engine-specific implementation boundary.

    Read CommittedRepeatable ReadSerializable
    30-second interview answer

    In the standard model, Read Uncommitted can expose dirty data; Read Committed prevents dirty reads but may allow changing and phantom results; Repeatable Read protects repeated reads more strongly; Serializable makes committed outcomes equivalent to some serial order. PostgreSQL, MySQL, and SQL Server implement these with different locking and versioning semantics. Choose from the business invariant and test contention and retry behavior.

  • SeniorArchitecture3 min

    When is Serializable isolation justified?

    Use serializable semantics when a critical invariant is otherwise difficult to protect and retries are acceptable.

    Serializableinvariantretry
    30-second interview answer

    Serializable is justified when concurrent transactions must preserve a cross-row or predicate invariant and a simpler atomic statement, constraint, or explicit lock cannot express it safely. It can reduce concurrency or abort transactions that must be retried, depending on engine. Keep transactions short, make retry semantics explicit, and apply the level narrowly rather than upgrading the entire application by habit.

    Concise answer
  • AdvancedScenario3 min

    How can a lost update occur, and how does optimistic concurrency prevent it?

    Two writers can overwrite each other's read-modify-write result unless the write checks observed state.

    lost updateversion columnconditional update
    30-second interview answer

    A lost update occurs when two transactions read the same value, compute changes, and the later write overwrites the earlier one. Prefer one atomic UPDATE when possible. Otherwise include a version or original value in the predicate—UPDATE ... WHERE id = ? AND version = ?—then treat zero updated rows as a conflict to merge, retry, or report.

    Concise answer
  • SeniorPerformance3 min

    Why are long-running transactions dangerous?

    Long transactions extend lock and snapshot lifetimes, increase retained versions, and make failure recovery larger.

    long transactionlocksrow versions
    30-second interview answer

    A long transaction may hold locks, prevent version cleanup, retain old snapshots, grow logs or undo/version stores, delay replication or vacuum-like maintenance, and enlarge rollback work. Keep user interaction and remote calls outside it, batch large changes, set timeouts, and monitor transaction age as well as statement time. Exact consequences vary by engine.

    Concise answer
  • SeniorScenario3 min

    How should an application retry deadlock or serialization victims safely?

    Retry the complete transaction with bounded policy and isolate external effects from the retry loop.

    retrydeadlock victimidempotency
    30-second interview answer

    A deadlock or serialization failure invalidates the transaction, so retry from its beginning, not the last statement. Bound attempts, add jitter where useful, preserve deadlines, and instrument retry rate. Use idempotency keys or unique constraints for repeated commands and keep emails, payments, and other external effects behind an outbox or confirmed post-commit step.

    Concise answer
  • IntermediateQueryQuery Lab12 min

    Return every SKU's available quantity after subtracting only unexpired active reservations at a supplied instant.

    Preserve inventory rows and subtract a conditional sum of reservations that are active at one consistent timestamp.

    LEFT JOINinventorytime predicate
    30-second interview answer

    One row per SKU. Preserve inventory rows and subtract a conditional sum of reservations that are active at one consistent timestamp. Start from inventory and left-join live reservations. Filter active, unexpired rows in the join. Return both reserved and available so negative stock is visible.

    Open Query Lab →

Locks, Deadlocks & MVCC

Blocking, lock order and duration, deadlock cycles, snapshots, row versions, visibility, and cleanup pressure.

Interview focus: Reconstruct the transaction timeline and identify who must make progress.

  • IntermediateDatabase Internals3 min

    What are shared and exclusive locks, and why does lock granularity matter?

    Lock modes control compatible access; finer granularity increases concurrency but adds management cost.

    shared lockexclusive lockrow lock
    30-second interview answer

    Conceptually, shared locks allow compatible reads while exclusive locks protect modifications from conflicting access. Engines also use update, intent, range, metadata, and other modes. Row locks improve concurrency but create more lock-management work; page or table locks reduce overhead but block more work. Granularity and escalation behavior are engine-specific and should not be forced without evidence.

    Concise answer
  • IntermediateConcept3 min

    What is the difference between blocking and a deadlock?

    Blocking may resolve when one transaction finishes; a deadlock is a wait cycle that cannot resolve unaided.

    blockingdeadlockwait graph
    30-second interview answer

    Blocking is waiting for an incompatible lock and may be normal if the owner commits promptly. A deadlock is a cycle: each transaction waits for a resource held by another in the cycle, so the database breaks it by aborting a victim. Timeouts limit waiting but do not diagnose the cycle. Investigate owners, waiters, statements, resources, and transaction duration.

    Concise answer
  • SeniorDebugging9 min

    Two API endpoints occasionally deadlock while updating orders and inventory. How do you investigate and fix it?

    Capture the exact cycle, reconstruct lock order, reduce the cause, and verify under concurrency.

    deadlocklock ordertransaction scope
    30-second interview answer

    Capture the engine's deadlock graph or log, including statements, resources, indexes, transaction boundaries, and call paths. Reconstruct the two acquisition sequences. Fix recurring cycles with consistent row/table order, shorter transactions, narrower predicates and useful indexes, and no remote work under locks. Keep bounded victim retries, then load-test the same interleaving and monitor recurrence.

  • AdvancedDatabase Internals3 min

    How can MVCC let readers and writers operate concurrently?

    Readers can see a consistent visible version while writers create a newer version, reducing read-write blocking.

    MVCCsnapshotrow version
    30-second interview answer

    MVCC keeps enough version information for a transaction or statement to decide which row version is visible in its snapshot. A writer can create a new version while readers continue using an older committed one, so ordinary reads need not block that write. Writers can still conflict with writers, and snapshots do not automatically protect every business invariant.

  • SeniorDatabase Internals3 min

    What operational costs come with MVCC row versions?

    Old versions consume space and cleanup work until no active snapshot can require them.

    MVCCvacuumversion store
    30-second interview answer

    Versioning shifts some blocking cost into storage, visibility checks, cleanup, and write amplification. Long-lived transactions or snapshots can prevent old-version removal, increasing table/undo/version-store pressure and recovery work. PostgreSQL vacuum, InnoDB purge/undo, and SQL Server version stores differ, so monitor the engine's transaction age, retained versions, cleanup lag, and storage headroom.

    Concise answer

Constraints, Integrity & SQL Security

Database constraints, parameterized queries, least privilege, sensitive data, and trigger/stored-code boundaries.

Interview focus: Use application validation and database guarantees for their different jobs.

  • SeniorArchitecture3 min

    Should validation live in application code or database constraints?

    Use application validation for workflow feedback and database constraints for cross-writer guarantees.

    CHECKUNIQUEvalidation
    30-second interview answer

    Application validation can explain errors early and enforce workflow-specific rules. Database constraints protect invariants under concurrency and across every writer, including scripts, jobs, and future services. Use both: validate for experience, constrain for truth, map constraint failures to useful responses, and avoid duplicating rapidly changing policy as rigid DDL when it is not a stable data invariant.

    Concise answer
  • IntermediateConcept3 min

    What production value do foreign keys provide?

    Foreign keys make parent-child validity a database guarantee and force explicit delete semantics.

    FOREIGN KEYreferential integrityindex
    30-second interview answer

    A foreign key prevents child references to missing parents and controls what happens when a parent changes or is deleted. It protects against races and nonapplication writers. It also adds validation and locking work, especially for large changes, and supporting indexes are often important. Disabling it trades a visible cost for reconciliation and integrity risk that must be owned explicitly.

    Concise answer
  • FundamentalConcept3 min

    How should applications prevent SQL injection?

    Bind values as parameters and allow-list the limited identifiers that cannot be parameterized.

    SQL injectionparametersprepared statements
    30-second interview answer

    Use parameterized queries or prepared statements so user data is never parsed as SQL code. Escaping strings manually is not the preferred boundary. Identifiers and sort directions often cannot be bound, so select them from an allow-list rather than concatenating arbitrary input. Add least-privilege credentials, safe error handling, and security tests as defense in depth.

    Concise answer
  • SeniorArchitecture3 min

    What does least privilege mean for a database-backed application?

    Give each workload only the schemas and operations it needs and separate migration, runtime, and analytical identities.

    permissionsrolesrow-level security
    30-second interview answer

    Runtime services should not own DDL or unrestricted administrative rights. Use separate roles for migrations, reads, writes, reporting, and operations; scope by schema/table and rotate credentials. Row-level security can add a database policy boundary where supported, but it must be tested for bypass paths and performance. Audit privileged actions and plan break-glass access.

    Concise answer
  • SeniorArchitecture3 min

    What are the trade-offs of stored procedures, functions, and triggers?

    Database-side code can centralize atomic work but can hide behavior and complicate deployment and testing.

    stored procedurefunctiontrigger
    30-second interview answer

    Stored routines can reduce round trips, execute near data, centralize permissions, and keep multi-step operations atomic. Triggers can enforce or capture cross-writer behavior. Costs include vendor coupling, specialized testing and deployment, hidden side effects, recursion, observability, and mixed ownership. Use them for a clear database-owned boundary, document execution, and keep failure behavior visible.

    Concise answer

Views, Materialization & Partitioning

Abstraction, precomputation, refresh policy, range/list/hash partitioning, pruning, and maintenance.

Interview focus: Separate table partitioning from distributed sharding and measure pruning.

  • IntermediateConcept3 min

    How does a view differ from a materialized view?

    A view stores a query definition; a materialized view stores a result that must be refreshed.

    viewmaterialized viewrefresh
    30-second interview answer

    A normal view is a named query whose underlying data is read when referenced, subject to optimizer transformations. A materialized view persists a computed result and trades storage and refresh complexity for cheaper reads. Engine support, indexing, incremental refresh, consistency, locking, and permissions vary, so the design must state freshness and failure behavior.

    Concise answer
  • SeniorScenario3 min

    Would a materialized view help a dashboard aggregating billions of rows?

    Precomputation can isolate repeated read work if freshness, refresh, and failure semantics are acceptable.

    materialized viewdashboardrefresh strategy
    30-second interview answer

    It may help when many reads reuse the same expensive aggregation and the product accepts a defined freshness window. Quantify dimensions, filters, update rate, refresh duration, storage, failure recovery, and whether incremental updates are possible. Alternatives include summary tables, streaming projections, caches, or an analytical store. Verify that refresh work does not overload the OLTP system.

    Concise answer
  • AdvancedArchitecture3 min

    How do range, list, and hash partitioning differ?

    Choose a partition key from pruning, lifecycle, distribution, and operational needs.

    partitioningpartition pruningmaintenance
    30-second interview answer

    Range partitioning groups ordered intervals and is useful for time retention and range scans. List partitioning maps explicit categories such as regions. Hash partitioning spreads keys more evenly but weakens range locality. Partitioning helps only when queries prune partitions or operations benefit from boundaries; too many partitions, skew, global uniqueness, and cross-partition work add cost.

    Concise answer
  • AdvancedArchitecture3 min

    What is the difference between table partitioning and database sharding?

    Partitioning divides a table inside one database system; sharding distributes ownership across database nodes.

    partitioningshardingdistributed systems
    30-second interview answer

    Table partitioning is usually managed by one database instance or cluster and preserves one query/transaction system while splitting storage into partitions. Sharding routes key ranges or buckets to independently owned database units, adding distributed transactions, rebalancing, failures, topology, and operational tooling. Partition before sharding only when it serves the workload; neither is a growth ritual.

    Concise answer

Replication & Database Scaling

Read replicas, lag, read-after-write behavior, scaling reads, scaling writes, and workload placement.

Interview focus: Define consistency per operation before routing traffic to copies.

  • IntermediateScenario3 min

    Why can a user update a profile and immediately read stale data?

    The write committed on the primary, but an asynchronously updated replica has not applied it yet.

    read replicareplication lagread-after-write
    30-second interview answer

    A read routed to an asynchronous replica can observe a state before the just-committed primary write. Remedies depend on the product requirement: read that entity from the primary for a bounded session, use a consistency token/high-water mark, wait for replica position, or make the UI tolerate staleness. Define the required operation, lag budget, and failure fallback.

    Concise answer
  • SeniorArchitecture3 min

    How can an application provide read-after-write consistency with replicas?

    Provide the guarantee only where needed through routing or causal progress rather than making every read synchronous.

    read-after-writesession guaranteereplica
    30-second interview answer

    Options include primary reads after a user's write for a time window, routing by entity/session, attaching a commit position and waiting for a replica to reach it, or returning the written representation directly. Each adds latency, primary load, state, or failover complexity. State whether the guarantee is per user, entity, region, or globally ordered.

    Concise answer
  • SeniorArchitecture3 min

    How do scaling database reads and writes differ?

    Replicas and caches can distribute reads; writes require authoritative ordering or partitioned ownership.

    read replicacachesharding
    30-second interview answer

    Read scaling can use indexes, caching, replicas, projections, or analytical copies, each with freshness semantics. Write scaling starts by reducing unnecessary writes, batching, and buying simple headroom; partitioned ownership may then distribute write load but adds routing, cross-partition invariants, rebalancing, and failure modes. Identify the actual bottleneck before proposing sharding.

    Concise answer
  • Staff / PrincipalArchitecture3 min

    When should work move from an OLTP database to an analytical system?

    Separate scan-heavy, historical, and multidimensional work when it harms transactional SLOs or needs a different storage model.

    OLTPOLAPworkload isolation
    30-second interview answer

    Move analytical work when large scans, wide aggregations, retention, or concurrency compete with latency-sensitive transactions, or when columnar storage and independent scaling materially help. Define ingestion and transformation ownership, freshness, correctness, backfills, security, cost, and reconciliation. A warehouse is not an excuse to abandon source-of-truth semantics or data contracts.

    Concise answer

SQL + Application Engineering

ORMs, N+1 queries, prepared statements, connection pools, batching, transaction scope, and raw SQL boundaries.

Interview focus: Connect database capacity and correctness to application ownership.

  • IntermediateArchitecture3 min

    When is an ORM helpful, and when should you write raw SQL?

    Use an ORM for routine persistence and unit-of-work patterns; use explicit SQL when query shape is the product requirement.

    ORMraw SQLquery visibility
    30-second interview answer

    ORMs reduce mapping boilerplate, centralize conventions, and make ordinary CRUD and relationships productive. They can hide N+1 queries, generate poor joins, blur transaction scope, and make advanced analytics or engine features awkward. Keep generated SQL observable. Use raw SQL for performance-critical, set-based, reporting, bulk, or dialect-specific work when the explicit query is clearer and testable.

    Concise answer
  • IntermediatePerformance3 min

    What is the N+1 query problem, and how do you fix it?

    One parent query triggers one child query per row, multiplying latency and database work.

    N+1ORMbatching
    30-second interview answer

    N+1 occurs when loading N parents causes N additional relationship queries. Detect it with query counts and traces, not only a slow-query log. Fix with a join, explicit eager loading, batch IN lookup, or data-loader pattern while preserving pagination and avoiding a huge Cartesian result. Verify query count, rows/bytes, memory, and latency under realistic N.

    Concise answer
  • SeniorPerformance3 min

    How do connection pools work, and why can an oversized pool hurt?

    Pools reuse expensive connections and bound database concurrency; too many active sessions create contention and memory pressure.

    connection poolqueueingcapacity
    30-second interview answer

    A pool leases established connections to callers and queues when none are available. Size it from database capacity, query latency, application instances, and workload isolation—not per-process intuition. An oversized fleet can raise runnable queries, lock contention, context switching, memory, and tail latency. Measure acquisition wait, usage time, saturation, leaks, and database active-session capacity.

    Concise answer
  • SeniorArchitecture3 min

    How should transaction scope cross application service methods?

    The business operation should own one explicit local transaction without hiding remote work inside it.

    transaction scopeservice methodunit of work
    30-second interview answer

    Place the transaction at the application boundary that owns the local database invariant, and pass repositories the same unit of work or connection. Avoid accidental nested/autocommit behavior and broad annotations that hold a transaction across network calls, user think time, or large computation. Make propagation rules, rollback conditions, timeouts, and post-commit side effects testable.

    Concise answer
  • IntermediatePerformance3 min

    What benefits do prepared statements and query batching provide?

    Parameters improve safety and may reuse work; batches reduce network and transaction overhead within capacity limits.

    prepared statementbatchingparameters
    30-second interview answer

    Prepared statements separate code from values and can reduce repeated parse/compile work depending on driver and engine. Batching combines writes or lookups to reduce round trips and commit overhead. Very large batches increase memory, lock duration, log spikes, rollback cost, and parameter limits, so choose a bounded size and preserve per-row error and idempotency semantics.

    Concise answer

Migrations, Incidents & Staff Judgment

Backward-compatible DDL, backfills, capacity, saturation, noisy neighbors, governance, SLOs, and failed changes.

Interview focus: Sequence reversible changes with evidence, owners, abort criteria, and verification.

  • Staff / PrincipalScenario10 min

    How would you add a NOT NULL column to a 500-million-row table without downtime?

    Sequence a backward-compatible schema change, bounded backfill, validation, and constraint enforcement using engine-specific capabilities.

    schema migrationbackfillNOT NULL
    30-second interview answer

    First check the engine/version because metadata-only defaults, online validation, and locks differ. A common sequence is add a nullable column, deploy writers that populate it, backfill in resumable rate-limited batches, validate missing rows and replicas, then enforce the constraint with the least-blocking supported mechanism. Keep old readers compatible, monitor lock/log/lag impact, and define abort and rollback boundaries.

  • SeniorScenario3 min

    How would you create an index on a huge production table safely?

    Use the engine's online/concurrent path where appropriate and treat the build as a capacity event.

    index buildonline DDLreplica lag
    30-second interview answer

    Verify the query need and estimate build time, storage, temporary space, I/O, log generation, replica lag, and lock phases. Use the engine's online or concurrent option if its limitations fit, schedule against headroom, set timeouts, and monitor abortability. Afterward validate index validity, plan adoption, write overhead, and whether an overlapping index can be removed safely.

    Concise answer
  • SeniorDebugging3 min

    Database CPU reaches 95%. How do you investigate?

    Protect users, identify which query shapes and frequency consume CPU, and distinguish useful work from amplified work.

    CPUquery frequencyincident
    30-second interview answer

    Confirm saturation and user impact, compare traffic and deployments, and rank queries by total CPU—not only slowest single call. Inspect frequency, scans, joins, sorts, parsing, parallelism, retries, and maintenance. Mitigate with rollback, rate limits, workload isolation, or a proven query/index change; then verify CPU per request, throughput, tail latency, and downstream effects.

    Concise answer
  • SeniorDebugging3 min

    Applications time out because no database connections are available. What do you investigate?

    Trace pool acquisition, lease duration, active database work, leaks, and traffic before increasing limits.

    connection exhaustionpoolslow queries
    30-second interview answer

    Measure pool wait, active/idle/leaked leases, transaction age, query latency, blocked sessions, instance count, and recent traffic or deployment changes. Long queries and lock waits can occupy every connection without a code leak. Protect users with admission control or rollback, fix ownership and timeouts, and size the whole fleet against database capacity rather than raising every pool.

    Concise answer
  • SeniorScenario3 min

    A production database migration fails halfway through. What should happen next?

    Determine the committed state, protect compatible application versions, and choose rollback or forward repair from evidence.

    failed migrationrollbackrecovery
    30-second interview answer

    Stop automatic retries until the exact committed DDL and data state is known; transactional DDL behavior differs by engine. Verify application compatibility, locks, replicas, and partial backfill progress. Prefer an idempotent forward repair when rollback would destroy new writes; otherwise execute the rehearsed rollback. Record phase state, validate invariants, and fix the migration's resumability and abort criteria.

    Concise answer
  • Staff / PrincipalArchitecture9 min

    Twenty teams share one PostgreSQL cluster and noisy workloads degrade production APIs. How would you approach it?

    Build workload attribution and guardrails before deciding which tenants, teams, or workloads need physical isolation.

    workload isolationgovernanceSLO
    30-second interview answer

    Start with per-team/query attribution, SLOs, connections, CPU/I/O, memory, locks, storage growth, and incident history. Add ownership, query and migration budgets, admission controls, pool limits, slow-query review, safe indexes, and workload scheduling. Separate analytical or untrusted workloads first when evidence shows incompatible SLOs. Any cluster split needs cost, migration, on-call, failover, and developer-productivity ownership.

    Concise answer

Answer in layers, not slogans

Start with the version you can say in 30 seconds, then add mechanism, SQL, dialect boundary, plan implications, mistakes, and Senior-level judgment.

FundamentalsIntermediateConcept8 min

What is SQL's logical query-processing order?

Back to library

30-second interview answer

A useful logical model is FROM and JOINs first, then WHERE, GROUP BY, aggregate calculation, HAVING, window functions, SELECT, DISTINCT, ORDER BY, and finally row limiting. Engines can physically reorder work when equivalence is preserved. The model explains scope rules—for example, a SELECT alias usually does not exist when WHERE is evaluated—and helps predict where rows disappear or multiply.

Mental model

Treat every clause as receiving a relation from the previous logical step. FROM builds rows, WHERE removes rows, grouping changes grain, windows annotate the grouped result, and SELECT shapes the output.

Understand the mechanism

Written SQL is declarative: it describes the required result, not a mandatory execution program.

The logical order is a reasoning tool for visibility, row preservation, and grain. The optimizer may physically reorder equivalent work.

Window functions conceptually run after grouping and HAVING, which is why they can rank aggregate results but normally need an outer query before filtering their output.

Aggregate first, rank second

Portable SQL
SELECT department_id, SUM(amount) AS revenue,
       DENSE_RANK() OVER (ORDER BY SUM(amount) DESC) AS revenue_rank
FROM sales
WHERE sold_at >= DATE '2026-01-01'
GROUP BY department_id
HAVING SUM(amount) > 10000
ORDER BY revenue_rank;

Dialect boundary

  • Alias visibility differs at a few vendor extensions; portable answers should not depend on a SELECT alias in WHERE.
  • QUALIFY is available in some platforms for filtering window results, but not in PostgreSQL or MySQL.

Performance and plan perspective

  • Logical order does not prove physical order. Read the actual plan before claiming a filter or join executed first.
  • Pushdown is valid only when it preserves semantics, especially around outer joins and aggregation.

Common candidate mistakes

  • Reciting SELECT first because it appears first in text.
  • Confusing logical processing with the engine's chosen operator order.

Interviewer follow-ups

  • Why can ORDER BY usually see a SELECT alias?
  • Where do DISTINCT and LIMIT fit?

Senior-level perspective

Use logical order to prove correctness, then use the physical plan to explain cost. Strong candidates never use one model as a substitute for the other.

Canonical answer linked by a stable fragment URL.
JOINsIntermediateQuery8 min

How can WHERE accidentally turn a LEFT JOIN into an INNER JOIN?

Back to library

30-second interview answer

After a LEFT JOIN, unmatched rows have NULLs on the right. A WHERE condition such as WHERE payments.status = 'settled' evaluates UNKNOWN for those rows and removes them, leaving only matches. Put the predicate in ON when it defines which right rows qualify while all left rows must remain. Keep it in WHERE when unmatched rows should be removed.

Mental model

ON controls which right rows match; LEFT JOIN then manufactures a NULL-extended right side for nonmatches; WHERE decides which completed joined rows survive.

Understand the mechanism

A predicate on the right table in WHERE often evaluates UNKNOWN for a manufactured NULL row, and WHERE keeps only TRUE.

Move the predicate into ON when it describes eligible matches but the left population must remain complete.

Keep the predicate in WHERE when the requirement truly excludes left rows without a qualifying match.

Preserve accounts with no settled invoice

Portable SQL
SELECT a.account_id, COUNT(i.invoice_id) AS settled_invoices
FROM accounts a
LEFT JOIN invoices i
  ON i.account_id = a.account_id
 AND i.status = 'settled'
GROUP BY a.account_id;

Performance and plan perspective

  • Predicate placement changes semantics first; any plan improvement is secondary.
  • An index starting with invoice.account_id can support the join probe.

Common candidate mistakes

  • Adding OR i.status IS NULL as a patch; a matching non-settled row can still suppress the intended zero-match case.
  • Using COUNT(*) when the desired count is matched invoices.

Interviewer follow-ups

  • How do you return only accounts with no settled invoice?
  • What changes with a FULL OUTER JOIN?

Senior-level perspective

State the preserved population and the output grain before touching syntax. That habit catches both correctness bugs and silent metric inflation in production reports.

Canonical answer linked by a stable fragment URL.
Window FunctionsIntermediateQuery8 min

How do ROW_NUMBER(), RANK(), and DENSE_RANK() differ?

Back to library

30-second interview answer

ROW_NUMBER assigns every row a unique sequence, so include a stable tiebreaker when the chosen row matters. RANK gives ties the same rank and leaves gaps; DENSE_RANK gives ties the same rank without gaps. Use ROW_NUMBER to select exactly one row, RANK for competition-style placement, and DENSE_RANK to rank distinct value groups.

Mental model

ROW_NUMBER ranks rows, RANK ranks competition positions, and DENSE_RANK ranks distinct ordered values.

Understand the mechanism

ROW_NUMBER always emits 1, 2, 3; tied sort values still receive different numbers, so the ORDER BY must include a stable tie-breaker when the winner matters.

RANK gives ties the same number and skips following positions: 1, 2, 2, 4.

DENSE_RANK gives ties the same number without gaps: 1, 2, 2, 3.

Compare tie behavior

Portable SQL
SELECT player_id, score,
  ROW_NUMBER() OVER (ORDER BY score DESC, player_id) AS row_number,
  RANK()       OVER (ORDER BY score DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM leaderboard;

Performance and plan perspective

  • Partitioned ranking usually requires ordering each partition; an aligned index can reduce sorting but does not remove the need to process qualifying rows.

Common candidate mistakes

  • Using ROW_NUMBER without a deterministic tie-breaker.
  • Using ROW_NUMBER for top-N-with-ties and silently dropping equal values.

Interviewer follow-ups

  • Which function returns exactly one latest row per account?
  • How would you return the top two distinct salaries per department?

Senior-level perspective

Translate product language—exactly N rows, all ties, or N distinct values—into a ranking contract before choosing the function.

Canonical answer linked by a stable fragment URL.
IndexesIntermediateDatabase Internals8 min

How does a B-tree index speed up a query?

Back to library

30-second interview answer

A B-tree keeps key values ordered in a balanced search structure. Equality predicates descend to a narrow key range; range and ordered queries can then scan adjacent leaf entries instead of the whole table. Entries identify rows or, in some engines, contain the clustered row. The benefit depends on selectivity and avoided work; base-row lookups, random I/O, and maintenance are part of the cost.

Mental model

A B-tree stores ordered keys in a shallow hierarchy. The engine descends to the first relevant leaf, scans a key range, then retrieves visible rows unless the index covers the query.

Understand the mechanism

Equality and ordered range predicates match B-tree ordering well. A usable prefix can also supply ORDER BY and sometimes GROUP BY order.

An index entry is not always the row: base-table lookups, visibility checks, and random I/O can dominate when many rows qualify.

Every extra index consumes storage and adds work to writes, vacuum/cleanup, logging, replication, and cache pressure.

Equality then range

PostgreSQL
CREATE INDEX idx_orders_customer_created
  ON orders (customer_id, created_at DESC);

SELECT order_id, created_at
FROM orders
WHERE customer_id = 42
  AND created_at >= TIMESTAMPTZ '2026-01-01 00:00:00+00'
ORDER BY created_at DESC
LIMIT 20;

Dialect boundary

  • PostgreSQL, MySQL, and SQL Server all use B-tree-family structures by default, but included columns, clustering, visibility, and scan terminology differ.

Performance and plan perspective

  • Index benefit depends on selectivity, correlation, cache state, returned columns, and table layout.
  • Validate with representative data and actual I/O—not only the presence of an index scan.

Common candidate mistakes

  • Saying an index makes every lookup O(log n) and stopping there.
  • Ignoring the write and maintenance path.

Interviewer follow-ups

  • When is a sequential scan cheaper?
  • What makes an index-only scan possible?

Senior-level perspective

An index is a workload-specific materialized access path. Defend it with read savings, write cost, operational footprint, and measured evidence.

Canonical answer linked by a stable fragment URL.
IndexesAdvancedPerformance9 min

How should you choose column order in a composite index?

Back to library

30-second interview answer

Start with the recurring query shape. Equality predicates often precede the first range predicate; following keys may support ORDER BY, GROUP BY, or coverage. 'Most selective first' is not universal, and newer engines may use skip-scan behavior in some cases. Compare important workload queries, data distribution, write cost, and the actual plan before replacing useful single-column indexes.

Mental model

A composite B-tree is lexicographically ordered: later keys are most useful after earlier keys have constrained the search interval.

Understand the mechanism

A recurring equality key commonly comes before the first range key because equality narrows one contiguous portion of the tree.

Columns after a range may still aid filtering, ordering, or coverage, but may not further narrow the initial seek in the same way.

Column order must be chosen across the important workload, not optimized for one isolated query or a blanket 'most selective first' rule.

Match tenant, state, and recent time

Portable SQL
CREATE INDEX idx_jobs_tenant_state_created
  ON jobs (tenant_id, state, created_at DESC);

SELECT job_id, created_at
FROM jobs
WHERE tenant_id = 7 AND state = 'queued'
ORDER BY created_at DESC
FETCH FIRST 50 ROWS ONLY;

Dialect boundary

  • PostgreSQL can sometimes use skip scans; MySQL and SQL Server have their own optimizer rules. Do not turn leftmost-prefix guidance into an absolute.

Performance and plan perspective

  • Compare existing index prefixes before adding a redundant index.
  • Wide keys and included columns amplify writes and may reduce cache density.

Common candidate mistakes

  • Sorting columns by global distinct count.
  • Ignoring ORDER BY, range boundaries, and the other high-value queries.

Interviewer follow-ups

  • Would you add status before tenant_id?
  • Can the index serve a query filtering only created_at?

Senior-level perspective

Present an index portfolio, not an index in isolation: covered query shapes, redundant structures removed, write budget, and rollout measurement.

Canonical answer linked by a stable fragment URL.
IndexesAdvancedDebugging8 min

Why might a database ignore an available index?

Back to library

30-second interview answer

The table may be small, the predicate may return many rows, the index prefix may not match, an expression or implicit cast may block a search condition, or base-row lookups may be expensive. Statistics, skew, parameters, and stale metadata can also mislead the optimizer. Inspect estimated and actual rows, bytes/pages read, and the predicate before forcing an access path.

Mental model

The optimizer chooses the estimated cheapest complete plan. An available index is merely one candidate access path, and using it may require expensive row lookups or poor join choices.

Understand the mechanism

A scan can be correct when a predicate returns much of the table, the table is small, or rows are physically cheap to read sequentially.

An index may be unusable or weak because the useful prefix is missing, types are implicitly cast, functions wrap keys, or collation and expression semantics differ.

A wrong choice often begins with stale statistics, skew, cross-column correlation, or parameter distributions that the estimates do not represent.

Collect evidence before forcing a path

PostgreSQL
EXPLAIN (ANALYZE, BUFFERS)
SELECT order_id, total_amount
FROM orders
WHERE status = 'completed';

Performance and plan perspective

  • Find the first estimated-versus-actual row divergence in the plan.
  • Measure pages, rows, loops, spills, and latency across representative parameter values.

Common candidate mistakes

  • Assuming any sequential scan is a bug.
  • Using an optimizer hint before fixing statistics, semantics, or the access path.

Interviewer follow-ups

  • What is sargability?
  • How does parameter skew change plan reuse?

Senior-level perspective

Treat the plan choice as a hypothesis produced from a cost model. Repair its inputs or workload shape, then verify; hints are a last-resort operational contract.

Canonical answer linked by a stable fragment URL.
Plans & OptimizersAdvancedDatabase Internals9 min

How does a database turn SQL text into a result?

Back to library

30-second interview answer

The engine parses SQL, resolves names and types, builds a logical relational expression, rewrites equivalent forms, estimates candidate physical plans, and chooses access paths, join algorithms, ordering, and aggregation operators. The executor runs that plan and streams or materializes results. Caches, statistics, parameters, memory, and concurrency influence the chosen and observed behavior.

Mental model

SQL travels through parse → bind/type-check → logical rewrite → costed physical alternatives → executable operators → rows and runtime telemetry.

Understand the mechanism

Parsing recognizes syntax; binding resolves tables, columns, functions, types, permissions, and dependencies.

Logical rewrites simplify and transform equivalent relational expressions before the optimizer estimates cardinality and cost.

Physical planning chooses access paths, join order and algorithms, aggregation, sorting, and parallelism under engine constraints.

Execution exposes the difference between predicted and observed behavior through rows, loops, I/O, memory, waits, and spills.

Inspect the physical contract

PostgreSQL
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT c.segment, SUM(o.total_amount)
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY c.segment;

Dialect boundary

  • Plan caches, adaptive behavior, compilation boundaries, and operator names vary considerably across engines.

Performance and plan perspective

  • Planning time and execution time are separate costs.
  • Prepared parameters, statistics, memory grants, cache state, and concurrency can change the observed path.

Common candidate mistakes

  • Describing the text order as the executor order.
  • Treating optimizer cost units as milliseconds.

Interviewer follow-ups

  • When is a plan reused?
  • Where can cardinality error enter the pipeline?

Senior-level perspective

Use this pipeline as a diagnostic map: correctness at binding/logical layers, plan quality at estimation/optimization, and operational reality at execution.

Canonical answer linked by a stable fragment URL.
Plans & OptimizersSeniorDebugging3 min

What should you compare in an estimated and actual execution plan?

Back to library

30-second interview answer

Look for the first large estimate error, repeated loops, rows removed, scans, join shape, sorts, spills, memory, and logical/physical I/O. Estimated cost is an internal comparison unit, not milliseconds. Actual-plan facilities may execute the statement, so use care with writes and production load. The most visually expensive node is not automatically the root cause.

Mental model

Read a plan from the leaves upward. At each node compare estimated rows with actual rows × loops, then connect the first large divergence to upstream operator choices.

Understand the mechanism

Estimated plans show optimizer beliefs; actual plans add measurements gathered while executing the chosen plan.

A large leaf estimate error can make a nested loop, join order, or memory allocation look cheap during planning and expensive at runtime.

Time at a parent includes child work in many displays. The biggest percentage or cost number is not automatically the root cause.

Capture runtime evidence

PostgreSQL
EXPLAIN (ANALYZE, BUFFERS, SETTINGS, WAL)
SELECT *
FROM orders
WHERE tenant_id = 42 AND state = 'pending';

Dialect boundary

  • PostgreSQL EXPLAIN ANALYZE executes the query. SQL Server actual plans and MySQL EXPLAIN ANALYZE expose similar evidence with different metrics and safety considerations.

Performance and plan perspective

  • Inspect rows, loops, rows removed, buffer or page reads, temp spills, memory, and waits together.
  • Test cold/warm cache and representative parameters when they matter to the incident.

Common candidate mistakes

  • Comparing cost directly with wall-clock time.
  • Tuning a parent node without finding the first bad estimate or excess row source.

Interviewer follow-ups

  • What causes cardinality error?
  • What if estimates are accurate but the query is still slow?

Senior-level perspective

Capture a baseline, state the suspected causal chain, change one material variable, and compare work—not just one lucky latency sample.

Canonical answer linked by a stable fragment URL.
TransactionsAdvancedConcept10 min

How do transaction isolation levels differ?

Back to library

30-second interview answer

In the standard model, Read Uncommitted can expose dirty data; Read Committed prevents dirty reads but may allow changing and phantom results; Repeatable Read protects repeated reads more strongly; Serializable makes committed outcomes equivalent to some serial order. PostgreSQL, MySQL, and SQL Server implement these with different locking and versioning semantics. Choose from the business invariant and test contention and retry behavior.

Mental model

Isolation is a contract over anomalies and serialization, not a speed ladder. Begin with the invariant and the concurrent histories that could violate it.

Understand the mechanism

Read Committed generally gives each statement a committed view but allows a later statement to observe different committed data.

Repeatable Read names a standard level, but engines differ in snapshot scope, phantom behavior, locking, and write-conflict handling.

Serializable requires committed transactions to have an outcome equivalent to some serial order; implementations may block, abort, or combine both.

Protect a state transition

Portable SQL
BEGIN;
SELECT state FROM orders WHERE order_id = 42 FOR UPDATE;
UPDATE orders SET state = 'shipped'
WHERE order_id = 42 AND state = 'paid';
COMMIT;

Dialect boundary

  • PostgreSQL Repeatable Read prevents some anomalies beyond the minimum standard; MySQL's default and gap-lock behavior differ; SQL Server offers locking and row-versioning modes.

Performance and plan perspective

  • Stronger isolation can add blocking, validation aborts, retained versions, or retries.
  • Keep transactions short and test the actual interleavings and contention profile.

Common candidate mistakes

  • Claiming one anomaly table completely describes every engine.
  • Raising isolation without designing retry and idempotency behavior.

Interviewer follow-ups

  • What is write skew?
  • When is a unique constraint stronger and cheaper than Serializable?

Senior-level perspective

Choose the narrowest mechanism that proves the invariant: constraint, atomic conditional write, explicit lock, optimistic version, or transaction isolation—with failure handling designed in.

Canonical answer linked by a stable fragment URL.
Locks & MVCCSeniorDebugging9 min

Two API endpoints occasionally deadlock while updating orders and inventory. How do you investigate and fix it?

Back to library

30-second interview answer

Capture the engine's deadlock graph or log, including statements, resources, indexes, transaction boundaries, and call paths. Reconstruct the two acquisition sequences. Fix recurring cycles with consistent row/table order, shorter transactions, narrower predicates and useful indexes, and no remote work under locks. Keep bounded victim retries, then load-test the same interleaving and monitor recurrence.

Mental model

A deadlock is a cycle in the wait-for graph. Timeouts reveal waiting; only the captured cycle identifies the resources and acquisition order that created a deadlock.

Understand the mechanism

Collect the engine deadlock report with statements, transaction IDs, locked objects or keys, access paths, and the chosen victim.

Map each statement back to application transaction boundaries and reconstruct resource acquisition order on both paths.

Break the recurring cycle with consistent ordering, shorter scope, narrower access, useful indexes, or a redesigned invariant boundary.

Use a consistent lock order

Portable SQL
BEGIN;
SELECT account_id
FROM accounts
WHERE account_id IN (7, 9)
ORDER BY account_id
FOR UPDATE;
-- Apply both balance changes after locks are acquired.
COMMIT;

Performance and plan perspective

  • An unselective update can lock more rows and for longer than intended.
  • Bounded jittered retries handle victims; they do not repair a recurring acquisition cycle.

Common candidate mistakes

  • Calling every lock timeout a deadlock.
  • Adding unlimited retries or increasing timeouts as the only fix.

Interviewer follow-ups

  • Why can a missing index contribute to deadlocks?
  • How do you reproduce the interleaving?

Senior-level perspective

The deliverable is not 'deadlocks can happen'; it is a captured graph, causal acquisition sequence, targeted mitigation, concurrency test, and recurrence monitor.

Canonical answer linked by a stable fragment URL.
Locks & MVCCAdvancedDatabase Internals3 min

How can MVCC let readers and writers operate concurrently?

Back to library

30-second interview answer

MVCC keeps enough version information for a transaction or statement to decide which row version is visible in its snapshot. A writer can create a new version while readers continue using an older committed one, so ordinary reads need not block that write. Writers can still conflict with writers, and snapshots do not automatically protect every business invariant.

Mental model

Rows have versions plus visibility metadata. A snapshot selects the version visible to a reader while a writer creates a newer version for future snapshots.

Understand the mechanism

MVCC reduces ordinary read/write blocking because a reader can continue from a committed older version rather than waiting for an in-place overwrite.

Writers can still conflict with other writers, and explicit locking reads deliberately change the concurrency contract.

Old versions require cleanup and cannot always be removed while long-running snapshots might still need them.

Optimistic writer conflict

Portable SQL
UPDATE documents
SET body = :body, version = version + 1
WHERE document_id = :id AND version = :expected_version;
-- Zero affected rows means the caller must reload or reconcile.

Dialect boundary

  • Visibility metadata, undo/version stores, cleanup, and default snapshot scope differ between PostgreSQL, MySQL/InnoDB, and SQL Server row versioning.

Performance and plan perspective

  • Monitor version-store or dead-tuple growth, cleanup lag, long transactions, and replica effects.
  • MVCC improves concurrency but can trade blocking for storage, cleanup, and abort cost.

Common candidate mistakes

  • Saying MVCC means no locks.
  • Assuming a snapshot prevents write skew or lost business invariants.

Interviewer follow-ups

  • What prevents an old version from being reclaimed?
  • How do two writers of the same row behave?

Senior-level perspective

Connect the visibility model to operations: long transactions, cleanup pressure, bloat/version-store growth, replicas, and safe invariant enforcement.

Canonical answer linked by a stable fragment URL.
Migrations & IncidentsStaff / PrincipalScenario10 min

How would you add a NOT NULL column to a 500-million-row table without downtime?

Back to library

30-second interview answer

First check the engine/version because metadata-only defaults, online validation, and locks differ. A common sequence is add a nullable column, deploy writers that populate it, backfill in resumable rate-limited batches, validate missing rows and replicas, then enforce the constraint with the least-blocking supported mechanism. Keep old readers compatible, monitor lock/log/lag impact, and define abort and rollback boundaries.

Mental model

Treat schema change as a multi-deploy state machine: expand compatibility, populate incrementally, prove completeness, enforce, then contract old behavior.

Understand the mechanism

Inspect the exact engine/version, table shape, replication topology, lock behavior, and whether a constant default is metadata-only.

Add a nullable or otherwise backward-compatible column first; deploy writers that populate it before historical backfill begins.

Backfill in resumable bounded batches with rate controls, checkpoints, replica-lag and log-volume monitoring, and an explicit abort threshold.

Validate no missing values and application compatibility before enforcing NOT NULL with the least-blocking supported technique.

Expand before enforcement

PostgreSQL
ALTER TABLE orders ADD COLUMN region_code text;
-- Deploy dual-compatible writers, then backfill bounded key ranges.
ALTER TABLE orders
  ADD CONSTRAINT orders_region_present
  CHECK (region_code IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_region_present;
ALTER TABLE orders ALTER COLUMN region_code SET NOT NULL;

Dialect boundary

  • Online DDL, validation locks, metadata-only defaults, and whether a validated check can accelerate NOT NULL enforcement vary by engine and version.

Performance and plan perspective

  • Watch lock waits, transaction log/WAL, replicas, I/O, autovacuum/cleanup, cache churn, and application error rate.
  • Throttle on system health, not a fixed batch size copied from another table.

Common candidate mistakes

  • Running one giant UPDATE inside a long transaction.
  • Assuming rollback of DDL and backfill is instant or harmless.

Interviewer follow-ups

  • What are the pause and abort conditions?
  • How do old binaries behave during rollback?

Senior-level perspective

Define ownership, observability, forward and backward compatibility, resumability, and the point of no return before scheduling the change.

Canonical answer linked by a stable fragment URL.

Continue into database system design

This hub is the canonical preparation route. Existing SQL, distributed-systems, and architecture material remains linked for deeper system context.

Editorial method and primary sources

The 102 canonical items are original and consolidated to avoid near-duplicate answer cards. Runtime behavior is checked against current official PostgreSQL, MySQL, and SQL Server documentation, with engine-specific claims labeled.

  • Define output grain before syntax
  • Explain the physical work after correctness
  • Verify production claims with evidence