๐Ÿ“ฆ Shared Sample Schema used in every example

Every example in this course reuses these four tables so you can actually run the queries yourself in a Snowflake trial account (or any SQL engine โ€” the syntax is standard except where marked Snowflake-specific). Run this once at the top of your worksheet.

DDL

CREATE OR REPLACE TABLE employees (
    emp_id       INT,
    name         VARCHAR(50),
    department   VARCHAR(30),
    salary       NUMBER(10,2),
    hire_date    DATE,
    manager_id   INT,
    bonus        NUMBER(10,2)
);

CREATE OR REPLACE TABLE orders (
    order_id     INT,
    customer_id  INT,
    order_date   DATE,
    amount       NUMBER(10,2),
    status       VARCHAR(20),
    region       VARCHAR(20)
);

CREATE OR REPLACE TABLE products (
    product_id     INT,
    product_name   VARCHAR(50),
    category       VARCHAR(30),
    price          NUMBER(10,2),
    stock_qty      INT
);

CREATE OR REPLACE TABLE customers (
    customer_id    INT,
    customer_name  VARCHAR(50),
    signup_date    DATE,
    total_spent    NUMBER(10,2),
    country        VARCHAR(30)
);

Sample Data

INSERT INTO employees VALUES
(1,'Asha','Engineering',95000,'2019-03-14',NULL,8000),
(2,'Ravi','Engineering',72000,'2021-07-01',1,3000),
(3,'Meera','Sales',58000,'2020-01-20',NULL,NULL),
(4,'John','Sales',61000,'2022-05-11',3,1500),
(5,'Divya','HR',49000,'2018-11-30',NULL,500),
(6,'Karan','Engineering',110000,'2016-02-01',1,12000);

INSERT INTO orders VALUES
(101,1,'2024-01-05',250.00,'DELIVERED','APAC'),
(102,2,'2024-01-08',80.50,'CANCELLED','EU'),
(103,1,'2024-02-14',1200.00,'DELIVERED','APAC'),
(104,3,'2024-02-20',NULL,'PENDING','US'),
(105,4,'2024-03-01',45.00,'RETURNED','US'),
(106,2,'2024-03-15',320.75,'DELIVERED','EU');

INSERT INTO products VALUES
(1,'Laptop','Electronics',75000,12),
(2,'Mouse','Electronics',600,0),
(3,'Desk Chair','Furniture',8500,5),
(4,'Notebook','Stationery',50,300);

INSERT INTO customers VALUES
(1,'Acme Corp','2020-06-01',45000,'India'),
(2,'Globex','2021-09-12',3200,'Germany'),
(3,'Initech','2019-01-01',0,'USA'),
(4,'Umbrella','2022-03-22',760,'USA');
๐Ÿ’ก Keep this worksheet open. Every code block below will run directly against these 4 tables โ€” nothing else to set up.

1. Introduction foundations

Think of CASE as SQL's version of "if this, then that, otherwise something else" โ€” but it's an expression, not a statement. It plugs into any place a value is allowed: a column, a filter, an ORDER BY, even inside SUM().

What is a CASE expression?

CASE evaluates a list of conditions in order and returns the value tied to the first condition that is true. If nothing matches, it returns the ELSE value (or NULL if there's no ELSE). It always returns exactly one value per row.

Example
SELECT name, salary,
    CASE
        WHEN salary >= 90000 THEN 'High'
        WHEN salary >= 60000 THEN 'Medium'
        ELSE 'Low'
    END AS salary_band
FROM employees;
NAMESALARYSALARY_BAND
Asha95000High
Karan110000High
Ravi72000Medium
Meera58000Low

Difference between CASE expression and IF logic

Procedural languages use IF as a statement that controls program flow (branching code paths). SQL's CASE is an expression that produces a value inline, inside a query โ€” it never "runs code," it just resolves to a scalar. Snowflake also has an IFF() function (Section 25) which behaves like a compact two-way IF, but it's still an expression under the hood, just shorthand for a simple two-branch CASE.

When to use CASE

How CASE is evaluated โ€” first matching condition wins

Snowflake checks WHEN clauses top to bottom and stops at the first TRUE. Order matters โ€” put the most specific condition first.

Order matters
-- WRONG: salary >= 60000 catches everyone above 60k first,
-- so nobody ever reaches the "High" branch
CASE
    WHEN salary >= 60000 THEN 'Medium'
    WHEN salary >= 90000 THEN 'High'   -- unreachable!
    ELSE 'Low'
END

ELSE behavior

ELSE is optional. If you skip it and no WHEN matches, the result is NULL โ€” not an error. Always add an explicit ELSE in production code so "unexpected" values are visible instead of silently becoming NULL.

NULL behavior

A WHEN condition that evaluates to NULL (not TRUE, not FALSE) is treated as not matching โ€” CASE just moves to the next WHEN. This is why WHEN column = NULL never works (see Section 6) โ€” you need IS NULL.

Interview Q: Why does WHEN col = NULL THEN ... never fire?
Because in SQL, NULL = NULL evaluates to NULL (unknown), not TRUE. CASE only fires a branch on TRUE, so this WHEN is always skipped, silently falling through to ELSE/NULL. Use WHEN col IS NULL instead.
Scenario: A junior engineer's CASE always returns the ELSE value even though the data clearly matches an earlier WHEN. What's your debugging checklist?
1) Check WHEN order โ€” an earlier broader condition may be catching rows first. 2) Check for = NULL instead of IS NULL. 3) Check data types โ€” comparing VARCHAR to NUMBER can silently fail or error depending on implicit casting. 4) Check for trailing whitespace/case sensitivity in string comparisons (use TRIM/UPPER). 5) Print the raw column value next to the CASE result to confirm what's actually being compared.

2. Simple CASE syntax

"Simple CASE" compares one expression against a list of exact values โ€” like a switch statement.

CASE expression
    WHEN value1 THEN result1
    WHEN value2 THEN result2
    ELSE result
END

Matching values & multiple WHEN clauses

Example
SELECT department,
    CASE department
        WHEN 'Engineering' THEN 'Tech'
        WHEN 'Sales' THEN 'Revenue'
        WHEN 'HR' THEN 'People'
        ELSE 'Other'
    END AS dept_group
FROM employees;

Data type consistency

Every THEN/ELSE result must resolve to a common data type. Snowflake will try to implicitly coerce (e.g., INT and NUMBER mix fine), but mixing VARCHAR and NUMBER without casting throws an error. Cast explicitly with ::VARCHAR when mixing types.

-- Error-prone: mixing text and number results
CASE WHEN bonus IS NULL THEN 'None' ELSE bonus END  -- fails
-- Fixed: cast bonus to text
CASE WHEN bonus IS NULL THEN 'None' ELSE bonus::VARCHAR END

Returning NULL & using aliases

You can explicitly return NULL from a branch, and you should always alias the CASE expression with AS โ€” otherwise Snowflake auto-names it something unreadable like CASE_WHEN....

CASE department
    WHEN 'HR' THEN NULL       -- deliberately hide HR from this report
    ELSE department
END AS visible_dept
Interview Q: When would you pick Simple CASE over Searched CASE?
When you're checking one column/expression for exact-value equality (like a lookup or a small enum) โ€” it reads cleaner. The moment you need ranges, ANDs/ORs, or different columns per branch, you need Searched CASE.

3. Searched CASE syntax

"Searched CASE" evaluates a full boolean condition per WHEN โ€” this is what you'll use 90% of the time in real ETL and reporting logic.

CASE
    WHEN condition THEN result
    WHEN condition THEN result
    ELSE result
END

Boolean expressions & comparison operators

Example
SELECT order_id, amount,
    CASE
        WHEN amount > 500 THEN 'Large'
        WHEN amount BETWEEN 100 AND 500 THEN 'Medium'
        WHEN amount IS NULL THEN 'Unknown'
        ELSE 'Small'
    END AS order_size
FROM orders;

Multiple conditions, range checks & complex expressions

Each WHEN can combine several columns, functions, subqueries โ€” anything that resolves to TRUE/FALSE.

SELECT e.name, e.salary, e.department,
    CASE
        WHEN e.department = 'Engineering' AND e.salary > 100000 THEN 'Senior Eng'
        WHEN e.department = 'Sales' AND e.hire_date < '2021-01-01' THEN 'Tenured Sales'
        ELSE 'Standard'
    END AS tier
FROM employees e;
Scenario: You need to classify orders as "Fraud Risk" only when amount is unusually high AND the order is from a brand-new customer. Which CASE style fits, and why?
Searched CASE โ€” because the rule spans two different columns/tables (order amount + customer signup recency) combined with AND, which Simple CASE can't express (it only checks one expression against literal values).

4. CASE with Comparison Operators operators

Every operator SQL supports for filtering also works inside a WHEN clause.

All operators in one query
SELECT name, salary, department,
  CASE
    WHEN salary = 95000 THEN 'Exactly 95k'
    WHEN salary <> 95000 AND salary != 72000 THEN 'Neither'       -- <> and != are identical in Snowflake
    WHEN salary < 60000 THEN 'Under 60k'
    WHEN salary > 100000 THEN 'Over 100k'
    WHEN salary <= 61000 THEN 'At most 61k'
    WHEN salary >= 90000 THEN 'At least 90k'
    WHEN salary BETWEEN 60000 AND 80000 THEN '60k-80k range'
    WHEN department IN ('HR','Sales') THEN 'Non-tech'
    WHEN department NOT IN ('HR') THEN 'Not HR'
    WHEN name LIKE 'A%' THEN 'Starts with A'
    WHEN name ILIKE 'ravi' THEN 'Case-insensitive match'   -- Snowflake-specific
    WHEN name RLIKE '^[A-K].*' THEN 'Regex A-K start'
    ELSE 'Other'
  END AS flag
FROM employees;
Interview Q: Is <> different from != in Snowflake?
No โ€” they're fully interchangeable synonyms. Most style guides pick one for consistency; <> is more portable across older SQL dialects.

5. CASE with Logical Operators AND / OR / NOT

Combine multiple conditions per branch with AND/OR, and always use parentheses to make precedence explicit โ€” SQL evaluates AND before OR, which surprises people.

Parentheses matter
SELECT department, salary, hire_date,
  CASE
    WHEN (department = 'Engineering' OR department = 'Sales')
         AND salary > 60000 THEN 'Priority Review'
    WHEN NOT (department = 'HR') THEN 'Non-HR'
    ELSE 'Standard'
  END AS review_flag
FROM employees;

Nested conditions

You can nest AND/OR/NOT arbitrarily deep โ€” but past 2-3 levels, readability suffers. Consider a CTE that pre-computes boolean flags as columns, then a simpler CASE reads those flags.

CASE
    WHEN (a AND (b OR c)) AND NOT (d AND e) THEN 'Complex Match'
    ELSE 'No Match'
END
Scenario: A CASE with 6 AND/OR conditions across 3 columns keeps producing wrong results in code review. How do you fix the underlying process, not just this query?
Break the raw conditions into named boolean columns in a CTE (e.g. is_senior, is_high_value), then write the CASE against those readable flags. This makes precedence bugs visually obvious and the logic unit-testable in isolation.

6. CASE and NULL Handling critical topic

This is the single most-tested CASE topic in interviews. NULL is not a value โ€” it's "unknown" โ€” and that changes how every comparison behaves.

IS NULL / IS NOT NULL / NULL comparison / ELSE NULL

SELECT order_id, amount,
    CASE
        WHEN amount IS NULL THEN 'Missing'
        WHEN amount IS NOT NULL THEN 'Present'
        ELSE NULL   -- unreachable here, but shown for completeness
    END AS amount_status
FROM orders;

NULL-handling functions (COALESCE, NVL, NVL2, IFNULL, NULLIF + Snowflake-specific)

FunctionBehavior
COALESCE(a,b,c,...)Returns the first non-NULL value in the list (ANSI standard, any number of args)
NVL(a,b)Returns b if a is NULL, else a โ€” 2-argument only (Oracle-style, supported in Snowflake)
NVL2(a,b,c)Returns b if a is NOT NULL, else c
IFNULL(a,b)Same as NVL โ€” Snowflake treats them as aliases
NULLIF(a,b)Returns NULL if a = b, otherwise returns a โ€” great for "blank out sentinel values"
ZEROIFNULL(a)Snowflake-specific. Returns 0 if a is NULL, else a
NULLIFZERO(a)Snowflake-specific. Returns NULL if a = 0, else a
Example: bonus reporting
SELECT name,
    COALESCE(bonus, 0) AS bonus_safe,
    NVL2(bonus, 'Has Bonus', 'No Bonus') AS bonus_flag,
    ZEROIFNULL(bonus) AS bonus_or_zero
FROM employees;
Interview Q: COALESCE vs a CASE with IS NULL โ€” which do you use and when?
They're logically equivalent for the basic "replace NULL with default" case, but COALESCE is shorter, more idiomatic, and lets the optimizer short-circuit more efficiently. Reach for CASE only when the NULL-replacement logic depends on other columns/conditions beyond a simple substitution.
Scenario: A revenue sum silently comes out lower than expected. Root cause turns out to be NULL amounts being skipped by SUM(). How do you fix the query defensively?
Wrap the column with SUM(COALESCE(amount,0)) so NULLs contribute 0 instead of being ignored โ€” and separately report a count of NULL rows (COUNT(*) FILTER/CASE WHEN amount IS NULL) so the data quality issue itself doesn't disappear from visibility.

7. CASE in SELECT derived columns

The most common use โ€” turning raw data into a business-readable column right in the output.

Conditional labels, status columns, data classification, category mapping

Example
SELECT order_id, status,
    CASE status
        WHEN 'DELIVERED' THEN 'โœ… Complete'
        WHEN 'CANCELLED' THEN 'โŒ Cancelled'
        WHEN 'PENDING' THEN 'โณ In Progress'
        WHEN 'RETURNED' THEN 'โ†ฉ๏ธ Returned'
        ELSE 'Unknown'
    END AS friendly_status
FROM orders;

You can put as many CASE-derived columns in one SELECT as you need โ€” each is independent.

8. CASE in WHERE conditional filtering

A CASE inside WHERE must ultimately evaluate to TRUE/FALSE for the row to be kept โ€” so it's typically written to return a boolean-like value that's then compared, or wrapped so the whole thing resolves to TRUE.

Dynamic filter โ€” different rule per region
SELECT *
FROM orders
WHERE CASE
        WHEN region = 'US' THEN amount > 100
        WHEN region = 'EU' THEN amount > 50
        ELSE TRUE
    END;

CASE returning TRUE/FALSE like this is powerful for "apply a different threshold per category" filters that would otherwise need multiple OR'd blocks.

Interview Q: Is CASE in WHERE ever bad for performance?
Yes โ€” a CASE wrapped around a filtered column typically defeats predicate pushdown and can prevent the optimizer/pruning from using clustering keys or micro-partition metadata, since Snowflake can't statically know which branch applies. Prefer plain AND/OR combinations when the logic can be expressed that way; reserve CASE-in-WHERE for genuinely branchy, per-category thresholds.

9. CASE in ORDER BY custom sorting

Sort by business priority instead of alphabetical/numeric order by mapping values to sort ranks with CASE.

Priority sorting โ€” custom status order
SELECT order_id, status
FROM orders
ORDER BY
    CASE status
        WHEN 'PENDING' THEN 1
        WHEN 'DELIVERED' THEN 2
        WHEN 'RETURNED' THEN 3
        WHEN 'CANCELLED' THEN 4
        ELSE 5
    END,
    order_date DESC;   -- multi-level ordering: tiebreak by date

This puts urgent PENDING orders first, regardless of alphabetical order, then breaks ties by most recent date โ€” a very common "business ordering" interview pattern.

10. CASE in GROUP BY bucket creation

Group rows by a computed bucket instead of a raw column โ€” the classic "bucket then aggregate" pattern.

Grouping salaries into bands
SELECT
    CASE
        WHEN salary >= 90000 THEN 'High'
        WHEN salary >= 60000 THEN 'Medium'
        ELSE 'Low'
    END AS salary_band,
    COUNT(*) AS headcount,
    AVG(salary) AS avg_salary
FROM employees
GROUP BY 1;   -- Snowflake lets you GROUP BY the column position

Tip: repeating the full CASE expression in GROUP BY is verbose โ€” Snowflake supports grouping by ordinal position (GROUP BY 1) or by the column alias directly, both of which are cleaner.

11. CASE in HAVING filter grouped data

HAVING filters after aggregation โ€” so CASE here usually filters on a conditionally-aggregated value.

Only departments where high earners are the majority
SELECT department,
    COUNT(*) AS total,
    SUM(CASE WHEN salary >= 90000 THEN 1 ELSE 0 END) AS high_earners
FROM employees
GROUP BY department
HAVING SUM(CASE WHEN salary >= 90000 THEN 1 ELSE 0 END) > COUNT(*) / 2;

12. CASE with Aggregate Functions conditional aggregation โ€” โญ most interviewed

This is the pattern to master. Putting a CASE inside an aggregate function lets you compute multiple conditional metrics in a single pass over the data, instead of running separate filtered queries โ€” this is exactly how pivot-style reports are built in plain SQL.

SUM(CASE...END) โ€” conditional sum

Revenue split by status, one row per nothing โ€” all in columns
SELECT
    SUM(CASE WHEN status = 'DELIVERED' THEN amount ELSE 0 END) AS delivered_revenue,
    SUM(CASE WHEN status = 'RETURNED' THEN amount ELSE 0 END) AS returned_revenue
FROM orders;

COUNT(CASE...END) โ€” conditional count

-- Pattern A: COUNT counts non-NULL rows, so ELSE NULL (or no ELSE) works
SELECT region,
    COUNT(CASE WHEN status = 'DELIVERED' THEN 1 END) AS delivered_count,
    COUNT(CASE WHEN status = 'CANCELLED' THEN 1 END) AS cancelled_count
FROM orders
GROUP BY region;
โš ๏ธ Gotcha: COUNT(CASE WHEN x THEN 1 ELSE 0 END) counts every row (since 0 is not NULL) โ€” always leave ELSE off (defaults to NULL) or explicitly ELSE NULL when the goal is a conditional count.

AVG / MIN / MAX with CASE

SELECT
    AVG(CASE WHEN department = 'Engineering' THEN salary END) AS avg_eng_salary,
    MAX(CASE WHEN department = 'Sales' THEN salary END) AS max_sales_salary,
    MIN(CASE WHEN bonus IS NOT NULL THEN bonus END) AS min_bonus_given
FROM employees;

Full pivot-style report (the interview favorite)

Department headcount pivoted into columns
SELECT
    SUM(CASE WHEN department = 'Engineering' THEN 1 ELSE 0 END) AS engineering,
    SUM(CASE WHEN department = 'Sales'       THEN 1 ELSE 0 END) AS sales,
    SUM(CASE WHEN department = 'HR'          THEN 1 ELSE 0 END) AS hr
FROM employees;
Interview Q: Write a query that returns total orders, delivered orders, and cancellation rate (%) in one row.
SELECT
    COUNT(*) AS total_orders,
    COUNT(CASE WHEN status='DELIVERED' THEN 1 END) AS delivered,
    ROUND(100.0 * COUNT(CASE WHEN status='CANCELLED' THEN 1 END) / COUNT(*), 2) AS cancel_rate_pct
FROM orders;
Scenario: Product wants a single dashboard tile showing revenue by 3 regions without 3 separate queries. How do you build it, and what's the tradeoff vs a PIVOT?
Use SUM(CASE WHEN region='X' THEN amount ELSE 0 END) three times, one column per region, in a single aggregate query โ€” one scan of the table. A native PIVOT clause is more concise when the bucket list is dynamic/large, but CASE-based pivoting is more explicit, easier to add custom logic to (e.g. weighted sums), and doesn't require knowing all category values up front for simple cases like this.

13. CASE with Window Functions ROW_NUMBER, RANK, LAG/LEAD...

CASE can wrap a window function's result, or a window function can be computed inside a CASE branch โ€” both are common.

CASE around ROW_NUMBER / RANK / DENSE_RANK / NTILE

Flag the top earner per department
SELECT name, department, salary,
    CASE
        WHEN ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) = 1
        THEN 'Top Earner'
        ELSE 'Regular'
    END AS flag
FROM employees;

CASE with LAG / LEAD โ€” detecting change

Flag when an order amount increased vs the customer's previous order
SELECT customer_id, order_date, amount,
    CASE
        WHEN amount > LAG(amount) OVER (PARTITION BY customer_id ORDER BY order_date) THEN 'Increased'
        WHEN amount < LAG(amount) OVER (PARTITION BY customer_id ORDER BY order_date) THEN 'Decreased'
        ELSE 'First Order / Same'
    END AS trend
FROM orders;

NTILE bucketing & FIRST_VALUE/LAST_VALUE inside CASE

SELECT name, salary,
    CASE NTILE(4) OVER (ORDER BY salary)
        WHEN 1 THEN 'Bottom Quartile'
        WHEN 4 THEN 'Top Quartile'
        ELSE 'Middle'
    END AS quartile_label
FROM employees;
๐Ÿ’ก Note the important rule: WHEN clauses evaluate after the window function computes its per-row value โ€” you can't put a window function's condition ahead of its own computation, but you can absolutely branch on its result.

14. Nested CASE decision trees

A CASE branch's result can itself be another full CASE expression โ€” this builds a decision tree, but readability drops fast past 2 levels.

CASE inside CASE
SELECT name, department, salary,
    CASE
        WHEN department = 'Engineering' THEN
            CASE
                WHEN salary > 100000 THEN 'Senior Engineer'
                ELSE 'Junior Engineer'
            END
        WHEN department = 'Sales' THEN
            CASE
                WHEN salary > 60000 THEN 'Senior Sales'
                ELSE 'Junior Sales'
            END
        ELSE 'Other'
    END AS title
FROM employees;

Best practices for nesting

Interview Q: Rewrite a 2-level nested CASE as a single flat searched CASE. When is that not possible?
Flattening works by ANDing the outer and inner conditions into one WHEN per final branch (e.g. WHEN department='Engineering' AND salary>100000 THEN 'Senior Engineer'). It stops being simple when the inner CASE's branches depend on results from multiple different outer branches with overlapping logic, or when inner branches vary in count per outer condition, making the flattened version excessively repetitive.

15. CASE with Date Functions DATEADD, DATEDIFF, DATE_TRUNC...

Extremely common in reporting: bucket rows into fiscal periods, weekday/weekend, or "recency" tiers.

Current month / weekend-weekday / quarter / month names / year buckets / date ranges

Order recency & calendar classification
SELECT order_id, order_date,
    CASE
        WHEN DATE_TRUNC('MONTH', order_date) = DATE_TRUNC('MONTH', CURRENT_DATE()) THEN 'This Month'
        WHEN order_date >= DATEADD(DAY, -30, CURRENT_DATE()) THEN 'Last 30 Days'
        ELSE 'Older'
    END AS recency_bucket,
    CASE
        WHEN DAYOFWEEK(order_date) IN (0,6) THEN 'Weekend'
        ELSE 'Weekday'
    END AS day_type,
    CASE QUARTER(order_date)
        WHEN 1 THEN 'Q1' WHEN 2 THEN 'Q2'
        WHEN 3 THEN 'Q3' ELSE 'Q4'
    END AS fiscal_quarter
FROM orders;

Fiscal year with DATEDIFF and LAST_DAY / EXTRACT

SELECT order_id,
    DATEDIFF(DAY, order_date, CURRENT_DATE()) AS days_old,
    CASE
        WHEN order_date = LAST_DAY(order_date) THEN 'Placed on month-end'
        ELSE 'Mid-month'
    END AS month_position,
    CASE
        WHEN EXTRACT(MONTH FROM order_date) IN (4,5,6) THEN 'FY-Q1'  -- example: fiscal year starting April
        ELSE 'FY-Other'
    END AS fiscal_period
FROM orders;
Scenario: Finance wants an "Active in last 90 days" customer flag refreshed daily. How would you implement it, and what's the performance concern at scale?
Compute it as CASE WHEN last_order_date >= DATEADD(day,-90,CURRENT_DATE()) THEN 'Active' ELSE 'Inactive' END off a pre-aggregated "last order date per customer" table (not recomputed by scanning all raw orders each time). At scale, wrapping a filtered/joined date column in CASE inside WHERE would block pruning โ€” better to materialize the flag in a daily incremental table.

16. CASE with String Functions UPPER, SUBSTRING, REGEXP...

String functions inside WHEN clauses power text-based classification โ€” the bread and butter of data cleaning.

Classifying customers by name pattern
SELECT customer_name,
    CASE
        WHEN UPPER(customer_name) LIKE '%CORP%' THEN 'Corporation'
        WHEN LENGTH(customer_name) > 10 THEN 'Long Name'
        WHEN SUBSTRING(customer_name,1,1) = 'A' THEN 'A-Name'
        ELSE INITCAP(customer_name)
    END AS name_category
FROM customers;

REPLACE, SPLIT_PART, CONCAT & Snowflake regex functions

SELECT customer_name,
    CASE
        WHEN REGEXP_LIKE(customer_name, '^[A-Z][a-z]+$') THEN 'Single clean word'
        WHEN SPLIT_PART(customer_name, ' ', 2) != '' THEN 'Multi-word name'
        ELSE 'Other'
    END AS name_shape
FROM customers;

17. CASE with Numeric Functions ROUND, FLOOR, MOD...

Pricing tiers using numeric functions
SELECT product_name, price,
    CASE
        WHEN MOD(product_id, 2) = 0 THEN 'Even ID'
        ELSE 'Odd ID'
    END AS id_parity,
    CASE
        WHEN FLOOR(price / 1000) >= 50 THEN 'Premium'
        WHEN ROUND(price,-2) >= 500 THEN 'Mid-range'
        ELSE 'Budget'
    END AS price_tier,
    CASE WHEN ABS(price - 600) < 100 THEN 'Near 600' ELSE 'Far from 600' END AS proximity
FROM products;

18. CASE with CTE classification pipelines

Use a CTE to compute a CASE-based classification once, then reference the labeled column downstream โ€” avoids repeating the same CASE logic multiple times in one query.

Classify once, reuse everywhere
WITH classified AS (
    SELECT *,
        CASE
            WHEN salary >= 90000 THEN 'High'
            WHEN salary >= 60000 THEN 'Medium'
            ELSE 'Low'
        END AS salary_band
    FROM employees
)
SELECT salary_band, COUNT(*) AS headcount, SUM(bonus) AS total_bonus
FROM classified
GROUP BY salary_band;

Multiple CASE columns as intermediate calculations

WITH flags AS (
    SELECT *,
        CASE WHEN salary > 90000 THEN 1 ELSE 0 END AS is_high_earner,
        CASE WHEN hire_date < '2020-01-01' THEN 1 ELSE 0 END AS is_tenured
    FROM employees
)
SELECT * FROM flags WHERE is_high_earner = 1 AND is_tenured = 1;

19. CASE with JOIN conditional columns across tables

Join transformation โ€” label order size relative to customer's history
SELECT c.customer_name, o.order_id, o.amount,
    CASE
        WHEN o.amount > c.total_spent * 0.5 THEN 'Major Order'
        WHEN c.country = 'USA' AND o.status = 'RETURNED' THEN 'US Return - Review'
        ELSE 'Normal'
    END AS order_flag
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;

CASE after a JOIN can reference columns from either side of the join in the same condition โ€” this is what makes cross-table business rules possible in one pass.

Interview Q: Your CASE references a column from the right side of a LEFT JOIN, and it returns unexpected 'Low' labels for rows that shouldn't even match. What's happening?
On a LEFT JOIN, unmatched right-side rows come back as NULL. If the CASE's ELSE branch (or a NULL-unaware WHEN) treats that NULL as a normal value rather than "no match," it wrongly falls into a default bucket. Add an explicit WHEN right_col IS NULL THEN 'No Match' branch before the general logic.

20. CASE with Subqueries EXISTS, IN, correlated

EXISTS / NOT EXISTS inside CASE
SELECT c.customer_name,
    CASE
        WHEN EXISTS (
            SELECT 1 FROM orders o
            WHERE o.customer_id = c.customer_id AND o.status = 'CANCELLED'
        ) THEN 'Has Cancellations'
        ELSE 'Clean Record'
    END AS risk_flag
FROM customers c;

IN with a subquery, and correlated subqueries

SELECT product_name,
    CASE
        WHEN product_id IN (SELECT product_id FROM products WHERE stock_qty = 0) THEN 'Out of Stock'
        ELSE 'Available'
    END AS availability
FROM products;
Scenario: A correlated-subquery CASE runs fine on 10K rows but times out at 50M rows. What's your fix?
A correlated subquery re-executes per outer row, which doesn't scale. Rewrite as a JOIN or a window/aggregate pre-computed in a CTE (e.g., pre-aggregate "has_cancellation" per customer_id once), then join that flag back โ€” turning an O(nร—m) pattern into a single-pass aggregation.

21. CASE inside Window Aggregation conditional running totals

Combine CASE + SUM + OVER to get a running total of only the rows matching a condition โ€” a favorite for "cumulative delivered revenue" style reports.

Running total of delivered orders only, per customer, ordered by date
SELECT customer_id, order_date, status, amount,
    SUM(
        CASE WHEN status = 'DELIVERED' THEN amount ELSE 0 END
    ) OVER (
        PARTITION BY customer_id ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_delivered_total
FROM orders;

This is different from Section 13: there, CASE wraps a window function's output; here, CASE feeds a filtered value into the window aggregate's input, before the running calculation happens.

22. CASE for Data Cleaning standardization

Standardizing values & replacing bad values

Fixing inconsistent country names
SELECT customer_name,
    CASE UPPER(TRIM(country))
        WHEN 'USA' THEN 'United States'
        WHEN 'US'  THEN 'United States'
        WHEN 'UK'  THEN 'United Kingdom'
        ELSE INITCAP(TRIM(country))
    END AS clean_country
FROM customers;

Flagging invalid records, default values & data quality flags

SELECT order_id, amount,
    CASE
        WHEN amount IS NULL THEN 'MISSING_AMOUNT'
        WHEN amount < 0 THEN 'NEGATIVE_AMOUNT'
        WHEN amount = 0 THEN 'ZERO_AMOUNT'
        ELSE 'VALID'
    END AS dq_flag,
    COALESCE(amount, 0) AS amount_cleaned
FROM orders;
Interview Q: How would you build a reusable "data quality flag" pattern across many columns without repeating CASE 10 times?
Define the flag logic once per column in a staging CTE/view, name each output clearly (e.g. amount_dq_flag, date_dq_flag), then a final summary query can COUNT(CASE WHEN col_flag != 'VALID' THEN 1 END) per flag column. For very repetitive rule sets across many tables, this is often templated with dbt macros or a code generator rather than hand-written SQL.

23. CASE for ETL SCD, status flags, business rules

Slowly Changing Dimensions (SCD) โ€” CASE to detect changes

SCD Type 2 pattern: decide if a record needs a new version
SELECT s.customer_id, s.country AS new_country, t.country AS current_country,
    CASE
        WHEN t.customer_id IS NULL THEN 'INSERT'              -- new record
        WHEN s.country <> t.country THEN 'EXPIRE_AND_INSERT'  -- attribute changed
        ELSE 'NO_CHANGE'
    END AS scd_action
FROM staging_customers s
LEFT JOIN customers t ON s.customer_id = t.customer_id;

Active/Inactive status flags & error flags

SELECT customer_id,
    CASE WHEN total_spent > 0 THEN 'ACTIVE' ELSE 'INACTIVE' END AS account_status,
    CASE WHEN signup_date > CURRENT_DATE() THEN 'ERROR: future signup date' ELSE 'OK' END AS validation_flag
FROM customers;

Business rules encoded as CASE (validation pipeline)

SELECT order_id, amount, status,
    CASE
        WHEN status = 'DELIVERED' AND amount IS NULL THEN 'REJECT: delivered order needs amount'
        WHEN status NOT IN ('DELIVERED','CANCELLED','PENDING','RETURNED') THEN 'REJECT: unknown status'
        ELSE 'PASS'
    END AS business_rule_check
FROM orders;
Scenario: You're designing the merge logic for a daily incremental load into a dimension table. Where exactly does CASE fit into the pipeline, and where should it NOT be used?
CASE fits in the staging/comparison step to classify each incoming row as INSERT / UPDATE / EXPIRE / NO_CHANGE (as above) โ€” this classification then drives a MERGE statement's WHEN MATCHED / WHEN NOT MATCHED branches. CASE should not be used to actually perform the write (that's MERGE's job) โ€” keep the classification and the mutation as separate concerns for clarity and easier debugging.

24. CASE for Reporting KPI labels, segmentation, buckets

KPI labels & revenue bands

Revenue band classification for dashboards
SELECT customer_name, total_spent,
    CASE
        WHEN total_spent >= 10000 THEN '๐Ÿ’Ž Platinum'
        WHEN total_spent >= 1000  THEN '๐Ÿฅ‡ Gold'
        WHEN total_spent > 0     THEN '๐Ÿฅˆ Silver'
        ELSE 'Inactive'
    END AS customer_tier
FROM customers;

Age groups & sales buckets

SELECT name,
    DATEDIFF(YEAR, hire_date, CURRENT_DATE()) AS tenure_years,
    CASE
        WHEN DATEDIFF(YEAR, hire_date, CURRENT_DATE()) >= 5 THEN 'Veteran (5+ yrs)'
        WHEN DATEDIFF(YEAR, hire_date, CURRENT_DATE()) >= 2 THEN 'Established (2-5 yrs)'
        ELSE 'New Hire (<2 yrs)'
    END AS tenure_band
FROM employees;
Interview Q: Write a query segmenting customers into Gold/Silver/Bronze based on total_spent, and show the count and average spend per segment.
WITH seg AS (
  SELECT *,
    CASE WHEN total_spent>=10000 THEN 'Gold'
         WHEN total_spent>=1000  THEN 'Silver'
         ELSE 'Bronze' END AS segment
  FROM customers)
SELECT segment, COUNT(*), AVG(total_spent)
FROM seg GROUP BY segment;

25. Snowflake Alternatives to CASE IFF, DECODE, NVL...

These shortcuts often make Snowflake queries shorter โ€” know when each one fits and when it doesn't.

IFF() โ€” the compact 2-way decision

IFF(condition, true_value, false_value)
IFF vs CASE
-- These two are identical:
IFF(salary > 90000, 'High', 'Low')

CASE WHEN salary > 90000 THEN 'High' ELSE 'Low' END

Limitation: IFF only handles exactly one condition (two branches). The moment you need a 3rd branch, you're back to CASE (or nested IFF, which gets messy fast โ€” prefer CASE at 3+ branches).

DECODE() โ€” Oracle-style value mapping

DECODE(expression,
       search1, result1,
       search2, result2,
       default)
DECODE vs Simple CASE
DECODE(department, 'Engineering', 'Tech', 'Sales', 'Revenue', 'Other')
-- equivalent to:
CASE department WHEN 'Engineering' THEN 'Tech' WHEN 'Sales' THEN 'Revenue' ELSE 'Other' END

Difference from Simple CASE: DECODE treats two NULLs as equal for matching purposes (DECODE(col, NULL, 'is null') actually works), whereas Simple CASE's WHEN NULL never matches (Section 1/6). This is DECODE's one real edge over CASE.

NULL functions & GREATEST / LEAST / EQUAL_NULL

GREATEST(a, b, c)   -- largest of the values, NULL if any input is NULL
LEAST(a, b, c)      -- smallest of the values
EQUAL_NULL(a, b)   -- Snowflake-specific: TRUE if a=b OR both NULL, unlike a=b which is NULL when either is NULL
Interview Q: When would you deliberately choose CASE over IFF even for a 2-branch decision?
When the branch condition itself is complex (multiple ANDs/ORs) โ€” CASE's WHEN keyword before the condition reads more clearly than cramming it as IFF's first argument. Also, if the code might grow a 3rd branch later, starting with CASE avoids a rewrite. Otherwise IFF is perfectly fine and often preferred for brevity in simple flags.

26. Performance Considerations optimization

CASE evaluation order

Since Snowflake stops at the first TRUE match, putting the most frequently true or cheapest-to-evaluate condition first can shave a little evaluation time per row โ€” noticeable mainly on very large scans with expensive conditions (regex, subqueries).

Predicate pushdown

Snowflake's optimizer can push simple column filters down to prune micro-partitions before scanning. A CASE expression wrapped around a filtered column in WHERE (Section 8) generally blocks this pushdown, since the optimizer can't statically resolve which branch applies per partition's min/max stats.

Avoid repeated CASE logic

If the same CASE expression appears in SELECT, WHERE, GROUP BY and ORDER BY of one query, compute it once in a CTE/subquery and reference the alias everywhere else (Section 18) โ€” this avoids re-evaluating identical logic per row multiple times and keeps the query maintainable.

Using CTEs for readability (and the optimizer generally handles this fine)

Modern Snowflake typically inlines simple CTEs during optimization, so splitting logic into a CTE for readability rarely costs performance โ€” don't avoid CTEs for fear of "extra materialization" unless you've confirmed it with EXPLAIN.

Expression simplification, data type consistency & NULL propagation

Scenario: A CASE-heavy transformation query got 3x slower after someone added a 7th nested condition with a subquery inside it. How do you diagnose and fix this?
Run EXPLAIN or check the Query Profile for repeated subquery execution per row. Rewrite the subquery-in-CASE as a pre-joined/pre-aggregated CTE (turning row-by-row lookups into a single joined dataset), then reference the resulting flag column in a much simpler CASE. Also check whether the added condition could be reordered earlier if it's cheap and frequently true.

27. Best Practices checklist

28. Common Interview Patterns practice bank โ€” attempt before revealing answers

These are the exact CASE patterns most frequently asked in Data Engineer / Analyst interviews. Try to write the query yourself before clicking to reveal the answer.

1. Customer segmentation (Gold/Silver/Bronze) based on total_spent
SELECT customer_name,
  CASE WHEN total_spent>=10000 THEN 'Gold'
       WHEN total_spent>=1000  THEN 'Silver'
       ELSE 'Bronze' END AS segment
FROM customers;
2. Salary bands across the employee table
SELECT name,
  CASE WHEN salary>=90000 THEN 'High'
       WHEN salary>=60000 THEN 'Medium'
       ELSE 'Low' END AS band
FROM employees;
3. Age buckets (given a birth_date column)
SELECT name,
  CASE
    WHEN DATEDIFF(YEAR,birth_date,CURRENT_DATE()) < 25 THEN 'Under 25'
    WHEN DATEDIFF(YEAR,birth_date,CURRENT_DATE()) < 40 THEN '25-39'
    ELSE '40+'
  END AS age_bucket
FROM employees;
4. Grade assignment from a numeric score
SELECT student_id, score,
  CASE
    WHEN score>=90 THEN 'A' WHEN score>=80 THEN 'B'
    WHEN score>=70 THEN 'C' WHEN score>=60 THEN 'D'
    ELSE 'F'
  END AS grade
FROM scores;
5. Pass/Fail classification
SELECT student_id, score,
  IFF(score>=40, 'Pass', 'Fail') AS result
FROM scores;
6. Revenue categories from orders
SELECT order_id,
  CASE WHEN amount>1000 THEN 'Large'
       WHEN amount>100  THEN 'Medium'
       ELSE 'Small' END AS revenue_category
FROM orders;
7. Conditional aggregation โ€” counts by status, in one row
SELECT
  COUNT(CASE WHEN status='DELIVERED' THEN 1 END) AS delivered,
  COUNT(CASE WHEN status='CANCELLED' THEN 1 END) AS cancelled,
  COUNT(CASE WHEN status='PENDING'   THEN 1 END) AS pending
FROM orders;
8. Pivot-like report using SUM(CASE WHEN...) โ€” monthly revenue columns
SELECT
  SUM(CASE WHEN MONTH(order_date)=1 THEN amount ELSE 0 END) AS jan_revenue,
  SUM(CASE WHEN MONTH(order_date)=2 THEN amount ELSE 0 END) AS feb_revenue
FROM orders;
9. Data quality flags across multiple columns
SELECT order_id,
  CASE WHEN amount IS NULL THEN 'missing_amount'
       WHEN order_date IS NULL THEN 'missing_date'
       ELSE 'ok' END AS dq_flag
FROM orders;
10. Conditional sorting โ€” VIP customers first, then by spend
SELECT * FROM customers
ORDER BY CASE WHEN total_spent>=10000 THEN 0 ELSE 1 END, total_spent DESC;
11. KPI calculation โ€” % of orders that are cancellations
SELECT ROUND(100.0*COUNT(CASE WHEN status='CANCELLED' THEN 1 END)/COUNT(*),2) AS cancel_pct
FROM orders;
12. Business rule implementation โ€” flag high-risk transactions
SELECT order_id, amount, customer_id,
  CASE
    WHEN amount > 5000 AND customer_id IN
        (SELECT customer_id FROM customers WHERE DATEDIFF(DAY,signup_date,CURRENT_DATE())<30)
    THEN 'High Risk'
    ELSE 'Normal'
  END AS risk_flag
FROM orders;

๐ŸŽฏ Scenario-Based Interview Questions

A. "Give me a single query that shows, per department, headcount, average salary, and % of employees earning above the company-wide average." Walk through your approach.
Compute the company-wide average once via a window function or a cross join to a scalar subquery, then use AVG(CASE WHEN salary > company_avg THEN 1.0 ELSE 0 END) per department group to get the percentage โ€” this avoids a self-join and keeps it a single pass:
WITH avgs AS (SELECT AVG(salary) AS co_avg FROM employees)
SELECT e.department, COUNT(*) AS headcount, AVG(e.salary) AS avg_salary,
  ROUND(100.0*AVG(CASE WHEN e.salary > a.co_avg THEN 1.0 ELSE 0 END),1) AS pct_above_avg
FROM employees e CROSS JOIN avgs a
GROUP BY e.department;
B. "We onboarded a new order status 'ON_HOLD' last week and now half the pivot dashboard shows blank/zero for it. Diagnose and fix."
The dashboard's CASE/pivot logic almost certainly enumerates a fixed, hardcoded list of statuses (Section 12/24 pattern) that didn't anticipate the new value โ€” it silently falls into ELSE 0 rather than erroring, which is why it went unnoticed. Fix: add an explicit branch for the new status, and going forward, add a catch-all monitoring query โ€” SELECT DISTINCT status FROM orders WHERE status NOT IN (<known list>) โ€” that alerts whenever an un-mapped value appears, rather than relying on someone visually noticing a blank tile.
C. "You must classify each customer as New / Returning / Churned based on order history, and the classification must be usable both in a dashboard filter (WHERE) and a report column (SELECT). How do you avoid writing the CASE twice?
Materialize the classification once as a column in a dimension/staging table or view (e.g. a daily-refreshed customer_status table using CASE logic based on last_order_date and order_count), then both the dashboard filter and the report simply reference that pre-computed column. This turns a repeated, hard-to-keep-in-sync CASE expression into a single governed definition.
โœ”๏ธ What to master for Snowflake Data Engineer interviews:
Simple & searched CASE ยท Conditional aggregation (SUM/COUNT + CASE) ยท CASE with window functions ยท CASE with joins & CTEs ยท NULL handling (COALESCE, NVL, ZEROIFNULL, NULLIFZERO) ยท Snowflake shortcuts (IFF, DECODE) ยท Data classification & ETL business rules ยท Performance & readability best practices.