DataLane
← All stacks

SQL

Windows, grain, and the query you can defend on a whiteboard

11 questions with solutions

  1. Q1StripeSquareShopify

    Deduplicate to the latest customer row. Why not filter the window in WHERE?

    Solution

    Use row_number() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) in a CTE, then keep rn = 1. Window functions run after WHERE, so a filter on rn in the same SELECT is invalid (unless the dialect has QUALIFY). Interviewers want that evaluation order, not a clever one-liner.

  2. Q2NetflixSpotifyAirbnb

    Sessionize click events with a 30-minute inactivity gap.

    Solution

    Flag a new session when occurred_at − lag(occurred_at) over the customer is greater than 30 minutes (or null for the first event). Then session_id = a running sum of that flag. That is islands-and-gaps. Do not group by a floored timestamp — that splits real sessions and merges strangers.

  3. Q3AmazonWalmartInstacart

    Write top-3 orders per country. What breaks if you use RANK() instead of ROW_NUMBER()?

    Solution

    RANK() (and DENSE_RANK()) ties share a rank, so you can return more than three rows when amounts match. ROW_NUMBER() always returns exactly N. Say when you want ties (leaderboard) versus a hard cap (sample / pagination).

  4. Q4Capital OneJPMorganPayPal

    A join exploded revenue. How do you find the grain mismatch in five minutes?

    Solution

    Count rows before and after the join. If the fact grew, the “unique” key on the dimension is not unique at the grain you joined. Check duplicates with GROUP BY key HAVING count(*) > 1, then decide: aggregate the dimension or pick the correct role-playing key. Never “fix” it with DISTINCT on the fact.

  5. Q5MetaUberDoorDash

    Running total of daily revenue. Why is SUM(SUM(amount)) OVER (...) valid?

    Solution

    The inner SUM is the GROUP BY aggregation (daily revenue). The window SUM then walks those grouped rows. You need both: group to the day first, then window. A bare SUM(amount) OVER (ORDER BY day) without grouping double-counts every order row.

  6. Q6GoogleSnowflakeDatabricks

    NULL = NULL is unknown. How does that bite a MERGE or an anti-join?

    Solution

    NOT IN with a NULL in the list returns empty. Anti-joins should be NOT EXISTS or a LEFT JOIN … WHERE right.key IS NULL. MERGE match conditions that compare nullable keys silently miss rows. Say this out loud; it is a common take-home trap.

  7. Q7LinkedInMicrosoftOracle

    When do you use a covering index / clustering key versus rewriting the query?

    Solution

    If the query already filters a selective column and still scans everything, storage layout (partition, cluster, index) is the lever. If it selects * and joins at the wrong grain, rewrite first. Do not add a clustering key to hide a CROSS JOIN.

  8. Q8SnowflakeGoogleDatabricks

    QUALIFY vs a CTE for window filters — when do you use each?

    Solution

    QUALIFY is cleaner in Snowflake/BigQuery when the dialect has it. A CTE is portable and easier to debug. Either is fine; filtering rn in WHERE on the same SELECT is not.

  9. Q9GoogleSnowflakeAmazon

    A query is correct but scans last year. How do you prove partition pruning?

    Solution

    Read the plan / bytes scanned / partitions assigned. Add the partition column to the filter (or clustering). EXPLAIN or the warehouse profile — not a guess.

  10. Q10Capital OnePayPalStripe

    EXISTS vs IN for an anti-join. Which do you write first?

    Solution

    NOT EXISTS (or LEFT JOIN … IS NULL). NOT IN dies on NULLs. That is the answer they want.

  11. Q11MetaSnapNetflix

    You need a 7-day rolling unique users. What breaks if you use COUNT DISTINCT in a window carelessly?

    Solution

    Many engines restrict DISTINCT in windows or make it expensive. A better story is a daily uniques table then a rolling sum of an inclusion flag — or a bitmap if the warehouse has one. Say the grain first.

↑↓ navigate openesc close