← Twitch Interview Insights

Twitch·Data Scientist·Technical Phone Screen·Senior

SeniorPrefer not to say
Jul 2026Remote

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)

Q1

Given 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.

Product Analytics & MetricsData Modeling
Author's notes

The year-boundary thing is what trips people up.

Create a free account to read the full note

AI HintsAI Generated

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

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q2

For 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.

Data ModelingProduct Analytics & Metrics
Author's notes

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

Create a free account to read the full note

AI HintsAI Generated

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

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q3

For 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.

Data ModelingAlgorithms & Data Structures
Author's notes

This one took me a minute.

Create a free account to read the full note

AI HintsAI Generated

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

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q4

For 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.

Data ModelingProduct Analytics & Metrics
Author's notes

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.

Create a free account to read the full note

AI HintsAI Generated

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

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q5

Explain SQL's logical query processing order and clarify whether AVG() is computed before or after the JOIN.

Technical Trade-offsData Modeling
Author's notes

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

Create a free account to read the full note

AI HintsAI Generated

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)

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.