I have following tables:
User – userId, userName, …
Settings – settingId, userId, settingKey, settingValue
for example for userId = 123, I might have:
settingId: 1 userId: 123 settingKey: "allowClient" settingValue: "0" settingId: 2 userId: 123 settingKey: "allowAccess" settingValue: "1"
Then for example how can I query for all users that have settingValue of “0” corresponding to settingKey of “allowClient” and settingValue of “1” corresponding to settingKey of “allowAccess”? Sometimes the settingKey and settingValue that I’m looking for might not even be there for a particular user, in which case, I would just want to ignore those users.
My “attempt”:
select * from User u inner join Settings s on u.userid = s.userid where s.settingKey = 'allowClient and s.settingValue = '0' and s.settingKey = 'allowAccess' and s.settingValue = '1'
this doesn’t work for obvious reason because it’s putting AND on all the conditions. I’m not aware of any sql construct that can get around this and allow me to just say what I actually want.
Advertisement
Answer
Your first attempt doesn’t work because the WHERE
clause check each row one at a time. And no single row fulfils all of those conditions at once.
So, you could use an EXISTS()
check on each of the two keys, for a very literal expression of your problem…
SELECT user.* FROM user WHERE EXISTS ( SELECT * FROM settings WHERE userId = user.userId AND settingKey = 'allowClient' AND settingValue = '0' ) AND EXISTS ( SELECT * FROM settings WHERE userId = user.userId AND settingKey = 'allowAccess' AND settingValue = '1' )
Depending on data characteristics, you may benefit from a single sub-query instead of two EXISTS()
checks.
This is closer to what you were trying to do.
- Filter to get two rows per user (using OR instead of AND)
- Aggregate back down to a single row and check if both conditions were met
(But I’d go with two EXISTS()
first, and let the optimiser do its work.)
WITH matching_user ( SELECT userId FROM settings WHERE (settingKey = 'allowClient' AND settingValue = '0') OR (settingKey = 'allowAccess' AND settingValue = '1') GROUP BY userId HAVING COUNT(DISTINCT settingKey) = 2 -- DISTINCT only needed if one user has the same key set twice ) SELECT user.* FROM user INNER JOIN matching_user ON user.userId = matching_user.userId
Finally, you could just join twice, which is functionally similar to the double-exists check, but shorter code, though not always as performant.
SELECT user.* FROM user INNER JOIN settings AS s0 ON s0.userId = user.userId AND s0.settingKey = 'allowClient' AND s0.settingValue = '0' INNER JOIN settings AS s1 ON s1.userId = user.userId AND s1.settingKey = 'allowAccess' AND s1.settingValue = '1'
Using the two different aliases prevents ambiguity (which would cause an error).
It does assume that the joins will only ever find 0 or 1 rows, if they can find many, you get duplication. EXISTS()
doesn’t have that problem.