s.plan_type IN (NULL, 'basic')
is just shorthand for (s.plan_type = NULL OR s.plan_type = 'basic')
.
Anything = NULL
always returns NULL
, not true
. And only true
qualifies in a WHERE
clause. So rephrase:
SELECT u.id, s.plan_type
FROM users u
LEFT JOIN subscriptions s ON s.user_id = u.id
WHERE (s.plan_type = 'basic' OR
s.plan_type IS NULL);
To be precise, that's only correct if subscriptions.plan_type
is defined NOT NULL
. Failing that, the resulting NULL
value could be just that, not a missing plan.
This alternative formulation does not share the same ambiguity for a resulting NULL
:
SELECT u.id, s.plan_type
FROM users u
JOIN subscriptions s ON s.user_id = u.id
WHERE s.plan_type = 'basic'
UNION ALL
SELECT u.id, NULL -- always means missing
FROM users u
WHERE NOT EXISTS (SELECT FROM subscriptions WHERE user_id = u.id);
And it may be faster, because Postgres (like most RDBMS) often doesn't perform well with OR
'ed predicates. See about "ugly OR
":
If referential integrity is guaranteed with a FK constraint from subscriptions.user_id
to users.id
, and s.user_id
is defined NOT NULL
you can omit the users
table from the first SELECT
:
SELECT user_id AS id, plan_type
FROM subscriptions
WHERE plan_type = 'basic'
UNION ALL
SELECT id, NULL
FROM users u
WHERE NOT EXISTS (SELECT FROM subscriptions WHERE user_id = u.id);
Might be fastest.
Aside: No array is involved here. You show the form of IN
using a list. See: