LIMITED TIME 🎁: Register now to get 60 minutes of AI Mock Interviewing for FREE!

Join
    Twitch Interview Insights
    Twitch logo
    Twitch·Data Scientist·Technical Phone Screen·Senior
    SeniorPrefer not to say
    Jul 2026Remote
    5

    Summary

    Twitch data scientist interview that was basically a full SQL gauntlet built around streaming telemetry data. Five tasks back to back, ranging from monthly aggregations to window functions to a conceptual question about query processing order. Felt more like a take-home than a live screen.

    Questions Asked(5)

    Product Analytics & MetricsData Modeling
    A
    Author's notesFirst line only

    The year-boundary thing is what trips people up.

    Suggested Approach

    Start by identifying the granularity of the source data (minute-level rows per streamer) and clarify what constitutes a 'streaming minute' to avoid double-counting. Then aggregate by extracting the year-month from the timestamp, summing total minutes across all streamers, and converting to hours. Finally, ensure the output is sorted chronologically using the YYYY-MM label, not lexicographically on a raw string.

    Pro tip: At Twitch's scale, minute-level data is enormous — mention partitioning the source table by date and filtering only necessary partitions to avoid full table scans, which signals production-aware thinking beyond just correctness.
    1

    Clarify the Schema & Grain

    Confirm the source table structure — e.g., columns like streamer_id, timestamp, and whether each row represents one minute of streaming or a session with a duration field. Clarifying this prevents logic errors in the aggregation.

    2

    Extract the YYYY-MM Period

    Use DATE_FORMAT(timestamp, '%Y-%m') or equivalent (e.g., TO_CHAR in PostgreSQL, FORMAT_DATE in BigQuery) to derive the month label. Avoid casting to just MONTH() alone, as that collapses data across multiple years.

    3

    Aggregate Minutes and Convert to Hours

    SUM the minute-level rows grouped by the YYYY-MM period to get total minutes, then divide by 60 to convert to hours. Consider whether to ROUND or keep decimals based on business requirements.

    4

    Sort Chronologically

    Order the results by the raw timestamp truncated to month (e.g., DATE_TRUNC('month', timestamp)) or by the YYYY-MM string, which sorts correctly due to its ISO format. Avoid ordering by a plain MONTH() integer, which breaks across years.

    5

    Validate & Handle Edge Cases

    Check for NULL timestamps, duplicate rows, or timezone inconsistencies (e.g., UTC vs. local time) that could misassign minutes to the wrong month. Mention that at Twitch's scale, late-arriving data or reprocessing windows may also need to be addressed.

    Key Points to Mention

    Using DATE_TRUNC or DATE_FORMAT to correctly extract YYYY-MM and avoid year-collapsing bugs when using MONTH() alone
    Dividing total row count (minutes) by 60 to convert to hours, and deciding on rounding precision
    Ordering by a date-typed expression rather than a plain string to guarantee true chronological order
    Handling NULL or malformed timestamps gracefully with COALESCE or WHERE filters
    Timezone normalization — ensuring all timestamps are in a consistent timezone (e.g., UTC) before aggregation
    Performance considerations such as partition pruning and avoiding full table scans on large minute-level datasets
    Data ModelingProduct Analytics & Metrics
    A
    Author's notesFirst line only

    The case-insensitive part is easy to miss if you just do category = cat_keyword.

    Suggested Approach

    Break the problem into two aggregation layers: first compute total streamed minutes per streamer, then compute keyword-matched minutes per streamer using a case-insensitive filter, and finally join the two to derive the ratio. Use SQL window functions or CTEs to keep the logic clean and readable, which is especially important in a data-heavy company like Twitch.

    Pro tip: Explicitly handle edge cases like streamers with zero total minutes (division by zero) and NULL categories upfront — interviewers at Twitch look for production-ready thinking, not just a working query on the happy path.
    1

    Clarify the Schema and Inputs

    Confirm the table structure (e.g., streamer_id, category, minutes_streamed) and how the keyword is passed — as a parameter or hardcoded. Ask whether a streamer can appear in multiple rows with different categories.

    2

    Compute Total Minutes per Streamer

    Use a GROUP BY on streamer_id with SUM(minutes_streamed) to get each streamer's total minutes. Store this in a CTE for reuse.

    3

    Filter and Aggregate Keyword-Matched Minutes

    In a second CTE, apply a case-insensitive substring match (e.g., LOWER(category) LIKE LOWER('%keyword%') or ILIKE in PostgreSQL) and SUM the matching minutes per streamer.

    4

    Join and Compute the Ratio

    LEFT JOIN the keyword-matched CTE onto the total-minutes CTE so streamers with no matching categories still appear. Divide keyword minutes by total minutes, using COALESCE to handle NULLs and NULLIF to prevent division by zero.

    5

    Validate and Discuss Edge Cases

    Walk through edge cases: streamers with zero total minutes, NULL category values, and keywords that match no rows. Mention that the ratio should be bounded between 0 and 1 as a sanity check.

    Key Points to Mention

    Using CTEs (WITH clauses) for modularity and readability over nested subqueries
    Case-insensitive matching via LOWER() + LIKE or dialect-specific ILIKE
    LEFT JOIN to preserve all streamers even when no categories match the keyword
    NULLIF(total_minutes, 0) to safely handle division by zero
    COALESCE to convert NULL keyword minutes to 0 for streamers with no matching rows
    Parameterizing the keyword (e.g., using a variable or placeholder) to make the query reusable and production-friendly
    Data ModelingAlgorithms & Data Structures
    A
    Author's notesFirst line only

    This one took me a minute.

    Suggested Approach

    Use a window function (LAG) partitioned by streamer and ordered by calendar month to fetch the previous month's streamed hours, while using a complete month spine or COALESCE to handle months with no activity as zero. Then filter the result set to only include rows where the current month's hours exceed the previous month's hours. This approach cleanly handles gaps in the data and avoids complex self-joins.

    Pro tip: Explicitly address the 'missing month as zero' requirement by generating a full calendar month spine (e.g., using a date dimension table or a series generator) and LEFT JOINing streaming data onto it — this ensures LAG correctly sees a zero for inactive months rather than skipping over them, which is a common pitfall interviewers look for.
    1

    Clarify Requirements & Edge Cases

    Confirm the definition of 'calendar month' (truncated date vs. year-month integer), the grain of the source data (event-level vs. pre-aggregated), and whether the very first month for a streamer should be included if it has activity (previous month would be zero).

    2

    Aggregate Hours by Streamer and Month

    Group the raw streaming data by streamer_id and calendar month (using DATE_TRUNC or equivalent), summing streamed hours to get a monthly total per streamer.

    3

    Build a Complete Month Spine

    Generate all (streamer, month) combinations across the full date range and LEFT JOIN the aggregated data onto this spine, replacing NULLs with zero using COALESCE to correctly represent inactive months.

    4

    Apply LAG Window Function

    Use LAG(hours, 1, 0) OVER (PARTITION BY streamer_id ORDER BY month) to retrieve the previous calendar month's hours for each row, defaulting to zero if no prior row exists.

    5

    Filter and Return Results

    Wrap the windowed query in a CTE or subquery and apply a WHERE clause to retain only rows where current_month_hours > previous_month_hours, then select the relevant output columns.

    Key Points to Mention

    LAG window function with PARTITION BY streamer and ORDER BY month to access the prior month's value
    Handling missing months as zero using a calendar spine (date dimension) and LEFT JOIN rather than relying solely on LAG's default parameter
    DATE_TRUNC or YEAR/MONTH extraction to normalize timestamps into calendar months
    COALESCE to convert NULL aggregated values (from months with no streams) into zero before comparison
    CTE structure for readability: one CTE for aggregation, one for the spine join, one for the LAG, and a final filter
    Edge case: a streamer's very first month with activity should appear in results since the implicit previous month is zero
    Data ModelingProduct Analytics & Metrics
    A
    Author's notesFirst line only

    The join-on-both-columns instruction is a hint that they've seen people mess this up by joining only on streamer and getting a cartesian product on time.

    Suggested Approach

    Start by identifying the two core tables — minute_streamed (one row per streamer per broadcast minute) and minute_viewed (one row per viewer per streamer per minute) — and join them on both streamer and timestamp before aggregating to prevent a Cartesian explosion. Filter to 2019 at the earliest possible stage, then compute concurrent viewers as the average across all streamed minutes and US viewer-minutes as the sum of US viewer rows (each row representing one viewer-minute). For the follow-up, add a conditional COUNT DISTINCT on timestamp where US viewers exist.

    Pro tip: Explicitly call out the fan-out risk in your explanation — interviewers at Twitch will be impressed if you proactively note that summing viewer counts before joining, or joining without both keys, would multiply rows and inflate metrics, and that aggregating on the viewer side first (or using a proper keyed join) is the correct guard.
    1

    Clarify Table Schemas & Grain

    Confirm that minute_streamed has grain (streamer, timestamp) and minute_viewed has grain (viewer_id, streamer, timestamp), and identify which columns carry concurrent_viewers and US viewer flags. This prevents assumptions that lead to incorrect joins.

    2

    Filter Early for 2019

    Apply a WHERE clause filtering timestamp to the year 2019 on both tables before or within the join to minimize data scanned and avoid carrying unnecessary rows into aggregation.

    3

    Join on Both Keys to Prevent Row Multiplication

    Join minute_streamed to minute_viewed on streamer AND timestamp together; joining on only one key would create a cross-product between minutes and viewers, inflating all downstream metrics.

    4

    Aggregate Core Metrics

    Group by streamer and compute AVG(concurrent_viewers) from the streamed side for average concurrent viewers, and SUM of a US-viewer indicator (e.g., SUM(CASE WHEN country = 'US' THEN 1 ELSE 0 END)) for total US viewer-minutes.

    5

    Add Follow-Up: Distinct Streamed Minutes with US Viewers

    Extend the query with COUNT(DISTINCT CASE WHEN country = 'US' THEN ms.timestamp END) per streamer, which counts only the unique broadcast minutes during which at least one US viewer was present.

    Key Points to Mention

    Row multiplication / fan-out risk when joining one-to-many tables without both join keys, and how it corrupts aggregated metrics
    Importance of joining on composite key (streamer + timestamp) to maintain the correct grain before aggregation
    Early filtering on year 2019 in both tables for query efficiency and correctness
    Distinction between AVG(concurrent_viewers) — averaged over streamed minutes — versus a naive average that could be skewed by join duplicates
    Using SUM of a boolean/indicator column to compute viewer-minutes, where each row in minute_viewed naturally represents one viewer-minute
    COUNT(DISTINCT timestamp) with a conditional filter as the clean approach for the follow-up metric on US-active minutes
    Technical Trade-offsData Modeling
    A
    Author's notesFirst line only

    AVG happens after the JOIN, in the GROUP BY and SELECT phase.

    Suggested Approach

    Start by clearly enumerating SQL's logical processing order from FROM through SELECT, then precisely locate where aggregate functions like AVG() fall in that sequence relative to JOINs. Ground your explanation in a concrete, Twitch-relevant example (e.g., average watch time per streamer after joining a users table) to make the answer tangible and demonstrate applied understanding.

    Pro tip: Distinguish between logical processing order and physical execution order — the optimizer may reorder operations for performance, but the logical order governs correctness and is what determines scoping rules for aliases and filters. Mentioning this nuance signals senior-level SQL maturity.
    1

    State the Full Logical Processing Order

    Enumerate all clauses in logical order: FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT/OFFSET. Be explicit and sequential to show command of the full pipeline.

    2

    Pinpoint WHERE AVG() Is Evaluated

    Clarify that AVG() is an aggregate function resolved during the GROUP BY/aggregation phase (step 5 logically), which occurs after FROM and JOIN (steps 1-2) have already produced the combined result set.

    3

    Explain the JOIN → Aggregate Relationship

    Emphasize that the JOIN first expands or filters the row set, and AVG() then operates on that already-joined dataset — meaning the join condition directly influences which rows are included in the average calculation.

    4

    Illustrate with a Concrete Example

    Use a Twitch-relevant scenario, such as joining a 'streams' table with a 'channels' table and computing AVG(viewer_count) per category, to show how an INNER JOIN vs. LEFT JOIN would yield different AVG() results because the row set differs.

    5

    Address Common Pitfalls and HAVING vs. WHERE

    Note that because AVG() is computed after WHERE but before HAVING, you cannot filter on an aggregate in a WHERE clause — you must use HAVING — which is a direct consequence of the logical processing order.

    Key Points to Mention

    Full logical order: FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
    AVG() (and all aggregates) are evaluated in the GROUP BY/aggregation phase, after all JOINs have been resolved
    JOIN type (INNER vs. LEFT/RIGHT) affects which rows exist before aggregation, directly impacting AVG() output — e.g., NULLs introduced by LEFT JOIN are ignored by AVG()
    Logical order vs. physical execution order distinction: the optimizer may reorder for performance but must preserve logical correctness
    HAVING filters on aggregated results (post-AVG), while WHERE filters on raw rows (pre-AVG) — a practical consequence of the processing order
    Column aliases defined in SELECT are not available in WHERE or GROUP BY in standard SQL because SELECT is processed later (though some engines like BigQuery allow exceptions)

    Discussion(5)

    Sign in to join the discussion.

    S
    SamTheRecruiter· 57d ago
    Q3For each streamer and calendar month, determine whether their streamed hours increased compared to the immediately preceding calendar month. Treat months with no activity as zero. Only return rows where current month exceeds the previous month.

    The spine approach is the right call here. LAG over a sparse result set is one of those things that looks correct until you hit a streamer who took a month off, at which point LAG happily grabs two months back and you never notice. Generating the spine with a cross join between a distinct streamer list and a distinct month list, then left joining your aggregated data onto it and coalescing nulls to zero, gives you a complete grid before the window function ever runs. After that LAG works exactly as you'd expect. One small thing: if you're sorting by a YYYY-MM string make sure it's zero-padded (2019-01 not 2019-1) otherwise lexicographic order breaks. DATE_TRUNC output avoids this entirely since it's a proper timestamp.

    MT
    Marcus Thorne· 57d ago
    Q2For each streamer, compute their total streamed minutes and the share of minutes spent in categories matching a given keyword (case-insensitive substring match). Return the keyword share as a ratio.

    NULLIF on the denominator is a good reflex to build. I once got burned in a take-home where the test data was clean but the grader ran it against a dataset with zero-minute streamers and my query threw a division error. Embarrassing. For the substring match, LOWER(category) LIKE LOWER('%' || cat_keyword || '%') is portable across most dialects if ILIKE isn't available. The ratio itself is just SUM(CASE WHEN ... THEN minutes ELSE 0 END) / NULLIF(SUM(minutes), 0). Keep the numerator filter and the denominator on the same grain so you're not accidentally mixing row counts with minute counts.

    M
    MisterReview· 57d ago
    Q4For 2019 only, return each streamer's average concurrent viewers and total US viewer-minutes. Join minute_viewed to minute_streamed on both streamer and timestamp to avoid row multiplication. As a follow-up, also count distinct streamed minutes where at least one US viewer was present.

    Joining on both streamer and timestamp is the whole ballgame here. I made exactly this mistake once, joined only on streamer ID, and suddenly my average concurrent viewers was off by an order of magnitude because every viewer row was fanning out against every streamed-minute row for that streamer. The follow-up about distinct streamed minutes with at least one US viewer is sneaky because it sounds like a viewer-side count but it's actually a streamer-side count. You want COUNT(DISTINCT ms.time_minute) after filtering mv.viewer_country = 'US', which means the join needs to be structured so you're anchoring on minute_streamed and bringing in viewer data, not the other way around. If you anchor on minute_viewed you'll end up counting viewer rows not stream minutes.

    C
    CodeWithMaya· 57d ago
    Q5Explain SQL's logical query processing order and clarify whether AVG() is computed before or after the JOIN.

    FROM and JOIN, WHERE, GROUP BY, HAVING, SELECT, ORDER BY. AVG runs in the SELECT phase, so the join is already done by then.

    MT
    Marcus Thorne· 57d ago
    Q1Given minute-level streaming data, compute total monthly hours streamed across all streamers. Output should be labeled as YYYY-MM and ordered chronologically, and must handle data spanning multiple years correctly.

    The year-boundary thing is a real gotcha and I've watched people confidently write MONTH() and GROUP BY MONTH() in live screens without realizing they're collapsing multiple years together. The fix is just making your grouping key carry the year too, either via DATE_TRUNC to a month-level timestamp or a formatted string like TO_CHAR(time_minute, 'YYYY-MM'). One thing worth flagging for Twitch specifically: if the data is stored in UTC and streamers are global, you might get a follow-up about timezone handling at year boundaries, like a stream that started at 11:58 PM December 31 UTC. Probably not in scope for a phone screen but good to have in your back pocket. The divide-by-60.0 (not 60) matters too since integer division will silently truncate in some dialects.

    Interview Details

    CompanyTwitch
    RoleData Scientist
    RoundTechnical Phone Screen
    LevelSenior
    OutcomePrefer not to say
    DateJul 2026
    LocationRemote

    Questions in this post

    Share your own experience

    Help the community by sharing what you went through.