Skip to main content

PL/SQL 101: Writing conditional logic in PL/SQL


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:

BEGIN
   IF l_hire_date < SYSDATE
   THEN
      send_survey_request (l_employee_id);
   END IF;
END;

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:

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.
OK, so much for the kinda boring IF statement. Let's have fun with CASE!

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).

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:

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.
Here's the function using the IF statement:

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:

  • 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!

Comments

  1. 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.

    Also, 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.

    ReplyDelete
    Replies
    1. I was thinking about including NVL but then, well, got lazy. :-)

      In 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.

      Delete
    2. I believe Oracle converts the decode to case at parse time?

      Delete
  2. Not only that DECODE should be converted to CASE.
    But also NVL should be converted to COALESCE. The reason is better evaluation.

    ReplyDelete
    Replies
    1. 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 :-)

      Delete
  3. Maybe contentious, but exception handling can be a form of conditional logic?

    Begin
    Select name Into vName From emp;
    ProcessName(vName)
    Exception When NoDataFound
    ProcessNoName;
    End;

    ReplyDelete
    Replies
    1. Well, it's not contentious in that you are correct that one could employ exception handling as a form of conditional logic.

      The 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

      :-)

      Delete
    2. I'd like to see a post with some alternatives to the above.

      (But not Select MAX(name)... to suppress the error!)

      Delete
    3. 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?

      Delete
    4. Yes. 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.

      Delete
    5. I 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?"

      I 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.

      Delete
    6. 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. ;)

      Delete
    7. Yes to both your tweaks; they are an improvement on my quick hack of code. Stay positive! :-)

      Delete
    8. Mmmm... 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.

      Delete
    9. I 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.

      Delete
    10. That'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.

      Specifically, 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.

      Delete
    11. 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.

      See, 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.

      Delete

Post a Comment

Popular posts from this blog

Running out of PGA memory with MULTISET ops? Watch out for DISTINCT!

A PL/SQL team inside Oracle made excellent use of nested tables and MULTISET operators in SQL, blending data in tables with procedurally-generated datasets (nested tables).  All was going well when they hit the dreaded: ORA-04030: out of process memory when trying to allocate 2032 bytes  They asked for my help.  The error occurred on this SELECT: SELECT  *    FROM header_tab trx    WHERE (generated_ntab1 SUBMULTISET OF trx.column_ntab)       AND ((trx.column_ntab MULTISET             EXCEPT DISTINCT generated_ntab2) IS EMPTY) The problem is clearly related to the use of those nested tables. Now, there was clearly sufficient PGA for the nested tables themselves. So the problem was in executing the MULTISET-related functionality. We talked for a bit about dropping the use of nested tables and instead doing everything in SQL, to avoid the PGA error. That would, however require lots of wo...

How to Pick the Limit for BULK COLLECT

This question rolled into my In Box today: In the case of using the LIMIT clause of BULK COLLECT, how do we decide what value to use for the limit? First I give the quick answer, then I provide support for that answer Quick Answer Start with 100. That's the default (and only) setting for cursor FOR loop optimizations. It offers a sweet spot of improved performance over row-by-row and not-too-much PGA memory consumption. Test to see if that's fast enough (likely will be for many cases). If not, try higher values until you reach the performance level you need - and you are not consuming too much PGA memory.  Don't hard-code the limit value: make it a parameter to your subprogram or a constant in a package specification. Don't put anything in the collection you don't need. [from Giulio Dottorini] Remember: each session that runs this code will use that amount of memory. Background When you use BULK COLLECT, you retrieve more than row with each fetch, ...

PL/SQL 101: Three ways to get error message/stack in PL/SQL

The PL/SQL Challenge quiz for 10 September - 16 September 2016 explored the different ways you can obtain the error message / stack in PL/SQL. Note: an error stack is a sequence of multiple error messages that can occur when an exception is propagated and re-raised through several layers of nested blocks. The three ways are: SQLERRM - The original, traditional and (oddly enough) not currently recommended function to get the current error message. Not recommended because the next two options avoid a problem which you are unlikely  to run into: the error stack will be truncated at 512 bytes, and you might lose some error information. DBMS_UTILITY.FORMAT_ERROR_STACK - Returns the error message / stack, and will not truncate your string like SQLERRM will. UTL_CALL_STACK API - Added in Oracle Database 12c, the UTL_CALL_STACK package offers a comprehensive API into the execution call stack, the error stack and the error backtrace.  Note: check out this LiveSQL script if...