You are on page 1of 19

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:

  

a new simple CASE expression (8i CASE was a "searched" or "switched" expression); a new CASE statement; a PL/SQL construct equivalent to IF-THEN-ELSE; and full PL/SQL support for both types of CASE expression; in SQL and in PL/SQL constructs (in 9i, the SQL and PL/SQL parsers are the same).

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 3 4 5 6 7 , , job CASE deptno WHEN 10 THEN 'ACCOUNTS' WHEN 20 THEN 'SALES'

8 9 10 11 12 13 14 FROM

WHEN 30 THEN 'RESEARCH' WHEN 40 THEN 'OPERATIONS' ELSE 'UNKNOWN' END AS department emp;

ENAME

JOB

DEPARTMENT

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

PRESIDENT ACCOUNTS SALESMAN CLERK CLERK ANALYST CLERK RESEARCH SALES RESEARCH SALES 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 3 4 5 6 7 8 9 10 11 12 FROM , , job CASE WHEN sal < 1000 THEN 'Low paid' WHEN sal BETWEEN 1001 AND 2000 THEN 'Reasonably well paid' WHEN sal BETWEEN 2001 AND 3001 THEN 'Well paid' ELSE 'Overpaid' END AS pay_status emp;

ENAME

JOB

PAY_STATUS

---------- --------- -------------------SMITH ALLEN CLERK SALESMAN Low paid Reasonably well paid

WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER

SALESMAN MANAGER SALESMAN MANAGER MANAGER ANALYST

Reasonably well paid Well paid Reasonably well paid Well paid Well paid Well paid

PRESIDENT Overpaid SALESMAN CLERK CLERK ANALYST CLERK Reasonably well paid Reasonably well paid Low paid Well paid 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 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 END; / DBMS_OUTPUT.PUT_LINE( 'Variable v_dummy is in '||v_assign||' type case.' ); v_assign := CASE v_dummy -WHEN 'Dummy' THEN 'INITCAP' -WHEN 'dummy' THEN 'LOWER' -WHEN 'DUMMY' THEN 'UPPER' -ELSE 'MIXED' -END; BEGIN v_dummy VARCHAR2(10) := 'DUMMY';

v_assign VARCHAR2(10);

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

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 3 4 5 6 7 8 9 10 11 12 BEGIN RETURN CASE WHEN sal_in < 1000 THEN 'Low paid' WHEN sal_in BETWEEN 1001 AND 2000 THEN 'Reasonably well paid' WHEN sal_in BETWEEN 2001 AND 3001 THEN 'Well paid' ELSE 'Overpaid' sal_in IN NUMBER ) RETURN VARCHAR2 IS

13 14 15 END; /

END;

Function created.

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

ENAME

PAY_STATUS

---------- -------------------SMITH ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER Low paid Reasonably well paid Reasonably well paid Well paid Reasonably well paid Well paid Well paid Well paid Overpaid Reasonably well paid Reasonably well paid Low paid Well paid 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:

 

when we need to order data with no inherent order properties; and when we need to support user-defined ordering from a front-end application.

In the following example, we will order the EMP data according to the JOB column but not alphabetically. SQL> SELECT ename 2 3 4 5 6 7 8 9 10 11 12 13 14 , FROM ORDER job emp BY CASE job WHEN 'PRESIDENT' THEN 1 WHEN 'MANAGER' THEN 2 WHEN 'ANALYST' THEN 3 WHEN 'SALESMAN' THEN 4 ELSE 5 END;

ENAME

JOB

---------- --------KING JONES BLAKE CLARK SCOTT FORD ALLEN PRESIDENT MANAGER MANAGER MANAGER ANALYST ANALYST SALESMAN

WARD MARTIN TURNER SMITH MILLER ADAMS JAMES

SALESMAN SALESMAN SALESMAN CLERK CLERK CLERK 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 OPEN v_rc FOR SELECT ename, job, hiredate, sal FROM ORDER emp BY CASE UPPER(p_column) WHEN 'ENAME' THEN ename WHEN 'SAL' THEN TO_CHAR(sal,'fm0000') WHEN 'JOB' THEN job WHEN 'HIREDATE' DBMS_OUTPUT.PUT_LINE('Ordering by ' || p_column || '...'); BEGIN v_rc SYS_REFCURSOR; RETURN SYS_REFCURSOR AS

21 22 23 24 25 26 27 END order_emps; / RETURN v_rc;

THEN TO_CHAR(hiredate,'YYYYMMDD') END;

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 FORD SMITH ADAMS MILLER JAMES JONES CLARK ANALYST ANALYST CLERK CLERK CLERK CLERK MANAGER MANAGER 19-APR-87 03-DEC-81 17-DEC-80 23-MAY-87 23-JAN-82 03-DEC-81 02-APR-81 09-JUN-81 3000 3000 800 1100 1300 950 2975 2450

BLAKE KING ALLEN MARTIN TURNER WARD

MANAGER

01-MAY-81

2850 5000 1600 1250 1500 1250

PRESIDENT 17-NOV-81 SALESMAN SALESMAN SALESMAN SALESMAN 20-FEB-81 28-SEP-81 08-SEP-81 22-FEB-81

14 rows selected.

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

PL/SQL procedure successfully completed.

ENAME

JOB

HIREDATE

SAL

---------- --------- --------- ---------SMITH ALLEN WARD JONES BLAKE CLARK TURNER MARTIN KING JAMES FORD MILLER SCOTT ADAMS CLERK SALESMAN SALESMAN MANAGER MANAGER MANAGER SALESMAN SALESMAN 17-DEC-80 20-FEB-81 22-FEB-81 02-APR-81 01-MAY-81 09-JUN-81 08-SEP-81 28-SEP-81 800 1600 1250 2975 2850 2450 1500 1250 5000 950 3000 1300 3000 1100

PRESIDENT 17-NOV-81 CLERK ANALYST CLERK ANALYST CLERK 03-DEC-81 03-DEC-81 23-JAN-82 19-APR-87 23-MAY-87

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 3 4 5 6 7 8 9 10 11 12 13 AND AND , , , , , FROM , WHERE AND e.empno e.job e.sal e.hiredate d.deptno dept d emp e

d.deptno = e.deptno NOT ( e.deptno = 10

AND e.sal >= 1000 ) e.hiredate <= DATE '1990-01-01' d.loc != 'CHICAGO';

ENAME

EMPNO JOB

SAL HIREDATE

DEPTNO

---------- ---------- --------- ---------- --------- ---------SMITH JONES SCOTT ADAMS FORD 7369 CLERK 7566 MANAGER 7788 ANALYST 7876 CLERK 7902 ANALYST 800 17-DEC-80 2975 02-APR-81 3000 19-APR-87 1100 23-MAY-87 3000 03-DEC-81 20 20 20 20 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 , , , , , FROM , WHERE AND e.empno e.job e.sal e.hiredate d.deptno dept d emp e

d.deptno = e.deptno CASE WHEN e.deptno = 10 AND e.sal >= 1000

THEN 0 WHEN e.hiredate > DATE '1990-01-01' THEN 0 WHEN d.loc = 'CHICAGO' THEN 0 ELSE 1 END = 1;

ENAME

EMPNO JOB

SAL HIREDATE

DEPTNO

---------- ---------- --------- ---------- --------- ---------SMITH JONES SCOTT ADAMS FORD 7369 CLERK 7566 MANAGER 7788 ANALYST 7876 CLERK 7902 ANALYST 800 17-DEC-80 2975 02-APR-81 3000 19-APR-87 1100 23-MAY-87 3000 03-DEC-81 20 20 20 20 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 3 4 5 6 7 8 9 10 11 12 13 14 15 AND AND FOR SELECT e.ename , , , , , FROM , WHERE AND e.empno e.job e.sal e.hiredate d.deptno dept d emp e

d.deptno = e.deptno NOT ( e.deptno = 10

AND e.sal >= 1000 ) e.hiredate <= DATE '1990-01-01' 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 1 | 2 | 3 | HASH JOIN TABLE ACCESS FULL TABLE ACCESS FULL | | | DEPT | EMP | | | | 10 | 10 | 3 | 10 | 360 | 360 | 27 | 270 | 5 | 5 | 2 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 FOR SELECT e.ename , , , , , FROM , WHERE AND e.empno e.job e.sal e.hiredate d.deptno dept d emp e

d.deptno = e.deptno CASE WHEN e.deptno = 10 AND e.sal >= 1000

THEN 0 WHEN e.hiredate > DATE '1990-01-01' THEN 0 WHEN d.loc = 'CHICAGO' THEN 0 ELSE 1 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 | 2 | 3 | HASH JOIN TABLE ACCESS FULL TABLE ACCESS FULL | | | DEPT | EMP | | | | 1 | 1 | 4 | 14 | 36 | 36 | 36 | 378 | 5 | 5 | 2 | 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 4 5 6 7 8 9 10 11 12 13 14 15 16 17 WHEN 'Dummy' THEN output('INITCAP'); CASE v_dummy BEGIN PROCEDURE output (input VARCHAR2) IS BEGIN DBMS_OUTPUT.PUT_LINE( 'Variable v_dummy is in '||input||' type case.'); END output; v_dummy VARCHAR2(10) := 'DUMMY';

18 19 20 21 22 23 24 25 26 27 28 29 END; /

WHEN 'dummy' THEN output('LOWER');

WHEN 'DUMMY' THEN output('UPPER');

ELSE output('MIXED');

END CASE;

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 mustevaluate 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 4 5 6 7 8 9 10 11 12 13 14 CASE v_dummy BEGIN PROCEDURE output (input VARCHAR2) IS BEGIN DBMS_OUTPUT.PUT_LINE( 'Variable v_dummy is in '||input||' type case.'); END output; v_dummy VARCHAR2(10) := 'dUmMy';

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 END; /

WHEN 'Dummy' THEN output('INITCAP');

WHEN 'dummy' THEN output('LOWER');

WHEN 'DUMMY' THEN output('UPPER');

END CASE;

EXCEPTION WHEN CASE_NOT_FOUND THEN DBMS_OUTPUT.PUT_LINE('Ooops!'); RAISE;

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.