Predicates — Learn EXISTS, IN subqueries, ANY/ALL, and IS DISTINCT FROM in Practice
AdvancedPredicatesEXISTS / NOT EXISTSIN SubqueriesANY / ALLIS DISTINCT FROMPostgreSQL5 questions
QUESTION 1
EXISTS Predicate — Check whether even one matching row exists with a correlated subquery
EXISTSCorrelated SubqueryExistence CheckSemi-Join
Background

The EXISTS predicate returns TRUE when a subquery returns at least one row, and FALSE when it returns zero rows. It is typically combined with a correlated subquery, which brings columns from the outer query into the inner subquery.

WHERE EXISTS (
  SELECT 1               -- The SELECT value is not evaluated (1, *, and NULL all behave the same)
  FROM   orders o
  WHERE  o.user_id = u.user_id  -- Correlation condition: bring outer u into the inner query
    AND  o.status  = 'completed'
)
Short-circuit evaluation: EXISTS immediately returns TRUE and stops when it finds one matching row. It is much more efficient than scanning every row with COUNT(*) > 0.
Problem

From the users table, retrieve users who have at least one completed order. Return user_id, name, plan, ordered by user_id ascending.

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadafree
5Saburo Takahashifree
▸ orders
order_iduser_idamountstatusordered_at
101115000completed2024-05-01
10218000cancelled2024-05-10
10325000pending2024-05-15
104322000completed2024-05-20
10543000cancelled2024-05-22
10639000completed2024-05-25
107512000completed2024-05-28
Expected Output

Expected output (user_id ascending):

user_idnameplan
1Taro Tanakapremium
3Ichiro Suzukipremium
5Saburo Takahashifree

Note: user 2 has only a pending order, and user 4 only a cancelled order → no completed order → excluded.

QUESTION 2
NOT EXISTS Predicate — The anti-join pattern and the NOT IN NULL trap
NOT EXISTSAnti-JoinAbsence CheckNOT IN Warning
Background

NOT EXISTS returns rows for which no matching row exists. It is the anti-join pattern, the counterpart to INNER JOIN.

WHERE NOT EXISTS (
  SELECT 1
  FROM   order_items oi
  WHERE  oi.product_id = p.product_id  -- FALSE if even one match exists; TRUE if none exists
)
NOT IN + NULL = zero rows (critical bug): If a NOT IN list contains even one NULL, x <> NULL is UNKNOWN, so the whole AND expression is UNKNOWN → every row is excluded. NOT EXISTS avoids this trap.
Problem

From the products table, retrieve products that have never been ordered. Use NOT EXISTS to check order_items, return product_id, name, category, price, and order by product_id ascending.

Tables used
▸ products
product_idnamecategoryprice
1Smartphone XSmartphone89800
2Smartbook ProPC128000
3Tablet AirTablet64800
4Wireless EarbudsAccessories12800
5USB CableAccessories980
6Gaming PC ProPC198000
▸ order_items
item_idorder_idproduct_idquantity
110111
210142
310421
410631
510711
6105NULL1

Note: item 6 references a deleted product → product_id = NULL.

Expected Output

Expected output (product_id ascending):

product_idnamecategoryprice
5USB CableAccessories980
6Gaming PC ProPC198000

Note: products 1, 2, 3, and 4 appear in order_items and are excluded. Products 5 and 6 have never been ordered.

QUESTION 3
IN (Subquery) Predicate — Dynamic sets, semi-joins, and the NOT IN NULL trap
IN SubqueriesSemi-JoinDynamic ListNOT IN Warning
Background

IN (subquery) checks whether a column value belongs to the set returned by a subquery. It is common in production because it dynamically derives a set from the table's current state instead of using a fixed list.

WHERE o.user_id IN (
  SELECT user_id        -- Dynamically generate the ID list of premium members with a subquery
  FROM   users
  WHERE  plan = 'premium'
)
-- ↑ Non-correlated subquery: evaluated once to build the set; often equivalent to EXISTS
NOT IN + NULL trap (recap): If a subquery returns NULL, NOT IN becomes UNKNOWN for every row → zero rows (see Q2). Always use NOT EXISTS, or add WHERE col IS NOT NULL before using NOT IN.
Problem

From the orders table, retrieve orders placed by premium members (plan = 'premium'). Use a subquery against users, return order_id, user_id, amount, status, ordered_at, and order by ordered_at ascending.

Tables used
▸ orders
order_iduser_idamountstatusordered_at
101115000completed2024-05-01
10218000cancelled2024-05-10
10325000pending2024-05-15
104322000completed2024-05-20
10543000cancelled2024-05-22
10639000completed2024-05-25
107512000completed2024-05-28
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadafree
5Saburo Takahashifree
Expected Output

Expected output (ordered_at ascending):

order_iduser_idamountstatusordered_at
101115000completed2024-05-01
10218000cancelled2024-05-10
104322000completed2024-05-20
10639000completed2024-05-25

Note: orders 103, 105, and 107 from user_ids 2 (free), 4 (free), and 5 (free) are excluded.

QUESTION 4
ALL / ANY Predicates — Compare with all or some subquery results (∀ and ∃)
ALLANYSet Comparison∀ / ∃ Quantifiers
Background

ANY (= SOME) returns TRUE when a comparison is true for at least one row in the subquery's set; ALL returns TRUE only when it is true for every row.

WHERE price > ANY (SELECT price FROM products WHERE category = 'Accessories')
-- ↑ Equivalent to price > MIN(accessory prices): “greater than any” means greater than the smallest value in the set

WHERE price > ALL (SELECT price FROM products WHERE category = 'Accessories')
-- ↑ Equivalent to price > MAX(accessory prices): “greater than all” means greater than the largest value in the set
= ANY is equivalent to IN: category = ANY (ARRAY['PC', 'Tablet']) is exactly equivalent to IN ('PC', 'Tablet'). ANY / ALL show their real value when used with comparison operators such as > and <.
Problem

From the products table, retrieve products priced higher than every product in the Accessories category using the ALL predicate. Return product_id, name, category, price, ordered by price ascending.

Tables used
▸ products
product_idnamecategorypricestock
1Smartphone XSmartphone8980050
2Smartbook ProPC1280000
3Tablet AirTablet6480030
4Wireless EarbudsAccessories12800100
5USB CableAccessories980200
6Gaming PC ProPC1980005
Expected Output

Expected output (price ascending):

product_idnamecategoryprice
3Tablet AirTablet64800
1Smartphone XSmartphone89800
2Smartbook ProPC128000
6Gaming PC ProPC198000

Note: the maximum Accessories price is 12,800 yen. The 4 products above satisfy price > ALL {12800, 980} = price > 12800.
Product 4 (12800) fails because 12800 > 12800 is false; product 5 (980) fails both comparisons → excluded.

QUESTION 5
IS DISTINCT FROM Predicate — NULL-safe equality and the trap of <>
IS DISTINCT FROMNULL-Safe ComparisonNULL HandlingThe <> Trap
Background

IS DISTINCT FROM is a predicate for safely comparing equality when NULL may be present. Ordinary <> produces UNKNOWN when compared with NULL and the row is excluded, whereas IS DISTINCT FROM treats NULL as a “definitely different value” and returns TRUE.

-- Truth table for a IS DISTINCT FROM b
'WINTER20' IS DISTINCT FROM 'SUMMER10'  -- → TRUE  (different values)
NULL       IS DISTINCT FROM 'SUMMER10'  -- → TRUE  (NULL is treated as different)
'SUMMER10' IS DISTINCT FROM 'SUMMER10'  -- → FALSE (same value)
NULL       IS DISTINCT FROM NULL        -- → FALSE (both NULL are treated as equal)
The <> trap: With coupon_code <> 'SUMMER10', rows where coupon_code is NULL evaluate to UNKNOWN → excluded. Use IS DISTINCT FROM when you also want to include rows where the coupon was unused (NULL).
Problem

From the orders table, retrieve orders whose coupon_code differs from 'SUMMER10', including rows where the coupon was unused (NULL). Use IS DISTINCT FROM, return order_id, user_id, amount, coupon_code, ordered_at, and order by order_id ascending.

Tables used
▸ orders
order_iduser_idamountcoupon_codeordered_at
101115000SUMMER102024-05-01
10218000NULL2024-05-10
10325000NULL2024-05-15
104322000WINTER202024-05-20
10543000NULL2024-05-22
10639000NULL2024-05-25
107512000SUMMER102024-05-28
Expected Output

Expected output (order_id ascending):

order_iduser_idamountcoupon_codeordered_at
10218000NULL2024-05-10
10325000NULL2024-05-15
104322000WINTER202024-05-20
10543000NULL2024-05-22
10639000NULL2024-05-25

Note: orders 101 and 107 (SUMMER10) are excluded. NULL rows (102, 103, 105, and 106) are included as “different from SUMMER10.”