41. NOT IN with a NULL in the list
What does this query return?
-- customers: id 1, 2, 3
-- banned: customer_id 2, NULL
SELECT id FROM customers
WHERE id NOT IN (SELECT customer_id FROM banned);
- A.1 and 3
- B.1, 2 and 3
- C.No rows
- D.An error, because the subquery contains NULL
Show answer
Answer: C. No rows
id NOT IN (2, NULL) means id <> 2 AND id <> NULL. Any comparison with NULL is unknown, so the condition is never true and no rows come back. Use NOT EXISTS with a correlated subquery, or filter out NULLs in the subquery, to get 1 and 3.
42. Better offline metric, worse A/B test
A new ranking model improves offline NDCG by 4% on logged click data, but the online A/B test shows lower engagement. Which explanation is most likely?
- A.The A/B test must have a bug, because offline metrics are always right
- B.NDCG cannot be computed on click data
- C.Logged data is biased by the old model (position and selection bias), so offline gains do not reflect real user behaviour
- D.The new model is too accurate for users
Show answer
Answer: C. Logged data is biased by the old model (position and selection bias), so offline gains do not reflect real user behaviour
Logged clicks only exist for items the old model showed, mostly in top positions. A new model is judged on this biased data, so offline gains can be misleading. Counterfactual evaluation (inverse propensity weighting), exploration traffic and trusting well-run online tests help close this gap.
43. PERCENTILE_CONT vs PERCENTILE_DISC
The values are 1, 2, 3 and 4. What do these two expressions return?
SELECT
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x),
PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY x)
FROM v;
- A.2 and 2
- B.2.5 and 2.5
- C.2 and 2.5
- D.2.5 and 2
Show answer
Answer: D. 2.5 and 2
PERCENTILE_CONT interpolates between values, so the median of 1, 2, 3, 4 is 2.5. PERCENTILE_DISC returns an actual value from the data: the first value whose cumulative share reaches 0.5, which is 2. Pick DISC when the answer must be a real observed value, such as a real price point.
44. Adding up daily active users
A manager adds the 30 daily active user counts for June and calls it June monthly active users. What is wrong?
- A.Nothing, it is the standard definition
- B.June has 30 days, so the sum should be divided by 31
- C.A user active on many days is counted many times; MAU must count distinct users across the whole month
- D.DAU should be averaged instead of summed to get MAU
Show answer
Answer: C. A user active on many days is counted many times; MAU must count distinct users across the whole month
Distinct counts are not additive: someone active on 20 days appears in 20 daily counts. MAU needs COUNT(DISTINCT user_id) over the month, which is always less than or equal to the sum of DAUs. The same trap applies to rolling up distinct customers from regions or weeks.
45. Financial year from a date
An Indian company's financial year runs April to March and is named by its ending year (FY2027 = Apr 2026 to Mar 2027). What does this return for 10-May-2026?
- A.2026
- B.2025
- C.2027
- D.#VALUE!
Show answer
Answer: C. 2027
MONTH(A2)>=4 is TRUE for May, and TRUE counts as 1 in arithmetic, so 2026 + 1 = 2027. For 15-Feb-2026 it would return 2026, since February is still in FY2026. The same trick works for any year that starts in a set month.
46. Monthly total from a date column
Dates are in A2:A5000 and amounts in C2:C5000. F2 holds 1-Mar-2026. Which formula totals March 2026 correctly?
- A.=SUMIFS(C:C,A:A,">="&F2,A:A,"<"&EDATE(F2,1))
- B.=SUMIFS(C:C,A:A,">="&F2,A:A,"<="&EOMONTH(F2,1))
- C.=SUMIFS(C:C,A:A,">="&F2,A:A,"<="&F2+31)
- D.=SUMIF(A:A,MONTH(F2),C:C)
Show answer
Answer: A. =SUMIFS(C:C,A:A,">="&F2,A:A,"<"&EDATE(F2,1))
Use a half-open range: on or after the 1st, and before the 1st of the next month, which EDATE gives. F2+31 with <= also picks up 1 April, EOMONTH(F2,1) runs to the end of April, and MONTH() returns 3, which never matches a date serial. This works even when dates carry times.
47. 7-day average over missing days
daily_sales has one row per day with sales, but days with no sales have no row. What is wrong with calling this a 7-day moving average?
- A.ROWS counts 7 rows, which can span more than 7 days
- B.AVG cannot be used as a window function
- C.ROWS BETWEEN only works with PARTITION BY
- D.The frame looks 6 rows ahead of the current row, not behind
Show answer
Answer: A. ROWS counts 7 rows, which can span more than 7 days
A ROWS frame counts physical rows, so when days are missing the 7 rows can cover 10 or more calendar days. Fill the gaps first by joining to a full calendar table with 0 sales, or use RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW where supported. Then decide whether missing days should count as zero.
48. Month-1 retention for a cohort
In January, 400 users signed up. In February, 520 users were active: 140 from the January cohort and 380 who signed up in February. What is the January cohort's month-1 retention?
- A.27%
- B.35%
- C.130%
- D.73%
Show answer
Answer: B. 35%
Cohort retention divides users from that cohort still active by the cohort size: 140 ÷ 400 = 35%. New February sign-ups do not belong to the January cohort. 27% (140 ÷ 520) uses the wrong denominator, a common mistake in SQL when the join is not restricted to the cohort.
49. Streaks broken by repeat logins
A login streak query uses login_date - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date). Users who log in twice on some days get wrong streaks. What fixes it?
- A.Reduce to one row per user per date before numbering
- B.Use RANK instead of ROW_NUMBER for the numbering
- C.Order by the login timestamp instead of the date
- D.Add login_date to the PARTITION BY of the window
Show answer
Answer: A. Reduce to one row per user per date before numbering
With two rows on one date, ROW_NUMBER still increases while the date does not, so the date minus row number shifts and the streak splits. Selecting DISTINCT user_id and login_date first makes one row per day. DENSE_RANK on the date also works, but RANK does not because it skips after ties.
50. Runs of the same machine status
Statuses by minute are UP, UP, DOWN, DOWN, UP. You compute ROW_NUMBER() OVER (ORDER BY ts) - ROW_NUMBER() OVER (PARTITION BY status ORDER BY ts). What does grouping by status and this difference give?
- A.Two groups: all UP rows and all DOWN rows
- B.Five groups, one per row
- C.Three runs: UP (2 rows), DOWN (2 rows), UP (1 row)
- D.Four groups, because the last UP is separate twice
Show answer
Answer: C. Three runs: UP (2 rows), DOWN (2 rows), UP (1 row)
The overall numbers are 1 to 5. Within UP the numbers are 1, 2, 3 and within DOWN 1, 2. Differences are UP 0, 0, DOWN 2, 2, last UP 5 - 3 = 2. Together with status, (UP,0), (DOWN,2) and (UP,2) are three runs of consecutive identical status.
51. PRECEDING with a descending order
What does this frame cover for the row dated 10 March, assuming one row per day?
SUM(sales) OVER (ORDER BY sale_date DESC
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)- A.10, 11 and 12 March
- B.8, 9 and 10 March
- C.9, 10 and 11 March
- D.Every date up to 10 March
Show answer
Answer: A. 10, 11 and 12 March
PRECEDING means earlier in the window's own order, not earlier in time. With DESC, the rows before 10 March in that order are 12 and 11 March. Mixing a DESC window with a "last 3 days" label is a common source of wrong numbers.
52. Users with a 3-day streak
After building islands with date - ROW_NUMBER() (one row per user per day), how do you find users who were active at least 3 days in a row?
- A.GROUP BY user and island key, keep COUNT(*) >= 3
- B.Keep rows where ROW_NUMBER() per user is >= 3
- C.Keep users with COUNT(DISTINCT date) >= 3
- D.Keep rows where LAG(date, 3) per user is not NULL
Show answer
Answer: A. GROUP BY user and island key, keep COUNT(*) >= 3
Each island key marks one run of consecutive days, so counting rows per island gives its length. ROW_NUMBER >= 3 and LAG(date, 3) only check that a user has at least three or four active days in total, not in a row. The same goes for COUNT(DISTINCT date).
53. One partition takes all the time
In Spark or BigQuery, a window partitioned by merchant_id is slow. One merchant has 40% of all rows. What is happening?
- A.Window functions always run on a single machine
- B.The big merchant’s rows are processed twice by mistake
- C.One worker handles that whole partition; others sit idle
- D.Partitioning by an id column disables the sort step
Show answer
Answer: C. One worker handles that whole partition; others sit idle
All rows of a window partition must be processed together on one worker. A skewed key gives that worker far more data, so the whole job waits on it and may spill to disk or fail. Options include partitioning on a finer key where the logic allows, or handling the heavy merchant separately.
54. What EXCLUDE TIES removes
In PostgreSQL, a frame ends with EXCLUDE TIES. Which rows does that take out of the frame?
- A.The current row together with all of its peers
- B.Other peers of the current row, keeping the row itself
- C.Only the current row, keeping all of its peers
- D.All rows with NULL in the ORDER BY column
Show answer
Answer: B. Other peers of the current row, keeping the row itself
Peers are rows with the same ORDER BY value. EXCLUDE TIES removes the other peers while keeping the current row. EXCLUDE GROUP removes the current row and its peers, and EXCLUDE CURRENT ROW removes only the current row.
55. Running total with tied dates
sales has (d, amt): (01-01, 10), (01-02, 20), (01-02, 30), (01-03, 40). What running total does the query show for the row (01-02, 20)?
SELECT d, amt, SUM(amt) OVER (ORDER BY d) AS running
FROM sales;
- A.30
- B.10
- C.60
- D.100
Show answer
Answer: C. 60
With ORDER BY and no frame clause, the default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. RANGE includes all peer rows with the same d, so both 01-02 rows get 10 + 20 + 30 = 60. Use ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, plus a unique tiebreaker, for a true row-by-row total.
56. Which day does an event belong to
Events are stored with UTC timestamps and partitioned by UTC date. The business in Dubai (UTC+4) reports by local date. An order at 2026-10-14 22:30 UTC falls on which local business date?
- A.2026-10-14
- B.2026-10-15
- C.2026-10-13
- D.It depends on the warehouse region
Show answer
Answer: B. 2026-10-15
22:30 UTC plus 4 hours is 02:30 on 2026-10-15 in Dubai. A daily local report must therefore read two UTC partitions and convert time zones before grouping. Storing timestamps in UTC is right, but date boundaries for reporting must be applied in the business time zone.
57. GST totals off by a few rupees
The warehouse sums GST computed and rounded on each invoice line. Finance computes GST on each invoice total, then rounds. Over 1 lakh invoices the totals differ by ₹1,240, with no missing rows. What is the likely cause and fix?
- A.Rounding at different levels; use billing's rule
- B.Missing invoices; reload the whole month of data
- C.Duplicate lines; add a DISTINCT on the amount
- D.Time zones; convert all dates to IST
Show answer
Answer: A. Rounding at different levels; use billing's rule
Rounding each line to paise and then adding gives slightly different totals from rounding once per invoice, and small gaps add up across many invoices. Find the rule the billing system uses for the legal invoice and apply the same one, then reconcile exactly rather than with a loose tolerance.
58. A filter that breaks a left join
This query should give Day 7 retention for all January signups, but it returns 100%. Why?
SELECT COUNT(DISTINCT s.user_id) AS cohort,
COUNT(DISTINCT a.user_id) AS retained
FROM signups s
LEFT JOIN activity a ON a.user_id = s.user_id
WHERE s.signup_month = '2026-01'
AND a.activity_date = s.signup_date + 7;- A.The WHERE test on a drops unmatched rows
- B.COUNT(DISTINCT) counts null values as users
- C.LEFT JOIN always returns 100% matches
- D.signup_month should be in GROUP BY
Show answer
Answer: A. The WHERE test on a drops unmatched rows
A condition on the right table in WHERE throws away rows where a is null, so non-returners vanish from the cohort count too. Move the date condition into the ON clause so the left join keeps everyone.
59. Retention above 100%?
A cohort retention query shows month-2 retention of 140% for a user cohort. Which bug is the most likely cause?
SELECT c.cohort_month, e.activity_month,
COUNT(*) * 1.0 / MAX(c.cohort_size) AS retention
FROM events e
JOIN cohorts c ON c.user_id = e.user_id
GROUP BY c.cohort_month, e.activity_month;
- A.GROUP BY must include user_id
- B.The join should be a RIGHT JOIN instead
- C.COUNT(*) counts events, not distinct users
- D.MAX should be replaced by MIN
Show answer
Answer: C. COUNT(*) counts events, not distinct users
Each user can have many events in a month, so COUNT(*) counts events and can exceed the cohort size. Use COUNT(DISTINCT e.user_id) so each retained user counts once; user retention for a fixed cohort can never exceed 100%.
60. YoY growth query mixes companies
A financials table holds several companies over several years. This query computes year-on-year revenue growth. What is the bug?
SELECT company, fy, revenue,
revenue * 1.0 / LAG(revenue) OVER (ORDER BY fy) - 1 AS yoy
FROM financials;- A.LAG needs ORDER BY revenue
- B.LAG has no PARTITION BY company
- C.The 1.0 causes integer division
- D.Window functions cannot use division
Show answer
Answer: B. LAG has no PARTITION BY company
Without PARTITION BY company, LAG takes the previous row across all companies ordered by year, so growth compares different firms. Use LAG(revenue) OVER (PARTITION BY company ORDER BY fy) so each company is compared only with its own prior year.