views:

58

answers:

4

That title is brutal, but I don't know how else to put it.

I have a key value table tied to a user_id that stores user preferences throughout the site. If a user hasn't gone in and updated any of their settings, any time I ask for a key (say "FAVORITE_COLOR" it's going to be null, so I need to pull the default setting that I have tied to the default user, user_id = 0.

I was thinking along the lines of UNION'ing the user_id = 0 results to the bottom of the query, but that'd just lead to duplicates. I'm not sure if it's even possible, but what'd I'd love to be able to say something like:

SELECT val FROM k_v WHERE user_id = 123 AND k = 'FAVORITE_COLOR'
UNION IF val IS NULL
SELECT val FROM k_v WHERE user_id = 0 AND k = 'FAVORITE_COLOR';

Any good way to do this?

Edit: Thanks for all the help on this. If your use case is only grabbing a single value which is what this question is, then NVL from dual is exactly what you want, but if you're planning on returning multiple pairs in the same query, take a look at some of the other answers as they may actually make more sense for implementation.

I wound up going with ZeissS's suggestion below as it's simple, still one query, works with multiple k,v pairs and I don't have a problem doing the possible duplicate filtering client side.

+5  A: 

Use this,

select
  nvl(
    (SELECT val FROM k_v WHERE user_id = 123 AND k = 'FAVORITE_COLOR'),
    (SELECT val FROM k_v WHERE user_id = 0 AND k = 'FAVORITE_COLOR')
  ) val 
from dual
;

From the Oracle docs,

NVL(expr1, expr2): NVL lets you replace null (returned as a blank) with a string in the results of a query. If expr1 is null, then NVL returns expr2. If expr1 is not null, then NVL returns expr1.

Janek Bogucki
Does nvl short-circuit? Would be pretty cool if it did.
Justin K
This is the correct answer to my question. I wanted to keep my question simple, but my implementation involves returning multiple k,v pairs with each query (ie SELECT k, v from k_v where k IN ('FAVORITE_COLOR','FAVORITE_FRUIT');) so I don't think NVL will work as I don't know how to keep the key tied to whatever NVL returned.
NVL does not short-circuit - but COALESCE, which will work just as well, does.
Jeffrey Kemp
A: 
SELECT nvl(k.k,kd.k) as k, 
       nvl(k.val, kd.val) as val 
FROM (select * from k_v where user_id = 0) kd 
     full outer join 
     (select * from k_v where user_id = 123) k 
     on kd.k = k.k 
WHERE 'FAVORITE_COLOR' in (k.k,kd.k)

I suggest doing the full outer join because it will guarantee a result both if there is no default or if there is no user-specific result. If there's no possibility of the default being missing, the query could be simplified a little:

SELECT nvl(k_v.k,kd.k) as k, 
       nvl(k_v.val, kd.val) as val 
FROM k_v kd 
     left outer join k_v 
     on kd.k = k_v.k 
        and k_v.user_id = 123
WHERE kd.k = 'FAVORITE_COLOR'
  and kd.user_id = 0
Allan
Don't you want the kd.user_id = 0 in the WHERE clause? Otherwise you'll be returning the favorite colors for all users, with only the non-default filled in for the default user when joined with the given user (123)
Tom H.
@Tom: I don't believe that is true. Putting those conditions in the join is the same as saying "(kd.user_id = 0 or kd.user_id is null)" in the where clause. This continues to hold true for the second query, because we're always getting the default and only getting the non-default for the specified user.
Allan
It's not true since you're using a LEFT OUTER JOIN. The criteria in the join will only be used to determine whether or not the joined table columns have values. That means that the only criteria used to filter your data is "kd.k = 'FAVORITE_COLOR'".
Tom H.
@Tom: Further testing shows that you're correct. I'm not as familiar with ANSI 1999 SQL syntax and was unaware of this effect.
Allan
+2  A: 

You could probably left join and then select the most appropriate one:

SELECT
  NVL(best.val, fallback.val) as val
FROM
  (SELECT val FROM k_v WHERE user_id = 0 AND k = 'FAVORITE_COLOR') as fallback
  left outer join (SELECT val FROM k_v WHERE user_id = 123 AND k = 'FAVORITE_COLOR') as best on 1 = 1

Not sure of the oracle syntax but you could probably substitute the "left outer on 1 = 1" join for some sort of cross join.

Aidan Kane
i'm trying to think if this would work if was returning multiple items so I had a k clause like "k IN ('FAVORITE_COLOR', 'FAVORITE_FRUIT', 'FAVORITE_SPORT')"...if I joined them on the k I should be able to just always return fallback.k to return k,v pairs I think. I'll have to give it a try. Thanks for your help!
+1  A: 

I would do something like this:

SELECT k_v.v FROM k_v WHERE userid IN (:userid, 0) ORDER BY userid DESC

and only use the first row returned.

ZeissS
This is the method I wound up going with as I'll be returning multiple k,v pairs most of the time. So I get the result, loop through it and only keep it if I don't already have that key in my array.