NoBullMan
NoBullMan

Reputation: 2188

PL/SQL "Where" conditional "WHERE" clause

I am trying to make a a query against a table and am kind of stuck in constructing the WHERE clause. Table has a column "SUBCLASS" that has a value like "UN" or "U*".

"U*" implies a matching of "U" and any other character(e.g. UB, UC, ...).

Table looks like:

ID    ISC    SUBCLASS
---   ----   ---------
1     ABC        UN
2     DEF        UN
3     DEF        U*

Given a string UC12341001000012 or UN12341001000012, how would I construct the WHERE clause.

I tried:

SELECT * FROM MYTABLE
WHERE x AND y AND
(SUBCLASS='UC' OR SUBSTR(SUBCLASS, 1, 1) = SUBSTR('UC',1,1))

but it returns all rows. (I am using 'UC' here but actually it is a parameter passed to a stored procedure).

So, given UC12341001000012, I should get the third record, given UN12341001000012 I should get first two records.

Upvotes: 2

Views: 110

Answers (1)

Alex Poole
Alex Poole

Reputation: 191570

The (slightly) tricky bit is not including the wildcard U* row when there are exact matches. There are various approaches with subqueries and unions etc.; this one uses an inline view to flag and count the exact and wildcard matches, and then filters that inline view:

select id, isc, subclass, exact, wild
from (
  select id, isc, subclass,
    case when subclass = substr(:str, 1, 2) then 'Y' end as exact,
    case when subclass = substr(:str, 1, 1) || '*' then 'Y' end as wild,
    count(case when subclass = substr(:str, 1, 2) then subclass end) over () as exact_cnt
  from mytable
  where subclass like substr(:str, 1, 1) || '%' -- optional
)
where exact = 'Y' or (wild = 'Y' and exact_cnt = 0)

I've used a :str bind variable in place of your shorter literal, partly because I think it's clearer, but also because with just UC it isn't obvious why I've used more substr() calls than you had; since with your full longer values you only want to look at the first two anyway.

You can change it around a bit to not repeat the case expression (with another layer of inline view/CTE that you then count from), or change the inner filter to explicitly look for the same things the case expression is checking (or leave it out - depends on volume, indexes...), etc., but hopefully gives you the idea.

With a CTE to provide your sample subclass data:

var str varchar2(20);

exec :str := 'UC12341001000012';

-- CTE for sample data
with mytable (id, isc, subclass) as (
  select 1, 'ABC', 'UN' from dual
  union all select 2, 'DEF', 'UN' from dual
  union all select 3, 'DEF', 'U*' from dual
)
-- actual query
select id, isc, subclass
from (
  select id, isc, subclass,
    case when subclass = substr(:str, 1, 2) then 'Y' end as exact,
    case when subclass = substr(:str, 1, 1) || '*' then 'Y' end as wild,
    count(case when subclass = substr(:str, 1, 2) then subclass end) over () as exact_cnt
  from mytable
  where subclass like substr(:str, 1, 1) || '%' -- optional
)
where exact = 'Y' or (wild = 'Y' and exact_cnt = 0);

        ID ISC SU
---------- --- --
         3 DEF U*

exec :str := 'UN12341001000012';

<same query>

        ID ISC SU
---------- --- --
         1 ABC UN
         2 DEF UN

in case of multiple rows being returned

If you only want one of the exact-match rows, you could add a row_number() call in the inline view - with a suitable order by for however you want to split ties - and then add that to the outer filter:

select id, isc, subclass
from (
  select id, isc, subclass,
    case when subclass = substr(:str, 1, 2) then 'Y' end as exact,
    case when subclass = substr(:str, 1, 1) || '*' then 'Y' end as wild,
    count(case when subclass = substr(:str, 1, 2) then subclass end) over () as exact_cnt,
    row_number() over (partition by subclass order by isc) as rn
  from mytable
  where subclass like substr(:str, 1, 1) || '%' -- optional
)
where (exact = 'Y' or (wild = 'Y' and exact_cnt = 0))
and rn =1

... or you can initially select all three rows but order them so that the wildcard one comes last, before applying a rownum filter:

select id, isc, subclass
from (
  select id, isc, subclass
  from mytable
  where subclass = substr(:str, 1, 2)
  or subclass = substr(:str, 1, 1) || '*'
  order by case when subclass like '_*' then 2 else 1 end,
    isc -- or however you want to split ties
)
where rownum = 1

Upvotes: 1

Related Questions