SQL Interview & Case Lab
Back to Business SQL Patterns

Recap — Flagging Outliers

Two flavours of in-SQL anomaly detection — z-scores for normal-ish data, IQR for skewed data.

16 minutesInteractive tutorial
🎯 Free Guest Mode: You are learning for free. Sign in to save your completion progress and quiz answers.

"Our signups dropped 30% yesterday — is that anomalous?" The PM is asking you to make a yes/no call from a single number. SQL has all you need.

Pattern 1 — Z-score anomalies (normal-ish data)

Z-score = how many standard deviations from the mean. |Z| > 2 is the conventional flag.

WITH stats AS (
    SELECT
        AVG(daily_signups) AS mean_signups,
        STDDEV(daily_signups) AS std_signups
    FROM daily_signups
    WHERE signup_date >= CURRENT_DATE - INTERVAL '90 days'
      AND signup_date <  CURRENT_DATE
)
SELECT
    d.signup_date,
    d.daily_signups,
    (d.daily_signups - s.mean_signups) / s.std_signups AS z_score,
    CASE
        WHEN ABS((d.daily_signups - s.mean_signups) / s.std_signups) > 2 THEN 'ANOMALY'
        ELSE 'normal'
    END AS flag
FROM daily_signups d, stats s
WHERE d.signup_date >= CURRENT_DATE - INTERVAL '30 days';

Works when signups are roughly normally distributed. Less reliable for skewed metrics.

Pattern 2 — IQR anomalies (skewed data)

For skewed metrics (revenue, latency, LTV), IQR is the more honest tool.

WITH bounds AS (
    SELECT
        PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY daily_signups) AS q1,
        PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY daily_signups) AS q3
    FROM daily_signups
    WHERE signup_date >= CURRENT_DATE - INTERVAL '90 days'
),
fences AS (
    SELECT q1, q3, q3 - q1 AS iqr,
           q1 - 1.5 * (q3 - q1) AS lower_fence,
           q3 + 1.5 * (q3 - q1) AS upper_fence
    FROM bounds
)
SELECT
    d.signup_date,
    d.daily_signups,
    CASE
        WHEN d.daily_signups < f.lower_fence OR d.daily_signups > f.upper_fence THEN 'ANOMALY'
        ELSE 'normal'
    END AS flag
FROM daily_signups d, fences f;

IQR uses percentiles, so outliers (whales, traffic spikes) don't distort the bounds the way they distort std.

Pattern 3 — Rolling z-score (seasonality-aware)

For data with trend or weekly seasonality, a fixed baseline misleads. Use a rolling window:

SELECT
    signup_date,
    daily_signups,
    AVG(daily_signups) OVER (
        ORDER BY signup_date
        ROWS BETWEEN 28 PRECEDING AND 1 PRECEDING
    ) AS rolling_mean,
    STDDEV(daily_signups) OVER (
        ORDER BY signup_date
        ROWS BETWEEN 28 PRECEDING AND 1 PRECEDING
    ) AS rolling_std,
    (daily_signups - AVG(daily_signups) OVER (ORDER BY signup_date ROWS BETWEEN 28 PRECEDING AND 1 PRECEDING))
        / NULLIF(STDDEV(daily_signups) OVER (ORDER BY signup_date ROWS BETWEEN 28 PRECEDING AND 1 PRECEDING), 0) AS rolling_z
FROM daily_signups
ORDER BY signup_date;

The 1 PRECEDING is important — exclude today from the baseline, otherwise today's anomaly partially defines what "normal" is.

When to use which

Metric typeUse
Daily signups (stable trend)Z-score
Daily revenue (whales drag mean)IQR
Daily traffic with weekly seasonalityRolling z-score with seasonal baseline
Long-tailed, no trendIQR on log(x)

The "what's the threshold" question

|Z| > 2 flags ~5% of days as anomalies if the data is truly normal. |Z| > 3 flags ~0.3%. Pick by tolerance for false positives:

  • Slack alerting → use |Z| > 3 (fewer false positives, less alert fatigue)
  • Daily standup discussion → |Z| > 2 (more sensitive)

Takeaway

Most analysts default to "30% below average" or eyeballing. Replacing that with z-scores or IQR is a small change that makes the work look much more rigorous — and is the right answer to the "how would you detect anomalies?" interview question.

Question 1 of 9Answer saved0 answered

Why is z-score a poor choice for detecting anomalies in daily revenue at a SaaS company?