LearnFrenzy
Sign in Take a free test

Database Management interview · Set 8 of 9

Advanced SQL Coding Questions

  • 6 follow-up questions
  • 4 key points
  • Code examples
Test yourself

Window functions and CTEs do the heavy lifting

Advanced SQL questions are mostly window functions (LAG, LEAD, ROW_NUMBER, frames) and CTEs (including recursive ones). Name the technique, then write the query step by step.

  • Time seriesGrowth, moving averages and gaps.
  • StreaksGaps-and-islands with ROW_NUMBER.
  • HierarchiesRecursive CTEs and set operations.

Data, analytics and backend interviews increasingly ask “harder” SQL: time-series comparisons, streaks, hierarchies and medians. These solutions use window functions and CTEs that run on PostgreSQL and MySQL 8. Tables: orders(id, user_id, amount, created_at), logins(user_id, login_date), employees(id, name, manager_id, salary).

1. Find users who logged in on 3 or more consecutive days.

The gaps-and-islands trick: subtract a row number from the date; consecutive days share the same difference, forming a group.

WITH days AS (
  SELECT DISTINCT user_id, login_date FROM logins
), grouped AS (
  SELECT user_id, login_date,
         login_date - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date)) * INTERVAL '1 day' AS grp
  FROM days
)
SELECT user_id, MIN(login_date) AS streak_start, COUNT(*) AS streak_days
FROM grouped
GROUP BY user_id, grp
HAVING COUNT(*) >= 3;

2. Calculate month-over-month revenue growth.

Aggregate per month, then compare with the previous row using LAG.

WITH monthly AS (
  SELECT DATE_TRUNC('month', created_at) AS month, SUM(amount) AS revenue
  FROM orders
  GROUP BY 1
)
SELECT month, revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
       ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
             / NULLIF(LAG(revenue) OVER (ORDER BY month), 0), 1) AS growth_pct
FROM monthly
ORDER BY month;

3. Find the median salary.

PostgreSQL has PERCENTILE_CONT. Portable version: rank rows from both ends and average the middle one or two.

-- PostgreSQL
SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median FROM employees;

-- Portable
SELECT AVG(salary) AS median
FROM (
  SELECT salary,
         ROW_NUMBER() OVER (ORDER BY salary) AS rn,
         COUNT(*) OVER () AS cnt
  FROM employees
) t
WHERE rn IN ((cnt + 1) / 2, (cnt + 2) / 2);

4. Show the full reporting chain under a manager (org chart).

A recursive CTE starts with the manager and repeatedly joins direct reports.

WITH RECURSIVE chain AS (
  SELECT id, name, manager_id, 0 AS level
  FROM employees WHERE id = 1                       -- the manager
  UNION ALL
  SELECT e.id, e.name, e.manager_id, c.level + 1
  FROM employees e
  JOIN chain c ON e.manager_id = c.id
)
SELECT REPEAT('  ', level) || name AS org_chart, level
FROM chain
ORDER BY level, name;

5. Find each user's first order and the days to their second order.

Number each user's orders, then use LEAD to look at the next one.

SELECT user_id, created_at AS first_order,
       next_order - created_at AS gap
FROM (
  SELECT user_id, created_at,
         ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS rn,
         LEAD(created_at) OVER (PARTITION BY user_id ORDER BY created_at) AS next_order
  FROM orders
) t
WHERE rn = 1;

6. Compute a 7-day moving average of daily revenue.

A window frame defines which rows to average.

SELECT day, revenue,
       ROUND(AVG(revenue) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), 2) AS avg_7d
FROM (
  SELECT CAST(created_at AS DATE) AS day, SUM(amount) AS revenue
  FROM orders GROUP BY 1
) d
ORDER BY day;

7. Find customers who bought in January but not in February.

Set logic with EXCEPT (or NOT EXISTS in MySQL versions without EXCEPT).

SELECT user_id FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'
EXCEPT
SELECT user_id FROM orders WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01';

8. Pivot: count orders per user per weekday.

Conditional aggregation works in every database.

SELECT user_id,
       COUNT(*) FILTER (WHERE EXTRACT(DOW FROM created_at) = 1) AS mon,
       COUNT(*) FILTER (WHERE EXTRACT(DOW FROM created_at) = 2) AS tue,
       COUNT(*) FILTER (WHERE EXTRACT(DOW FROM created_at) = 3) AS wed
FROM orders
GROUP BY user_id;
-- MySQL: SUM(CASE WHEN DAYOFWEEK(created_at) = 2 THEN 1 ELSE 0 END) AS mon

Remember

Key points

  • LAG/LEAD for comparisons

    Previous and next rows.

  • date − row_number

    Groups consecutive dates.

  • NULLIF avoids divide by zero

    In percentage calculations.

  • Half-open date ranges

    >= start and < next start.

Interview drill

Follow-up questions

What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.

  1. 1 Find the top 3 products by revenue in each category. Intermediate

    Aggregate revenue per product, rank with DENSE_RANK() OVER (PARTITION BY category ORDER BY revenue DESC) and keep rank ≤ 3.

  2. 2 Calculate a running total that resets each month. Intermediate

    SUM(amount) OVER (PARTITION BY DATE_TRUNC('month', created_at) ORDER BY created_at).

  3. 3 What is the difference between ROWS and RANGE frames? Advanced

    ROWS counts physical rows; RANGE groups rows with equal ORDER BY values, which changes results when there are ties.

  4. 4 Find duplicate emails ignoring case. Basic

    SELECT LOWER(email), COUNT(*) FROM users GROUP BY LOWER(email) HAVING COUNT(*) > 1.

  5. 5 How do you compute 30-day retention? Advanced

    Find each user's first activity date (cohort), then count users with activity between day 1 and day 30 after it, grouped by cohort.

  6. 6 Delete all but the latest row per user. Intermediate

    Rank rows with ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) and delete where the rank is above 1.

Preparing with friends?

Share this page with your study group or placement batch.

Search LearnFrenzy

Esc

Quick links