PL/pgSQL CASE
Master the PL/pgSQL CASE construct — Simple CASE for value matching, Searched CASE for predicate matching, and the critical distinction between CASE statement (ends with END CASE) and CASE expression (ends with END alone, used in assignments). Covers CASE_NOT_FOUND exception handling.
PL/pgSQL's CASE statement selects one of several blocks of code to execute, based on a condition or value match. It's the structured alternative to long IF ... ELSIF ... ELSIF chains, especially when you're matching one expression against multiple values.
CASE comes in two forms:
| Form | Best for |
|---|---|
Simple CASE — CASE expr WHEN value THEN ... | Matching one expression against several fixed values (like a switch statement) |
Searched CASE — CASE WHEN condition THEN ... | Each branch tests a different condition (ranges, multiple variables, complex predicates) |
A common point of confusion: CASE appears in two different roles in PL/pgSQL.
| CASE statement | CASE expression |
|---|---|
| A control-flow construct that executes statements in the matching branch | An expression that evaluates to a value, useful inside SELECT or assignment |
| Each WHEN has a block of statements ending at the next WHEN/ELSE/END CASE | Each WHEN has a single expression after THEN; ends at END (no CASE) |
Ends with END CASE; | Ends with END (and is part of a larger statement) |
If no branch matches and there's no ELSE, raises CASE_NOT_FOUND | If no branch matches and there's no ELSE, evaluates to NULL |
This page focuses on the CASE statement. The CASE expression works the same way the SQL CASE does — and we'll show how to use it for assignment in the last example.
The search_expression is evaluated once. Each WHEN compares it (using =) against a value or list of values. The first match wins. If nothing matches and there's no ELSE, you get a CASE_NOT_FOUND error.
CASE search-expression THEN with a THEN right after the expression (wrong — there's no THEN there), then put a "Simple CASE" example that was actually a Searched CASE. The simple form has the matching expression on the CASE line; the WHEN clauses list literal values to compare against. The example below is a true Simple CASE.Match a day-of-week number to its name. The expression is matched against literal values:
DO $$
DECLARE
day_num INTEGER := 3;
day_name TEXT;
BEGIN
CASE day_num
WHEN 1 THEN day_name := 'Monday';
WHEN 2 THEN day_name := 'Tuesday';
WHEN 3 THEN day_name := 'Wednesday';
WHEN 4 THEN day_name := 'Thursday';
WHEN 5 THEN day_name := 'Friday';
WHEN 6, 7 THEN day_name := 'Weekend';
ELSE day_name := 'Invalid';
END CASE;
RAISE NOTICE 'Day %: %', day_num, day_name;
END
$$;
The WHEN 6, 7 form matches either value — useful for grouping cases that share a body. The ELSE handles anything outside 1-7.
No expression on the CASE line. Each WHEN has a full Boolean condition. The first true condition wins. Like the simple form, no match plus no ELSE raises CASE_NOT_FOUND.
Searched CASE is what you reach for when each branch tests something different — ranges, multiple variables, complex predicates. Anything an IF ... ELSIF chain could express.
Pick a shipping method based on the package weight (a range, not a fixed value — so this is genuine Searched CASE territory):
DO $$
DECLARE
package_weight NUMERIC := 3.5;
shipping_method VARCHAR(50);
BEGIN
CASE
WHEN package_weight <= 1 THEN
shipping_method := 'Standard';
WHEN package_weight <= 5 THEN
shipping_method := 'Priority';
WHEN package_weight <= 10 THEN
shipping_method := 'Express';
ELSE
shipping_method := 'Special';
END CASE;
RAISE NOTICE 'Shipping Method: %', shipping_method;
END
$$;
package_weight <= 1 fails, we know weight > 1, so the next clause only needs <= 5 (not > 1 AND <= 5). The original tutorial wrote WHEN package_weight > 1 AND package_weight <= 5 THEN — correct but redundant. The cleaner cumulative form runs the same way and reads better.The big advantage of Searched CASE over Simple CASE: each branch can test different variables and use complex predicates:
DO $$
DECLARE
is_member BOOLEAN := TRUE;
order_total NUMERIC := 250;
membership_years INTEGER := 3;
discount_pct NUMERIC;
BEGIN
CASE
WHEN NOT is_member THEN
discount_pct := 0;
WHEN order_total >= 500 THEN
discount_pct := 20;
WHEN order_total >= 200 AND membership_years >= 2 THEN
discount_pct := 15;
WHEN order_total >= 100 THEN
discount_pct := 5;
ELSE
discount_pct := 0;
END CASE;
RAISE NOTICE 'Discount: %%%', discount_pct;
END
$$;
This is what Simple CASE can't do — each WHEN tests a different combination of variables.
The other use of CASE: as an expression that evaluates to a value. Useful inside SELECT, in RAISE NOTICE, and for direct assignment without an explicit := ... END CASE; block.
DO $$
DECLARE
package_weight NUMERIC := 3.5;
shipping_method VARCHAR(50);
BEGIN
-- CASE here is an expression — evaluates to a value, used in assignment
shipping_method := CASE
WHEN package_weight <= 1 THEN 'Standard'
WHEN package_weight <= 5 THEN 'Priority'
WHEN package_weight <= 10 THEN 'Express'
ELSE 'Special'
END; -- note: just END, not END CASE
RAISE NOTICE 'Shipping Method: %', shipping_method;
END
$$;
Two differences from the statement form:
- Each
WHEN ... THENhas a single expression after THEN, not a sequence of statements with semicolons. - Closes with
ENDalone — notEND CASE.
If your branches each just assign a value to the same variable, the CASE-expression form is shorter. If branches do different things — multiple assignments, RAISE, side effects — you need the CASE statement.
If no branch matches and there's no ELSE, the CASE statement raises CASE_NOT_FOUND. You can catch it explicitly:
DO $$
DECLARE
grade CHAR(1) := 'F';
msg TEXT;
BEGIN
BEGIN
CASE grade
WHEN 'A' THEN msg := 'Excellent';
WHEN 'B' THEN msg := 'Good';
WHEN 'C' THEN msg := 'Average';
-- no ELSE clause — anything else triggers CASE_NOT_FOUND
END CASE;
RAISE NOTICE '%', msg;
EXCEPTION
WHEN CASE_NOT_FOUND THEN
RAISE NOTICE 'Unrecognized grade: %', grade;
END;
END
$$;
Alternatively, just include an ELSE and avoid the exception path:
CASE grade
WHEN 'A' THEN msg := 'Excellent';
WHEN 'B' THEN msg := 'Good';
WHEN 'C' THEN msg := 'Average';
ELSE msg := 'Unrecognized: ' || grade;
END CASE;
| Situation | Best fit |
|---|---|
| Match one expression against a list of values | Simple CASE |
| Each branch tests a different variable or complex condition | Searched CASE — or IF/ELSIF |
| Each branch assigns the same variable to a different value | CASE expression with := CASE ... END; |
| Branches do different things — multiple statements each | CASE statement (with END CASE;) |
| Just two paths: condition true vs false | IF / IF-ELSE |
| Want a hard error when no branch matches | CASE without ELSE — gets CASE_NOT_FOUND |
- Use Simple CASE for value matching, Searched CASE for predicate matching. If each WHEN tests a fresh condition, you want Searched.
- Always include
ELSE— defensive programming. Even if you think every value is covered, an unexpected one will raiseCASE_NOT_FOUNDand abort the function. - Use cumulative conditions in Searched CASE for ranges. Once a higher threshold has failed, you don't need to repeat its lower bound.
WHEN x <= 5 THENreads better thanWHEN x > 1 AND x <= 5 THENwhen previous WHENs already excluded x <= 1. - Reach for the CASE expression when assigning a single variable. Shorter than the full statement form.
- Don't confuse
ENDwithEND CASE. The expression closes withEND; the statement closes withEND CASE. - If you'd write a long IF/ELSIF chain matching one variable, switch to CASE. Cleaner intent, easier to scan.
CASEselects one of several blocks of code based on a match. Two flavors: Simple (match one expression against values) and Searched (each branch tests a different condition).- The statement form ends with
END CASE;and runs blocks of statements. The expression form ends with justENDand produces a value for assignment. - If no branch matches and there's no
ELSE: the statement raisesCASE_NOT_FOUND; the expression evaluates toNULL. - Use Simple CASE for value matching (like a switch statement), Searched CASE for range/predicate matching.
- Always include an
ELSEbranch unless you specifically want unmatched values to raise an exception. - The CASE expression is often a cleaner way to assign a variable than a multi-line CASE statement.