Q3 used a fixed mean and standard deviation over the entire period as the z-score baseline. But when a series has a trend or a level shift, a fixed baseline quickly becomes stale. In practice, a moving z-score (rolling z) based on only the "most recent N days" is standard. The two keys are to exclude the current day from the baseline (do not evaluate a value against itself—avoid leakage) and to defer classification until enough baseline data has accumulated (min_periods).
AVG(amount) OVER (ORDER BY dt ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING) -- "3 PRECEDING AND 1 PRECEDING" = use only the prior three days, excluding today -- With "... AND CURRENT ROW", today (the value being evaluated) would enter the baseline
1 PRECEDING instead of CURRENT ROW keeps today's value out of the baseline calculation. This prevents an anomalous value from inflating its own baseline mean and standard deviation and making itself look normal (an application of Q1's frame specification).From sensor_readings, calculate the moving average roll_avg and moving standard deviation roll_std over the prior three days (excluding today), then calculate z =(amount − roll_avg)/roll_std. Rows with fewer than three usable baseline days, n_base<3, must be held as 'warmup'; otherwise, classify |z|≥2 as 'anomaly' and smaller values as 'normal'. Return dt, amount, roll_avg, roll_std, z_score, status in ascending dt order. Round the average, standard deviation, and z-score to two decimal places.
| dt | amount |
|---|---|
| 2024-01-01 | 100 |
| 2024-01-02 | 96 |
| 2024-01-03 | 104 |
| 2024-01-04 | 100 |
| 2024-01-05 | 96 |
| 2024-01-06 | 200 |
| 2024-01-07 | 100 |
| 2024-01-08 | 96 |
| 2024-01-09 | 104 |
※ The normal pattern cycles through 96/100/104. 01-06 (200) is the only spike. Its prior three days are always {96,100,104}, giving a stable mean of 100 and SD of 4, so 200 produces the obvious z=25.
Expected output (ascending dt):
| dt | amount | roll_avg | roll_std | z_score | status |
|---|---|---|---|---|---|
| 2024-01-01 | 100 | NULL | NULL | NULL | warmup |
| 2024-01-02 | 96 | 100.00 | NULL | NULL | warmup |
| 2024-01-03 | 104 | 98.00 | 2.83 | 2.12 | warmup |
| 2024-01-04 | 100 | 100.00 | 4.00 | 0.00 | normal |
| 2024-01-05 | 96 | 100.00 | 4.00 | -1.00 | normal |
| 2024-01-06 | 200 | 100.00 | 4.00 | 25.00 | anomaly |
| 2024-01-07 | 100 | 132.00 | 58.92 | -0.54 | normal |
| 2024-01-08 | 96 | 132.00 | 58.92 | -0.61 | normal |
| 2024-01-09 | 104 | 132.00 | 58.92 | -0.48 | normal |
01-03 has |z|=2.12, but its baseline contains only two days (n_base<3), so it is held as warmup to suppress a false positive. From 01-06 onward, note that the baseline SD has expanded to 58.92 because the spike contaminates later baselines.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
A one-off anomaly (point anomaly) means something different from a persistent incident where the anomaly continues for several days. The latter is the issue that usually deserves a response. Gaps & Islands is the classic technique for grouping consecutive rows into one island. Its core property is that the difference from a sequence number stays constant only while rows are consecutive.
-- Sort anomalous days by date and assign a row number... dt: 01-05 01-06 01-07 01-10 01-11 rn: 1 2 3 4 5 dt-rn: 01-04 01-04 01-04 01-06 01-06 ← the value is constant within each consecutive run
From service_health (daily error rates), define error_rate > 5 as an anomalous day and extract only runs of at least three consecutive anomalous days (persistent incidents). For each run, calculate start_dt, end_dt, consecutive-day count run_days, and the maximum error rate peak_error. Return rows ordered by start_dt ascending.
| dt | error_rate |
|---|---|
| 2024-01-01 | 2 |
| 2024-01-02 | 8 |
| 2024-01-03 | 3 |
| 2024-01-04 | 2 |
| 2024-01-05 | 9 |
| 2024-01-06 | 7 |
| 2024-01-07 | 6 |
| 2024-01-08 | 1 |
| 2024-01-09 | 2 |
| 2024-01-10 | 10 |
| 2024-01-11 | 12 |
| 2024-01-12 | 3 |
※ Anomalous days (error_rate>5) are 01-02 / 01-05–07 / 01-10–11. There are three islands: one isolated day, one three-day run, and one two-day run. Filtering for "at least three days" leaves only 01-05–07.
Expected output (ascending start_dt):
| start_dt | end_dt | run_days | peak_error |
|---|---|---|---|
| 2024-01-05 | 2024-01-07 | 3 | 9 |
The isolated 01-02 and the two-day run 01-10–11 are removed by HAVING COUNT(*) >= 3. Only the three-day run 01-05–07 remains as a persistent incident.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
Many metrics have weekly seasonality. Weekdays may be low while weekends are high, for example. Monitoring with a "previous-day comparison" turns normal weekly changes into false alerts every weekend and Monday. The fix is simple: compare with "the same weekday one week ago" instead of yesterday. LAG(amount, 7) retrieves the value seven rows earlier.
LAG(amount, 7) OVER (ORDER BY dt) -- Seven days earlier (same weekday last week) LAG(amount) OVER (ORDER BY dt) -- If omitted, the second argument defaults to 1 (yesterday) -- Change the offset to switch the comparison baseline between yesterday and the same weekday last week
LAG(col, n) returns the value n rows before the current row (n defaults to 1). With daily data ordered by date, seven rows back is the same weekday last week. The first seven days have no comparison value and become NULL, so exclude them from classification as no_baseline.From daily_traffic (14 days with high weekend values and weekly seasonality), compare each day's amount with the value seven days earlier and calculate the week-over-week change rate wow_pct =(amount − base_7d)/base_7d×100. Assign 'no_baseline' to the first seven days without a seven-day baseline, 'anomaly' when |wow_pct| exceeds 50%, and 'normal' otherwise. Return dt, amount, base_7d, wow_pct, status in ascending dt order. Round pct to one decimal place.
| dt | amount |
|---|---|
| 2024-01-01 | 100 |
| 2024-01-02 | 110 |
| 2024-01-03 | 120 |
| 2024-01-04 | 130 |
| 2024-01-05 | 140 |
| 2024-01-06 | 300 |
| 2024-01-07 | 310 |
| 2024-01-08 | 105 |
| 2024-01-09 | 115 |
| 2024-01-10 | 125 |
| 2024-01-11 | 135 |
| 2024-01-12 | 500 |
| 2024-01-13 | 305 |
| 2024-01-14 | 315 |
※ Strong weekly cycle: weekdays 100–140 on 01-01 (Mon)–05 (Fri), weekends around 300 on 01-06 (Sat)–07 (Sun). A previous-day comparison would falsely alert on every weekend (+114%) and Monday (−66%). 01-12 (Fri) is a true anomaly: 500 versus 140 on the same weekday last week (+257%).
Expected output (ascending dt):
| dt | amount | base_7d | wow_pct | status |
|---|---|---|---|---|
| 2024-01-01 | 100 | NULL | NULL | no_baseline |
| 2024-01-02 | 110 | NULL | NULL | no_baseline |
| 2024-01-03 | 120 | NULL | NULL | no_baseline |
| 2024-01-04 | 130 | NULL | NULL | no_baseline |
| 2024-01-05 | 140 | NULL | NULL | no_baseline |
| 2024-01-06 | 300 | NULL | NULL | no_baseline |
| 2024-01-07 | 310 | NULL | NULL | no_baseline |
| 2024-01-08 | 105 | 100 | 5.0 | normal |
| 2024-01-09 | 115 | 110 | 4.5 | normal |
| 2024-01-10 | 125 | 120 | 4.2 | normal |
| 2024-01-11 | 135 | 130 | 3.8 | normal |
| 2024-01-12 | 500 | 140 | 257.1 | anomaly |
| 2024-01-13 | 305 | 300 | 1.7 | normal |
| 2024-01-14 | 315 | 310 | 1.6 | normal |
The weekend (01-13: 305 vs. 300 the previous week) is only +1.7% week over week and is normal. Aligning the seasonal cycle removes the false alerts and isolates the genuine 01-12 spike.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
Operational alerts need more than a binary "anomaly or not" flag. A common approach is to combine multiple signals and rank events by severity. This question combines two independent perspectives—level (is the value high?) and jump (did it change sharply from the previous day?)—to produce four levels. The keys are the order of CASE branches and using boolean values directly as conditions.
CASE WHEN lvl_hi AND jump_hi THEN 'CRITICAL' -- High level and sharp jump (classify first) WHEN lvl_hi THEN 'WARNING' -- High level but gradual WHEN jump_hi THEN 'WATCH' -- Sharp jump but low level ELSE 'OK' END
From metric_stream, calculate two signals: level signal lvl_hi =(amount > 200) and sudden-change signal jump_hi =(amount − previous amount ≥ 100). Combine them with CASE and assign severity as both true → 'CRITICAL' / level only → 'WARNING' / jump only → 'WATCH' / neither → 'OK'. Return dt, amount, delta, severity in ascending dt order, where delta = amount − previous amount.
| dt | amount |
|---|---|
| 2024-01-01 | 100 |
| 2024-01-02 | 105 |
| 2024-01-03 | 215 |
| 2024-01-04 | 220 |
| 2024-01-05 | 120 |
| 2024-01-06 | 235 |
| 2024-01-07 | 90 |
| 2024-01-08 | 195 |
※ Level means above 200; a jump means at least +100 from the previous day. 01-03 (215,+110) and 01-06 (235,+115) satisfy both and are CRITICAL. 01-04 (220,+5) is high but gradual → WARNING. 01-08 (195,+105) jumps while remaining below 200 → WATCH.
Expected output (ascending dt):
| dt | amount | delta | severity |
|---|---|---|---|
| 2024-01-01 | 100 | NULL | OK |
| 2024-01-02 | 105 | +5 | OK |
| 2024-01-03 | 215 | +110 | CRITICAL |
| 2024-01-04 | 220 | +5 | WARNING |
| 2024-01-05 | 120 | -100 | OK |
| 2024-01-06 | 235 | +115 | CRITICAL |
| 2024-01-07 | 90 | -145 | OK |
| 2024-01-08 | 195 | +105 | WATCH |
Among high-level rows, 01-03 and 01-06 are CRITICAL because they also jump, while gradual 01-04 is WARNING. 01-08 is below 200 but jumps sharply, so it is classified as WATCH as an early signal.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
The advanced section closes by building an "alert summary" for operations. Extend Q10 from the fundamentals with COUNT(*) FILTER columns for multiple severity levels, count each severity at once, and use GROUP BY ROLLUP to place category detail rows and one grand-total row in the same result. One query produces a dashboard-ready table.
COUNT(*) FILTER (WHERE amount >= 300) -- Critical count COUNT(*) FILTER (WHERE amount >= 200 AND amount < 300) -- Warning count GROUP BY ROLLUP (category) -- Category rows + one grand-total row (category=NULL)
GROUP BY ROLLUP (category) adds one grand-total row to the ordinary category groups. That row has category=NULL, so label it with COALESCE(category,'(ALL)') and control ordering with GROUPING(category).From event_log, define amount>=300 as critical and 200≤amount<300 as warning. Aggregate total events, critical count, warning count, and critical rate % by category. Include a grand-total row for all categories with ROLLUP, label that row (ALL), and return category, total_events, critical, warning, crit_pct with category rows ascending and the grand total last. Round pct to one decimal place.
| category | dt | amount |
|---|---|---|
| api | 2024-01-01 | 350 |
| api | 2024-01-02 | 120 |
| api | 2024-01-03 | 130 |
| api | 2024-01-04 | 95 |
| auth | 2024-01-01 | 260 |
| auth | 2024-01-02 | 280 |
| auth | 2024-01-03 | 240 |
| auth | 2024-01-04 | 90 |
| web | 2024-01-01 | 100 |
| web | 2024-01-02 | 250 |
| web | 2024-01-03 | 300 |
| web | 2024-01-04 | 320 |
※ Red=critical(≥300); orange=warning(200–299). api: crit1/warn0, auth: crit0/warn3, web: crit2/warn1. Overall: crit3, warn4.
Expected output (ascending category, grand total last):
| category | total_events | critical | warning | crit_pct |
|---|---|---|---|---|
| api | 4 | 1 | 0 | 25.0 |
| auth | 4 | 0 | 3 | 0.0 |
| web | 4 | 2 | 1 | 50.0 |
| (ALL) | 12 | 3 | 4 | 25.0 |
The final (ALL) row is the ROLLUP grand total. web is most severe by critical rate at 50%, while auth has three warnings. Detail and overall scope are visible in one table.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free