Interview Prep

SQL Interview Questions for Experienced (2026 Guide)

Ace sql interview questions for experienced data analysts. Master window functions, query optimization, join fan-out, and scenario CTEs with code.

Anuj SainiSep 20, 202616 min read

When interviewing for mid-level, senior, or lead analytics and data engineering roles, technical screening moves far beyond basic SELECT and GROUP BY syntax. Hiring panels evaluate your mastery of sql interview questions for experienced professionals by testing execution plan optimization, relational grain control, recursive modeling, and complex analytical window frames. The primary solution to acing these interviews is treating SQL as a declarative execution engine: identifying query bottlenecks with EXPLAIN ANALYZE, pre-aggregating one-to-many joins to stop metric inflation, and writing deterministic window logic.

This sql interview questions for experienced practical guide delivers battle-tested query patterns, verifiable failure traps, and live sandbox drills vetted against senior engineering hiring standards at top product tech companies, fintech platforms, and quantitative analytics teams. To evaluate market benchmarks and salary ranges for these senior levels, consult our Data Analyst Salary Guide 2026 or follow the comprehensive curriculum in our 12-Week Data Analyst Career Track.



Senior SQL Evaluation Rubric: What Hiring Managers Actually Test

Junior interviews test whether you can extract requested data. Senior interviews test whether your query can execute reliably against a 500-million-row warehouse without exhausting buffer memory or generating false metrics.

       Junior Assessment                         Senior & Experienced Assessment
┌───────────────────────────────┐               ┌──────────────────────────────────────────────┐
│ • Can you join two tables?    │      VS       │ • Did your join fan out row totals?          │
│ • Do you know GROUP BY?       │               │ • How does the optimizer handle the hash?    │
│ • Can you sort top 10?        │               │ • Is your window frame bounded or unbounded? │
│ • Did the query run?          │               │ • Will NULLs poison the subquery anti-join?  │
└───────────────────────────────┘               └──────────────────────────────────────────────┘

When hiring managers evaluate experienced candidates, they score responses across six architectural dimensions:

  1. Relational Grain Discipline: Understanding how joining across distinct cardinality grains (1:N or N:M) creates silent row inflation.
  2. Explicit Window Framing: Differentiating between physical row offsets (ROWS BETWEEN) and logical value ranges (RANGE BETWEEN).
  3. Execution Plan Fluency: Diagnosing sequential table scans, hash spills, nested loop degradation, and work memory limits using EXPLAIN (ANALYZE, BUFFERS).
  4. Three-Valued Boolean Semantics: Predicting how UNKNOWN truth states propagate through NOT IN, outer joins, and conditional case aggregations.
  5. Hierarchical & Graph Navigation: Writing deterministic recursive common table expressions (WITH RECURSIVE) to traverse trees and detect circular dependency cycles.
  6. SARGable Query Design: Constructing search-argument-able predicates that leverage B-Tree index scans instead of forcing full-table scans.

Let us explore the core question archetypes with concrete schemas, failure modes, and production solutions.


1. Advanced Relational Grain & The Join Fan-Out Pitfall

One of the most frequent live coding questions tests multi-table financial rollups across parent and child tables.

The Interview Scenario

You are given two e-commerce tables: orders (tracking order dates and fixed checkout discounts) and order_items (tracking individual line items, unit prices, and quantities). You are asked to write a query reporting each customer's total gross sales and total order discounts applied.

Schema Context

Table: orders

order_idcustomer_idorder_datediscount_amount
1019012026-02-01$50.00
1029012026-02-15$20.00
1039022026-02-10$15.00

Table: order_items

item_idorder_idproduct_idquantityunit_priceline_total
110150012$100.00$200.00
210150021$150.00$150.00
310250031$400.00$400.00
410350043$80.00$240.00

The Trap Query: Fan-Out Metric Inflation

Candidates often jump directly into joining the tables and aggregating both metrics in the same query:

sql
-- TRAP QUERY: Joining before aggregating causes metric inflation
SELECT 
    o.customer_id,
    SUM(oi.quantity * oi.unit_price) AS gross_revenue,
    SUM(o.discount_amount) AS total_discount
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.customer_id = 901
GROUP BY o.customer_id;

Trap Query Output

customer_idgross_revenuetotal_discountStatus
901$750.00$120.00❌ Corrupted ($50 discount duplicated)

Why this fails: Order 101 has two line items. Joining orders to order_items expands order 101 into two rows before aggregation. As a result, the $50 discount is counted twice ($50 + $50 + $20 = $120), corrupting financial reporting by $50.

The Fix Query: CTE Pre-Aggregation

To prevent metric inflation, experienced engineers pre-aggregate line items to the order grain before joining to the parent orders table:

sql
-- FIX QUERY: Pre-aggregating line items preserves parent table grain
WITH line_item_summary AS (
    SELECT 
        order_id,
        SUM(quantity * unit_price) AS order_gross_revenue
    FROM order_items
    GROUP BY order_id
),
order_rollups AS (
    SELECT 
        o.customer_id,
        COALESCE(SUM(lis.order_gross_revenue), 0) AS gross_revenue,
        SUM(o.discount_amount) AS total_discount
    FROM orders o
    LEFT JOIN line_item_summary lis ON o.order_id = lis.order_id
    GROUP BY o.customer_id
)
SELECT customer_id, gross_revenue, total_discount
FROM order_rollups
WHERE customer_id = 901;

Fix Query Output

customer_idgross_revenuetotal_discountStatus
901$750.00$70.00✅ Accurate ($50 + $20)

Senior Interview Trap: The Hidden Fan-Out

Never calculate aggregations across two tables with differing grain in the same SELECT without pre-aggregation or window isolation. For an exhaustive walkthrough of join explosions across four-table production ledgers, review our SQL JOIN Fan-Out Guide and test the query directly in the Four-Table JOIN: Invoice Details interactive sandbox.


2. Advanced Window Functions: Beyond Basic ROW_NUMBER

Junior developers memorize ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC). Experienced candidates understand the performance implications of window frame clauses, tie breaking, and running accumulations.

Ranking Nuances: Ties and Density

When asked to retrieve the "top 3 salaries per department", senior interviewers deliberately introduce duplicate salary values:

sql
SELECT 
    employee_id,
    department_id,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS row_num,
    RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk,
    DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dense_rnk
FROM employees;
employee_iddepartment_idsalaryrow_numrnkdense_rnk
E101Engineering$160,000111
E102Engineering$140,000222
E103Engineering$140,000322
E104Engineering$120,00044 (gap)3 (no gap)
  • If filtered WHERE rnk <= 3, employee E104 is omitted because the rank jumped from 2 to 4.
  • If filtered WHERE dense_rnk <= 3, employee E104 is correctly included as the third unique compensation tier.
  • For deep comparison benchmarks, read our guide on ROW_NUMBER vs RANK vs DENSE_RANK.

Window Frames: ROWS BETWEEN vs. RANGE BETWEEN

A critical technical question in experienced interviews: "What is the default window frame when an ORDER BY is supplied without a frame clause?"

The default frame is:

sql
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

The Danger: RANGE treats duplicate values in the ORDER BY column as a single logical peer group. If two transactions share the exact same timestamp, SUM(amount) OVER (ORDER BY txn_time) adds both transactions simultaneously to the cumulative sum for both rows, rather than calculating a true sequential progression.

Furthermore, in PostgreSQL, MySQL 8+, and Oracle, RANGE requires in-memory sorting and buffering of peer groups, making it significantly slower than physical row-based calculations.

sql
-- Production Standard: Explicitly specify physical ROWS framing
SELECT 
    account_id,
    txn_date,
    amount,
    -- True rolling 3-transaction moving average
    AVG(amount) OVER (
        PARTITION BY account_id 
        ORDER BY txn_date, txn_id
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS rolling_3_txn_avg,
    -- Deterministic running balance
    SUM(amount) OVER (
        PARTITION BY account_id 
        ORDER BY txn_date, txn_id
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_balance
FROM bank_transactions;

Sessionization & Gap Identification with LAG/LEAD

Senior analytics roles frequently require sessionizing clickstreams or measuring customer inactivity gaps:

sql
-- Identify users who were inactive for more than 30 days between purchases
WITH order_lags AS (
    SELECT 
        customer_id,
        order_id,
        order_date,
        LAG(order_date) OVER (
            PARTITION BY customer_id 
            ORDER BY order_date
        ) AS prev_order_date
    FROM customer_orders
)
SELECT 
    customer_id,
    order_id,
    order_date,
    prev_order_date,
    (order_date - prev_order_date) AS days_since_last_order
FROM order_lags
WHERE (order_date - prev_order_date) > 30;

You can practice offset functions on real transaction timestamps in our Lead & Lag Invoice Dates Sandbox.

Production Pro Tip: Window Frame Memory Optimization

Always use ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW instead of omitting the frame clause. On a 10-million-row ledger, explicit ROWS execution skips peer group collation and reduces window processing time by up to 45%.


Practice Senior SQL Interview Scenarios Live

Master execution plans, window frames, and multi-table anti-joins in Topfolio's interactive PostgreSQL browser sandbox.

Explore Data Analyst Track

3. Hierarchical Data & Recursive Common Table Expressions (CTEs)

Recursive queries separate senior engineers from mid-level analysts. When interviewers ask you to traverse organizational charts, folder file systems, or multi-level assembly bill-of-materials (BOM), you must use recursive CTEs.

Anatomy of a Recursive CTE

A recursive CTE consists of four parts:

  1. Anchor Member: The base query initializing the traversal (e.g. finding the CEO or root nodes).
  2. UNION ALL: The operator combining the anchor with recursive results.
  3. Recursive Member: The iterative query referencing the CTE itself, executing until returning an empty set.
  4. Termination Condition: A WHERE clause or join condition preventing infinite loops.

The Problem: Multi-Level Management Rollup

Given an employees table, compute the management depth (level in hierarchy) and full management chain path for every employee.

sql
WITH RECURSIVE org_hierarchy AS (
    -- 1. Anchor Member: Top-level executive (no manager)
    SELECT 
        employee_id,
        name,
        manager_id,
        1 AS org_level,
        CAST(name AS VARCHAR(1000)) AS hierarchy_path
    FROM corporate_employees
    WHERE manager_id IS NULL
 
    UNION ALL
 
    -- 2. Recursive Member: Join subordinate employees to existing hierarchy
    SELECT 
        e.employee_id,
        e.name,
        e.manager_id,
        h.org_level + 1 AS org_level,
        CAST(h.hierarchy_path || ' -> ' || e.name AS VARCHAR(1000)) AS hierarchy_path
    FROM corporate_employees e
    JOIN org_hierarchy h ON e.manager_id = h.employee_id
    WHERE h.org_level < 10 -- Guardrail against circular cycles
)
SELECT 
    employee_id,
    name,
    org_level,
    hierarchy_path
FROM org_hierarchy
ORDER BY org_level, name;

Execution Output

employee_idnameorg_levelhierarchy_path
1Priya Sharma (CEO)1Priya Sharma (CEO)
4David Chen (VP Eng)2Priya Sharma (CEO) → David Chen (VP Eng)
9Elena Rostova (Dir)3Priya Sharma (CEO) → David Chen (VP Eng) → Elena Rostova (Dir)
18Marcus Vance (Staff)4Priya Sharma (CEO) → David Chen (VP Eng) → Elena Rostova (Dir) → Marcus Vance (Staff)

For further architectural patterns on structuring clean intermediate query layers, consult our comprehensive SQL CTE Guide.


4. Query Performance Tuning & Execution Plan Analysis

In senior rounds, after you write a working query, the interviewer will say: "This query runs in 4 minutes on a table with 80 million rows. How do you analyze and optimize it?"

Dissecting EXPLAIN (ANALYZE, BUFFERS)

Never guess query performance. Execute EXPLAIN (ANALYZE, BUFFERS) to inspect what the database engine actually does:

sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT customer_id, SUM(total_amount)
FROM invoices
WHERE invoice_date >= '2026-01-01'
GROUP BY customer_id;

Key execution metrics experienced engineers look for:

  • Scan Type:
    • Seq Scan: Full table read. Problematic when filtering a small subset of rows.
    • Index Scan: Navigates B-Tree, fetches row pointers, and visits heap pages.
    • Index Only Scan: All requested columns exist directly in the index leaf pages (zero heap visits).
    • Bitmap Index / Heap Scan: Batches index row pointers into memory bitmap before reading disk blocks sequentially.
  • Join Algorithms:
    • Nested Loop: Highly efficient when outer set is tiny and inner table has a tight index lookup. Disastrous on large unindexed tables ($O(N \times M)$).
    • Hash Join: Builds an in-memory hash table of the smaller table, then scans the larger table. If work_mem is insufficient, the hash table spills to disk (Batches: > 1).
    • Merge Join: Both inputs sorted by join key, then merged linearly. Preferred for large sorted streams.
  • Buffers: Inspect Buffers: shared hit=... read=.... High read numbers indicate physical disk I/O bottlenecks.

SARGable Predicates: Index-Aware Filtering

A predicate is SARGable (Search Argument Able) if the database query engine can use a B-Tree index to navigate directly to the matching values.

sql
-- NON-SARGABLE (Forces a full Seq Scan on 50M rows)
-- Wrapping the indexed column in a function prevents index range scans:
SELECT order_id, order_date, total_amount
FROM orders
WHERE EXTRACT(YEAR FROM order_date) = 2026;
 
-- SARGABLE FIX (Enables B-Tree Index Range Scan in O(log N))
-- Leaves the indexed column bare and computes boundary constants:
SELECT order_id, order_date, total_amount
FROM orders
WHERE order_date >= '2026-01-01' 
  AND order_date < '2027-01-01';

Composite Index Ordering: The Equality-First Rule

When building composite indexes (e.g. CREATE INDEX idx_orders_status_date ON orders(status, order_date)), column order determines utility:

  1. Place columns tested with exact equality (status = 'COMPLETED') first.
  2. Place columns tested with inequalities or ranges (order_date >= '2026-01-01') second.
  3. If an inequality column is listed first, the index engine cannot use subsequent columns to narrow down lookups.

5. Three-Valued Logic & Subquery Anti-Join Traps

One of the most notorious elimination questions in senior interviews targets the difference between NOT IN and NOT EXISTS in the presence of NULL values.

The Interview Scenario

"Write a query to find all products that have never been purchased."

Schema Context

Table: products

product_idproduct_nameunit_price
10Enterprise Server$4,500.00
20Developer Laptop$1,800.00
30Quantum Workstation$9,200.00

Table: order_items

item_idorder_idproduct_id
150110
250220
3503NULL (Draft unassigned item)

The Trap Query: NOT IN with NULL

sql
-- TRAP QUERY: NOT IN subquery with NULL values
SELECT product_id, product_name
FROM products
WHERE product_id NOT IN (
    SELECT product_id FROM order_items
);

Trap Query Output

(0 rows returned)

Why this fails: In SQL's Three-Valued Logic, product_id NOT IN (10, 20, NULL) translates to:

sql
product_id <> 10 AND product_id <> 20 AND product_id <> NULL

Because any comparison with NULL evaluates to UNKNOWN, the expression becomes:

sql
TRUE AND TRUE AND UNKNOWN => UNKNOWN

Because a WHERE clause requires a condition to evaluate strictly to TRUE to retain a row, the entire query returns zero rows, completely hiding unpurchased inventory.

The Fix Query: NOT EXISTS or Anti-Join

sql
-- FIX QUERY: NOT EXISTS handles NULL values safely and short-circuits
SELECT p.product_id, p.product_name
FROM products p
WHERE NOT EXISTS (
    SELECT 1 
    FROM order_items oi 
    WHERE oi.product_id = p.product_id
);

Fix Query Output

product_idproduct_nameStatus
30Quantum Workstation✅ Accurately returned

You can explore full three-valued truth tables in our Guide to NULL Semantics in SQL or test this exact query against live data in our interactive Tracks That Have Never Been Purchased Sandbox.


6. Real-World SQL Interview Questions for Experienced Examples

Here are three scenario-based questions drawn directly from Tier-1 product tech and fintech hiring loops.

Scenario A: Fraud Detection & Rapid Successive Transactions (Fintech)

Prompt: Detect credit card accounts that have executed 3 or more transactions within any rolling 10-minute window.

sql
WITH flagged_txns AS (
    SELECT 
        card_id,
        txn_id,
        txn_time,
        amount,
        -- Look ahead 2 transactions
        LEAD(txn_time, 2) OVER (
            PARTITION BY card_id 
            ORDER BY txn_time
        ) AS time_third_txn
    FROM credit_card_transactions
)
SELECT DISTINCT card_id
FROM flagged_txns
WHERE time_third_txn IS NOT NULL
  AND time_third_txn <= txn_time + INTERVAL '10 minutes';

Scenario B: Month-over-Month Cohort Retention Matrix (Product Tech)

Prompt: Calculate the percentage of users who returned to make a purchase in month $N$ after their first acquisition purchase month.

sql
WITH user_first_month AS (
    SELECT 
        user_id,
        DATE_TRUNC('month', MIN(purchase_date)) AS cohort_month
    FROM purchases
    GROUP BY user_id
),
user_activities AS (
    SELECT DISTINCT 
        p.user_id,
        u.cohort_month,
        DATE_TRUNC('month', p.purchase_date) AS activity_month
    FROM purchases p
    JOIN user_first_month u ON p.user_id = u.user_id
),
cohort_sizes AS (
    SELECT cohort_month, COUNT(*) AS cohort_users
    FROM user_first_month
    GROUP BY cohort_month
)
SELECT 
    a.cohort_month,
    s.cohort_users,
    -- Calculate month index difference
    (EXTRACT(YEAR FROM a.activity_month) - EXTRACT(YEAR FROM a.cohort_month)) * 12 +
    (EXTRACT(MONTH FROM a.activity_month) - EXTRACT(MONTH FROM a.cohort_month)) AS month_number,
    COUNT(DISTINCT a.user_id) AS active_users,
    ROUND(COUNT(DISTINCT a.user_id) * 100.0 / s.cohort_users, 2) AS retention_rate_pct
FROM user_activities a
JOIN cohort_sizes s ON a.cohort_month = s.cohort_month
GROUP BY a.cohort_month, s.cohort_users, month_number
ORDER BY a.cohort_month, month_number;

Scenario C: FIFO Inventory Depletion Allocation (E-Commerce & Supply Chain)

Prompt: Allocate customer orders against incoming inventory batches using First-In, First-Out (FIFO) logic.

sql
WITH running_inventory AS (
    SELECT 
        batch_id,
        product_id,
        received_date,
        quantity_received,
        SUM(quantity_received) OVER (
            PARTITION BY product_id 
            ORDER BY received_date 
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS cumulative_supply
    FROM inventory_batches
),
running_demand AS (
    SELECT 
        order_id,
        product_id,
        order_date,
        quantity_demanded,
        SUM(quantity_demanded) OVER (
            PARTITION BY product_id 
            ORDER BY order_date 
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS cumulative_demand
    FROM sales_orders
)
SELECT 
    d.order_id,
    s.batch_id,
    d.product_id,
    -- Determine overlap between supply and demand windows
    GREATEST(0, 
        LEAST(s.cumulative_supply, d.cumulative_demand) - 
        GREATEST(s.cumulative_supply - s.quantity_received, d.cumulative_demand - d.quantity_demanded)
    ) AS allocated_quantity
FROM running_demand d
JOIN running_inventory s 
  ON d.product_id = s.product_id
 AND s.cumulative_supply > d.cumulative_demand - d.quantity_demanded
 AND s.cumulative_supply - s.quantity_received < d.cumulative_demand
ORDER BY d.order_id, s.batch_id;

7. How to Use SQL Interview Questions for Experienced Preparation

Preparing effectively for senior technical assessments requires a structured, multi-phase engineering regimen:

                  4-Phase Senior SQL Preparation Framework
┌────────────────────┐    ┌────────────────────┐    ┌────────────────────┐    ┌────────────────────┐
│ Phase 1: Baseline  │───>│ Phase 2: Execution │───>│ Phase 3: Edge Case │───>│ Phase 4: System   │
│ Grain Validation   │    │ Plan Profiling     │    │ Stress Testing     │    │ Communication      │
│                    │    │                    │    │                    │    │                    │
│ • Identify entities│    │ • EXPLAIN ANALYZE  │    │ • NULL poisoning   │    │ • Clarify schema   │
│ • Audit fan-out    │    │ • SARGable index   │    │ • Ties in ranking  │    │ • State complexity │
│ • Check table keys │    │ • Buffer memory    │    │ • Zero divide      │    │ • Discuss scaling  │
└────────────────────┘    └────────────────────┘    └────────────────────┘    └────────────────────┘
  1. Step 1: Baseline Grain Audit: Before writing any SQL in an interview, explicitly state the primary key and grain of every input table. Confirm whether relationships are 1:1, 1:N, or M:N.
  2. Step 2: Execution Plan Profiling: After drafting your query, verbally review how the database engine executes it. State whether your join condition leverages an index scan or falls back to an unindexed hash join.
  3. Step 3: Edge Case Stress Testing: Proactively address nullability, timestamp ties, empty tables, and duplicate foreign keys before the interviewer points them out.
  4. Step 4: System Scaling Communication: Discuss how your query behaves if the underlying table grows from 100,000 rows to 100,000,000 rows (e.g. partition pruning, materialized rollups, incremental processing with dbt).

If you are expanding your foundational data toolkit across both spreadsheets and database engines, review our comprehensive tutorials on Excel Basics for Analytics alongside hands-on SQL practice.


8. Summary Comparison: Junior vs. Senior vs. Staff SQL Expectations

The table below summarizes what interview panels expect across experience tiers:

Feature / Criteria

Level Up to Senior Data Analytics Leadership

Master production SQL, Python pipelines, dbt data modeling, and end-to-end interview casework with Topfolio's guided career track.

Start the Data Analyst Track Free

Frequently Asked Questions

What do interviewers look for in SQL interview questions for experienced candidates?

For experienced candidates (3+ years), interviewers evaluate query optimization, index usage, execution plan analysis with EXPLAIN, window function frames, edge-case NULL semantics, and avoiding table fan-out rather than basic syntax.

What is the difference between ROW_NUMBER, RANK, and DENSE_RANK?

ROW_NUMBER assigns a unique sequential integer to every row regardless of ties. RANK assigns identical numbers to tied rows and skips subsequent numbers. DENSE_RANK assigns identical numbers to tied rows without skipping numbers.

How do you detect and fix a SQL JOIN fan-out bug?

Join fan-out occurs when joining one-to-many tables without pre-aggregation, duplicating parent rows and inflating SUM or COUNT aggregates. Detect it by comparing COUNT(*) before and after joins, and fix it by pre-aggregating child tables in CTEs before joining.

Why is EXISTS generally preferred over IN for subqueries in large datasets?

EXISTS stops scanning as soon as a single matching record is found (short-circuit evaluation) and safely handles NULLs. IN must evaluate the entire set and returns UNKNOWN (evaluating to zero rows in NOT IN) if any subquery row contains NULL.

How do you optimize a slow-running SQL query with billions of rows?

First inspect execution plans using EXPLAIN ANALYZE to identify sequential scans and hash spills. Then eliminate SELECT *, replace correlated subqueries with window functions or CTEs, add targeted composite or partial indexes, and prune partitions early using WHERE clauses.

Anuj Saini

Written by

Anuj SainiFounder & Lead Instructor

Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.