Skip to Main Content

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, stephan.

Asked: January 22, 2024 - 11:30 pm UTC

Last updated: January 26, 2024 - 6:42 pm UTC

Version: 19c

Viewed 1000+ times

You Asked

Here's the simplest test case I could come up with that demonstrates what I'm seeing. The actual query I'm trying to write is against a couple v$ views, and livesql doesn't seem to come with privileges on those.

create table mytab
( name varchar2(30),
  value varchar2(60) );

insert into mytab values( 'home_phone', null );
insert into mytab values( 'cell_phone', '867-5309' );

commit;

select coalesce( home.value, cell.value, 'None' )
from
( select value from mytab where name = 'home_phone' ) home,
( select value from mytab where name = 'cell_phone' ) cell;

select coalesce( home.value, cell.value, 'None' )
from
( select value from mytab where name = 'bad_phone' ) home,
( select value from mytab where name = 'cell_phone' ) cell;




The first query behaves exactly as expected; because the value for home_phone is null and the value for cell_phone isn't, it returns the value for cell_phone. The second one is the one that threw me - I would expect if there are no rows matching, for "bad_server" coalesce would move to the second result and return the value for cell_phone again.

So my question is two-fold:
Is this expected and correct behavior? I think there's an argument to be made that it doesn't match the sorta intuitive understanding most people will have of "returns the first non-null expression".

And if this is correct, what's the best way to write a query that will return the value for home_phone if there's a row that matches, and moves on to cell_phone if there isn't a home_phone record?

with LiveSQL Test Case:

and Chris said...

This has nothing to do with coalesce, remove it and you still get no rows:
select *
from
( select value from mytab where name = 'bad_phone' ) home,
( select value from mytab where name = 'cell_phone' ) cell;

--no rows selected


The issue is you have a cross join between the queries. This returns the Cartesian product of the two data sets.

If either data set is empty (returns no rows), the final data set is empty.

Nothing matches bad_phone => no rows from this query => the cross join returns no rows

There are various ways you can solve this, here's a couple:

- PIVOT the values, then you can use coalesce to return the first you want
- Use a CASE expression to check the values in the order you want

e.g.:

select coalesce ( home, cell, 'none' ) from mytab 
pivot ( 
  min ( value ) for name in ( 'bad_phone' as home, 'cell_phone' as cell )
);
/*
COALESCE(HOME,CELL,'NONE')
------------------------------------------------------------
867-5309
*/
select * from mytab
where  case 
  when name = 'bad_phone' then 1 
  when name = 'cell_phone' then 1
end = 1;
/*
NAME                           VALUE
------------------------------ ------------------------------------------------------------
cell_phone                     867-5309  
*/

Rating

  (3 ratings)

Comments

COALESCE solution: use scalar subqueries

mathguy, January 24, 2024 - 3:00 pm UTC

A solution closer in spirit to what you were trying to do looks like this:

select coalesce(
  ( select value from mytab where name = 'bad_phone' ),
  ( select value from mytab where name = 'cell_phone' )
)    -- as some_alias
from  dual;


Here the SELECT VALUE ... subqueries are in the outer SELECT clause, not the FROM clause. Used like that in SELECT, they are supposed to return a single value (that is: one column in one row); if the query returns no rows, SQL interprets that as NULL, as you were trying to do. If either "scalar" subquery returns more than one row, you'll get an error, as you probably should. (Perhaps that is not even possible, through constraints on MYTAB.)

Note that the outer query selects from DUAL - only the scalar subqueries are against the real "data" table.
Connor McDonald
January 25, 2024 - 5:56 am UTC

nice input

Using SQL/JSON

Stew Ashton, January 25, 2024 - 9:44 am UTC

When accessing a field in a JSON object, by default a missing field returns a NULL value, which is what you want here. Not necessarily the "best" way, but it does avoid a bit of repetition in the code.
with json_data(j) as (
  select json_objectagg(name value value) from mytab
)
select coalesce(jd.j.home_phone, jd.j.cell_phone, 'none') phone
from json_data jd
union all
select coalesce(jd.j.bad_phone, jd.j.cell_phone, 'none') phone
from json_data jd;

PHONE       
867-5309    
867-5309 

Best regards,
Stew
Chris Saxon
January 25, 2024 - 2:56 pm UTC

Thanks for the suggestion.

A general way to solve this kind of task

mathguy, January 26, 2024 - 1:32 am UTC

Not sure this is the "best" way, but it is a good way (I think).

I assume the end user will search for a phone number (perhaps for a single customer, identified by ID) by giving several options for "name" and an order of priority. This can be done in various ways in the user interface; since that isn't really the question here, I will simulate it with a subquery named REQUEST in the WITH clause. Given the list 'bad_phone', 'home_phone', 'cell_phone', we are looking for the first non-null phone number matching one of the phone types, in the order given.

Here's how I would do that:

with
  request (type, priority) as (
    select 'bad_phone' , 1 from dual union all
    select 'home_phone', 2 from dual union all
    select 'cell_phone', 3 from dual
  )
select min(m.name)  keep (dense_rank first order by r.priority) as name,
       min(m.value) keep (dense_rank first order by r.priority) as value
from   mytab m join request r on m.name = r.type
where  m.value is not null
;


Note that if none of the names match, or if the only names that do match have NULL value, the query will still return one row: both NAME and VALUE will be NULL. If this is not the desired output (although that IS the output from the other solutions offered so far), you can add GROUP BY NULL at the end of the query; then the query will return no rows. This is a subtle difference between a query with aggregate functions with or without a trivial GROUP BY clause, when there are no rows surviving to the GROUP BY step.

Alternatively, if the query is to return nothing if there is no non-NULL value matching any of the inputs, the query can be written more simply like this:

with
  request (type, priority) as (
    select 'bad_phone' , 1 from dual union all
    select 'home_phone', 2 from dual union all
    select 'cell_phone', 3 from dual
  )
select name, value
from   mytab m join request r on m.name = r.type
where  m.value is not null
order  by r.priority
fetch  first row only
;


I expect this to be less efficient (it uses analytic ROW_NUMBER() under the hood, and analytic functions are generally slower than aggregate functions), although that is probably undetectable unless the number of input "names" to search for (given here in the WITH clause) is large.
Chris Saxon
January 26, 2024 - 6:42 pm UTC

Another neat approach! "Best" is always a bit subjective - and there's not really enough background in the question for us to analyze the trade-offs fully.