case expressions and statements in oracle 9i

The CASE expression was introduced by Oracle in version 8i. It was a SQL-only expression that provided much greater flexibility than the functionally-similar DECODE function. The PL/SQL parser didn't understand CASE in 8i, however, which was a major frustration for developers (the workaround was to use views, dynamic SQL or DECODE).

Oracle 9i Release 1 (9.0) extends CASE capabilities with the following enhancements:

In this article, we will work through each of the new features and show a range of possibilities for the new syntax.

simple case expression

The simple CASE expression is new in 9i. In SQL, it is functionally equivalent to DECODE in that it tests a single value or expression for equality only. This is supposedly optimised for simple equality tests where the cost of repeating the test expression is high (although in most cases it is extremely difficult to show a performance difference over DECODE or the older searched CASE expression).

A simple CASE expression takes the following format. As with all CASE expression and statement formats in this article, it will evaluate from top to bottom and "exit" on the first TRUE condition.

CASE {value or expression}
   WHEN {value}
   THEN {something}
   [WHEN...]
   [THEN...]
   [ELSE...] --<-- NULL if not specified and no WHEN tests satisfied
END

The following is a contrived example of a simple CASE expression against the EMP table.

SQL> SELECT ename
  2  ,      job
  3  ,      CASE deptno
  4            WHEN 10
  5            THEN 'ACCOUNTS'
  6            WHEN 20
  7            THEN 'SALES'
  8            WHEN 30
  9            THEN 'RESEARCH'
 10            WHEN 40
 11            THEN 'OPERATIONS'
 12            ELSE 'UNKNOWN'
 13         END AS department
 14  FROM   emp;

ENAME      JOB       DEPARTMENT
---------- --------- ----------
SMITH      CLERK     SALES
ALLEN      SALESMAN  RESEARCH
WARD       SALESMAN  RESEARCH
JONES      MANAGER   SALES
MARTIN     SALESMAN  RESEARCH
BLAKE      MANAGER   RESEARCH
CLARK      MANAGER   ACCOUNTS
SCOTT      ANALYST   SALES
KING       PRESIDENT ACCOUNTS
TURNER     SALESMAN  RESEARCH
ADAMS      CLERK     SALES
JAMES      CLERK     RESEARCH
FORD       ANALYST   SALES
MILLER     CLERK     ACCOUNTS

14 rows selected.

searched case expression

The searched CASE expression is the 8i variant. This is much more flexible than a simple CASE expression or DECODE function. It can conduct multiple tests involving a range of different columns, expressions and operators. Each WHEN clause can include a number of AND/OR tests. It takes the following format (note that the expressions to evaluate are included within each WHEN clause).

CASE
   WHEN {test or tests}
   THEN {something}
   [WHEN {test or tests}]
   [THEN...]
   [ELSE...]
END

For example:

CASE
   WHEN column IN (val1, val2)
   AND  another_column > 0
   THEN something
   WHEN yet_another_column != 'not this value'
   THEN something_else
END

The following query against EMP shows how we might use searched CASE to evaluate the current pay status of each employee.

SQL> SELECT ename
  2  ,      job
  3  ,      CASE
  4            WHEN sal < 1000
  5            THEN 'Low paid'
  6            WHEN sal BETWEEN 1001 AND 2000
  7            THEN 'Reasonably well paid'
  8            WHEN sal BETWEEN 2001 AND 3001
  9            THEN 'Well paid'
 10            ELSE 'Overpaid'
 11         END AS pay_status
 12  FROM   emp;

ENAME      JOB       PAY_STATUS
---------- --------- --------------------
SMITH      CLERK     Low paid
ALLEN      SALESMAN  Reasonably well paid
WARD       SALESMAN  Reasonably well paid
JONES      MANAGER   Well paid
MARTIN     SALESMAN  Reasonably well paid
BLAKE      MANAGER   Well paid
CLARK      MANAGER   Well paid
SCOTT      ANALYST   Well paid
KING       PRESIDENT Overpaid
TURNER     SALESMAN  Reasonably well paid
ADAMS      CLERK     Reasonably well paid
JAMES      CLERK     Low paid
FORD       ANALYST   Well paid
MILLER     CLERK     Reasonably well paid

14 rows selected.

case expressions in pl/sql

As stated earlier, the SQL and PL/SQL parsers are the same from 9i onwards. This means that CASE expressions can be used in static implicit and explicit SQL cursors within PL/SQL. In addition to this, the CASE expression can also be used as an assignment mechanism, which provides an extremely elegant method for IF-THEN-ELSE-type constructs. For example, the following construct...

IF something = something THEN
   variable := value;
ELSE
   variable := alternative_value;
END IF;

...can now be written as a CASE expression as follows.

variable := CASE something
               WHEN something
               THEN value
               ELSE alternative_value
            END;

This flexibility is something that DECODE doesn't provide as it is a SQL-only function. Needless to say, both simple and searched CASE expressions can be used as above. The following example shows a simple CASE expression being used to assign a variable.

SQL> DECLARE
  2
  3     v_dummy  VARCHAR2(10) := 'DUMMY';
  4     v_assign VARCHAR2(10);
  5
  6  BEGIN
  7
  8     v_assign := CASE v_dummy
  9                    --
 10                    WHEN 'Dummy'
 11                    THEN 'INITCAP'
 12                    --
 13                    WHEN 'dummy'
 14                    THEN 'LOWER'
 15                    --
 16                    WHEN 'DUMMY'
 17                    THEN 'UPPER'
 18                    --
 19                    ELSE 'MIXED'
 20                    --
 21                 END;
 22
 23     DBMS_OUTPUT.PUT_LINE(
 24        'Variable v_dummy is in '||v_assign||' type case.'
 25        );
 26
 27  END;
 28  /
Variable v_dummy is in UPPER type case.

PL/SQL procedure successfully completed.

We can take this example a stage further and use the CASE expression directly inside the call to DBMS_OUTPUT as follows.

SQL> DECLARE
  2     v_dummy  VARCHAR2(10) := 'DUMMY';
  3  BEGIN
  4     DBMS_OUTPUT.PUT_LINE(
  5        'Variable v_dummy is in ' || CASE v_dummy
  6                                        WHEN 'Dummy'
  7                                        THEN 'INITCAP'
  8                                        WHEN 'dummy'
  9                                        THEN 'LOWER'
 10                                        WHEN 'DUMMY'
 11                                        THEN 'UPPER'
 12                                        ELSE 'MIXED'
 13                                     END || ' type case.' );
 14  END;
 15  /
Variable v_dummy is in UPPER type case.

PL/SQL procedure successfully completed.

Here we have removed the need for an intermediate variable. Similarly, CASE expressions can be used directly in function RETURN statements. In the following example, we will create a function that returns each employee's pay status using the CASE expression from our earlier examples.

SQL> CREATE FUNCTION pay_status (
  2                  sal_in IN NUMBER
  3                  ) RETURN VARCHAR2 IS
  4  BEGIN
  5     RETURN CASE
  6               WHEN sal_in < 1000
  7               THEN 'Low paid'
  8               WHEN sal_in BETWEEN 1001 AND 2000
  9               THEN 'Reasonably well paid'
 10               WHEN sal_in BETWEEN 2001 AND 3001
 11               THEN 'Well paid'
 12               ELSE 'Overpaid'
 13            END;
 14  END;
 15  /

Function created.

SQL> SELECT ename
  2  ,      pay_status(sal) AS pay_status
  3  FROM   emp;

ENAME      PAY_STATUS
---------- --------------------
SMITH      Low paid
ALLEN      Reasonably well paid
WARD       Reasonably well paid
JONES      Well paid
MARTIN     Reasonably well paid
BLAKE      Well paid
CLARK      Well paid
SCOTT      Well paid
KING       Overpaid
TURNER     Reasonably well paid
ADAMS      Reasonably well paid
JAMES      Low paid
FORD       Well paid
MILLER     Reasonably well paid

14 rows selected.

Of course, we need to balance the good practice of rules encapsulation with our performance requirements. If the CASE expression is only used in one SQL statement in our application, then in performance terms we will benefit greatly from "in-lining" the expression directly. If the business rule is used in numerous SQL statements across the application, we might be more prepared to pay the context-switch penalty and wrap it in a function as above.

Note that in some earlier versions of 9i, we might need to wrap the CASE expression inside TRIM to be able to return it directly from a function (i.e. RETURN TRIM(CASE...)). There is a "NULL-terminator" bug similar to a quite-well known variant in 8i Native Dynamic SQL (this would sometimes appear when attempting to EXECUTE IMMEDIATE a SQL statement fetched directly from a table).

ordering data with case expressions

We have already seen that CASE expressions provide great flexibility within both SQL and PL/SQL. CASE expressions can also be used in ORDER BY clauses to dynamically order data. This is especially useful in two ways:

In the following example, we will order the EMP data according to the JOB column but not alphabetically.

SQL> SELECT ename
  2  ,      job
  3  FROM   emp
  4  ORDER  BY CASE job
  5               WHEN 'PRESIDENT'
  6               THEN 1
  7               WHEN 'MANAGER'
  8               THEN 2
  9               WHEN 'ANALYST'
 10               THEN 3
 11               WHEN 'SALESMAN'
 12               THEN 4
 13               ELSE 5
 14            END;

ENAME      JOB
---------- ---------
KING       PRESIDENT
JONES      MANAGER
BLAKE      MANAGER
CLARK      MANAGER
SCOTT      ANALYST
FORD       ANALYST
ALLEN      SALESMAN
WARD       SALESMAN
MARTIN     SALESMAN
TURNER     SALESMAN
SMITH      CLERK
MILLER     CLERK
ADAMS      CLERK
JAMES      CLERK

14 rows selected.

As stated earlier, the second possibility is for user-defined ordering. This is most common on search screens where users can specify how they want their results ordered. It is quite common for developers to code complicated dynamic SQL solutions to support such requirements. With CASE expressions, however, we can avoid such complexity, especially when the number of ordering columns is low. In the following example, we will create a dummy procedure to output EMP data according to a user's preference for ordering.

SQL> CREATE FUNCTION order_emps( p_column IN VARCHAR2 )
  2     RETURN SYS_REFCURSOR AS
  3
  4     v_rc SYS_REFCURSOR;
  5
  6  BEGIN
  7
  8     DBMS_OUTPUT.PUT_LINE('Ordering by ' || p_column || '...');
  9
 10     OPEN v_rc FOR SELECT ename, job, hiredate, sal
 11                   FROM   emp
 12                   ORDER  BY
 13                          CASE UPPER(p_column)
 14                             WHEN 'ENAME'
 15                             THEN ename
 16                             WHEN 'SAL'
 17                             THEN TO_CHAR(sal,'fm0000')
 18                             WHEN 'JOB'
 19                             THEN job
 20                             WHEN 'HIREDATE'
 21                             THEN TO_CHAR(hiredate,'YYYYMMDD')
 22                          END;
 23
 24     RETURN v_rc;
 25
 26  END order_emps;
 27  /

Function created.

CASE expressions can only return a single datatype, so we need to cast NUMBER and DATE columns to VARCHAR2 as above. This can change their ordering behaviour, so we ensure that the format masks we use enable them to sort correctly.

Now we have the function in place, we can simulate a front-end application by setting up a refcursor variable in sqlplus and calling the function with different inputs as follows.

SQL> var rc refcursor;

SQL> set autoprint on

SQL> exec :rc := order_emps('job');
Ordering by job...

PL/SQL procedure successfully completed.

ENAME      JOB       HIREDATE         SAL
---------- --------- --------- ----------
SCOTT      ANALYST   19-APR-87       3000
FORD       ANALYST   03-DEC-81       3000
SMITH      CLERK     17-DEC-80        800
ADAMS      CLERK     23-MAY-87       1100
MILLER     CLERK     23-JAN-82       1300
JAMES      CLERK     03-DEC-81        950
JONES      MANAGER   02-APR-81       2975
CLARK      MANAGER   09-JUN-81       2450
BLAKE      MANAGER   01-MAY-81       2850
KING       PRESIDENT 17-NOV-81       5000
ALLEN      SALESMAN  20-FEB-81       1600
MARTIN     SALESMAN  28-SEP-81       1250
TURNER     SALESMAN  08-SEP-81       1500
WARD       SALESMAN  22-FEB-81       1250

14 rows selected.

SQL> exec :rc := order_emps('hiredate');
Ordering by hiredate...

PL/SQL procedure successfully completed.

ENAME      JOB       HIREDATE         SAL
---------- --------- --------- ----------
SMITH      CLERK     17-DEC-80        800
ALLEN      SALESMAN  20-FEB-81       1600
WARD       SALESMAN  22-FEB-81       1250
JONES      MANAGER   02-APR-81       2975
BLAKE      MANAGER   01-MAY-81       2850
CLARK      MANAGER   09-JUN-81       2450
TURNER     SALESMAN  08-SEP-81       1500
MARTIN     SALESMAN  28-SEP-81       1250
KING       PRESIDENT 17-NOV-81       5000
JAMES      CLERK     03-DEC-81        950
FORD       ANALYST   03-DEC-81       3000
MILLER     CLERK     23-JAN-82       1300
SCOTT      ANALYST   19-APR-87       3000
ADAMS      CLERK     23-MAY-87       1100

14 rows selected.

The overall benefits of this method are derived from having a single, static cursor compiled into our application code. With this, we do not need to resort to dynamic SQL solutions which are more difficult to maintain and debug but can also be slower to fetch due to additional soft parsing.

filtering data with case expressions

In addition to flexible ordering, CASE expressions can also be used to conditionally filter data or join datasets. In filters, CASE expressions can replace complex AND/OR filters, but this can sometimes have an impact on CBO arithmetic and resulting query plans, so care will need to be taken. We can see this as follows. First we will write a fairly complex set of predicates against an EMP-DEPT query.

SQL> SELECT e.ename
  2  ,      e.empno
  3  ,      e.job
  4  ,      e.sal
  5  ,      e.hiredate
  6  ,      d.deptno
  7  FROM   dept d
  8  ,      emp  e
  9  WHERE  d.deptno = e.deptno
 10  AND    NOT (    e.deptno = 10
 11              AND e.sal >= 1000 )
 12  AND    e.hiredate <= DATE '1990-01-01'
 13  AND    d.loc != 'CHICAGO';

ENAME           EMPNO JOB              SAL HIREDATE      DEPTNO
---------- ---------- --------- ---------- --------- ----------
SMITH            7369 CLERK            800 17-DEC-80         20
JONES            7566 MANAGER         2975 02-APR-81         20
SCOTT            7788 ANALYST         3000 19-APR-87         20
ADAMS            7876 CLERK           1100 23-MAY-87         20
FORD             7902 ANALYST         3000 03-DEC-81         20

5 rows selected.

We can re-write this using a CASE expression. It can be much easier as a "multi-filter" in certain scenarios, as we can work through our predicates in a much more logical fashion. We can see this below. All filters evaluating as true will be give a value of 0 and we will only return data that evaluates to 1.

SQL> SELECT e.ename
  2  ,      e.empno
  3  ,      e.job
  4  ,      e.sal
  5  ,      e.hiredate
  6  ,      d.deptno
  7  FROM   dept d
  8  ,      emp  e
  9  WHERE  d.deptno = e.deptno
 10  AND    CASE
 11            WHEN e.deptno = 10
 12            AND  e.sal >= 1000
 13            THEN 0
 14            WHEN e.hiredate > DATE '1990-01-01'
 15            THEN 0
 16            WHEN d.loc = 'CHICAGO'
 17            THEN 0
 18            ELSE 1
 19         END = 1;

ENAME           EMPNO JOB              SAL HIREDATE      DEPTNO
---------- ---------- --------- ---------- --------- ----------
SMITH            7369 CLERK            800 17-DEC-80         20
JONES            7566 MANAGER         2975 02-APR-81         20
SCOTT            7788 ANALYST         3000 19-APR-87         20
ADAMS            7876 CLERK           1100 23-MAY-87         20
FORD             7902 ANALYST         3000 03-DEC-81         20

5 rows selected.

As stated, care needs to be taken with this as it can change the CBO's decision paths. As we are only dealing with EMP and DEPT here, the following example ends up with the same join mechanism, but note the different filter predicates reported by DBMS_XPLAN (this is a 9i Release 2 feature). When costing the predicates, Oracle treats the entire CASE expression as a single filter, rather than each filter separately. With histograms or even the most basic column statistics, Oracle is able to cost the filters when we write them the "AND/OR way". With CASE, Oracle has no such knowledge to draw on.

SQL> EXPLAIN PLAN SET STATEMENT_ID = 'FILTERS'
  2  FOR
  3  SELECT e.ename
  4  ,      e.empno
  5  ,      e.job
  6  ,      e.sal
  7  ,      e.hiredate
  8  ,      d.deptno
  9  FROM   dept d
 10  ,      emp  e
 11  WHERE  d.deptno = e.deptno
 12  AND    NOT (    e.deptno = 10
 13              AND e.sal >= 1000 )
 14  AND    e.hiredate <= DATE '1990-01-01'
 15  AND    d.loc != 'CHICAGO';

Explained.

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY('PLAN_TABLE','FILTERS'));

PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------

--------------------------------------------------------------------
| Id  | Operation            |  Name       | Rows  | Bytes | Cost  |
--------------------------------------------------------------------
|   0 | SELECT STATEMENT     |             |    10 |   360 |     5 |
|*  1 |  HASH JOIN           |             |    10 |   360 |     5 |
|*  2 |   TABLE ACCESS FULL  | DEPT        |     3 |    27 |     2 |
|*  3 |   TABLE ACCESS FULL  | EMP         |    10 |   270 |     2 |
--------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("D"."DEPTNO"="E"."DEPTNO")
   2 - filter("D"."LOC"<>'CHICAGO')
   3 - filter(("E"."DEPTNO"<>10 OR "E"."SAL"<1000) AND
              "E"."HIREDATE"<=TO_DATE(' 1990-01-01 00:00:00', 'syyyy-mm-dd
              hh24:mi:ss'))

Note: cpu costing is off

20 rows selected.

SQL> EXPLAIN PLAN SET STATEMENT_ID = 'CASE'
  2  FOR
  3  SELECT e.ename
  4  ,      e.empno
  5  ,      e.job
  6  ,      e.sal
  7  ,      e.hiredate
  8  ,      d.deptno
  9  FROM   dept d
 10  ,      emp  e
 11  WHERE  d.deptno = e.deptno
 12  AND    CASE
 13            WHEN e.deptno = 10
 14            AND  e.sal >= 1000
 15            THEN 0
 16            WHEN e.hiredate > DATE '1990-01-01'
 17            THEN 0
 18            WHEN d.loc = 'CHICAGO'
 19            THEN 0
 20            ELSE 1
 21         END = 1;

Explained.

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY('PLAN_TABLE','CASE'));

PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------

--------------------------------------------------------------------
| Id  | Operation            |  Name       | Rows  | Bytes | Cost  |
--------------------------------------------------------------------
|   0 | SELECT STATEMENT     |             |     1 |    36 |     5 |
|*  1 |  HASH JOIN           |             |     1 |    36 |     5 |
|   2 |   TABLE ACCESS FULL  | DEPT        |     4 |    36 |     2 |
|   3 |   TABLE ACCESS FULL  | EMP         |    14 |   378 |     2 |
--------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("D"."DEPTNO"="E"."DEPTNO")
       filter(CASE  WHEN ("E"."DEPTNO"=10 AND "E"."SAL">=1000) THEN
              0 WHEN "E"."HIREDATE">TO_DATE(' 1990-01-01 00:00:00', 'syyyy-mm-dd
              hh24:mi:ss') THEN 0 WHEN "D"."LOC"='CHICAGO' THEN 0 ELSE 1 END =1)

Note: cpu costing is off

19 rows selected.

case statements (pl/sql only)

We have spent a lot of time looking at CASE expressions in this article. We will finish with a look at the new CASE statement. Most developers seem to use this term when they are in fact describing CASE expressions. The CASE statement is a PL/SQL-only construct that is similar to IF-THEN-ELSE. Its simple and searched formats are as follows.

CASE {variable or expression}
   WHEN {value}
   THEN {one or more operations};
   [WHEN..THEN]
   ELSE {default operation};
END CASE;

CASE
   WHEN {expression test or tests}
   THEN {one or more operations};
   [WHEN..THEN]
   ELSE {default operation};
END CASE;

Note the semi-colons. CASE statements do not return values like CASE expressions. CASE statements are IF tests that are used to decide which action(s) or operation(s) to execute. Note also the END CASE syntax. This is mandatory. In the following example, we will return to our dummy test but call a procedure within each evaluation.

SQL> DECLARE
  2
  3     v_dummy VARCHAR2(10) := 'DUMMY';
  4
  5     PROCEDURE output (input VARCHAR2) IS
  6     BEGIN
  7        DBMS_OUTPUT.PUT_LINE(
  8           'Variable v_dummy is in '||input||' type case.');
  9     END output;
 10
 11  BEGIN
 12
 13     CASE v_dummy
 14
 15        WHEN 'Dummy'
 16        THEN output('INITCAP');
 17
 18        WHEN 'dummy'
 19        THEN output('LOWER');
 20
 21        WHEN 'DUMMY'
 22        THEN output('UPPER');
 23
 24        ELSE output('MIXED');
 25
 26     END CASE;
 27
 28  END;
 29  /
Variable v_dummy is in UPPER type case.

PL/SQL procedure successfully completed.

CASE statements can be useful for very simple, compact and repeated tests (such as testing a variable for a range of values). Other than this, it is unlikely to draw many developers away from IF-THEN-ELSE. The main difference between CASE and IF is that the CASE statement must evaluate to something. Oracle has provided a built-in exception for this event; CASE_NOT_FOUND. The following example shows what happens if the CASE statement cannot find a true test. We will trap the CASE_NOT_FOUND and re-raise the exception to demonstrate the error message.

SQL> DECLARE
  2
  3     v_dummy VARCHAR2(10) := 'dUmMy';
  4
  5     PROCEDURE output (input VARCHAR2) IS
  6     BEGIN
  7        DBMS_OUTPUT.PUT_LINE(
  8           'Variable v_dummy is in '||input||' type case.');
  9     END output;
 10
 11  BEGIN
 12
 13     CASE v_dummy
 14
 15        WHEN 'Dummy'
 16        THEN output('INITCAP');
 17
 18        WHEN 'dummy'
 19        THEN output('LOWER');
 20
 21        WHEN 'DUMMY'
 22        THEN output('UPPER');
 23
 24     END CASE;
 25
 26  EXCEPTION
 27     WHEN CASE_NOT_FOUND THEN
 28        DBMS_OUTPUT.PUT_LINE('Ooops!');
 29        RAISE;
 30  END;
 31  /
Ooops!
DECLARE
*
ERROR at line 1:
ORA-06592: CASE not found while executing CASE statement
ORA-06512: at line 29

The workaround to this is simple: add an "ELSE NULL" to the CASE statement.

further reading

This completes our look at CASE expressions and statements. For further reading, see the online SQL Reference and the PL/SQL User's Guide and Reference.

source code

The source code for the examples in this article can be downloaded from here.

Adrian Billington, May 2002

Back to Top