CASE Statement in SQL

Here's the thing about CASE WHEN. You're going to write it more than almost any other SQL construct in your first year as a data engineer, and nobody ever sits you down and teaches it properly. You pick up the syntax in pieces, you get something working, and then an interviewer asks you to bucket revenue into five tiers under a three minute timer and your brain locks. That's what this page is for.

28%
Conditional logic qs
429
Verified SQL rounds
61%
Senior-level questions
2
CASE forms to know
Source: DataDriven analysis of 1,042 verified data engineering interview rounds.
Prepare for the interview
01 / Open invite
02min.

Know CASE statements the way the interviewer who asks it knows it.

a CASE statements query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1SELECT user_id,
2 COUNT(*) AS sessions
3FROM events
4WHERE ts >= NOW() - INTERVAL '7 day'
5
Execute your solution0.4s avg.
TikTokInterview question
Solve a CASE statements problem

Simple CASE Syntax

Think of simple CASE as the gentle introduction. You hand it one column, you list the values you care about, and you get back a label for each match. It's the form you'll reach for when you're renaming status codes or rewriting enum strings. You're not doing anything clever here, and that's fine: most production CASE expressions live at this level of complexity, and interviewers respect the candidate who knows when the simple form is enough.
-- Simple CASE: compare one expression to fixed values
SELECT
  order_id,
  status,
  CASE status
    WHEN 'Pending'   THEN 'Awaiting Processing'
    WHEN 'Shipped'   THEN 'In Transit'
    WHEN 'Completed' THEN 'Complete'
    WHEN 'Returned'  THEN 'Returned to Warehouse'
    ELSE 'Unknown'
  END AS status_label
FROM orders;
Simple CASE only supports equality checks. You cannot write CASE status WHEN LIKE '%active%'. For anything beyond exact value matching, use the searched CASE syntax below.

When to Use Simple CASE

Mapping codes to labels: Converting status codes, country codes, or category IDs into readable names when a lookup table does not exist or when you need the mapping inline. Rewriting enum values: Translating database enums into display values for reports or API responses. Ordering by custom priority: Using CASE in ORDER BY to sort rows by business priority rather than alphabetical or numeric order.
-- Custom sort order using simple CASE
SELECT order_id, status
FROM orders
ORDER BY
  CASE status
    WHEN 'Pending'    THEN 1
    WHEN 'Processing' THEN 2
    WHEN 'Shipped'    THEN 3
    WHEN 'Completed'  THEN 4
    ELSE 5
  END;

Searched CASE Syntax

Searched CASE evaluates independent boolean expressions. Each WHEN clause can test a different column, use a different operator, or combine multiple conditions with AND/OR. This is the form you will use 90% of the time.
-- Searched CASE: each WHEN is an independent condition
SELECT
  employee_id,
  emp_name,
  salary,
  CASE
    WHEN salary >= 150000 THEN 'Executive'
    WHEN salary >= 100000 THEN 'Senior'
    WHEN salary >= 70000  THEN 'Mid-Level'
    WHEN salary >= 40000  THEN 'Junior'
    ELSE 'Entry'
  END AS salary_band
FROM employees;
Order matters: CASE evaluates WHEN clauses top to bottom and stops at the first match. An employee earning $160,000 matches both the first and second conditions, but only 'Executive' is returned because it comes first. If you reversed the order, that employee would be labeled 'Senior' because $160,000 >= $100,000 matches before reaching the $150,000 check.

Multi-Column Conditions

Searched CASE can reference multiple columns in each WHEN clause. This is useful for classification logic that depends on more than one attribute.
-- Classify employees by both salary and tenure
SELECT
  employee_id,
  salary,
  CAST((julianday('now') - julianday(hire_date)) / 30 AS INT) AS months_active,
  CASE
    WHEN salary >= 120000 AND (julianday('now') - julianday(hire_date)) / 30 >= 24
      THEN 'Senior Leader'
    WHEN salary >= 80000 OR (julianday('now') - julianday(hire_date)) / 30 >= 12
      THEN 'Established'
    WHEN (julianday('now') - julianday(hire_date)) / 30 >= 3
      THEN 'Growing'
    ELSE 'New'
  END AS employee_tier
FROM employees;

NULL Handling in CASE

CASE does not treat NULL as equal to anything, including another NULL. If you need to check for NULL, use IS NULL explicitly.
-- NULL-safe CASE
SELECT
  content_id,
  publish_date,
  CASE
    WHEN publish_date IS NULL THEN 'Not Published'
    WHEN publish_date > date('now', '-7 days')
      THEN 'Recently Published'
    ELSE 'Published'
  END AS publish_status
FROM content_items;

-- WRONG: this never matches NULL
-- CASE publish_date WHEN NULL THEN 'Not Published' END
-- NULL = NULL evaluates to NULL (not TRUE), so it skips

CASE with Aggregation: Conditional Counting and Summing

The most powerful use of CASE in data engineering is inside aggregate functions. This pattern lets you count or sum different subsets of rows in a single pass without needing multiple queries or subqueries. Interviewers test this pattern constantly because it shows you can think about data at the group level.
-- Conditional count: orders by status, per region, in one query
SELECT
  region,
  COUNT(*) AS total_orders,
  COUNT(CASE WHEN status = 'Completed' THEN 1 END) AS completed,
  COUNT(CASE WHEN status = 'Returned' THEN 1 END) AS returned,
  COUNT(CASE WHEN status = 'Pending' THEN 1 END) AS pending
FROM orders
GROUP BY region
ORDER BY region;
This works because COUNT ignores NULLs. When the CASE condition is false and there is no ELSE, the expression returns NULL. COUNT skips it. Only rows matching the condition contribute to the count.
-- Conditional sum: profit by region in one query
SELECT
  status,
  SUM(CASE WHEN region = 'US'   THEN profit ELSE 0 END) AS us_profit,
  SUM(CASE WHEN region = 'EU'   THEN profit ELSE 0 END) AS eu_profit,
  SUM(CASE WHEN region = 'APAC' THEN profit ELSE 0 END) AS apac_profit,
  SUM(CASE WHEN region = 'LATAM' THEN profit ELSE 0 END) AS latam_profit,
  SUM(profit) AS total_profit
FROM orders
GROUP BY status;
Interview note: This is called a pivot. When an interviewer says 'pivot the data so each status is a column,' they want this pattern: SUM(CASE WHEN ... THEN value ELSE 0 END) or COUNT(CASE WHEN ... THEN 1 END) for each category.

Top Region by Order Volume

> Which single region generates the most orders? Return the region and its order count.

CASE with AVG and Ratios

-- Completion rate by region
SELECT
  region,
  ROUND(
    100.0
    * COUNT(CASE WHEN status = 'Completed' THEN 1 END)
    / NULLIF(COUNT(*), 0),
    1
  ) AS completion_rate_pct
FROM orders
GROUP BY region
ORDER BY region;
NULLIF prevents division by zero. If a month has zero orders, NULLIF(COUNT(*), 0) returns NULL, and the division produces NULL instead of an error.

CASE with Window Functions

CASE works inside window function expressions. This combination lets you compute conditional running totals, conditional rankings, and row-level flags based on partition-level calculations.
-- Running total of bulk-order revenue only (quantity > 1)
SELECT
  transaction_id,
  transaction_date,
  quantity,
  total_amount,
  SUM(CASE WHEN quantity > 1 THEN total_amount ELSE 0 END)
    OVER (ORDER BY transaction_date) AS running_bulk_revenue
FROM transactions
ORDER BY transaction_date;
-- Flag rows where an employee's salary is above department average
SELECT
  employee_id,
  department,
  salary,
  AVG(salary) OVER (PARTITION BY department) AS dept_avg,
  CASE
    WHEN salary > AVG(salary) OVER (PARTITION BY department)
      THEN 'Above Average'
    WHEN salary = AVG(salary) OVER (PARTITION BY department)
      THEN 'At Average'
    ELSE 'Below Average'
  END AS salary_position
FROM employees;
-- Classify change direction from previous row
SELECT
  transaction_date,
  total_amount,
  LAG(total_amount) OVER (ORDER BY transaction_date) AS prev_value,
  CASE
    WHEN total_amount > LAG(total_amount) OVER (ORDER BY transaction_date)
      THEN 'Increase'
    WHEN total_amount < LAG(total_amount) OVER (ORDER BY transaction_date)
      THEN 'Decrease'
    WHEN total_amount = LAG(total_amount) OVER (ORDER BY transaction_date)
      THEN 'No Change'
    ELSE 'N/A'  -- first row has no previous value
  END AS direction
FROM transactions;

CASE in Data Engineering Pipelines

In production pipelines, CASE is used for data cleaning, standardization, and transformation. Here are the patterns that show up in real dbt models and ETL jobs.

Data Cleaning: Standardize Messy Input

-- Standardize country names from raw data
SELECT
  customer_id,
  CASE
    WHEN UPPER(TRIM(country)) IN ('US', 'USA', 'UNITED STATES', 'U.S.A.')
      THEN 'United States'
    WHEN UPPER(TRIM(country)) IN ('UK', 'GB', 'UNITED KINGDOM', 'GREAT BRITAIN')
      THEN 'United Kingdom'
    WHEN UPPER(TRIM(country)) IN ('CA', 'CAN', 'CANADA')
      THEN 'Canada'
    ELSE TRIM(country)
  END AS country_standardized
FROM customers;

SCD Type 2 Flag

-- Mark current vs historical records
SELECT
  token_id,
  scope,
  owner_id,
  issued AS valid_from,
  expires AS valid_to,
  CASE
    WHEN expires IS NULL OR expires > datetime('now')
      THEN 1
    ELSE 0
  END AS is_current
FROM api_tokens;

Data Quality Scoring

-- Score row completeness for a data quality dashboard
SELECT
  user_id,
  (
    CASE WHEN email IS NOT NULL AND email != '' THEN 1 ELSE 0 END
    + CASE WHEN age_bucket IS NOT NULL AND age_bucket != '' THEN 1 ELSE 0 END
    + CASE WHEN account_status IS NOT NULL AND account_status != '' THEN 1 ELSE 0 END
    + CASE WHEN username IS NOT NULL AND username != '' THEN 1 ELSE 0 END
  ) AS completeness_score,
  CASE
    WHEN (
      CASE WHEN email IS NOT NULL AND email != '' THEN 1 ELSE 0 END
      + CASE WHEN age_bucket IS NOT NULL AND age_bucket != '' THEN 1 ELSE 0 END
      + CASE WHEN account_status IS NOT NULL AND account_status != '' THEN 1 ELSE 0 END
      + CASE WHEN username IS NOT NULL AND username != '' THEN 1 ELSE 0 END
    ) = 4 THEN 'Complete'
    WHEN (
      CASE WHEN email IS NOT NULL AND email != '' THEN 1 ELSE 0 END
      + CASE WHEN age_bucket IS NOT NULL AND age_bucket != '' THEN 1 ELSE 0 END
      + CASE WHEN account_status IS NOT NULL AND account_status != '' THEN 1 ELSE 0 END
      + CASE WHEN username IS NOT NULL AND username != '' THEN 1 ELSE 0 END
    ) >= 2 THEN 'Partial'
    ELSE 'Poor'
  END AS quality_tier
FROM users;

CASE Statement Patterns for Interviews

These are the CASE patterns that come up most in interview SQL questions. Each one solves a specific category of problem.

Pattern 1: Bucketing Continuous Values

"Group users by age bracket" or "categorize orders by amount." Use searched CASE with range conditions. List ranges from largest to smallest (or smallest to largest) to avoid overlaps.

SELECT
  CASE
    WHEN price >= 500 THEN '500+'
    WHEN price >= 200 THEN '200-499'
    WHEN price >= 50 THEN '50-199'
    WHEN price >= 10 THEN '10-49'
    ELSE 'Under 10'
  END AS price_band,
  COUNT(*) AS product_count
FROM products
GROUP BY 1
ORDER BY MIN(price);

Pattern 2: Conditional Aggregation (Pivot)

"Show each department's headcount broken out by gender." Use CASE inside COUNT or SUM to create columns from row values.

SELECT
  department,
  COUNT(CASE WHEN salary >= 120000 THEN 1 END) AS senior_count,
  COUNT(CASE WHEN salary BETWEEN 70000 AND 119999 THEN 1 END) AS mid_count,
  COUNT(CASE WHEN salary < 70000 THEN 1 END) AS junior_count,
  COUNT(*) AS total
FROM employees
GROUP BY department;

Pattern 3: Decode Boolean Flags

Converting 0/1, true/false, or Y/N into readable labels for reporting.

SELECT
  product_id,
  CASE in_stock
    WHEN 1 THEN 'In Stock'
    WHEN 0 THEN 'Out of Stock'
  END AS stock_status,
  CASE WHEN rating >= 4 THEN 'Top Rated'
       WHEN rating < 4 THEN 'Standard'
  END AS rating_status
FROM products;

Pattern 4: Safe Division

Prevent division-by-zero errors in ratio calculations.

SELECT
  pipe_id,
  rows_in,
  rows_out,
  CASE
    WHEN rows_in = 0 THEN 0
    ELSE ROUND(100.0 * rows_out / rows_in, 2)
  END AS throughput_rate
FROM data_pipes;

Common CASE Statement Mistakes

These mistakes appear in interviews and production code. Knowing them prevents debugging headaches.

Overlapping Conditions

Because CASE stops at the first match, overlapping ranges silently produce wrong results.

-- BUG: salary of 120000 matches first condition
CASE
  WHEN salary > 50000 THEN 'Mid'     -- 120000 > 50000 is TRUE
  WHEN salary > 100000 THEN 'Senior' -- never reached for 120000
END

-- FIX: order from most restrictive to least
CASE
  WHEN salary > 100000 THEN 'Senior'
  WHEN salary > 50000 THEN 'Mid'
  ELSE 'Junior'
END

Missing ELSE Returns NULL

Forgetting ELSE when you do not want NULL results.

-- BUG: status 'canceled' returns NULL
SELECT CASE status
  WHEN 'active' THEN 'Active'
  WHEN 'inactive' THEN 'Inactive'
END AS label  -- 'canceled' -> NULL

-- FIX: always handle the default
SELECT CASE status
  WHEN 'active' THEN 'Active'
  WHEN 'inactive' THEN 'Inactive'
  ELSE 'Other'
END AS label

Mixing Data Types in THEN Clauses

All THEN and ELSE values must be the same data type (or implicitly castable). Mixing strings and numbers causes errors in strict-type databases.

-- BUG in strict-type engines: mixing int and text
CASE WHEN quantity > 0 THEN quantity ELSE 'N/A' END

-- FIX: cast to consistent type
CASE WHEN quantity > 0 THEN CAST(quantity AS TEXT) ELSE 'N/A' END

Frequently asked questions

What is the CASE statement in SQL?+
The CASE statement is SQL's way of writing if/then/else logic inside a query. It evaluates conditions in order and returns a value when the first condition is true. If no condition matches and there is no ELSE clause, it returns NULL. CASE works in SELECT, WHERE, ORDER BY, GROUP BY, and HAVING clauses. It does not change the data in the table. It only changes what the query returns.
What is the difference between simple CASE and searched CASE?+
Simple CASE compares one expression to a list of values: CASE status WHEN 'active' THEN 1 WHEN 'inactive' THEN 0 END. It works like a switch statement. Searched CASE evaluates independent boolean conditions: CASE WHEN salary > 100000 THEN 'high' WHEN salary > 50000 THEN 'mid' ELSE 'low' END. Searched CASE is more flexible because each WHEN can test a different column or a different comparison operator. In practice, searched CASE covers every scenario that simple CASE covers, plus more. Most SQL developers default to searched CASE.
Can you use CASE in a WHERE clause?+
Yes, but it is rarely the best approach. You can write WHERE CASE WHEN ... THEN 1 ELSE 0 END = 1, but this is harder to read than using AND/OR logic directly. The main use case for CASE in WHERE is when you need conditional filtering based on a parameter, such as a stored procedure argument that determines which filter to apply. For standard queries, rewrite the logic using AND/OR instead.
What happens if no CASE condition matches and there is no ELSE?+
The CASE expression returns NULL. This catches many people off guard because there is no error or warning. If you use the CASE result in a SUM or COUNT, NULL values are silently skipped. If you use it in a string concatenation, the entire result becomes NULL in most databases. Always include an ELSE clause unless you specifically want NULL for unmatched cases.
02 / Why practice

You'll be fluent by next Tuesday

  1. 01

    Active recall beats re-reading by 50%

    Cognitive-science meta-reviews (Dunlosky et al., 2013) rank practice testing as a top-tier study technique, while re-reading and highlighting rank near the bottom

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    Five problem shapes cover 80% of data engineer loops

    Dedup, sessionization, top-N-per-group, slowly-changing dimensions, partition tricks. Writing the shapes by hand turns the unfamiliar into pattern recognition

More reading