Documentos de Académico
Documentos de Profesional
Documentos de Cultura
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.
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.
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
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.
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);
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';
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.
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).
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
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
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;
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
MANAGER
01-MAY-81
PRESIDENT 17-NOV-81 SALESMAN SALESMAN SALESMAN SALESMAN 20-FEB-81 28-SEP-81 08-SEP-81 22-FEB-81
14 rows selected.
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.
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
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
AND e.sal >= 1000 ) e.hiredate <= DATE '1990-01-01' d.loc != 'CHICAGO';
Explained.
PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------
-------------------------------------------------------------------| |* |* |* 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 |
--------------------------------------------------------------------
---------------------------------------------------
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'))
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
THEN 0 WHEN e.hiredate > DATE '1990-01-01' THEN 0 WHEN d.loc = 'CHICAGO' THEN 0 ELSE 1 END = 1;
Explained.
PLAN_TABLE_OUTPUT ---------------------------------------------------------------------------
-------------------------------------------------------------------| |* | | 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 |
--------------------------------------------------------------------
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)
19 rows selected.
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; /
ELSE output('MIXED');
END 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; /
END CASE;
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.