PL/SQL offers a number of options when it comes to writing conditional logic, as in variations on "If this then that". This post reviews them, and provides examples.
You can choose from the following:
- IF statement - IF, IF-THEN, IF-ELSE, IF-ELSIF-ELSE
- CASE statement - simple and searched CASE
- CASE expression - the expression "version" of the statement (and one of my favorite elements of PL/SQL)
IF Statement
It's hard to imagine a programming language without a version of IF, and PL/SQL is no exception.
The IF statement either runs or skips a sequence of one or more statements, depending on a condition. The IF statement can take one of three forms, but they all have this in common: they start with "IF" and then end with "END IF;".
IF THEN
The simplest IF statement: if the boolean expression evaluates to TRUE then execute the statements between THEN and END IF. Example:
The simplest IF statement: if the boolean expression evaluates to TRUE then execute the statements between THEN and END IF. Example:
BEGIN
IF l_hire_date < SYSDATE
THEN
send_survey_request (l_employee_id);
END IF;
END;
Tips:
....consider writing your code as follows:
Of course, you will need to call your own trace utility, or better yet use the open source Logger.
IF THEN ELSE
Offer an alternative action if your boolean expression does not evaluate to TRUE.
Tips:
- You can put the IF expression inside parentheses, but you do not have to.
- If you've got a really complex, multi-part expression, consider assigning it to a variable first, as I show below. That way, you can more easily trace and test what that expression evaluates to.
So instead of this (an example from the code base of the Oracle Dev Gym)....
BEGIN
IF l_is_competitive = qdb_config.c_yes
AND l_ce_status = qdb_competition_mgr.c_compev_ranked
AND ( qdb_comp_event_mgr.is_closed (comp_event_id_in)
OR comp_event_id IS NULL)
THEN
get_answer_info;
END IF;
END;
/
....consider writing your code as follows:
BEGIN
l_ok_to_see_answer :=
l_is_competitive = qdb_config.c_yes
AND l_ce_status = qdb_competition_mgr.c_compev_ranked
AND ( qdb_comp_event_mgr.is_closed (comp_event_id_in)
OR comp_event_id IS NULL);
qdb_utilities.trace_activity ('get_answer check', l_ok_to_see_answer);
IF l_ok_to_see_answer
THEN
get_answer_info;
END IF;
END;
Of course, you will need to call your own trace utility, or better yet use the open source Logger.
IF THEN ELSE
Offer an alternative action if your boolean expression does not evaluate to TRUE.
BEGIN
IF l_hire_date < SYSDATE
THEN
send_survey_request (l_employee_id);
ELSE
send_invite_to_new_hire_meetup (l_employee_id);
END IF;
END;
Tips:
- Same tip as with IF for moving complex expressions into a variable.
- There is a big difference between "does not evaluate to TRUE" and "evaluates to FALSE". That expression could evaluate to NULL. In the above IF-ELSE, an invitation will be sent to that "employee" even if the hire_date is NULL. Probably not what you want to do.
- If you need different actions for FALSE and NULL conditions, then you need to use ELSIF (below).
IF THEN ELSIF
When the logic gets tough, the tough start using ELSIFs.
Sometimes it's not an either-or situation. It's more of a this-or that-or the other-or wait a minute something else! Or as noted above, maybe you just need a separate clause for NULL, as in:
When the logic gets tough, the tough start using ELSIFs.
Sometimes it's not an either-or situation. It's more of a this-or that-or the other-or wait a minute something else! Or as noted above, maybe you just need a separate clause for NULL, as in:
BEGIN
IF l_hire_date < SYSDATE
THEN
send_survey_request (l_employee_id);
ELSIF l_hire_date >= SYSDATE
THEN
send_invite_to_new_hire_meetup (l_employee_id);
ELSE
remove_from_employee_table (l_employee_id);
END IF;
END;
And of course there's nothing stopping you from having LOTS of ELSIF clauses:
BEGIN
IF l_hire_date < ADD_MONTHS (SYSDATE, -12)
THEN
send_survey_request (l_employee_id);
ELSIF l_hire_date BETWEEN ADD_MONTHS (SYSDATE, -12) AND SYSDATE
THEN
send_newhire_survey (l_employee_id);
ELSIF EXTRACT (YEAR FROM l_hire_date) = EXTRACT (YEAR FROM SYSDATE)
THEN
send_invite_to_new_hire_meetup (l_employee_id);
ELSIF EXTRACT (YEAR FROM l_hire_date) = EXTRACT (YEAR FROM SYSDATE) + 1
THEN
add_to_waitlist (l_employee_id);
ELSE
remove_from_employee_table (l_employee_id);
END IF;
END;
Tips:
- Avoid repeated evaluations of the same expressions in multiple IF-ELSIF clauses. I extract the year number for hire date and SYSDATE twice in the block above. These are fast operations, so it's no big deal, but if you call an "expensive" application function multiple times, assign the call to a variable and then reference the variable multiple times in the IF statement.
- If you start building a long list of ELSIF clauses, consider switching to a CASE statement. It's cleaner and easier to read/manage.
CASE Statement
CASE was added to PL/SQL in Oracle Database 9i, back in 2001 or so. How do I know? Because I looked through the various editions of Oracle PL/SQL Programming until I found its first appearance in the index. :-). I was very happy when CASE was added, especially CASE expressions (which can be used in both SQL and PL/SQL, by the way)
A CASE statement is logically equivalent to an IF-ELSIF statement, but a CASE statement "presents" better (easier to read, often involving less code).
A CASE statement is logically equivalent to an IF-ELSIF statement, but a CASE statement "presents" better (easier to read, often involving less code).
The CASE statement chooses from a sequence of conditions, and runs the corresponding statement. , and has these forms:
- Simple, which evaluates a single expression and compares it to several potential values.
- Searched, which evaluates multiple conditions and chooses the first one that is true.
Here's a simple CASE statement:
Here's a searched CASE statement version of that same logic:
Hopefully it's clear to you that using a searched CASE statement in which each WHEN clause has the same "variable = value" format is a poor choice. That's a perfect use case for a simple CASE.
Also, if all you are doing inside each WHEN clause is assigning a value to the same variable, that's a piece of code that is crying out for a CASE expression (next section).
Here's a more interesting example of a searched CASE statement, taken from the workout manager package for the Oracle Dev Gym:
The CASE statement is appropriate when a different action is to be taken for each alternative. The CASE expression is great for when you need to evaluate or return a different value for each alternative. I love how CASE expressions de-clutter my code. Let's take a look.
FUNCTION grade_translator (grade_in IN VARCHAR2)
RETURN VARCHAR2
IS
retval VARCHAR2 (100);
BEGIN
CASE grade_in
WHEN 'A'
THEN
retval := 'Excellent';
WHEN 'B'
THEN
retval := 'Very Good';
WHEN 'C'
THEN
retval := 'Good';
WHEN 'D'
THEN
retval := 'Fair';
WHEN 'F'
THEN
retval := 'Poor';
END CASE;
RETURN retval;
END;
Here's a searched CASE statement version of that same logic:
FUNCTION grade_translator (grade_in IN VARCHAR2)
RETURN VARCHAR2
IS
retval VARCHAR2 (100);
BEGIN
CASE
WHEN grade_in = 'A'
THEN
retval := 'Excellent';
WHEN grade_in = 'B'
THEN
retval := 'Very Good';
WHEN grade_in = 'C'
THEN
retval := 'Good';
WHEN grade_in = 'D'
THEN
retval := 'Fair';
WHEN grade_in = 'F'
THEN
retval := 'Poor';
ELSE
retval := 'No such grade';
END CASE;
RETURN retval;
END;
Hopefully it's clear to you that using a searched CASE statement in which each WHEN clause has the same "variable = value" format is a poor choice. That's a perfect use case for a simple CASE.
Also, if all you are doing inside each WHEN clause is assigning a value to the same variable, that's a piece of code that is crying out for a CASE expression (next section).
Here's a more interesting example of a searched CASE statement, taken from the workout manager package for the Oracle Dev Gym:
FUNCTION can_change_workout (workout_id_in IN INTEGER)
RETURN BOOLEAN
IS
l_count INTEGER;
l_start DATE;
l_end DATE;
BEGIN
SELECT wo.start_date, wo.end_date
INTO l_start, l_end
FROM dg_workouts wo
WHERE workout_id = workout_id_in;
CASE
WHEN l_end <= SYSDATE
THEN
RETURN FALSE;
WHEN l_start > SYSDATE
THEN
RETURN TRUE;
ELSE
/* The week has started. Have you done anything yet? */
SELECT COUNT (*)
INTO l_count
FROM dg_user_wo_results uwor, dg_user_workouts uwo
WHERE uwor.completed_date IS NOT NULL
AND uwo.workout_id = workout_id_in;
RETURN l_count = 0;
END CASE;
END;
The CASE statement is appropriate when a different action is to be taken for each alternative. The CASE expression is great for when you need to evaluate or return a different value for each alternative. I love how CASE expressions de-clutter my code. Let's take a look.
CASE Expression
A CASE expression "liberates" CASE from being its own statement. As an expression, CASE can be part of a statement:
- The right hand side of an assignment
- Passed as an actual argument to the formal parameter of a subprogram
- Part of a larger expression
It's one of my favorite parts of PL/SQL, because it let's me write more concise code. To give you a sense of that, suppose that I need to write a function that implements this silly requirement:
- Put a "-" between all three numbers.
- If s1 is like 'T%' then upper case the string.
- If s2 is NULL, there should be just one - between s1 and s2.
- If s3's length > 6 then lower case the string.
FUNCTION plch_full_string (
s1 IN VARCHAR2
, s2 IN VARCHAR2
, s3 IN VARCHAR2) RETURN VARCHAR2
IS
l_return VARCHAR (32767);
BEGIN
IF s1 LIKE 'T%'
THEN
l_return := UPPER (s1);
ELSE
l_return := s1;
END IF;
IF s2 IS NOT NULL
THEN
l_return := l_return || '-' || s2;
END IF;
IF LENGTH (s3) > 6
THEN
l_return := l_return || '-' || LOWER (s3);
ELSE
l_return := l_return || '-' || s3;
END IF;
RETURN l_return;
END;
And now with a CASE expression:
FUNCTION plch_full_string (
s1 IN VARCHAR2
, s2 IN VARCHAR2
, s3 IN VARCHAR2) RETURN VARCHAR2
IS
BEGIN
RETURN CASE
WHEN s1 LIKE 'T%' THEN UPPER (s1)
ELSE s1
END
|| CASE WHEN s2 IS NULL THEN NULL ELSE '-' END
|| s2
|| '-'
|| CASE
WHEN LENGTH (s3) > 6 THEN LOWER (s3)
ELSE s3
END;
END;
Right? Isn't that so much better? Here's another example of shrinking your code with CASE expressions, a progression from CASE statement to CASE expression to CASE expression directly inside call to DBMS_OUTPUT.PUT_LINE.
PROCEDURE show_pig_species_v1 (feature_in IN VARCHAR2)
IS
l_guinea_pig_species VARCHAR2 (100);
BEGIN
CASE feature_in
WHEN 'curly'
THEN
l_guinea_pig_species := 'Texel';
WHEN 'long'
THEN
l_guinea_pig_species := 'Peruvian';
WHEN 'rosettes'
THEN
l_guinea_pig_species := 'Abyssinian';
END CASE;
DBMS_OUTPUT.put_line ('Species=' || l_guinea_pig_species);
END;
PROCEDURE show_pig_species_v2 (feature_in IN VARCHAR2)
IS
l_guinea_pig_species VARCHAR2 (100);
BEGIN
l_guinea_pig_species :=
CASE feature_in
WHEN 'curly' THEN 'Texel'
WHEN 'long' THEN 'Peruvian'
WHEN 'rosettes' THEN 'Abyssinian'
END;
DBMS_OUTPUT.put_line ('Species=' || l_guinea_pig_species);
END;
PROCEDURE show_pig_species_v3 (feature_in IN VARCHAR2)
IS
BEGIN
DBMS_OUTPUT.put_line (
'Species='
|| CASE feature_in
WHEN 'curly' THEN 'Texel'
WHEN 'long' THEN 'Peruvian'
WHEN 'rosettes' THEN 'Abyssinian'
END);
END;
Tips on CASE
- If a CASE statement fails to find a match in any WHEN clauses and there is no ELSE clause, an error is raised: ORA-06592: CASE not found while executing CASE statement.
- If a CASE expression fails to find a match in any WHEN clauses and there is no ELSE clause, it returns NULL.
- You can use the CASE expression inside SQL. You can't use IF and CASE statements inside a SQL statement, because, well, it's already it's own statement, and statements cannot have within them other statements.
Hey, What About DECODE?
And then there is DECODE. If you've used DECODE, that means that either you've been working with Oracle Database for a looooong time, or you should have used CASE but instead somehow latched onto this anachronism. :-)
DECODE offers CASE-like functionality, but can only be used inside SQL (either outside of a PL/SQL block, or from within PL/SQL). It's been a part of SQL long before CASE was added to SQL. Haven't used it yet? Don't bother. You should use CASE instead. Have it in your existing SQL statements? Consider converting it to CASE the next time you need to work on those SQL statements.
I won't even bother showing you examples, but here is a link to appropriately aged doc on DECODE.
To Conclude
When you need to write conditional logic in your PL/SQL blocks (and who doesn't, now and again?), you have a nice range of offerings. Whether you go with the "traditional" IF statement or opt for CASE, you'll be able to get the job done with a minimum of fuss.
I simply suggest that you follow a few handy guidelines:
I won't even bother showing you examples, but here is a link to appropriately aged doc on DECODE.
To Conclude
When you need to write conditional logic in your PL/SQL blocks (and who doesn't, now and again?), you have a nice range of offerings. Whether you go with the "traditional" IF statement or opt for CASE, you'll be able to get the job done with a minimum of fuss.
I simply suggest that you follow a few handy guidelines:
- Avoid redundancies in your code ("WHEN x = y" over and over in your CASE clauses or repeated execution of same union in ELSIF clauses).
- Look for opportunities to use the CASE expression to cut down on code volume and improve readability.
- Assign complex Boolean expressions to variables or constants, rather than "embedding" those expressions inside your conditional statements. That way you can examine/trace the results of those expression more easily, and debug your code more quickly.
Finally, visit the Oracle Dev Gym to take lots of quizzes on these PL/SQL features!
decode is useful if you have something short and concise, e.g. decode(some_value, 'Y', 1, 0) For anything long and complicated case is better.
ReplyDeleteAlso, the null related functions nvl and nvl2 are incredibly handy particularly for something short and concise e.g. nvl2(some_value, 'A', 'B') Again, for anything long and complicated, case is better.
I was thinking about including NVL but then, well, got lazy. :-)
DeleteIn terms of DECODE, yes, it can save you a few keystrokes, but it isn't part of the ANSI standard and can be harder to read (tho not with the most trivial, which is what you are proposing it for).
I do suggest that developers not take the shortcut in this case, and stick with CASE.
I believe Oracle converts the decode to case at parse time?
DeleteWhy do you believe that?
DeleteNot only that DECODE should be converted to CASE.
ReplyDeleteBut also NVL should be converted to COALESCE. The reason is better evaluation.
I assume you mean that NVL always evaluates the second expression, even if the first is NOT NULL. Good point! And a great topic for a quiz at the Oracle Dev Gym. https://devgym.oracle.com :-)
DeleteMaybe contentious, but exception handling can be a form of conditional logic?
ReplyDeleteBegin
Select name Into vName From emp;
ProcessName(vName)
Exception When NoDataFound
ProcessNoName;
End;
Well, it's not contentious in that you are correct that one could employ exception handling as a form of conditional logic.
DeleteThe controversy is over whether or not your SHOULD (I know, I know: that's what you meant).
I believe you should not and I do not want to even encourage people to think that way. Perhaps a good subject for another post:
Writing conditional logic the wrong way in PL/SQL
:-)
I'd like to see a post with some alternatives to the above.
Delete(But not Select MAX(name)... to suppress the error!)
Peter, just so I understand, do you mean alternatives to the use of exception handling and other sub-optimal ways to implement conditional logic, with a reworking that uses the intended forms?
DeleteYes. I'll admit to writing coded like the above. I've also coded subqueries in a SQL CASE statement. But if there's a more elegant solution to writing PL/SQL conditional logic that involves a query that may return zero rows, I'm all eyes and ears.
DeleteI think that the only consideration is what goes into the exception section, not "Should I trap NDF with my select-into and then execute different code as a result?"
DeleteI suggest that the exception section should hold an absolute minimum of application logic, so I would opt for something like this:
BEGIN
BEGIN
SELECT .. INTO var FROM ...;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
l_notfound := TRUE;
END;
IF l_notfound THEN ...
ELSE
...continue program flow...
END IF;
END;
Argh, I hate how blogger doesn't let me use fixed font. But I hope you get the idea.
I *LIKE* it, Steven. I will use this form from on, but with two tweaks. I will set the boolean in either case, and I will use a true-named boolean. Following the true leg of an IF with a negative_name gives me fits on days when I'm not playing my A-game. ;)
DeleteYes to both your tweaks; they are an improvement on my quick hack of code. Stay positive! :-)
DeleteMmmm... this is the way I used to do it in the past, but after some discussions with colleagues in the past, I slowly switched to their approach of using a cursor fetch together with the %NOTFOUND option for the second case. The result is the same (not sure about performance), but you avoid using exceptions for your application logic.
DeleteI have demonstrated to a few developers how much less efficient using exception handling as a conditional expression can be. Once they understand the performance implications they usually avoid the practice.
DeleteThat's the way to do it. But that should not in and of itself push you away from implicits. It would argue, instead, for a nuanced coding approach, with this more nuanced understanding.
DeleteSpecifically, if you know that a select-into will mostly or very often fail, then switch to explicit. But generally our select-into's almost always will find their row. So the overall efficiency for the application could be greater with implicits.
Reply to Erwin Knop: sorry about delay. Generally select-into is faster than an explicit fetch, but perhaps not enough for you to worry about. Perhaps.
DeleteSee, context is all. If this is a query executed lots and lots of times, and almost always returns a row - select-into is better. If not executed often, meh.
If executed lots of times, and mostly fails, an explicit cursor is probably best.