The interview is a conversation, not a quiz.
Senior interviewers care more about your logic than your memorization of SQL engine-specific quirks. This notebook is built around that premise — eight deep sections (Calibration, Modeling, SQL, Python, Streaming, Behavioral, System-Design Lite, Day-Of), each with the patterns these rounds actually probe, worked solutions, and the questions you should be asking them. Drawn from a Netflix case study; reads as a generic senior-DE prep.
Contents
- Calibration · the Netflix bar & what they're probing
- Data modeling · five-move sequence + five real-life scenarios
- SQL · seven patterns + six real-life scenarios + dialect cheatsheet
- Python · five reflexes + six real-life scenarios
- Streaming · Flink/Kafka deep-dive + 8-step play-event narrative
- Behavioral & technical deep-dive bank
- System-design lite · four designs to rehearse
- Day-of playbook · questions to ask · one-page cheatsheet
- 5-day drill pack · the five days before the conversation
The Netflix bar, and what they're actually probing.
Before any code, understand the rubric. Netflix DE rounds aren't LeetCode contests. They're structured conversations where the interviewer is silently scoring four things on every answer:
- Clarification reflex. Do you ask sharp questions before coding? "What's the grain?" "Is this batch or stream?" "Are user_ids stable?" Silence = junior signal.
- Trade-off articulation. Every answer has a cost. Star schema vs. wide table. Window function vs. self-join. Can you name the trade and pick one with reasoning?
- Failure thinking. What happens when this pipeline runs at 3am, the upstream is late, and the watermark is wrong? Senior candidates volunteer this.
- Communication under pressure. Narrate as you code. "I'm writing a CTE first to isolate qualified sessions." Silent typing = unscoreable.
DATE_DIFF or DATEDIFF is a waste. Say "in Snowflake syntax this is roughly…" and move on.
What Netflix DE rounds typically look like
- Data Modeling (45–60 min): One open-ended scenario. They want to see you go from ambiguous prompt → clarifying questions → conceptual model → physical model → indexing/partitioning → "how does this break?"
- SQL (45–60 min): 3–6 questions of escalating difficulty against a shared schema. Expect window functions, sessionization, and at least one "data quality" or "deduplication" question.
- Programming (45–60 min): CodeSignal-style. 1–2 problems. Then 15–20 min deep-dive on streaming, data systems, or a project from your resume.
The articulation framework — the four beats on every answer
| Beat | What you say (out loud) | Time |
|---|---|---|
| Clarify | "Before I model — who reads this? What's the SLA — minutes, hours, or days? Are user_ids stable across devices?" | 30–60s |
| Frame | "I'll treat this as a star schema with one fact — fact_X — at the grain of one row per Y per Z." | 15s |
| Build | Walk the model / write the SQL / type the code, narrating each decision. | bulk |
| Stress | "This breaks if late events arrive after watermark, if dim_title is reloaded mid-day, or if a GDPR delete cascades." Volunteer two failure modes unprompted. | 60s |
The three phrases that signal seniority:
- "The grain of this fact is…"
- "The trade-off here is X vs Y; I'm picking X because…"
- "This breaks when…" (volunteered, not asked)
The one phrase that signals junior: silence + typing.
Modeling is a conversation — five moves, every time.
The modeling round is the hardest to fake and the easiest to fail by jumping straight to tables. Use the same five-move sequence on any prompt — viewing history, billing, A/B experiments, content metadata, payouts — and you will look senior even when the domain is unfamiliar.
The five-move sequence
- Clarify the use cases. "Who reads this? What questions does it answer? What's the SLA — minutes, hours, days?" Don't model anything until you have 3–5 named consumers.
- Pin the grain. One sentence per fact table: "One row per user per title per session." If you can't say this cleanly, the model isn't ready.
- Conceptual → logical → physical. Entities and relationships first. Then attributes and types. Then partitioning, clustering, and SCD strategy.
- SCDs and time. Which dimensions change? How do you preserve history — Type 1 overwrite, Type 2 versioned rows, or Type 6 hybrid? Most candidates skip this and lose senior signal.
- Stress test. "How does this break?" Late-arriving facts, time zones, GDPR delete requests, schema drift, hot partitions. Volunteer at least two failure modes — don't wait to be asked.
fact_views with no clear grain, then later realizing one user can have multiple device sessions per title and the row counts don't add up. Say the grain out loud before writing a single column.
The vocabulary you must use fluently
| Term | One-sentence definition |
|---|---|
| Grain | The meaning of one row, stated as a sentence. |
| SCD Type 1 / 2 / 6 | Overwrite vs. versioned-rows vs. hybrid (current + history columns). |
| Conformed dimension | Same dim used by multiple fact tables (dim_date, dim_geography). Enables cross-fact analysis. |
| Bridge / factless fact | M:N resolver, or an event-occurrence table with no measure. |
| Late-arriving dimension | Fact arrives before the dim is loaded. Use a placeholder SK (-1) and back-fill, or block the load. |
| Slowly-changing fact | Restating financials. Append corrections; never UPDATE in place. |
| Idempotent load | Re-running yesterday's batch produces the same result. Always achievable via MERGE on a natural key. |
| Watermark | The latest event time you trust as "complete enough" to materialize. |
| Surrogate key | Synthetic integer PK (decoupled from natural key); enables SCD2. |
| Star vs snowflake | Star = denormalized dims. Snowflake = normalized. Star is default for analytics. |
| Wide table | One denormalized fact pre-joined to dims. Trade: read speed vs storage + drift. |
| Activity schema | Single-table model where every event is a row. Useful for product analytics. |
Trade-offs you must articulate
| Decision | Trade-off you say out loud |
|---|---|
| Star vs Snowflake | "Star — fewer joins, faster reads, more storage. Snowflake — normalized, less drift, more joins. Default to star for analytical workloads." |
| Wide table vs star | "Wide is faster for one consumer; drifts when 5 consumers want different cuts. Star wins long-term." |
| SCD1 vs SCD2 | "Type 1 if no one queries history. Type 2 the moment someone says 'as of date X'. Type 6 if 90/10." |
| Surrogate vs natural PK | "Surrogate decouples PK from upstream changes. Required for SCD2. Always use both — surrogate as PK, natural as a column." |
| Partition by date vs entity | "Date partitioning fits time-series scans. Entity partitioning fits per-customer queries. Default: partition by date, cluster by entity." |
| Append-only vs upsert | "Append-only is cheap, idempotent, replay-friendly. Upsert/MERGE gives latest-state cheaply. Modern lakehouse: append + materialized view." |
| Materialize vs view | "Materialize when read frequency × cost > rebuild cost. View for ad-hoc, materialize for dashboards." |
Five real-life scenarios — full walk-throughs
Scenario A — Viewing history (the canonical Netflix prompt)
Move 1 — Clarify (out loud)
- "Is a 'view' play-start or qualified-view (≥120s)? Both — qualified is a derived flag."
- "Grain: per session or per heartbeat? Heartbeats land in
fact_play_event_raw; I'll roll up tofact_viewing_session." - "Latency: recs need <5min, finance is monthly. One model, two consumers."
- "Stable IDs: profile_id stable, title_id stable, device_id rotates."
Move 2 — Grain
One row per profile per title per device per session in fact_viewing_session. A session = continuous play with gaps ≤ 30 min.
Move 3 — Conceptual
dim_profile (SCD2) ─┐
dim_title (SCD2) ─┤
dim_device (SCD1) ─┼─▶ fact_viewing_session ─▶ fact_royalty_event
dim_geography ─┤ (one row per session) (one row per title per studio per period)
dim_date ─┘
Move 3 — Physical (Snowflake / Iceberg)
CREATE TABLE fact_viewing_session (
session_sk BIGINT, -- surrogate
session_id STRING, -- natural, from client SDK
profile_sk BIGINT, -- FK to SCD2 effective row
title_sk BIGINT,
device_sk BIGINT,
geo_sk BIGINT,
date_sk INT,
session_start_ts TIMESTAMP_TZ,
session_end_ts TIMESTAMP_TZ,
watch_seconds INT,
qualified_view BOOLEAN,
bitrate_avg_kbps INT,
ingest_ts TIMESTAMP_TZ, -- for idempotent MERGE
source_system STRING
)
PARTITION BY date_sk
CLUSTER BY (title_sk, profile_sk);
Move 4 — SCD strategy
dim_profile— Type 2 (plan changes, country moves affect royalty + recs).dim_title— Type 2 (rating changes, regional availability changes).dim_device— Type 1 (current fingerprint is enough).dim_geography— Type 1.
Move 5 — Stress tests (volunteer)
- Late events: stream-side watermark 60s; daily batch backfills 48h late tail.
- Time zones: UTC at storage; royalty uses contract TZ.
- GDPR delete: token mapping table — invalidate token, sessions become un-rejoinable.
- Schema drift: Avro + Confluent Schema Registry, backward-compat mode.
- Hot partitions: popular new releases — sub-partition by
HASH(title_sk) MOD 16on release week.
fact_viewing_session is conformed with fact_royalty_event through dim_title, finance can do per-territory amortization without rebuilding the join — and that's the unlock for content leadership."
Scenario B — Production lifecycle (Studio/Content)
The trap: This looks like a "status" column on dim_title. It's not — you lose history. The right answer is a factless fact.
dim_title ──┐
dim_stage ──┼──▶ fact_title_stage_transition (factless)
dim_user ──┤ grain: one row per title per stage entry
dim_date ──┘
└──▶ snap_title_stage_daily (daily snapshot for "where is title X now?")
CREATE TABLE fact_title_stage_transition (
title_sk BIGINT,
from_stage_sk INT, -- nullable for greenlight (first stage)
to_stage_sk INT,
transition_ts TIMESTAMP_TZ,
actor_user_sk BIGINT, -- who moved it
comment STRING
);
CREATE TABLE snap_title_stage_daily (
snap_date DATE,
title_sk BIGINT,
current_stage_sk INT,
days_in_stage INT,
total_days_in_pipeline INT
)
PARTITION BY snap_date;
The "stuck project" query reduces to:
SELECT title_sk, current_stage_sk, days_in_stage
FROM snap_title_stage_daily s
WHERE snap_date = CURRENT_DATE
AND days_in_stage > (
SELECT PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY days_in_stage)
FROM snap_title_stage_daily
WHERE snap_date = CURRENT_DATE - 7
AND current_stage_sk = s.current_stage_sk
);
dim_stage_sla so 'stuck' is a per-stage threshold, not global — pre-prod normally takes 6 months, post takes 3."
Scenario C — Talent contracts & royalties (the SCD2 trap)
Why it's hard: Deal terms get renegotiated. A query for "as of 2025-Q1" must use the deal terms current at that moment.
dim_talent (SCD1)
dim_deal_overall (SCD2) -- talent ↔ Netflix master agreement
deal_id, talent_sk, term_start, term_end,
effective_from, effective_to, is_current
dim_deal_show (SCD2) -- deal under an overall deal, per show
show_deal_id, overall_deal_id, title_sk,
base_episode_fee, residual_pct, mfn_flag,
effective_from, effective_to, is_current
fact_payout_accrued (grain: one row per deal per period)
payout_id, talent_sk, show_deal_id, accrual_date,
base_amount_usd, residual_amount_usd, bonus_amount_usd,
fx_rate_to_usd, source_currency
fact_payout_disbursed (grain: one row per actual payment)
payout_id, accrual_id, paid_date, amount_usd, status
The "as of date D" query — show this verbatim
SELECT
d.talent_sk,
SUM(d.base_episode_fee * episodes_aired_by(D, d.title_sk))
+ SUM(estimated_residual(d, D)) AS total_liability_usd
FROM dim_deal_show d
WHERE d.effective_from <= :as_of_date
AND (d.effective_to IS NULL OR d.effective_to > :as_of_date)
GROUP BY d.talent_sk;
Things to volunteer
- MFN clauses: If Actor A has MFN, raising Actor B's fee triggers a backfilled adjustment. Need an audit log of fee changes; recompute affected MFN deals.
- Multi-currency:
dim_fx_rateis itself SCD2. Lock to accrual date FX rate, not today's. - Forecast: Monte Carlo on residuals — separate ML model produces
forecast_payout_low/mid/high; DE owns the join, not the model.
Scenario D — A/B experimentation at scale
The model is two facts joined late:
fact_assignment (grain: one row per profile per experiment)
profile_sk, experiment_id, variant, assigned_ts, bucketing_hash
-- sticky on profile_sk + experiment_id
fact_exposure (grain: one row per profile per experiment per day)
profile_sk, experiment_id, variant, exposure_date, exposure_count
dim_experiment (SCD2)
experiment_id, name, primary_metric,
start_date, end_date, traffic_alloc_pct, mex_layer
Lift compute is metric-agnostic
WITH exposed AS (
SELECT profile_sk, variant
FROM fact_assignment
WHERE experiment_id = :exp
AND assigned_ts >= :start AND assigned_ts < :end
),
metric AS (
SELECT profile_sk, SUM(qualified_view_seconds) AS m
FROM fact_viewing_session
WHERE session_start_ts BETWEEN :start AND :end
GROUP BY profile_sk
)
SELECT
e.variant,
COUNT(*) AS users,
AVG(COALESCE(m.m, 0)) AS metric_avg,
STDDEV(COALESCE(m.m, 0)) AS metric_std
FROM exposed e LEFT JOIN metric m USING (profile_sk)
GROUP BY e.variant;
Scenario E — Renewal scorecard (Sr/Staff differentiator)
This is a data product, not a table. The right answer is to design the layer above the warehouse:
mart_series_renewal_scorecard (one row per series per snapshot_date)
├── viewing_signal — fact_viewing_session, normalized for catalog effect
├── cost_signal — fact_production_spend + amortization curve
├── talent_signal — projected renewal cost from dim_deal
└── benchmark_signal — k-NN on dim_title features → top 5 comparable
That sentence alone moves you from Senior to Staff signal.
The eight modeling traps — and the senior counter-move
| Trap | Counter-move |
|---|---|
| Writing columns before stating grain | Say grain first, every time. |
| Using natural keys as PKs | Surrogate as PK, natural as a column. Mandatory for SCD2. |
UPDATE on facts | Append corrections; let MV pick latest. |
| One status column on a dim ("current_stage") | Factless fact for transitions + daily snapshot for current. |
| Hierarchies via recursive joins for hot queries | Denormalize parent IDs onto leaf, drift-check nightly. |
GDPR via cascade DELETEs | Tokenize at ingest; invalidate token on delete. |
| "We'll add SCD later" | Adding SCD2 to a populated dim is a 6-month migration. Decide upfront. |
| Per-team conformed dim copies | One conformed dim, owned by platform. Otherwise joins lie. |
SQL — the seven patterns + the dialects.
Netflix's SQL round is engine-agnostic by design. Master these seven patterns and you can adapt any of them to whatever schema they put in front of you.
Pattern 1 — Top-N per group
WITH agg AS (
SELECT profile_id, title_id, SUM(watch_seconds) AS total_seconds
FROM fact_viewing_session
WHERE session_start_ts >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY profile_id, title_id
),
ranked AS (
SELECT profile_id, title_id, total_seconds,
ROW_NUMBER() OVER (
PARTITION BY profile_id
ORDER BY total_seconds DESC, title_id ASC
) AS rn
FROM agg
)
SELECT profile_id, title_id, total_seconds
FROM ranked WHERE rn <= 3
ORDER BY profile_id, rn;
ROW_NUMBERfor exactly N rows.RANKif ties should both qualify (you might get >N). Mention the trade.- Tiebreaker: always add deterministic secondary sort (
title_id ASC). Without it, results are non-deterministic and tests flake. - Snowflake shortcut:
QUALIFY ROW_NUMBER() OVER (...) <= 3. Mention it; don't depend on it. - Performance: at 10B sessions,
GROUP BYis the expensive step — partition pruning onsession_start_tsdoes the heavy lifting.
Pattern 2 — Sessionization (gaps and islands)
WITH with_prev AS (
SELECT profile_id, ts, event_type,
LAG(ts) OVER (PARTITION BY profile_id ORDER BY ts) AS prev_ts
FROM fact_play_event
),
flagged AS (
SELECT profile_id, ts, event_type,
CASE
WHEN prev_ts IS NULL OR TIMESTAMPDIFF('second', prev_ts, ts) > 1800
THEN 1 ELSE 0
END AS is_new_session
FROM with_prev
),
sessionized AS (
SELECT profile_id, ts, event_type,
SUM(is_new_session) OVER (
PARTITION BY profile_id ORDER BY ts
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS session_num
FROM flagged
)
SELECT profile_id, session_num,
MIN(ts) AS session_start, MAX(ts) AS session_end,
COUNT(*) AS event_count
FROM sessionized
GROUP BY profile_id, session_num
ORDER BY profile_id, session_start;
- Canonical gaps-and-islands: LAG → flag boundary → cumulative SUM = session_id → GROUP BY. Memorize the shape.
- Dialect note:
TIMESTAMPDIFFis Snowflake/MySQL. BigQuery:TIMESTAMP_DIFF(ts, prev_ts, SECOND). Postgres:EXTRACT(EPOCH FROM (ts - prev_ts)). - Senior signal: for a 30-day stream, run incrementally with a backfill window of (30min × 2) at the boundary to avoid splitting sessions across batches.
Pattern 3 — Funnels and conversion
WITH per_user AS (
SELECT profile_id,
MAX(CASE WHEN step_name = 'landing' THEN 1 END) AS hit_landing,
MAX(CASE WHEN step_name = 'plan_select' THEN 1 END) AS hit_plan,
MAX(CASE WHEN step_name = 'payment_info' THEN 1 END) AS hit_pay,
MAX(CASE WHEN step_name = 'confirm' THEN 1 END) AS hit_confirm
FROM fact_signup_event
GROUP BY profile_id
)
SELECT
SUM(hit_landing) AS landing_n,
SUM(hit_plan) AS plan_n,
SUM(hit_pay) AS pay_n,
SUM(hit_confirm) AS confirm_n,
SUM(hit_plan)::FLOAT / NULLIF(SUM(hit_landing), 0) AS conv_landing_to_plan,
SUM(hit_pay)::FLOAT / NULLIF(SUM(hit_plan), 0) AS conv_plan_to_pay,
SUM(hit_confirm)::FLOAT / NULLIF(SUM(hit_pay), 0) AS conv_pay_to_confirm,
SUM(hit_confirm)::FLOAT / NULLIF(SUM(hit_landing), 0) AS overall_conversion
FROM per_user;
- Conditional aggregation (CASE inside aggregate) beats self-joining four times. Always preferred for funnels.
- Strict vs inclusive: above is inclusive. For strict (must complete previous in order), add
ts > landing_tschecks. NULLIFon denominator prevents division-by-zero. Always.- Senior signal: real funnel has a per-step time window — "did they reach plan_select within 7 days of landing?" — via
FIRST_VALUEper step.
Pattern 4 — Deduplication / data quality
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY event_id
ORDER BY ingest_ts DESC
) AS rn
FROM fact_play_event_raw
)
SELECT event_id, profile_id, ts, event_type, ingest_ts
FROM ranked WHERE rn = 1;
- Why ROW_NUMBER over DISTINCT: DISTINCT collapses on all columns; if anything else differs, dupes survive. ROW_NUMBER picks one deterministically.
- Why
ingest_tsnotevent_ts: producer's clock may be wrong. Trust broker's ingest time for "which write wins." - Senior signal: make idempotent at write time —
MERGE INTO target USING source ON event_id WHEN MATCHED AND source.ingest_ts > target.ingest_ts THEN UPDATE.
Pattern 5 — Cohort retention
WITH cohort AS (
SELECT profile_id, DATE_TRUNC('week', signup_ts) AS cohort_week
FROM dim_profile
),
activity AS (
SELECT DISTINCT profile_id,
DATE_TRUNC('week', session_start_ts) AS activity_week
FROM fact_viewing_session
WHERE qualified_view_flag = TRUE
),
sized AS (
SELECT cohort_week, COUNT(DISTINCT profile_id) AS cohort_size
FROM cohort GROUP BY 1
),
joined AS (
SELECT c.cohort_week, c.profile_id, a.activity_week,
DATEDIFF('week', c.cohort_week, a.activity_week) AS week_offset
FROM cohort c
LEFT JOIN activity a
ON c.profile_id = a.profile_id
AND a.activity_week >= c.cohort_week
)
SELECT j.cohort_week, j.week_offset,
COUNT(DISTINCT j.profile_id) AS retained,
s.cohort_size,
COUNT(DISTINCT j.profile_id)::FLOAT / s.cohort_size AS retention_rate
FROM joined j JOIN sized s ON j.cohort_week = s.cohort_week
WHERE j.week_offset BETWEEN 0 AND 12
GROUP BY 1, 2, s.cohort_size
ORDER BY 1, 2;
- Cohort definition is a choice. Calendar week vs rolling 7-day; first-event vs signup. State your choice.
LEFT JOINkeeps cohort users with zero activity — essential for the denominator.- Senior signal: materialize as
mart_cohort_retention_dailypartitioned bycohort_week, refresh trailing 12 weeks. Recomputing from raw on every dashboard load is the rookie move.
Pattern 6 — Running totals and pacing
WITH daily AS (
SELECT title_id, DATE(session_start_ts) AS view_date,
COUNT(*) AS daily_views
FROM fact_viewing_session
WHERE qualified_view_flag = TRUE
GROUP BY title_id, DATE(session_start_ts)
),
cumulative AS (
SELECT title_id, view_date, daily_views,
SUM(daily_views) OVER (
PARTITION BY title_id ORDER BY view_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_views,
SUM(daily_views) OVER (
PARTITION BY title_id ORDER BY view_date
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING
) AS prev_cumulative
FROM daily
)
SELECT title_id, view_date, daily_views, cumulative_views,
CASE
WHEN cumulative_views >= 1000000
AND (prev_cumulative IS NULL OR prev_cumulative < 1000000)
THEN TRUE ELSE FALSE
END AS crossed_1m_today
FROM cumulative
ORDER BY title_id, view_date;
- The
ROWS BETWEEN ... PRECEDINGtrick gets "previous cumulative" without LAG — works even with date gaps. - Edge case to volunteer: if a title has no views on day N, densify with
generate_seriesor a date spine before the cumulative — otherwise "crossed today" fires on the wrong day.
Pattern 7 — DAU / MAU and HyperLogLog
-- Exact, expensive:
SELECT DATE(session_start_ts) AS d, COUNT(DISTINCT profile_id) AS dau
FROM fact_viewing_session
WHERE session_start_ts >= CURRENT_DATE - 30
GROUP BY 1;
-- Approximate, cheap:
SELECT DATE(session_start_ts) AS d,
APPROX_COUNT_DISTINCT(profile_id) AS dau_approx
FROM fact_viewing_session
WHERE session_start_ts >= CURRENT_DATE - 30
GROUP BY 1;
-- HLL sketches for rollup-friendly storage (Snowflake):
CREATE TABLE dau_sketches AS
SELECT DATE(session_start_ts) AS d,
HLL_ACCUMULATE(profile_id) AS sketch
FROM fact_viewing_session GROUP BY 1;
-- Then MAU is a cheap merge of 30 sketches:
SELECT HLL_ESTIMATE(HLL_COMBINE(sketch)) AS mau_approx
FROM dau_sketches WHERE d >= CURRENT_DATE - 30;
- Senior signal — name HyperLogLog sketches. Explain they're additive: weekly/monthly/yearly rollups become trivial sketch merges.
- Typical accuracy: ~1.5% error at default precision. Acceptable for product analytics, not finance.
- BigQuery:
HLL_COUNT.INIT / MERGE / EXTRACT. Snowflake:HLL_ACCUMULATE / COMBINE / ESTIMATE.
Dialect cheatsheet — don't lose 2 minutes recalling syntax
| Operation | Snowflake | BigQuery | Spark SQL | Postgres |
|---|---|---|---|---|
| Date diff (sec) | TIMESTAMPDIFF('second',a,b) | TIMESTAMP_DIFF(b,a,SECOND) | unix_timestamp(b)-unix_timestamp(a) | EXTRACT(EPOCH FROM b-a) |
| Truncate to week | DATE_TRUNC('week',ts) | DATE_TRUNC(ts,WEEK) | date_trunc('week',ts) | DATE_TRUNC('week',ts) |
| Date add | DATEADD(day,7,d) | DATE_ADD(d,INTERVAL 7 DAY) | date_add(d,7) | d + INTERVAL '7 day' |
| Approx distinct | APPROX_COUNT_DISTINCT(x) | APPROX_COUNT_DISTINCT(x) | approx_count_distinct(x) | (extension) |
| Top-N shortcut | QUALIFY ROW_NUMBER()=1 | QUALIFY ROW_NUMBER()=1 | QUALIFY (3.4+) | (use CTE) |
| Array agg | ARRAY_AGG(x) WITHIN GROUP (...) | ARRAY_AGG(x ORDER BY ...) | collect_list(x) | ARRAY_AGG(x ORDER BY ...) |
| String concat | LISTAGG(x,',') | STRING_AGG(x,',') | concat_ws(',',collect_list(x)) | STRING_AGG(x,',') |
| Null-safe equal | IS NOT DISTINCT FROM | IS NOT DISTINCT FROM | <=> | IS NOT DISTINCT FROM |
DATE_TRUNC semantics here") and write what you remember. Never freeze on syntax.Six real-life SQL scenarios
SQL Q1 — Cross-device sessionization (Studio twist)
WITH events AS (
SELECT profile_id, device_id, ts,
LAG(ts) OVER (PARTITION BY profile_id ORDER BY ts) AS prev_ts
FROM fact_play_event
),
flagged AS (
SELECT *, CASE WHEN prev_ts IS NULL
OR TIMESTAMPDIFF('second', prev_ts, ts) > 1800
THEN 1 ELSE 0 END AS is_new_session
FROM events
),
sessionized AS (
SELECT *, SUM(is_new_session) OVER (PARTITION BY profile_id ORDER BY ts) AS session_num
FROM flagged
)
SELECT profile_id, session_num,
MIN(ts) AS session_start, MAX(ts) AS session_end,
COUNT(*) AS event_count,
COUNT(DISTINCT device_id) AS devices_in_session,
CASE WHEN COUNT(DISTINCT device_id) > 1 THEN TRUE ELSE FALSE END AS is_handoff
FROM sessionized GROUP BY 1, 2;
- Partitioning only on
profile_id(not device) is what makes device handoff stay in one session. devices_in_sessionis free senior touch — answers "what % of sessions involve handoff?" without a second query.- For 30-day batch: 60-min boundary buffer to avoid splitting sessions across daily partitions.
SQL Q2 — SCD2 point-in-time join (Studio/Finance)
SELECT t.title_id,
SUM(s.watch_seconds * p.price_per_second_usd) AS revenue_usd
FROM fact_viewing_session s
JOIN dim_title t ON s.title_sk = t.title_sk
JOIN dim_pricing_plan p
ON p.plan_id = t.plan_id
AND s.session_start_ts >= p.effective_from
AND s.session_start_ts < COALESCE(p.effective_to, '9999-12-31')
WHERE s.session_start_ts >= '2025-01-01'
AND s.session_start_ts < '2025-04-01'
GROUP BY t.title_id;
- Interval-overlap predicate on the dim is the SCD2 point-in-time idiom.
WHERE p.is_current = TRUEwould be wrong for historical compute. COALESCE(effective_to, '9999-12-31')— sentinel for current rows. Some shops use NULL; ask which convention.
SQL Q3 — Tiered deduplication with quality preference
fact_play_event_raw has dupes from a flaky producer. Pick the "best" record — prefer non-null watch_seconds, then latest ingest.SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY event_id
ORDER BY
CASE WHEN watch_seconds IS NOT NULL THEN 0 ELSE 1 END,
ingest_ts DESC
) AS rn
FROM fact_play_event_raw
)
WHERE rn = 1;
- Multi-tier ORDER BY — quality first, recency second. Portable trick when
NULLS LASTis unavailable. - Push into the
MERGEat write time; daily dedup scan is the rookie option.
SQL Q4 — Density-aware running total with date spine
WITH dates AS (
SELECT DATEADD('day', seq4(), '2025-01-01') AS d
FROM TABLE(GENERATOR(ROWCOUNT => 365))
),
title_release AS (
SELECT title_id, MIN(DATE(session_start_ts)) AS release_date
FROM fact_viewing_session GROUP BY title_id
),
spine AS (
SELECT t.title_id, d.d
FROM title_release t CROSS JOIN dates d
WHERE d.d >= t.release_date
),
daily AS (
SELECT s.title_id, s.d,
COALESCE(SUM(CASE WHEN f.qualified_view_flag THEN 1 END), 0) AS daily_views
FROM spine s
LEFT JOIN fact_viewing_session f
ON f.title_id = s.title_id AND DATE(f.session_start_ts) = s.d
GROUP BY 1, 2
)
SELECT title_id, d, daily_views,
SUM(daily_views) OVER (PARTITION BY title_id ORDER BY d) AS cumulative,
CASE
WHEN SUM(daily_views) OVER (PARTITION BY title_id ORDER BY d) >= 1000000
AND COALESCE(SUM(daily_views) OVER (
PARTITION BY title_id ORDER BY d
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0) < 1000000
THEN TRUE ELSE FALSE
END AS crossed_1m_today
FROM daily ORDER BY title_id, d;
- Date spine prevents the running total from being wrong on zero-view days.
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDINGgives previous-cumulative without LAG, robust to gaps.
SQL Q5 — Streaming SQL (Flink tumbling window)
-- Flink SQL
CREATE TABLE play_events (
profile_id BIGINT,
title_id BIGINT,
watch_seconds INT,
event_ts TIMESTAMP(3),
WATERMARK FOR event_ts AS event_ts - INTERVAL '60' SECOND
) WITH ('connector' = 'kafka', 'topic' = 'play_events_v1', ...);
SELECT title_id,
TUMBLE_START(event_ts, INTERVAL '5' MINUTE) AS window_start,
TUMBLE_END (event_ts, INTERVAL '5' MINUTE) AS window_end,
COUNT(*) AS qualified_events,
COUNT(DISTINCT profile_id) AS unique_viewers
FROM play_events
WHERE watch_seconds >= 120
GROUP BY title_id, TUMBLE(event_ts, INTERVAL '5' MINUTE);
- Watermark of 60s — promise that events older than (max_seen_ts − 60s) won't arrive.
- Tumbling for non-overlapping 5-min buckets. Hopping if consumer wanted moving 5-min average updated every minute.
- Late events past watermark go to a side output → DLQ topic, not
/dev/null. - State backend: RocksDB —
COUNT DISTINCT profile_idstate grows; spills to disk.
The eight SQL traps
| Trap | Counter-move |
|---|---|
Forgetting tiebreaker in ROW_NUMBER | Always add deterministic secondary sort. |
COUNT(DISTINCT) on petabyte fact | Mention HLL; use APPROX_COUNT_DISTINCT. |
Division without NULLIF | numerator / NULLIF(denominator, 0) always. |
is_current = TRUE for historical compute | Interval-overlap predicate on SCD2 instead. |
DISTINCT to dedupe | ROW_NUMBER + WHERE rn=1 — DISTINCT collapses on all columns. |
UNION instead of UNION ALL | UNION dedupes (hidden DISTINCT). UNION ALL by default. |
LEFT JOIN with WHERE right.x = ... | Predicate kills the LEFT JOIN — move to ON, or use right.x IS NULL OR .... |
| Cumulative running total without date spine | Densify dates first; otherwise gaps fire wrong thresholds. |
Python — fluency over cleverness.
The recruiter explicitly said: "we care more about coding fluency than perfect solutioning." A working O(n²) you can explain beats a clever O(n log n) you can't articulate. Here are the patterns Netflix actually tests.
The five reflexes — when to reach for which
| Need | Reach for | Why |
|---|---|---|
| Group + aggregate | collections.defaultdict(int / list / set) | No KeyError; clean code. |
| Top-K | heapq.nlargest(k, ...) or manual min-heap of size k | O(n log k); manual heap saves memory. |
| Sliding window over time | collections.deque + running totals dict | O(1) append/popleft. |
| Sessionization | Sort within group, walk pairs, track open session | Single pass after sort. |
| O(1) cache | collections.OrderedDict + move_to_end / popitem(last=False) | Backed by DLL+hashmap. |
| K-way merge | heapq.merge(*iterables) | Lazy generator; bounded memory. |
| Counting | collections.Counter | Built-in most_common(k). |
| Probabilistic / approx | Reservoir sample, MinHash, Count-Min Sketch | Sub-linear memory. |
Six real-life Python scenarios
Python P1 — Sessionization (the warmup)
[(profile_id, ts), ...] into sessions where consecutive same-profile events are within gap_sec. Return [(profile_id, start, end, count)].from collections import defaultdict
def sessionize(events, gap=1800):
by_user = defaultdict(list)
for uid, ts in events:
by_user[uid].append(ts)
sessions = []
for uid, ts_list in by_user.items():
ts_list.sort()
start = end = ts_list[0]
count = 1
for ts in ts_list[1:]:
if ts - end <= gap:
end, count = ts, count + 1
else:
sessions.append((uid, start, end, count))
start = end = ts
count = 1
sessions.append((uid, start, end, count))
return sessions
- O(n log n) total — dominated by per-user sort. Pre-sorted (Kafka): single pass O(n).
- Memory O(n) — at scale, partition by
profile_idacross workers. - Edge: two events with same
ts→ same session. State the assumption.
Python P2 — Top-K from a stream (the Netflix favorite)
(ts, title_id, watch_seconds). Return top-K most-watched titles in the last hour. Implement add_event(ev) and top_k(k).from collections import defaultdict, deque
import heapq
class TopKWatched:
def __init__(self, window_sec=3600):
self.window = window_sec
self.events = deque() # (ts, title, watch)
self.totals = defaultdict(int) # title -> watch in window
def add_event(self, ts, title, watch):
self.events.append((ts, title, watch))
self.totals[title] += watch
self._evict(ts)
def _evict(self, now):
cutoff = now - self.window
while self.events and self.events[0][0] < cutoff:
_, t, w = self.events.popleft()
self.totals[t] -= w
if self.totals[t] <= 0:
del self.totals[t]
def top_k(self, k):
return heapq.nlargest(k, self.totals.items(), key=lambda x: x[1])
add_eventO(1) amortized, eviction amortized O(1).top_kO(n log k) — called rarely.- If
top_kis hot, switch to max-heap with lazy deletion — push(-total, title)on update; pop stale on read. - Distributed: partition by
title_id, each worker maintains local top-K, then merge — top-K is associative for sums. - Out-of-order events:
_evictuses last-seen-ts; for true event-time, use a watermark.
Python P3 — Watermark sessionizer (out-of-order stream)
(ts, profile, watch_sec), events arrive out-of-order. Emit closed sessions only when watermark passes.from collections import defaultdict
import heapq
class WatermarkSessionizer:
def __init__(self, gap=1800, lag=300):
self.gap = gap
self.lag = lag
self.buf = defaultdict(list)
self.max_ts = 0
def add(self, ts, profile, watch):
heapq.heappush(self.buf[profile], (ts, watch))
self.max_ts = max(self.max_ts, ts)
def poll(self):
watermark = self.max_ts - self.lag
closed = []
for pid, heap in list(self.buf.items()):
start = end = total = None
while heap and heap[0][0] < watermark:
ts, w = heapq.heappop(heap)
if start is None or ts - end > self.gap:
if start is not None:
closed.append((pid, start, end, total))
start, end, total = ts, ts, w
else:
end, total = ts, total + w
if start is not None:
heapq.heappush(heap, (start, total))
if not heap:
del self.buf[pid]
return closed
- Watermark =
max_seen − lag. Close sessions whose end < watermark; everything more recent stays buffered. - Exactly Flink's keyed state with event-time. Production: state backed by RocksDB, checkpointed.
Python P4 — Co-occurrence at scale (the trap)
[(user_id, title_id), ...], find pairs of users who watched ≥3 titles in common.from collections import defaultdict
from itertools import combinations
def overlap_pairs(views, threshold=3):
title_to_users = defaultdict(set)
for uid, tid in views:
title_to_users[tid].add(uid)
pair_counts = defaultdict(int)
for users in title_to_users.values():
for u1, u2 in combinations(sorted(users), 2):
pair_counts[(u1, u2)] += 1
return [pair for pair, c in pair_counts.items() if c >= threshold]
- Volunteer the complexity warning: a popular title with M users → M² pairs. Intractable at Netflix scale.
- Better: MinHash + LSH (locality-sensitive hashing) — approximate set intersection without enumerating pairs. O(n) instead of O(n²).
- Alternative: cap users per title (sample), or only consider pairs sharing rare titles (more signal anyway).
Python P5 — Reservoir sampling
import random
def reservoir_sample(stream, k):
reservoir = []
for i, item in enumerate(stream):
if i < k:
reservoir.append(item)
else:
j = random.randint(0, i)
if j < k:
reservoir[j] = item
return reservoir
- Each item has probability
k/nof being kept after seeing n items — provable by induction. - Senior signal: mention weighted reservoir (A-Res) when items have unequal weights.
Python P6 — Causal event pairing
play_started and play_ended. Pair them by (profile, title). Some started events never get an ended (crashes/timeouts).def pair_events(events, timeout_sec=14400):
open_starts = {}
pairs, orphans = [], []
for ev in sorted(events, key=lambda e: e['ts']):
key = (ev['profile_id'], ev['title_id'])
if ev['type'] == 'play_started':
if key in open_starts:
orphans.append(open_starts[key])
open_starts[key] = (ev['session_id'], ev['ts'])
elif ev['type'] == 'play_ended':
if key in open_starts:
sid, sts = open_starts.pop(key)
if ev['ts'] - sts <= timeout_sec:
pairs.append((sid, sts, ev['ts']))
else:
orphans.append((sid, sts))
orphans.extend(open_starts.values())
return pairs, orphans
- Volunteer the orphan output — interviewer was about to ask "what about events without partners?".
- Sort once: O(n log n). Pre-sorted Kafka:
heapq.mergefor O(n) merged iteration.
Three high-leverage patterns to memorize verbatim
Type these from a blank file every drill day. By rep five, your fingers know them.
(a) Sessionization — see Python P1 above. (b) Top-K with sliding window — see Python P2. (c) Counter + sliding window over time:
from collections import Counter, deque
def windowed_counts(events, window_sec):
"""events: iterable of (ts, key). Yields (ts, Counter) at each ts."""
buf = deque()
counts = Counter()
for ts, key in events:
buf.append((ts, key))
counts[key] += 1
cutoff = ts - window_sec
while buf and buf[0][0] < cutoff:
_, old = buf.popleft()
counts[old] -= 1
if counts[old] == 0:
del counts[old]
yield ts, counts.copy()
This third pattern shows up in ~1 of 3 streaming-flavored CodeSignal problems.
The eight Python traps
| Trap | Counter-move |
|---|---|
list.pop(0) on big list | Use deque — popleft() is O(1). |
Mutable default arg (def f(x=[])) | def f(x=None): x = x or [] |
Sort with cmp_to_key | Use tuple keys: key=lambda x: (-x.score, x.name). |
| Recursion deeper than 1000 | Convert to iteration with explicit stack. |
for k in dict: del dict[k] | Iterate list(dict.keys()) first. |
== for floats | math.isclose(a, b, rel_tol=1e-9) |
Forgetting enumerate | for i, x in enumerate(xs) |
| Reading whole file in memory | with open(...) as f: for line in f: — generator. |
Coding under pressure — the script
- Restate the problem. "So I'm getting a list of tuples and need to return…" 30 sec.
- Walk through one example by hand. Forces edge case confirmation.
- Brute-force first. "I'll start with the obvious O(n²), then optimize." Get something passing.
- Optimize with reasoning. "The bottleneck is the inner loop. If I precompute a dict, that becomes O(1)…"
- Test mentally. Run your code on the example. Catch off-by-ones before the timer does.
- Discuss complexity. Time and space, both. State explicitly even if not asked.
Streaming concepts — the vocabulary that signals seniority.
Netflix runs Keystone (a massive Kafka pipeline) and uses Flink heavily. Even if you don't write a line of Flink, knowing this vocabulary is what separates a senior DE from a SQL specialist.
The vocabulary you must say without hesitation
| Term | One-liner |
|---|---|
| Event-time | "When it happened — client clock embedded in the event." |
| Processing-time | "When the pipeline saw it. Fallback for events without reliable timestamps." |
| Ingestion-time | "When the broker received it. Compromise — server clock, not user clock." |
| Watermark | "Promise: 'I have seen all events with timestamp ≤ W.' Lets us close windows." |
| Allowed lateness | "Tolerance budget after watermark for late firings before window is dropped." |
| Tumbling window | "Non-overlapping fixed buckets — every 5 min, distinct." |
| Hopping (sliding) | "Overlapping fixed buckets — every 1 min, look back 5 min." |
| Session window | "Gap-defined dynamic boundaries — sessionization." |
| Exactly-once | "Each event affects state once, even on failure. Flink: checkpoint + 2PC sink." |
| At-least-once | "Events may duplicate on retry. Cheap. Requires idempotent consumers." |
| Backpressure | "Downstream slower than upstream — system pauses upstream automatically." |
| Checkpoint barrier | "Flink's snapshot trigger — flows through DAG; each operator snapshots state." |
| State backend | "RocksDB (disk, big state) vs heap (small, fast)." |
| Savepoint | "Manually-triggered checkpoint for upgrades and version migration." |
| Keyed state | "Partitioned state — one key, one worker. Sessionization needs it." |
| Schema registry | "Centralized schema validation — backward / forward / full compatibility." |
| DLQ | "Dead-letter queue — bad events go here, not /dev/null." |
| Lambda architecture | "Separate batch + stream paths, reconcile in serving. Two codebases." |
| Kappa architecture | "One streaming pipeline. Backfills via log replay. Modern default." |
| Idempotent sink | "Re-applying the same write produces the same result. MERGE on natural key." |
| Skew | "One key dominates traffic; one worker pegged. Salt the key, sub-partition." |
Trade-offs you must articulate
| Decision | The line you say |
|---|---|
| Event-time vs processing-time | "Event-time for analytics — otherwise mobile reconnects look like spikes. Processing-time only when client clocks are unreliable." |
| Exactly-once vs at-least-once | "Exactly-once costs latency and storage. At-least-once + idempotent writes is the common shape — same outcome, half the cost." |
| Tumbling vs hopping vs session | "Tumbling for periodic reports. Hopping for moving averages. Session for user-defined boundaries." |
| Lambda vs Kappa | "Lambda is two codebases that drift. Kappa is one path with replay-from-log. Modern lakehouse made Kappa the default." |
| RocksDB vs heap state | "RocksDB for large state — spills to disk. Heap for small state and tight latency. Default RocksDB at scale." |
| Watermark lag (tight vs loose) | "Tight = fresh, late events drop. Loose = output delayed, more captured. Set to p99 lateness + safety buffer." |
| DLQ vs drop | "DLQ always — inspecting bad events is how you find producer bugs." |
The 8-step play-event walkthrough — memorize verbatim
Memorize this and rehearse out loud till you can deliver in 90 seconds:
- Client SDK emits the event from device — TV, phone, browser. Embeds
event_ts(client clock),profile_id,title_id,device_id,watch_seconds. Buffers locally on bad network. - Regional collector (Keystone) — Kafka cluster at the edge. Producer SDK uses schema-registry client to validate before publish.
- Schema validation + DLQ. Bad events → DLQ topic, not dropped. DLQ is monitored.
- Multi-tier Kafka topics:
play_events_raw→play_events_validated→play_events_enriched. Each step is a Flink job adding metadata. - Two consumers fork: real-time (recommendations, alerting — sub-second SLA) and batch (S3/Iceberg sink for analytics).
- S3/Iceberg sink — partitioned by
event_date/hour. Iceberg gives ACID + time-travel + multi-engine read. Files compacted nightly. - dbt / Spark builds the dimensional layer (
fact_viewing_session) on a daily schedule. 48h backfill window for late events. - BI (Tableau/Superset) queries the modeled layer. Recommendation features re-publish to a feature store (Feast/EVCache) for sub-second model serving.
The credibility-test phrase to drop mid-narration: "…we use Iceberg over Delta because of multi-engine support — Trino and Spark both read it natively."
Eight scenarios — the questions actually asked
S1 — Event-time vs processing-time
A: "Event-time is when the user pressed play — the client clock embedded in the event. Processing-time is when our pipeline received it. The gap is small on good networks, hours on mobile reconnects. Production analytics always uses event-time — otherwise a deferred sync from someone in airplane mode looks like a traffic spike. Choosing event-time forces us to handle out-of-order arrivals, which is what watermarks are for."
S2 — Watermarks
A: "A watermark is the pipeline's promise: 'I have seen all events with timestamp ≤ W.' Choosing it is a trade-off — too tight, late events drop; too loose, output is delayed. Standard heuristic: max-seen-ts minus the 99th percentile of observed lateness, plus a safety buffer. For Netflix mobile traffic where lateness p99 is around 30s, I'd start with a 60s lag. The metric to monitor is late_event_count per topic per hour."
S3 — Exactly-once
A: "Each event affects state exactly once, even on failure. Flink achieves it with checkpoint barriers — periodic markers in input streams. When a barrier reaches an operator, it snapshots state to S3. Sinks use two-phase commit — pre-commit on checkpoint, commit on next checkpoint. Cost: latency equal to checkpoint interval, plus snapshot CPU. Most Netflix pipelines run at-least-once + idempotent sinks — same observable behavior, half the cost. Exactly-once is reserved for billing where retries change the answer."
S4 — Lambda vs Kappa
A: "Lambda runs separate batch and stream pipelines, reconciles in serving — gives correctness from batch and freshness from stream. Cost: two codebases, drift. Kappa runs only streaming, replays log for backfills — single codebase, but you must own the replay tooling. I'd pick Kappa today. Iceberg/Delta gave us reliable replay-from-log on commodity object store, which used to be the missing piece."
S5 — Schema evolution
A: "Avro or Protobuf, plus a centralized schema registry. Producers register a schema; consumers fetch by ID embedded in messages. Compatibility modes: backward (consumer reads old + new), forward (old consumer reads new producer), full (both). Default to backward. Without this, a renamed field on the producer side silently breaks every consumer at 3 AM. CI test is a registry pre-commit hook. Breaking changes go to a new topic version (play_events_v2) with a sunset window."
S6 — Backpressure
A: "Downstream operator slower than upstream. Without handling, upstream buffer overflows — OOM or data loss. Flink handles it natively: when a downstream task can't keep up, network buffers fill, upstream naturally slows. Diagnostic is the backpressure indicator in the Flink UI — red on a task means it's the bottleneck. Fixes: scale that operator, find skew (one key dominating — salt or sub-partition), or simplify the operator."
S7 — Tumbling vs hopping vs session
A: "Tumbling — fixed, non-overlapping. Use for periodic reports — hourly DAU. Hopping — fixed, overlapping. Use for moving averages — '5-minute trending titles, updated every minute'. Session — gap-defined dynamic. Use for user behavior — sessionization, binge sequences. The mistake is using tumbling for trending — gives stepwise jumps; hopping smooths them."
S8 — The 8-step narrative
Cover when asked: "Walk me through how a play event travels from a user's TV to the executive dashboard." Use the 8 steps above. Memorize; deliver in 90 seconds.
Five operational war-stories to have ready
| Probe | Shape of the answer |
|---|---|
| Pipeline that broke at 3 AM | DAU dropped 40% → watermark stalled because one partition's producer was clock-skewed → fix (split watermark per partition) → result (lateness budget cut from 5min to 60s). |
| Schema-evolution incident | Producer renamed field; backward-compat missed it because rename = remove + add → consumers crashed → fix (registry pre-commit hook) → outcome (zero schema regressions in 18 months). |
| Duplicate-events problem | At-least-once + non-idempotent sink → revenue double-counted by 0.3% → fix (MERGE on event_id at sink) → outcome (idempotency invariant added to pipeline contract). |
| Skew problem | One title (global tentpole) had 100× the events → one Flink task pegged → fix (salt the key with title_id || hash(profile_id) % 16, two-stage aggregation) → outcome (latency p99 cut from 12s to 1s). |
| Checkpoint-induced latency spike | Checkpoint interval 1 min, RocksDB state 200 GB per task → 30-second pause every minute → fix (incremental checkpoints + smaller state via TTL) → outcome (p99 latency from 30s to 2s). |
Six streaming traps
| Trap | Counter-move |
|---|---|
| Watermark per topic, not per partition | Per-partition watermarks. One slow partition shouldn't stall the entire job. |
| Exactly-once everywhere by default | At-least-once + idempotent sink is cheaper and observably identical for analytics. |
| Dropping bad events | DLQ topic. Always inspectable. |
| Sessionization in unkeyed state | Keyed state by profile_id. Otherwise sessions cross workers and break. |
| Single Kafka topic for raw + enriched | Multi-tier topics (raw → validated → enriched). Lets you replay only the failed stage. |
| Schema changes without registry | Registry + CI hook. Without it, breaking changes hit prod silently. |
Ten Netflix-system names worth knowing
| System | What it is |
|---|---|
| Keystone | Netflix's Kafka pipeline backbone. |
| Mantis | Real-time stream processing platform (older — partly superseded by Flink). |
| Iceberg | Table format for the lakehouse — Netflix originated it. |
| Atlas | Time-series telemetry / metrics store. |
| Genie | Job orchestration / submission service for Spark/Hive/Presto. |
| Metaflow | ML workflow tool (Netflix-originated). |
| Maestro | DAG scheduler (replaced Meson). |
| EVCache | Memcached-based distributed cache. |
| DBLog | Change-data-capture framework. |
| Bulldozer | Data movement / S3-to-online-store loader. |
The technical deep-dive — questions to rehearse.
After the coding section, expect 15–20 minutes of "tell me about a time you…" and "how would you design…". Have polished answers. Use STAR (Situation, Task, Action, Result) but compress to 90 seconds each.
From your resume — likely probes
| Probe | Shape of your answer (90 sec) |
|---|---|
| Hard data-quality problem | Symptom (row counts off 0.3%) → diagnosis (late-arriving events from misconfigured collector) → fix (watermark adjustment + backfill window) → result (SLA met, alert added). |
| Pipeline you owned end-to-end | Pipeline name, scale (rows/day, latency SLA), tech stack, what went wrong, how you fixed it. |
| Model you'd redesign today | Self-awareness probe. Pick one — wrong grain, over-normalized, missed SCD2 — and what you learned. |
| Handle PII at scale | Tokenization, column-level encryption, separate access tiers, audit logging, GDPR delete cascades. Lean in. |
| Disagreed and committed | Netflix value. Story where you pushed back, lost, executed anyway, captured what you learned. |
| Influence without authority | Cross-team migration; stakeholder you don't manage; how you built the case. |
The 90-second STAR template
- Situation (15s): "We had a [type of] pipeline running [scale]; the issue was [observable symptom]."
- Task (15s): "I needed to [decision / outcome you owned]."
- Action (45s): "I [diagnosed by X], decided to [Y because Z trade-off], implemented by [details]."
- Result (15s): "[measurable outcome] within [timeframe]; [what changed structurally]."
Designs to rehearse.
D1 — Real-time recommendation pipeline
Kafka events → Flink for feature aggregation → feature store (Feast/Redis) → model serving (Triton/TF-Serving). Discuss feature freshness SLA (sub-second) vs. batch features (daily). Cold-start handling (use content embeddings + popularity priors for new users).
D2 — Data quality framework
Layered checks: schema (registry), volume (row count vs 7-day average ± stddev), distribution (KS test on key columns), referential integrity (FK orphans). Write to a monitoring topic; route critical failures to PagerDuty. Each check has a severity (block downstream / alert / log).
D3 — GDPR delete-on-request
Token mapping service: replace user_id with token at ingest. On delete, invalidate the token mapping — events become un-rejoinable. Plus a propagation queue for downstream materialized views. SLA: 30 days. For finance/tax (7-year retention), use anonymization (replace name, keep transaction).
D4 — A/B experimentation infra
Assignment service (sticky bucketing on profile_id hash), exposure logging (every served variant logged), metric pipeline (joins exposures to outcome facts), stats engine (CUPED + sequential testing). SRM monitor as a first-class citizen — observed split deviation >0.5% from expected pages on-call.
The interview-day playbook.
The morning
- Don't cram new material. Re-read your one-page cheat sheet (seven SQL patterns, modeling five-move sequence, streaming vocabulary).
- Test setup 30 min early. Camera, mic, screen-share, CodeSignal login, scratch paper, water. Have a second laptop or phone ready.
- Have the recruiter's contact open in another tab. If something breaks, message them immediately.
In the round — micro-behaviors
- First 30 sec of every problem: ask 1–2 clarifying questions. Even if the prompt seems complete. Muscle memory you should drill.
- Narrate everything. "I'm going to start with a CTE that aggregates per user. Then I'll rank within user. Then filter." Silent for 60+ seconds = lost signal.
- If stuck: say so. "I'm trying to remember the exact syntax for the date function in Snowflake — can I write it as
date_truncand we move on?" The interviewer will say yes 100% of the time. - If you finish early: don't sit silently. Walk through edge cases. "What if input is empty? What if duplicate timestamps?" Free senior signal.
- Last 5 minutes: always reserved for your questions. Have 3 ready.
If something goes wrong
- Wrong answer realized 2 minutes later: say so immediately. "Actually, I want to revise — the LEFT JOIN should be on cohort, not activity, otherwise we drop users with zero activity." Strong signal.
- You don't know the answer at all: don't fake. "I haven't worked with Iceberg specifically; I've used Delta. Can I tell you how I'd approach it from first principles?"
- Blank on syntax: "In standard SQL, this is X. I'm forgetting the exact Snowflake function name — would you remind me?" Asking is fine. Stalling is not.
Questions to ask them
Pick 2–3 per round; tailor by the interviewer's role. Your questions are scored — they reveal what you care about.
For an engineering interviewer
- "What does your team's batch-vs-stream split look like? Are most pipelines moving toward Kappa, or do you still run dual-mode?"
- "How does the team handle schema evolution across producers and consumers — registry-driven, or convention?"
- "What's the most painful operational issue your team has dealt with in the last quarter, and how did the team grow from it?"
- "How is data quality scored at Netflix? Is there a centralized framework, or does each team build its own?"
For a manager / hiring manager
- "What does success look like in the first 90 days for someone in this role?"
- "How does the team balance platform work versus product-aligned analytics?"
- "How autonomous are individual engineers in choosing tools and patterns?"
- "What's the team's current biggest unsolved problem, and what's blocking progress?"
About culture (Netflix specifically)
- "How does the 'context, not control' principle play out day-to-day on your team?"
- "How do you handle the tension between high-performing individuals and team cohesion in practice?"
- "What kinds of mistakes are tolerated, and which aren't?"
One-page cheat sheet
Modeling — the five moves
| Move | What you say / do |
|---|---|
| 1. Clarify | 3–5 questions before drawing. Use cases, SLA, retention, key stability, time zone. |
| 2. Grain | One sentence per fact: "One row per X per Y per Z." Out loud. |
| 3. Conceptual → physical | Entities first. Then partitioning, clustering, SCD strategy. |
| 4. SCDs & time | Type 1 (overwrite), Type 2 (versioned rows), Type 6 (hybrid). Justify. |
| 5. Stress test | Volunteer 2 failure modes. Late data, GDPR, schema evolution, hot partitions. |
SQL — the seven patterns
| Pattern | Shape |
|---|---|
| Top-N per group | ROW_NUMBER() OVER (PARTITION BY g ORDER BY metric DESC) → WHERE rn ≤ N |
| Gaps & islands | LAG → flag boundary → cumulative SUM = session_id → GROUP BY |
| Funnel | MAX(CASE WHEN step='X' THEN 1 END) per user → ratios with NULLIF |
| Dedup | ROW_NUMBER PARTITION BY natural_key ORDER BY ingest_ts DESC = 1 |
| Cohort retention | cohort_week × activity_week LEFT JOIN, weeks_since = DATEDIFF |
| Running total | SUM() OVER (ORDER BY ... ROWS UNBOUNDED PRECEDING) |
| HLL / approx | APPROX_COUNT_DISTINCT — mention it for scale |
Python — the five reflexes
| Need | Reach for |
|---|---|
| Group + aggregate | collections.defaultdict(int / list / set) |
| Top-K | heapq.nlargest, or manual min-heap of size K |
| Sliding window | collections.deque + running totals dict |
| Sessionization | Sort within group, walk pairs, track open session |
| O(1) cache | collections.OrderedDict, move_to_end / popitem(last=False) |
Studio/Content vocabulary
| Term | Meaning |
|---|---|
| Slate | Planned set of releases for a period |
| Greenlight | Formal approval to begin production |
| Tentpole | High-budget flagship release |
| P&A | Prints & Advertising — marketing spend |
| Residuals | Ongoing payments to talent post-release |
| MFN clause | Most-favored-nation: my terms match the best granted to anyone else |
| Amortization | Spreading content cost over expected useful life |
| License window | Period during which a title is available in a territory |
The five days before the conversation.
~2.5 hours per day. Not negotiable: do the timed drills. The point isn't to find the answer — it's to get used to the clock running.
-
Day 1 · Foundation · Modeling (~2.5 hrs)
- 30 min · read — Re-read the five-move sequence (§02). Practice saying the moves out loud. Sticky note next to monitor.
- 60 min · drill — Modeling drills #1 and #2 (timed, 25 min each): Production lifecycle + Talent & royalties.
- 30 min · vocabulary — Studio/Content vocabulary. Look up any term you can't define crisply (P&A, MFN, residuals, slate, tentpole).
- 30 min · reflect — Did you ask clarifying questions before drawing? State grain explicitly? Volunteer 2 failure modes?
-
Day 2 · SQL · Window functions & gaps/islands (~2.5 hrs)
- 45 min · drill — SQL drills #1, #2, #3 (timed, 15 min each). Solve at SQL playground. Don't read solutions — type them.
- 30 min · pattern reps — Type the gaps-and-islands template (LAG → flag → cumulative SUM) three times from memory.
- 45 min · drill — SQL drills #4 and #5 (timed, 20 min each). Multi-CTE, finance-flavored. Talk out loud while solving.
- 30 min · read — Patterns 1, 2, 6. Note the QUALIFY shortcut (Snowflake).
-
Day 3 · Python · CodeSignal pace (~2.5 hrs)
- 30 min · warm-up — Type from memory: (a) sessionization, (b) top-K with heap, (c) sliding-window aggregator.
- 75 min · drill — Python drills #1, #2, #3 (timed, 25 min each). Fresh editor — closer to CodeSignal feel. Narrate while coding.
- 30 min · solve again, optimize — Take the slowest of three; rewrite for better complexity. Practice the verbal arc: "First pass O(n²); I see I can use a hash map for O(n)…"
- 15 min · reflect — Where did the clock catch you? Off-by-ones? Edge cases?
defaultdict(int)recall?
-
Day 4 · Streaming · Deep-dive prep (~2.5 hrs)
- 45 min · drill — Streaming Q&A (§05). Answer each on a 90-sec timer. Then read the model answer.
- 45 min · mock — Trace a play event from device to dashboard. 8 stages, 30 sec each. Repeat 3 times until it flows.
- 30 min · resume rehearsal — Pick three projects. Write 90-sec STAR for each. Rehearse out loud.
- 30 min · catch-up — Re-do any drill from days 1–3 you didn't finish.
-
Day 5 · Integration · Calm (~2 hrs)
- 30 min · one-page review — Read the cheat sheet twice. Don't drill new material. Trust the reps.
- 30 min · one mock end-to-end — Pick one modeling + one SQL + one Python. 15 min each. Light pace.
- 30 min · logistics — Test camera, mic, screen-share. Clear desktop. Water. Recruiter's number open.
- 30 min · the 3 questions — Pick your 3 questions per interviewer. Write on paper. Different for engineer vs manager.
- Then · stop. No drills the night before. Walk, eat, sleep. The reps are already in.
Interview prep notebook · built for the conversation, not the quiz · go well.