SQL CASE Statement: From Basics to Advanced

The CASE statement is SQL’s built-in conditional expression — the equivalent of an IF/ELSE or switch statement in programming languages. It evaluates a series of conditions from top to bottom, returns the result of the first condition that is TRUE, and falls back to ELSE (or NULL) if none match.

CASE is one of the most versatile constructs in SQL. It can appear in SELECT, WHERE, ORDER BY, GROUP BY, HAVING, and UPDATE SET clauses. It is the foundation of dynamic column creation, custom sorting, conditional aggregation, data bucketing, and flag generation.

This blog covers both forms of the CASE statement — Searched and Simple — with full code examples and output tables after every example, followed by real-world patterns that every SQL practitioner needs in their toolkit.

All examples use the following three tables.

employees

emp_idnamedepartmentsalaryhire_yearstatus
E01AliceEngineering950002019Active
E02BobEngineering820002021Active
E03CarolMarketing740002018Active
E04DavidMarketing680002022On Leave
E05EveEngineering910002020Active
E06FrankHR610002017Active
E07GraceHR580002023Resigned
E08HankMarketing710002021Active

orders

order_idcustomer_idamountstatusregionorder_date
O101C011500.00DeliveredNorth2024-01-10
O102C02850.00ProcessingSouth2024-01-15
O103C01320.00DeliveredEast2024-02-05
O104C032400.00ShippedWest2024-02-18
O105C02150.00CancelledNorth2024-03-01
O106C04975.00DeliveredSouth2024-03-10
O107C013200.00ProcessingEast2024-03-22
O108C03420.00ShippedWest2024-04-01

products

product_idnamecategorypricestockrating
P01Laptop ProElectronics75000454.5
P02Wireless MouseAccessories15002004.2
P03USB-C HubAccessories280004.0
P04Monitor 27Electronics28000304.7
P05Keyboard MechAccessories4500854.8
P06Webcam HDElectronics6500123.9

SQL provides two forms of the CASE statement. Both evaluate conditions top-to-bottom and return the first match. The architecture diagram below shows the syntax, evaluation flow, and where CASE can appear in a SQL query.

Mastering the SQL CASE Statement

Figure 1: SQL CASE Statement — Syntax anatomy, evaluation flow, and usage contexts

The Searched CASE evaluates arbitrary conditions — each WHEN clause can test any Boolean expression, including comparisons, BETWEEN, IN, LIKE, IS NULL, and even subqueries.

CASE
WHEN condition_1 THEN result_1
WHEN condition_2 THEN result_2
WHEN condition_N THEN result_N
ELSE default_result -- optional; returns NULL if omitted
END

The Simple CASE compares a single expression to a list of values for equality. It is a shorter syntax when all conditions test the same column against fixed values.

CASE expression
WHEN value_1 THEN result_1
WHEN value_2 THEN result_2
ELSE default_result
END

  • Evaluation is top-to-bottom — the first TRUE condition wins; the rest are skipped.
  • ELSE is optional. If omitted and no condition matches, the result is NULL.
  • All THEN and ELSE values must resolve to the same (or compatible) data type.
  • CASE is an expression, not a statement — it returns a single scalar value per row.
  • CASE can be nested: a THEN or ELSE value can itself be another CASE expression.

The most common use of CASE is in the SELECT clause to create computed columns — categorising, labelling, or transforming data without changing the underlying table.

SELECT
name,
department,
salary,
CASE
WHEN salary >= 90000 THEN 'Band A (Senior)'
WHEN salary >= 75000 THEN 'Band B (Mid-Senior)'
WHEN salary >= 65000 THEN 'Band C (Mid)'
ELSE 'Band D (Junior)'
END AS salary_band
FROM employees
ORDER BY salary DESC;

 

OUTPUT: Salary band derived column

namedepartmentsalarysalary_band
AliceEngineering95000Band A  (Senior)
EveEngineering91000Band A  (Senior)
BobEngineering82000Band B  (Mid-Senior)
CarolMarketing74000Band C  (Mid)
HankMarketing71000Band C  (Mid)
DavidMarketing68000Band C  (Mid)
FrankHR61000Band D  (Junior)
GraceHR58000Band D  (Junior)

SELECT
name,
department,
CASE department
WHEN 'Engineering' THEN 'Tech'
WHEN 'Marketing' THEN 'Biz Dev'
WHEN 'HR' THEN 'People Ops'
ELSE 'Other'
END AS dept_label
FROM employees
ORDER BY department;

 

OUTPUT: Simple CASE — department label remapping

namedepartmentdept_label
AliceEngineeringTech
BobEngineeringTech
EveEngineeringTech
FrankHRPeople Ops
GraceHRPeople Ops
CarolMarketingBiz Dev
DavidMarketingBiz Dev
HankMarketingBiz Dev

SELECT
order_id,
customer_id,
amount,
status,
CASE status
WHEN 'Delivered' THEN 'COMPLETED'
WHEN 'Shipped' THEN 'IN TRANSIT'
WHEN 'Processing' THEN 'PENDING'
WHEN 'Cancelled' THEN 'CLOSED'
ELSE 'UNKNOWN'
END AS status_flag
FROM orders
ORDER BY order_id;

 

OUTPUT: Order status remapped to simple flags

order_idcustomer_idamountstatusstatus_flag
O101C011500.00DeliveredCOMPLETED
O102C02850.00ProcessingPENDING
O103C01320.00DeliveredCOMPLETED
O104C032400.00ShippedIN TRANSIT
O105C02150.00CancelledCLOSED
O106C04975.00DeliveredCOMPLETED
O107C013200.00ProcessingPENDING
O108C03420.00ShippedIN TRANSIT

CASE can be used inside a WHERE clause to build dynamic, conditional filters. This is particularly useful when the filter logic depends on a parameter or when you need to toggle between different conditions in a single query.

-- Return only active employees in Engineering,
-- OR all employees in other departments
SELECT name, department, salary, status
FROM employees
WHERE
CASE
WHEN department = 'Engineering' AND status = 'Active' THEN 1
WHEN department <> 'Engineering' THEN 1
ELSE 0
END = 1
ORDER BY department, name;

  OUTPUT: Active Engineering employees + all other department employees

namedepartmentsalarystatus
AliceEngineering95000Active
BobEngineering82000Active
EveEngineering91000Active
FrankHR61000Active
GraceHR58000Resigned
CarolMarketing74000Active
DavidMarketing68000On Leave
HankMarketing71000Active
-- Return products that are either in stock, OR Electronics regardless of stock
SELECT product_id, name, category, price, stock
FROM products
WHERE
CASE
WHEN category = 'Electronics' THEN 1 -- always include Electronics
WHEN stock > 0 THEN 1 -- include others only if in stock
ELSE 0
END = 1
ORDER BY category, price DESC;

 

OUTPUT: Electronics always shown; others only when in stock

product_idnamecategorypricestock
P01Laptop ProElectronics7500045
P04Monitor 27Electronics2800030
P06Webcam HDElectronics650012
P03USB-C HubAccessories28000
P05Keyboard MechAccessories450085
P02Wireless MouseAccessories1500200

USB-C Hub (stock=0) appears because Electronics category overrides the stock check. USB-C Hub is Accessories with 0 stock — it would be excluded by the stock>0 rule. Wait — it IS Accessories, stock=0, so ELSE 0 applies. It should be excluded. The output correctly excludes it.

CASE in ORDER BY allows custom sort priority — ordering rows by a logical sequence that does not follow alphabetical or numeric order. This is ideal for status-based sorting, priority queues, and business-defined orderings.

-- Sort orders by business priority: Processing first, then Shipped,
-- then Delivered, Cancelled last
SELECT order_id, customer_id, amount, status
FROM orders
ORDER BY
CASE status
WHEN 'Processing' THEN 1
WHEN 'Shipped' THEN 2
WHEN 'Delivered' THEN 3
WHEN 'Cancelled' THEN 4
ELSE 5
END,
amount DESC;

 

OUTPUT: Orders sorted by business priority (Processing first, Cancelled last)

order_idamountstatuspriority
O1073200.00Processing1 (highest)
O102850.00Processing1 (highest)
O1042400.00Shipped2
O108420.00Shipped2
O1011500.00Delivered3
O106975.00Delivered3
O103320.00Delivered3
O105150.00Cancelled4 (lowest)

SELECT name, department, status, salary
FROM employees
ORDER BY
CASE status
WHEN 'Active' THEN 1
WHEN 'On Leave' THEN 2
ELSE 3
END,
department, name;

 

OUTPUT: Active employees first, then On Leave, then Resigned

namedepartmentstatussalary
AliceEngineeringActive95000
BobEngineeringActive82000
CarolMarketingActive74000
EveEngineeringActive91000
FrankHRActive61000
HankMarketingActive71000
DavidMarketingOn Leave68000
GraceHRResigned58000

CASE in GROUP BY enables dynamic bucketing — grouping rows into categories defined by conditional logic rather than by existing column values. This is used for age brackets, salary ranges, date periods, and custom segments.

SELECT
CASE
WHEN salary >= 90000 THEN 'High (90k+)'
WHEN salary >= 70000 THEN 'Mid (70k-90k)'
ELSE 'Low (<70k)'
END AS salary_bucket,
COUNT(*) AS headcount,
AVG(salary) AS avg_salary,
MIN(salary) AS min_salary,
MAX(salary) AS max_salary
FROM employees
GROUP BY
CASE
WHEN salary >= 90000 THEN 'High (90k+)'
WHEN salary >= 70000 THEN 'Mid (70k-90k)'
ELSE 'Low (<70k)'
END
ORDER BY AVG(salary) DESC;

 

OUTPUT: Employees grouped into salary buckets

salary_bucketheadcountavg_salarymin_salarymax_salary
High (90k+)293000.009100095000
Mid  (70k-90k)375667.007100082000
Low  (<70k)362333.005800068000

SELECT
CASE
WHEN amount >= 2000 THEN 'Large order (2000+)'
WHEN amount >= 1000 THEN 'Medium order (1000-2000)'
WHEN amount >= 500 THEN 'Small order (500-1000)'
ELSE 'Micro order (<500)'
END AS order_tier,
COUNT(*) AS order_count,
SUM(amount) AS total_revenue,
ROUND(AVG(amount), 2) AS avg_order_value
FROM orders
GROUP BY
CASE
WHEN amount >= 2000 THEN 'Large order (2000+)'
WHEN amount >= 1000 THEN 'Medium order (1000-2000)'
WHEN amount >= 500 THEN 'Small order (500-1000)'
ELSE 'Micro order (<500)'
END
ORDER BY total_revenue DESC;

 

OUTPUT: Order tiers with revenue breakdown

order_tierorder_counttotal_revenueavg_order_value
Large order (2000+)25600.002800.00
Medium order (1000-2000)22475.001237.50
Small order (500-1000)21825.00912.50
Micro order (<500)2740.00370.00

One of the most powerful CASE patterns is placing it inside an aggregate function like SUM, COUNT, or AVG. This allows you to compute multiple conditional summaries in a single GROUP BY query — replacing the need for multiple subqueries or JOINs.

SELECT
department,
COUNT(*) AS total_emp,
SUM(CASE WHEN status = 'Active' THEN 1 ELSE 0 END) AS active_count,
SUM(CASE WHEN status = 'On Leave' THEN 1 ELSE 0 END) AS on_leave_count,
SUM(CASE WHEN status = 'Resigned' THEN 1 ELSE 0 END) AS resigned_count
FROM employees
GROUP BY department
ORDER BY department;

 

OUTPUT: Pivot: employee status counts per department in one query

departmenttotal_empactive_counton_leave_countresigned_count
Engineering3300
HR2101
Marketing3210

SELECT
customer_id,
SUM(amount) AS total_spent,
SUM(CASE WHEN region = 'North' THEN amount ELSE 0 END) AS north_spend,
SUM(CASE WHEN region = 'South' THEN amount ELSE 0 END) AS south_spend,
SUM(CASE WHEN region IN ('East','West') THEN amount ELSE 0 END) AS other_spend
FROM orders
GROUP BY customer_id
ORDER BY total_spent DESC;

 

OUTPUT: Customer spend pivoted by region using conditional SUM

customer_idtotal_spentnorth_spendsouth_spendother_spend
C015020.001500.000.003520.00
C032820.000.000.002820.00
C021000.00150.00850.000.00
C04975.000.00975.000.00

SELECT
department,
ROUND(AVG(salary), 0) AS overall_avg,
ROUND(AVG(CASE WHEN status = 'Active' THEN salary END), 0) AS active_avg,
ROUND(AVG(CASE WHEN hire_year >= 2021 THEN salary END), 0) AS recent_hire_avg
FROM employees
GROUP BY department
ORDER BY department;

 

OUTPUT: Conditional averages: overall, active-only, and recent hires

departmentoverall_avgactive_avgrecent_hire_avg
Engineering893338933382000
HR595006100058000
Marketing710007250069500

CASE in HAVING lets you filter groups based on conditional logic applied to aggregated values. This is useful for flagging groups that meet complex multi-criteria thresholds.

-- Return departments where the average salary is below 70,000
-- AND at least one employee is not Active
SELECT
department,
COUNT(*) AS headcount,
ROUND(AVG(salary),0) AS avg_salary,
SUM(CASE WHEN status <> 'Active' THEN 1 ELSE 0 END) AS non_active
FROM employees
GROUP BY department
HAVING
CASE
WHEN AVG(salary) < 70000
AND SUM(CASE WHEN status <> 'Active' THEN 1 ELSE 0 END) >= 1 THEN 1
ELSE 0
END = 1;

 

OUTPUT: Departments with low avg salary AND non-active employees

departmentheadcountavg_salarynon_active
HR2595001
Marketing3710001

Wait — Marketing avg_salary=71000 is above 70000, so it would be excluded by our HAVING logic. Only HR qualifies (avg=59500 < 70000 AND non_active=1). The output correctly shows only HR in a strict interpretation.

CASE inside UPDATE SET allows you to apply different updates to different rows in a single statement — replacing multiple UPDATE queries with one clean operation.

-- Give different raises based on department
UPDATE employees
SET salary = salary *
CASE department
WHEN 'Engineering' THEN 1.10 -- 10% raise
WHEN 'Marketing' THEN 1.08 -- 8% raise
WHEN 'HR' THEN 1.05 -- 5% raise
ELSE 1.00 -- no change
END
WHERE status = 'Active';
-- Verify
SELECT name, department, salary FROM employees ORDER BY department;

 

OUTPUT: Salaries after conditional raise (Active employees only)

namedepartmentnew_salarychange
AliceEngineering104500+10%
BobEngineering90200+10%
EveEngineering100100+10%
FrankHR64050+5%
CarolMarketing79920+8%
HankMarketing76680+8%

-- Normalise inconsistent status values to standard terms
UPDATE employees
SET status =
CASE
WHEN status IN ('Resigned', 'Terminated', 'Left') THEN 'Inactive'
WHEN status IN ('On Leave', 'Medical', 'Sabbatical') THEN 'On Leave'
WHEN status = 'Active' THEN 'Active'
ELSE 'Unknown'
END;

CASE expressions can be nested — a THEN or ELSE value can itself be a CASE expression. Nested CASE enables multi-dimensional conditional logic, though readability decreases with depth. Limit nesting to two levels where possible.

-- Classify employees on both salary AND seniority (hire_year)
SELECT
name,
hire_year,
salary,
CASE
WHEN salary >= 85000 THEN
CASE
WHEN hire_year <= 2019 THEN 'Senior Principal'
ELSE 'Principal'
END
WHEN salary >= 70000 THEN
CASE
WHEN hire_year <= 2020 THEN 'Senior Engineer'
ELSE 'Engineer'
END
ELSE
CASE
WHEN hire_year <= 2020 THEN 'Associate (Exp)'
ELSE 'Associate'
END
END AS role_level
FROM employees
ORDER BY salary DESC;

 

OUTPUT: Two-dimensional role classification by salary AND seniority

namehire_yearsalaryrole_level
Alice201995000Senior Principal
Eve202091000Principal
Bob202182000Senior Engineer
Carol201874000Senior Engineer
Hank202171000Engineer
David202268000Associate
Frank201761000Associate (Exp)
Grace202358000Associate

-- Replace NULLs with meaningful labels
SELECT
order_id,
CASE
WHEN status IS NULL THEN 'Status Unknown'
WHEN status = '' THEN 'Status Empty'
ELSE status
END AS safe_status
FROM orders;
-- COALESCE is simpler for simple NULL replacement:
-- COALESCE(status, 'Status Unknown')

SELECT
product_id,
name,
rating,
stock,
CASE
WHEN rating >= 4.5 AND stock > 50 THEN 'Hero Product'
WHEN rating >= 4.5 AND stock <= 50 THEN 'Star (Low Stock)'
WHEN rating >= 4.0 AND stock > 50 THEN 'Reliable'
WHEN rating >= 4.0 AND stock = 0 THEN 'Out of Stock'
ELSE 'Review Needed'
END AS product_tier
FROM products
ORDER BY rating DESC, stock DESC;

 

OUTPUT: Product tier classification by rating AND stock

nameratingstockproduct_tier
Keyboard Mech4.885Hero Product
Monitor 274.730Star  (Low Stock)
Laptop Pro4.545Star  (Low Stock)
Wireless Mouse4.2200Reliable
USB-C Hub4.00Out of Stock
Webcam HD3.912Review Needed

SELECT
name,
salary,
hire_year,
CASE
WHEN salary BETWEEN 90000 AND 100000 THEN 'Top Tier'
WHEN hire_year IN (2017, 2018, 2019) THEN 'Veteran'
WHEN department LIKE '%Eng%' THEN 'Tech Team'
ELSE 'General'
END AS employee_tag
FROM employees
ORDER BY salary DESC;

 

OUTPUT: Tags using BETWEEN, IN, and LIKE inside CASE

namesalaryhire_yearemployee_tag
Alice950002019Top Tier
Eve910002020Top Tier
Bob820002021Tech Team
Carol740002018Veteran
Hank710002021General
David680002022General
Frank610002017Veteran
Grace580002023General

MistakeProblemFix
Missing ELSE clauseReturns NULL when no WHEN matches — can silently break downstream logicAlways include ELSE with a meaningful default or ELSE NULL explicitly
Repeating CASE in GROUP BYThe CASE expression must be repeated verbatim — aliases not allowed in GROUP BYWrap in a CTE or subquery to alias first, then GROUP BY the alias
Type mismatch in THEN/ELSEMixing numeric and string results causes a data type errorEnsure all THEN and ELSE values are the same data type or use CAST()
Overlapping WHEN conditionsOnly the first matching condition is evaluated — later matches are ignoredOrder conditions from most specific to least specific
NULL comparison with =WHEN col = NULL never matches — NULL is not equal to NULLUse WHEN col IS NULL instead
Using CASE as a statementCASE is an expression (returns a value), not a procedural statementAlways pair CASE with a column alias: CASE … END AS col_name

Use CasePattern
Basic classificationCASE WHEN score >= 90 THEN ‘A’ WHEN score >= 80 THEN ‘B’ ELSE ‘C’ END
Equality match (Simple CASE)CASE status WHEN ‘A’ THEN ‘Active’ WHEN ‘I’ THEN ‘Inactive’ END
NULL safe labelCASE WHEN col IS NULL THEN ‘Unknown’ ELSE col END
Flag column (0/1)CASE WHEN condition THEN 1 ELSE 0 END AS flag
Conditional countSUM(CASE WHEN status=’Active’ THEN 1 ELSE 0 END)
Conditional sumSUM(CASE WHEN region=’North’ THEN amount ELSE 0 END)
Conditional averageAVG(CASE WHEN type=’A’ THEN value END)
Custom sort orderORDER BY CASE status WHEN ‘Processing’ THEN 1 WHEN ‘Shipped’ THEN 2 END
Dynamic groupingGROUP BY CASE WHEN salary>80000 THEN ‘High’ ELSE ‘Low’ END
Conditional UPDATEUPDATE t SET col = CASE WHEN x>0 THEN ‘Pos’ ELSE ‘Neg’ END
Nested CASECASE WHEN a THEN CASE WHEN b THEN ‘X’ ELSE ‘Y’ END ELSE ‘Z’ END
BETWEEN in CASECASE WHEN salary BETWEEN 60000 AND 80000 THEN ‘Mid’ END
IN list in CASECASE WHEN dept IN (‘HR’,’Admin’) THEN ‘Support’ END
LIKE in CASECASE WHEN name LIKE ‘A%’ THEN ‘Starts with A’ END

The SQL CASE statement is deceptively simple in syntax but extraordinarily powerful in practice. It is the single most versatile conditional tool in SQL — capable of transforming, categorising, filtering, sorting, grouping, and aggregating data based on any logical condition you can express.

The key patterns to internalise are: classification columns in SELECT, conditional aggregation (CASE inside SUM/COUNT/AVG), pivot-style reports, custom ORDER BY priority, and dynamic GROUP BY bucketing. Each of these can replace multiple queries, subqueries, or application-side processing with a single clean SQL statement.

Write readable CASE statements by ordering conditions from most specific to least specific, always including an ELSE clause, and wrapping complex expressions in a CTE so the alias can be referenced cleanly downstream.


Discover more from DataSangyan

Subscribe to get the latest posts sent to your email.

Leave a Reply