Skip to Main Content
  • Questions
  • WHERE w/ CASE WHEN and NESTED CASE WHEN

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, Dane.

Asked: June 09, 2008 - 8:59 pm UTC

Last updated: August 20, 2008 - 8:51 am UTC

Version: 10.2.0.1

Viewed 10K+ times! This question is

You Asked

Good day.

I'm trying to use nested "CASE WHEN" clauses in my WHERE statement to in essence create a dynamic query based on a few input variables...

I know there are other programming languages available to me, but I'm trying to keep to as much a SQL based solution as possible (save for the reference variables on input)

They are nested CASE WHEN clauses. And I get differing errors based on the value I pass to the specific when clause.

I can't find anything 'advanced' on what I'm trying to do: or if I should just re-construct this into a series of UNION / UNION ALL statements based on the specific situation.

pseudo is as follows, as the primary SELECT can be anything:
:value is the variable
'ZZZZZ' is a generic term to return all if they exist (left joins seem applicable)

obviously, I'd like to replace additional (SELECT blabity FROM blah) statements in place of my ('X') values but the first doesn't fly and I'm not fond of extra typing w/o compiling so to speak.


    -- pseudo where clause
    WHERE my_key IN
    -- it doesn't appear to matter where you place your '(' and ')' so I've
    -- placed them in the when / then statement blocks for clarity of the 'IN'
    CASE
        WHEN :chk_Prospect = 'IP' THEN
          -- NOW Define the Prospect Refinements
             CASE
                 -- Now check each status - each is set with something
                 -- so each MUST exist in each area
                 WHEN :drpProsStatus  != 'ZZZZZ' THEN
                   -- this var holds either 'A', 'I', 'P', 'U'
                   -- for active, inactive, pending, unknown
                   -- THIS WORKS
                   ('X')
                   -- THIS DOES NOT WORK???
                   -- it returns oracle error ' missing right parenthesis'
                    -- ('X', 'Y', 'Z', 'A', 'B', 'C')
                   -- this returns oracle error 'expected number got char'
                   --(SELECT Amrinfo_Pidm FROM Amrinfo
                   --  WHERE Amrinfo_Status = :drpProsStatus.Main)
                 WHEN :drpProsStafAsgn != 'ZZZZZ' THEN
                   -- this holds the ID of the person responsible
                   ('X')
                 WHEN :drpProsProjInt  != 'ZZZZZ' THEN
                   -- this holds a person's interests
                   ('X')
                 WHEN :drpProsExtRate  != 'ZZZZZ' THEN
                   -- this holds a person's ratings
                   ('X')
                 ELSE
                   ('X')
             END
        WHEN :chk_NonProspect = 'NP' THEN
          -- NOW find those that exist not above, but in the following
          CASE
              WHEN :drpNonProsLifeAmt  != 'ZZZZZ' THEN
                -- some magical amount of purchasing
                ('X')
              WHEN :drpNonProsActc  != 'ZZZZZ' THEN
                -- some activity around their purchasing
                ('X')
              WHEN :drpNonProsSpecPurp  != 'ZZZZZ' THEN
                -- did they use any special internal codes
                ('X')
              WHEN:drpNonProsVarPurp  != 'ZZZZZ' THEN
               -- did they use any external related codes
                ('X')
              ELSE
                ('X')
          END
        ELSE
          ('X')
    END


Regards,
Dane

and Tom said...

A case statement returns a "thing", an attribute.

It does not return a "set"

...
-- THIS WORKS
('X')
-- THIS DOES NOT WORK???
-- it returns oracle error ' missing right parenthesis'
-- ('X', 'Y', 'Z', 'A', 'B', 'C')

....

('X') works because that is just long hand for 'X' - the () are meaningless there, ('X','Y') doesn't work because ('X','Y') is not an attribute, it is a 'set' and case cannot return that.


So, your CASE must return a single thing - in this "case", a collection.

And a collection can be used as a "set"


ops$tkyte%ORA10GR2> exec :x := 1

PL/SQL procedure successfully completed.

ops$tkyte%ORA10GR2> select *
  2    from all_users
  3   where user_id in
  4    (select column_value from TABLE(
  5       case when :x=1 then sys.odcinumberlist( 1,2,3,4,5 )
  6            when :x=2 then cast( multiset(select user_id from all_users where username like '%A%') as sys.odcinumberlist )
  7            end
  8    ) )
  9  /

USERNAME                          USER_ID CREATED
------------------------------ ---------- ---------
SYSTEM                                  5 30-JUN-05

ops$tkyte%ORA10GR2> exec :x := 2

PL/SQL procedure successfully completed.

ops$tkyte%ORA10GR2> /

USERNAME                          USER_ID CREATED
------------------------------ ---------- ---------
ANONYMOUS                              39 30-JUN-05
OLAPSYS                                47 30-JUN-05
SI_INFORMTN_SCHEMA                     45 30-JUN-05
SYSMAN                                 51 30-JUN-05
MDDATA                                 50 30-JUN-05
OPS$ORA10GR2                           56 14-DEC-05
BIG_TABLE                              58 14-DEC-05
A                                     330 30-MAY-08
MAP                                   263 07-JAN-08
QUOTA                                  94 22-FEB-06
AQ                                    228 15-OCT-07
PERFSTAT                              148 31-MAR-06

12 rows selected.

Rating

  (7 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Follow Up: WHERE w/ CASE WHEN and NESTED CASE WHEN

Dane, June 10, 2008 - 7:06 pm UTC

Your answer knocked loose what rocks were stuck in my head.

I simply changed the WHEN <> THEN SELECT <> to:
SELECT DISTINCT MY_KEY FROM MY_TABLE WHERE MY_KEY = KEY_IN_MY_TABLE AND OTHER_CONSTRAINTS
and it was like automagic...

I also appreciated the CAST(MULTISET(...)) example. I will have to play with that a bit more.

Regards

Excellent just what I needed

Matt, June 11, 2008 - 5:15 pm UTC

I had played around with using the CASE statement in the where clause to sql more dynamic but had also run into the same problem with needing multiple values returned for the in. Tried a whole host of methods using STRAGG and in-list functions but kept running into limitations Thanks for showing how I can do dynamic where clauses without using pl/sql.....

On a related note, if the requestor had been passing on value in or returning one value from the case statement couldn't he have used the case statement to either use the variable or use the column name, sort of like using the NVL OR EXPANSION where username = (nvl(:x,username)) but more readable.

variable x varchar2(30);

exec :x := 'SYSTEM';

select /*+
        gather_plan_statistics
    */
*
      from all_users
     where username = (case
                        when :x = 'default'
                        then username
                        else :x
                       end);

USERNAME                          USER_ID CREATED
------------------------------ ---------- ---------
SYSTEM                                  5 16-NOV-06

select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------
SQL_ID  1v6vcy1r5vgx2, child number 0
-------------------------------------
select /*+         gather_plan_statistics     */ *       from all_users
where username = (case                         when :x = 'default'
         then username                         else :x                        end)

Plan hash value: 3780144807

------------------------------------------------------------------------------------------
| Id  | Operation              | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------
|   1 |  NESTED LOOPS          |       |      1 |      1 |      1 |00:00:00.01 |      16 |
|   2 |   NESTED LOOPS         |       |      1 |      1 |      1 |00:00:00.01 |      13 |
|*  3 |    TABLE ACCESS FULL   | USER$ |      1 |      1 |      1 |00:00:00.01 |      10 |
|   4 |    TABLE ACCESS CLUSTER| TS$   |      1 |      1 |      1 |00:00:00.01 |       3 |
|*  5 |     INDEX UNIQUE SCAN  | I_TS# |      1 |      1 |      1 |00:00:00.01 |       1 |
|   6 |   TABLE ACCESS CLUSTER | TS$   |      1 |      1 |      1 |00:00:00.01 |       3 |
|*  7 |    INDEX UNIQUE SCAN   | I_TS# |      1 |      1 |      1 |00:00:00.01 |       1 |
------------------------------------------------------------------------------------------

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

   3 - filter(("U"."TYPE#"=1 AND "U"."NAME"=CASE :X WHEN 'default' THEN
              "U"."NAME" ELSE :X END ))
   5 - access("U"."DATATS#"="DTS"."TS#")
   7 - access("U"."TEMPTS#"="TTS"."TS#")


28 rows selected.

exec :x := 'default';

PL/SQL procedure successfully completed.

select /*+
        gather_plan_statistics
    */
*
      from all_users
     where username = (case
                        when :x = 'default'
                        then username
                        else :x
                       end);

USERNAME                          USER_ID CREATED
------------------------------ ---------- ---------
SYS                                     0 16-NOV-06
SYSTEM                                  5 16-NOV-06
OUTLN                                  11 16-NOV-06
DIP                                    19 16-NOV-06
......etc

70 rows selected.

select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST')); 

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
SQL_ID  1v6vcy1r5vgx2, child number 0
-------------------------------------
select /*+         gather_plan_statistics     */ *       from all_users
where username = (case                         when :x = 'default'
         then username                         else :x                        end)

Plan hash value: 3780144807

------------------------------------------------------------------------------------------
| Id  | Operation              | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------
|   1 |  NESTED LOOPS          |       |      1 |      1 |     70 |00:00:00.01 |     241 |
|   2 |   NESTED LOOPS         |       |      1 |      1 |     70 |00:00:00.01 |     160 |
|*  3 |    TABLE ACCESS FULL   | USER$ |      1 |      1 |     70 |00:00:00.01 |      14 |
|   4 |    TABLE ACCESS CLUSTER| TS$   |     70 |      1 |     70 |00:00:00.01 |     146 |
|*  5 |     INDEX UNIQUE SCAN  | I_TS# |     70 |      1 |     70 |00:00:00.01 |       6 |
|   6 |   TABLE ACCESS CLUSTER | TS$   |     70 |      1 |     70 |00:00:00.01 |      81 |
|*  7 |    INDEX UNIQUE SCAN   | I_TS# |     70 |      1 |     70 |00:00:00.01 |       6 |
------------------------------------------------------------------------------------------

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

   3 - filter(("U"."TYPE#"=1 AND "U"."NAME"=CASE :X WHEN 'default' THEN
              "U"."NAME" ELSE :X END ))
   5 - access("U"."DATATS#"="DTS"."TS#")
   7 - access("U"."TEMPTS#"="TTS"."TS#")


28 rows selected.

SQL> 


I'm going to play around with your method and see how far I can stretch it, so far it looks like it performs well, but I have one report where they have 15 different predicates that they may or may not use depending on the users response.

I may have to try harder to get them to let me send the sql statment to a procedure to get some speed out of it but maybe your solution will work.

Thanks again for the great work

WHERE w/ CASE WHEN and NESTED CASE WHEN

ravi, July 10, 2008 - 4:58 pm UTC

Tom,

Can you please explain about sys.odcinumberlist

Thanks,
Ravi
Tom Kyte
July 10, 2008 - 6:08 pm UTC

it is just a type that comes with the database in current releases, you could instead

create type yourNumberList as table of number
/

and use that instead.

Can I achieve the same in 8i?

Alexandre Beaulieu, August 15, 2008 - 11:26 am UTC

Hello Tom,

I'm trying to do exactly what is demonstrated to work in 10g in your answer to the original question but in 8i(8.1.7.4.0).

I keep getting the error "ORA-22905: cannot access rows from a non-nested table item".

I created the following type in a package:

TYPE LocStatusList IS TABLE OF VARCHAR2(2);

And then try to use it as in you example:

<...query>
WHERE
<my field> IN(
SELECT
COLUMN_VALUE
FROM
TABLE(
CASE WHEN '&parameter' = 'EA' THEN <package name>.LocStatusList('EA', 'PA')
<other whens...>
END
)
)

As far as you know, is it supposed to work the same in 8i? If not, is there a workaround?

If you need more specific information to help me, I'll of course be glad to provide you with it. I just thought i'd ask this follow-up question in a general manner since there is unfortunately quite a lot of questions I had in the past (although not here) about 8i to which the answer was "Does not work in 8i."...

Thank you very much.

Alex
Tom Kyte
August 20, 2008 - 8:44 am UTC

give full, simple, small example. I see "&" in there and all - need you to help me reproduce your issue.


You may well have to use CAST in 8i - but I want to see your FULL example in order to answer your question.

yes, you can do this in 8i


ops$tkyte@ORA817DEV> create or replace type myTabType as table of varchar2(30)
  2  /

Type created.

ops$tkyte@ORA817DEV> declare
  2          l_data myTabType := myTabType( user, 'SYS' );
  3  begin
  4          for x in (select * from all_users where username in
  5          (select * from TABLE( cast( l_data as myTabType ) )) )
  6          loop
  7                  dbms_output.put_line( x.username || ', ' || x.user_id );
  8          end loop;
  9  end;
 10  /
OPS$TKYTE, 772
SYS, 0

PL/SQL procedure successfully completed.

Can I achieve the same in 8i? (2)

Alexandre Beaulieu, August 15, 2008 - 2:02 pm UTC

Hello Tom,

I'd like to update my previous follow-up question above.

I tried to cast the table as its own type as I read it was necessary for 8i.

However, I still get the ORA-22905 error!?


Here is how it looks now:

WHERE
<my field> IN(
SELECT
COLUMN_VALUE
FROM
TABLE(
CASE WHEN '&PARAMETER' = 'EA' THEN
CAST(<package name>.LocStatusList('EA', 'PA') AS <package name>.LocStatusList)
<other whens>
END
)
)

Thank you,

Alex
Tom Kyte
August 20, 2008 - 8:48 am UTC

ops$tkyte@ORA817DEV> declare
  2          l_data myTabType := myTabType( user, 'SYS' );
  3  begin
  4          for x in (select * from all_users where username in
  5          (select * from TABLE( myTabType(user,'SYS') )) )
  6          loop
  7                  dbms_output.put_line( x.username || ', ' || x.user_id );
  8          end loop;
  9  end;
 10  /
OPS$TKYTE, 772
SYS, 0

PL/SQL procedure successfully completed.



full example please, although I see:

CAST(<package name>.LocStatusList('EA', 'PA') AS <package
name>.LocStatusList



you need to use a SQL type, not a plsql type - create the type in SQL - not in a package.

Can I achieve the same in 8i? (3)

Alexandre Beaulieu, August 15, 2008 - 5:02 pm UTC

Hello again Tom,

Another update for my follow-up question...

I read that I couldn't use a type declared in a package from SQL so I dropped it from the package and did a regular create type.

create or replace type LocStatusList is table of char(2);

Still got ORA-22905 when executing my query against it...

then I declared it in the following way, as seen in SQL reference examples for cast():

create or replace type LocStatus is object(status char(2));
create or replace TYPE LocStatusList IS TABLE OF LocStatus;

Still got ORA-22905 when executing my query against it...


Hope the supplementary information helps when you have some time to look into this...

Thanks,

Alex
Tom Kyte
August 20, 2008 - 8:51 am UTC

need that example :)

Can I achieve the same in 8i? (4th and final!)

Alexandre Beaulieu, August 20, 2008 - 11:41 pm UTC

Hello Tom,

This is my final follow-up "message" regarding my previous follow-up question entitled "Can I achieve the same in 8i?", for which I also posted a few updates during the day following my original posting, as I found in other threads here some element to solve my problem. I guess I wanted to show I was somewhat progressing...

Here is the updated version of a non-working example I should have provided initially (yes it is only sql, not pl/sql, it wasn't obvious since I was referring to using a type in a package while using sqlplus '&' type parameters...mea culpa):

create or replace type myTabType as table of varchar2(30);


SELECT
*
FROM
ALL_USERS
WHERE
USERNAME IN(
SELECT
COLUMN_VALUE
FROM
TABLE(
CASE
WHEN '&MYPARAM' = '1'
THEN CAST(MyTabType('SYS', USER) AS MyTabType)
ELSE CAST(MyTabType('SYSTEM', USER) AS MyTabType)
END
)
);

And here is the same example, modified to demonstrate the way it finally worked for me in 8i:

SELECT
*
FROM
ALL_USERS
WHERE
USERNAME IN(
SELECT
*
FROM
TABLE(CAST(
CASE
WHEN '&MYPARAM' = '1'
THEN MyTabType('SYS', USER)
ELSE MyTabType('SYSTEM', USER)
END
AS MyTabType))
)

As you can see, all I needed to do (after knowing I had to use a sql type and to cast the collection, of course...) was casting the whole case statement instead of casting each of its potential outputs separately.

Thank you for your time, Tom!

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library