learn-interview-prep-with-phoebe / Session 4 of 6
Learn Interview Prep with Phoebe · Session 4 of 6

The practice-round bench

Two rounds decide most data interviews before anyone talks about culture: the technical round, where your query either returns the right rows or does not, and the behavioural round, where you either have a story for the question or improvise one. Tonight both are practised against something that keeps score. The SQL half runs six interview-style questions in a real SQLite engine inside this page, on 2,085 real orders, and checks your result set against the reference the way a strict interviewer would. The story half crosses Ren's constructed story bank with a job description and computes how much of the job your stories actually cover. Both halves can report a worse number, and both show you exactly where.

🔴 Bench night Bring your own story list SQL typed live in the page 45 min
0-3 · Welcome 3-10 · How the checker reads you 10-38 · The SQL round, then the story bank 38-45 · Read the numbers, Q&A
Part 0

Where we start

Ren, the constructed analyst moving into a data science role, has a technical screen on Thursday and a behavioural loop the week after. Sessions 1 to 3 built the parts: a job description read into competencies, a bank of eight stories, and a habit of reading a SQL question for its traps before typing. Tonight Ren practises against a checker instead of a mirror. The SQL data is real: a seeded sample of the public Olist e-commerce dataset, 2,000 people and every order they placed between October 2016 and September 2018, with ids renumbered and no names or text. The story bank and both job descriptions are constructed, and the widget says so on every number.

🎫 Clear all three quiz questions to stamp your passport real = computed from the Olist rows in this page constructed = Ren's stories and fits, labelled

★ What you walk out with tonight. Six SQL questions attempted against a strict result-set checker, the five planted traps seen failing with the first wrong row printed, a coverage matrix for your own story bank, the honest size of what tagging and trimming buy, why one great story stretched across every question is the most expensive habit in a behavioural loop, and what happens to a bank when the job description changes.

Part 1 · covers how the bench checks an answer

A result set is right or it is not

7 min live

An interviewer reading your query on a shared screen is checking one thing first: does it return the rows the question asked for. The bench does the same, mechanically. It runs a reference query, runs yours, and compares the two result sets value by value. It does not care how you wrote the query, what you named the columns, or whether you used a join or a subquery. It cares about the rows.

WHAT THE CHECKER DOES WITH YOUR QUERY, IN ORDER 1 · TWO RESULT SETS 2 · SHAPE 3 · ROW ORDER 4 · ROW BY ROW reference yours Same number of columns? no: fail, and say how many each has Any row order: sort both sides Order asked for: keep as returned Walk the rows together stop at the first row that differs and print both sides of it, plus the two row counts Q2 trap, run in this page: expected [802], got [850] at row 1. The join added 48 rows, one per extra item. Values are compared to six decimals. Column names are ignored; column order is not.
🔍 Click to zoom - the checker never reads your SQL, only its output, which is exactly what a result-focused interviewer does first
LiveWhy the checker is strict, and what it ignores3 min▶

Strict on rows. One extra row, one missing row, or one average off in the second decimal is a fail. That is deliberate. A screen that asks "how many orders" has one right number, and an answer that is close is the answer of someone who did not check. Interviewers who run your query see the same thing.

Relaxed on style. The checker ignores column names and accepts any query that produces the reference rows: a join with COUNT(DISTINCT ...), an EXISTS subquery, or a common table expression all pass the same question. Row order is ignored unless the question asks for an order, and then it is checked. Column order is checked, because the question names the columns in order.

The first differing row. When you fail, the bench prints the first row where your result and the reference part company, with both sides sorted the same way. That one row is usually enough to find the bug: a count that is too high points at a join, a share that is too low points at NULLs, a missing seller points at a filter in the wrong place.

Self-studyThe data, and what it can and cannot tell you3 min▶

The Brazilian E-Commerce Public Dataset by Olist is published on Kaggle under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 licence. The sample here draws 2,000 people with a fixed seed and keeps every order they placed: 2,085 orders, 2,359 order items from 775 sellers. Ids are renumbered; there are no names, reviews or text. Drawing people rather than orders keeps repeat customers repeat, which question 6 depends on.

The engine is SQLite compiled to WebAssembly (sql.js 1.10.3, which reports SQLite 3.45.2). The canon result for every question was produced twice, once by that engine and once by Python's own sqlite3 module running SQLite 3.51.1, and the two agree on all 356 values compared. Nothing you type leaves the page.

Real world

The answer that was almost right. A familiar pattern on live SQL screens: the candidate writes a clean join, reads out a number, and moves on. The interviewer asks one question, "how many rows does the join return before you count?", and the number turns out to be counted per item, not per order. The query was fluent and the answer was wrong. Checking the grain of a join before aggregating is the habit session 3 drilled, and question 2 is where it gets tested.

Build-along · the SQL round

Six questions, a real engine, one strict checker

★ 16 min · the main event

Work through the questions in order. For each one, read the question twice, write your query from the starter, and press Run and check before looking at anything else. When a question carries a planted trap, press Load the trap afterwards and read the first differing row out loud: say which line of the trap query produced it. Q1 is a warm-up with no trap. Q5 is the one where row order is checked.

Read it in this order. The verdict line first, then the row counts, then the first differing row. A fail that says "you returned 850, the reference returns 802" is already a diagnosis: something multiplied rows. Do not reach for the reference answer until you can say in one sentence why your result differs.

Part 2 · covers the planted traps, measured

Five traps, and how small the damage looks

6 min live

Each trap query below is one a fluent candidate could write in a hurry, and each runs without an error. The numbers are the ones the bench prints when you press Load the trap, and an independent Python run printed the same ones. Look at how close most of them are to the right answer. That closeness is why they survive a quick glance.

REFERENCE AGAINST TRAP, AS PRINTED BY THE BENCH Q2 join duplicates Q3 NULL in average Q4 WHERE vs HAVING Q5 date range Q6 wrong id 802 850 .943 .916 24 18 111 109 57 0 ref trap ref trap ref trap ref trap ref trap orders share on time sellers listed Dec 2017 orders repeat customers Each pair on its own scale from zero. Only Q6 looks wrong at a glance; the other four look plausible.
🔍 Click to zoom - four of the five traps produce an answer within shouting distance of the truth, which is why a number alone proves nothing
LiveJoin duplicates and NULLs: Q2 and Q32 min▶

Q2. Joining orders to order items returns one row per item. Of the 2,085 orders, 203 have more than one item, so any count taken after the join is a count of items unless you say COUNT(DISTINCT o.order_id). The trap counts 850; the right answer is 802 orders with at least one item priced above 100. Six percent too high, and fluent.

Q3. 59 orders in the sample never arrived, so their delivered_date is NULL. A comparison with NULL is neither true nor false; it is NULL, and CASE WHEN ... THEN 1 ELSE 0 sends it to the ELSE branch. The trap scores every undelivered order as late and reports 0.916 on time; restricted to delivered orders, as the question asks, the answer is 0.943. SQLite's own documentation says that avg() ignores NULL inputs, which is exactly why writing the CASE so that undelivered orders produce NULL, or filtering them out, gives the right denominator.

LiveFilter before or after grouping: Q42 min▶

The question asks for sellers whose average price over all their items is above 150, among sellers with at least five items. The trap filters WHERE price > 150 first. That throws away each seller's cheap items before the average is taken, so every surviving average is above 150 by construction, and "at least five items" becomes "at least five pricey items".

The reference lists 24 sellers; the trap lists 18. Only 14 appear in both: the trap misses 10 real answers, adds 4 sellers who do not qualify, and of the 14 it shares, 8 carry an inflated average. Seller 1286, for one, averages 239.72 over all items and 415.00 in the trap. WHERE filters rows before grouping; HAVING filters groups after. Say that sentence out loud in the interview while you write the query, and the interviewer hears that you know which one the question needs.

Self-studyThe last day of the year and the id that is not a person: Q5 and Q62 min▶

Q5. purchase_ts is a timestamp stored as text. BETWEEN '2017-01-01' AND '2017-12-31' compares strings, and '2017-12-31 09:14:00' sorts after '2017-12-31', so every order placed on 31 December falls outside the range. In this sample that is 2 orders: December reads 109 instead of 111. Eleven months are right and one is quietly short, which is the worst kind of wrong. A half-open range, >= '2017-01-01' AND < '2018-01-01', has no such edge.

Q6. In this dataset customer_id is issued with each order, so no customer_id ever has two orders, and the trap reports 0 repeat customers. The person is customer_unique_id: 57 people placed more than one order. The schema note says so in one line, which is the point. In an interview, asking "what does one row of this table represent, and what identifies a person?" before writing anything is the move that catches this, and it costs ten seconds.

Real world

Ren's Thursday screen, constructed. Ren passes Q1, Q2 and Q6 on the first run, fails Q4 with the WHERE version, reads the first differing row (seller 904 expected, seller 966 returned), and fixes it in under a minute by moving the condition to HAVING. Q5 passes because Ren wrote the half-open range out of habit. The point is not a perfect score. It is that every fail came with a row Ren could reason about out loud.

Part 3 · covers the story-bank coverage rules

How a story bank becomes a coverage number

5 min live

Session 2 built the bank; this part scores it. Every competency in the job description is a row, every story a column, and each cell holds how well that story evidences that competency: strong, thin, or not at all. The fits are Ren's constructed self-assessment. The arithmetic on top of them is real, and the Python reference run computed every number below independently of the page.

FROM CELLS TO ONE NUMBER: THE SCORING RULE, ON A SMALL BANK S1 S2 S6 BEST EVIDENCE SQL Experiments Metric choice Vague asks Pushing back covered covered thin missing covered coverage = (3 + 0.5 x 1) / 5 = 70% Three of Ren's stories, tagged, on five competencies. The bench scores all eight stories on eight.
🔍 Click to zoom - a row is only as strong as its best story, and an empty row costs a whole point no matter how good the other stories are
LiveThe rules, stated once3 min▶

Status. For each competency the best evidence in the bank decides: a strong story makes it covered, a thin one makes it thin, nothing makes it missing. Coverage is covered plus half of thin, divided by the number of competencies. Tagged or untagged. An untagged bank uses each story only for the competency it was written for; a tagged bank uses each story for every competency it can honestly evidence, which is the session 2 exercise. The story you tell is the shortest one at the best level. Fits in 2 minutes counts the competencies that have evidence and whose chosen story runs 120 seconds or less.

The two-minute line is a common coaching rule of thumb, not a research finding; no study behind a particular answer length was found, and the widget says so. It is in the bench because a behavioural loop has a clock, and a four-minute story eats the next question.

Self-studyWhat the matrix cannot know2 min▶

Every fit in the matrix is a judgement, and here it is Ren's own. A candidate's self-assessment is generous in predictable places: a story you are proud of feels strong for more competencies than it really evidences. The habit worth building is to mark a fit strong only when the story contains a specific action you took and a result you can state, the situation, task, action and result shape session 2 used. The bench's arithmetic is only as honest as those marks.

Build-along · the story bank

Eight stories against eight competencies

★ 12 min · climb the ladder

The bench opens on the top rung, eight stories tagged and trimmed, against job description A, a constructed product analytics role. Start instead at the first rung and climb: three stories, five, eight tagged, eight trimmed. At each rung say what changed in the matrix. Then press the anti-lever and read the two coverage numbers side by side. Last, press the break button and swap the job description.

HONEST COVERAGE OF JOB DESCRIPTION A, RUNG BY RUNG IN 2 MIN 3 stories, untagged headline use only 5 stories, untagged headline use only 8 stories, tagged every honest fit used 8 tagged, trimmed each cut to 115 s Stretch one story anti-lever Swap to JD B break button 37.5% 62.5% 93.8% 93.8% honest 31.3% claimed 100% 62.5% 0 of 8 0 of 8 0 of 8 8 of 8 1 story x 8 6 of 8 Stories, fits and lengths are constructed; every percentage is computed by the bench and the Python reference.
🔍 Click to zoom - tagging buys the coverage, trimming buys the clock, and the stretched story buys nothing but the feeling of being ready
LiveWhat each rung buys2 min▶

3 to 5 stories, untagged: 37.5 to 62.5 percent. Each new story adds the one competency it was written for. 8 stories, tagged: 93.8 percent, 7 covered and 1 thin, told with 6 distinct stories. Tagging is the big jump, and it costs no new stories: it is the work of asking, for every story you already have, which other questions it can honestly answer. The one thin row, turning a vague ask into a question, is the gap the bank cannot fill by rearranging. The fix is a new story, not a cleverer tag.

Trimmed: coverage does not move, and 0 of 8 answers fitting in two minutes becomes 8 of 8. Trimming changes nothing about what you can evidence and everything about whether you get to finish saying it.

LiveThe anti-lever: one great story, stretched2 min▶

Ren's best story is the A/B test that was stopped early: a clear conflict, a statistical point, a good ending. The temptation is to tell it for every question. The model: the same story answers all eight competencies. Claimed coverage reads 100 percent, because every question got an answer. Honest coverage is 31.3 percent: the story is strong evidence for experiments and for pushing back, thin for metric choice, and evidence of nothing for the other five. Distinct stories told: 1. Most-reused story: 8 of 8.

The honest nuance: a strong story told twice in a loop is fine, and the matrix itself picked one story for two rows on the top rung. The cost of stretching is not the repetition. It is that five competencies were asked about and none was evidenced, and a structured interviewer scoring each question against its own rubric scores those five low, however good the story was.

Self-studyThe break button: a different job2 min▶

A bench that can only go up is measuring nothing. The break button keeps Ren's bank exactly as it is and swaps the job description for B, a constructed machine learning engineer role. Coverage falls from 93.8 to 62.5 percent: 4 covered, 2 thin, and 2 missing, ML system design and deploying models, competencies none of an analyst's stories evidence. Stretching the A/B test story across job description B scores 0 percent honest coverage, because it fits none of the eight.

The lesson is the session 2 habit in reverse: a bank is built for a job description, and a new job description means a new pass of tagging and, usually, one or two new stories. Reusing last month's prep for a different role is the break button pressed by accident.

Do it for yourself tonight. Write your own competencies down the side of a page and your stories across the top, mark each cell strong, thin or blank, and apply the rule. Your numbers will differ. The row you leave blank is the story you write this week.

Homework

Before session 5

30 min
  1. Finish the six. Pass every SQL question with a query you wrote. For the three you found hardest, write one sentence each on what the first differing row told you.
  2. Write a second correct answer to Q2. If you used COUNT(DISTINCT ...), write it with EXISTS, and confirm the checker passes both.
  3. Score your own bank. Your competencies against your stories, by the rules in Part 3. Note your coverage and your thinnest row.
  4. Time your two longest stories out loud. If either runs past two minutes, cut it and time it again.
Check yourself

Three questions

Clear all three and session 4 gets stamped in your passport.

1 · Your Q2 query returns 850 and the checker says the reference returns 802. What is the most likely bug?

A count that is too high after a join points at the join's grain. 203 of the 2,085 orders have more than one item, and the trap's 48 extra rows come from them. COUNT(DISTINCT o.order_id) or an EXISTS subquery fixes it. The checker ignores column names.

2 · Stretching one great story across all eight competencies gives claimed coverage 100 percent and honest coverage 31.3 percent. What is the honest way to read this?

The story is strong evidence for two competencies and thin for one, which is why it is the stretch candidate. The top rung itself reuses a story for two rows. The numbers come from constructed fits and real arithmetic, not from interview outcomes.

3 · Why does the bench include a break button that swaps the job description?

The break button proves the score responds to the thing it claims to measure. The two missing rows under job description B, ML system design and deploying models, are the ones an analyst's stories cannot evidence: that is a prep gap, not a verdict on the candidate.

Sources covered

What this session teaches, and where it came from

✓Every SQL result on the benchComputed in the browser by SQLite 3.45.2 (sql.js 1.10.3) on a seeded sample of the Brazilian E-Commerce Public Dataset by Olist (Kaggle, CC BY-NC-SA 4.0), derived and renumbered; verified against Python sqlite3 (SQLite 3.51.1), 356 values identical
✓SQLite documentation: aggregate functions, NULL handling, SELECTRead at source on sqlite.org: avg() ignores NULL inputs; WHERE filters rows before grouping and HAVING filters groups after
◐Story-bank coverageRen, both job descriptions, the eight stories, every fit and every length are constructed; the scoring rule is stated on the widget and its arithmetic verified against an independent Python run
◐The two-minute answerA coaching rule of thumb; no study supporting a particular answer length was found, and it is used here as a stated threshold, not a finding

Every fact on this page, and its verification tier, is recorded in the course's source map.

Session 4 cheat sheet · pin this

Rows, not styleA result set is right or wrong. Check the grain before you aggregate.
Read the first wrong rowToo many rows: a join. Too low a share: NULLs. Missing groups: a filter in the wrong place.
WHERE then HAVINGWHERE filters rows before grouping; HAVING filters groups after.
Half-open dates>= the first day, < the day after the last. BETWEEN loses the last day of timestamps.
Tag, then trimTagging took Ren from 62.5 to 93.8 percent coverage; trimming took answers in time from 0 to 8 of 8.
Do not stretch one storyClaimed 100 percent, honest 31.3. Every competency needs its own evidence.