Predicates — Learn Dynamic Lists, NULL Traps, EXISTS, and Compound Conditions from the Basics
BasicPredicatesBETWEEN / LIKEIS NULL / INEXISTS / NOT EXISTSCompound PredicatesPostgreSQL-ready5 questions
QUESTION 6
IN Subqueries and the NULL Trap in NOT IN — The danger of dynamic lists and three-valued logic
IN SubqueriesNOT INDynamic ListsWatch for NULLs
Background

A subquery inside IN can generate a list dynamically according to the state of the tables. There is also a NULL trap: if even one NULL is included in a NOT IN list, the result becomes zero rows.

-- IN subquery: dynamically generate user_ids whose plan is premium
WHERE user_id IN (
  SELECT user_id FROM users WHERE plan = 'premium'
)

-- NOT IN trap: one NULL in user_id makes the result zero rows
WHERE user_id NOT IN (
  SELECT user_id FROM orders
  WHERE  user_id IS NOT NULL  -- preventing NULL contamination is essential
)
The NOT IN + NULL trap: 5 NOT IN (1, NULL, 3)5<>1 AND 5<>NULL AND 5<>3 → TRUE AND UNKNOWN AND TRUE → UNKNOWN → excluded. If even one NULL is included, every row is excluded.
Problem

From the orders table, retrieve orders for users whose plan is 'premium' using an IN subquery. Return order_id, user_id, amount, status, ordered by amount descending.

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
▸ orders
order_iduser_idamountstatus
10118000completed
102112000completed
10323500pending
10439500completed
10546000cancelled
Expected Output

Expected output (amount descending):

order_iduser_idamountstatus
102112000completed
10439500completed
10118000completed

Note: the subquery dynamically generates [1, 3]. Order 103 (user 2: free) and order 105 (user 4: standard) are excluded.

Model Answer
SELECT
  order_id, user_id, amount, status
FROM   orders
WHERE  user_id IN (             -- IN: keep rows matching the subquery result list
  SELECT user_id                -- must return exactly one column (multiple columns cause an error)
  FROM   users
  WHERE  plan = 'premium'      -- dynamically generate the premium user_id list
)
ORDER BY amount DESC;

/*
  Execution order (SQL's logical evaluation order):
  1. Evaluate the subquery first
       → SELECT user_id FROM users WHERE plan='premium'
       → result list: [1, 3]
  2. FROM orders                   → read 5 rows
  3. WHERE user_id IN (1, 3)       → filter down to 3 rows
  4. SELECT order_id, user_id, ... → select 4 columns
  5. ORDER BY amount DESC          → sort by amount descending
*/
Explanation (table transitions & key points)
SELECT order_id, user_id, amount, status FROM orders WHERE user_id IN ( SELECT user_id FROM users WHERE plan = 'premium' ) ORDER BY amount DESC;
LEGEND
Rows read / loaded
① Main query: FROM orders
FROM ordersAll 5 rows in the orders table are the input for the outer main query.
1 / 6
order_iduser_idamountstatus
10118000completed
102112000completed
10323500pending
10439500completed
10546000cancelled
5 rows read
LEARNING POINTS
How an IN subquery works: The “value list” returned inside IN is fixed before the outer query uses it. It is logically equivalent to a fixed IN (1, 3), but the important difference is that it changes dynamically with the state of the tables.
The NULL trap in NOT IN: If even one NULL is included in a NOT IN list, col <> NULL becomes UNKNOWN and every row is excluded. Always add WHERE col IS NOT NULL inside a NOT IN subquery, or use NOT EXISTS (explained in Q8).
Equivalent transformation to JOIN: WHERE user_id IN (SELECT user_id FROM users WHERE plan='premium') produces the same result as INNER JOIN users ON orders.user_id = users.user_id WHERE users.plan='premium'. When columns from the joined table do not need to appear in SELECT, an IN subquery often makes the intent clearer.
ANTI-PATTERNS
Returning multiple columns inside IN: Returning multiple columns, such as SELECT user_id, name FROM users, causes a syntax error. SELECT only the single column that matches the IN comparison.
Getting zero rows from NOT IN + NULL: NULL contamination in data imported through an external API or ETL process happens frequently in production. When using NOT IN, always add WHERE col IS NOT NULL inside it, or switch to NOT EXISTS.
Practical column: How to choose among IN, JOIN, and EXISTS
All three are used to “filter by a condition in another table.” Use an IN subquery when the inner list is small to medium-sized and the intent is clear. Use JOIN when columns from the joined table should also appear in SELECT. Use EXISTS when you only need to check whether something exists, do not want to worry about duplicates, and want to avoid NULL-related effects. These are practical guidelines for making the choice.
QUESTION 7
EXISTS Predicate (Basic) — The most efficient pattern for checking only whether a row exists
EXISTSCorrelated SubqueryExistence CheckDetect Active Users
Background

WHERE EXISTS (subquery) keeps an outer row when the subquery returns even one row. Since it checks only “whether a row exists”, regardless of what the subquery returns, SELECT 1 is sufficient inside it.

SELECT * FROM users u
WHERE EXISTS (
  SELECT 1                       -- the value does not matter; only row existence is checked
  FROM   orders o
  WHERE  o.user_id = u.user_id  -- a “correlated subquery” that references an outer column
);
EXISTS characteristics: As soon as one row is found in the inner query, it immediately returns TRUE and stops searching (short-circuit evaluation). This is a safe pattern that is not affected by NULL.
Problem

From the users table, retrieve users with at least one order whose status is 'completed'. Return user_id, name, plan, ordered by user_id ascending.

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
5Saburo Itofree
▸ orders
order_iduser_idstatus
1011completed
1021completed
1032pending
1043completed
1054cancelled
Expected Output

Expected output (user_id ascending):

user_idnameplan
1Taro Tanakapremium
3Ichiro Suzukipremium

Note: user 2 has only pending orders; user 4 has only cancelled orders; user 5 has no orders. All are excluded.

Model Answer
SELECT
  user_id, name, plan
FROM   users u
WHERE  EXISTS (                   -- TRUE if the subquery returns even one row (short-circuit evaluation)
  SELECT 1                       -- the returned value does not matter; only row existence is checked
  FROM   orders o
  WHERE  o.user_id = u.user_id  -- correlated condition referencing the outer u.user_id (required)
    AND  o.status  = 'completed'
)
ORDER BY user_id;

/*
  Execution order (SQL's logical evaluation order):
  1. FROM users u                → process one row at a time
  2. EXISTS(...)                 → run the subquery for each user
  3. SELECT user_id, name, plan  → select rows that pass
  4. ORDER BY user_id            → sort by user_id ascending
  */
Explanation (table transitions & key points)
SELECT user_id, name, plan FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND o.status = 'completed' ) ORDER BY user_id;
LEGEND
Rows read / loaded
① Main query: FROM users
FROM users uThese are all 5 rows in users, the target of the main query. The subquery is evaluated for each row.
1 / 4
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
5Saburo Itofree
5 rows read
LEARNING POINTS
EXISTS = “TRUE if even one row exists”: As soon as the subquery returns one row, EXISTS immediately returns TRUE and ends the search (short-circuit evaluation). Since the SELECT list does not matter, SELECT 1 is conventional.
The correlated condition is required: If you omit the condition WHERE o.user_id = u.user_id linking the inner query to the outer query, every user passes as long as orders contains even one row. The correlated condition is essential.
Not affected by NULL: EXISTS uses a two-valued evaluation of “does a row exist?” Unlike IN, it behaves as intended even when NULL is present. EXISTS is safe for production data that may contain NULLs.
ANTI-PATTERNS
Forgetting the correlated condition: WHERE EXISTS (SELECT 1 FROM orders WHERE status='completed') means “if at least one completed order exists in orders,” so every user passes. Always include the join condition o.user_id = u.user_id to the outer table.
Writing SELECT * inside EXISTS: It works, but SELECT * can suggest that you intend to retrieve every column. Using SELECT 1 makes the intent—checking existence only—clear.
Practical column: Comparing EXISTS, IN, and JOIN performance
Optimizers in modern databases (PostgreSQL, MySQL 8.0+) often transform EXISTS, IN, and JOIN into equivalent execution plans. Still, for large datasets EXISTS is often advantageous because of short-circuit evaluation: it stops as soon as one match is found. EXISTS is also safe from the NULL trap. Build the habit of considering EXISTS first for readability and safety.
QUESTION 8
NOT EXISTS Predicate — A safer way than NOT IN to check that something does not exist
NOT EXISTSCorrelated SubqueryDetect Dormant UsersComparison with NOT IN
Background

WHERE NOT EXISTS (subquery) keeps an outer row when the subquery returns zero rows. It is the negation of EXISTS and is the pattern for checking that no corresponding row exists in another table.

SELECT * FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM   orders o
  WHERE  o.user_id = u.user_id   -- correlated condition
);
Why NOT EXISTS is safer than NOT IN: A NULL in a NOT IN list makes the result zero rows, whereas NOT EXISTS only evaluates whether a row exists as TRUE or FALSE and is not affected by NULL. NOT EXISTS is recommended for production data.
Problem

From the users table, retrieve users with no completed orders using NOT EXISTS. Return user_id, name, plan, ordered by user_id ascending.

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
5Saburo Itofree
▸ orders
order_iduser_idstatus
1011completed
1021completed
1032pending
1043completed
1054cancelled
Expected Output

Expected output (user_id ascending):

user_idnameplan
2Hanako Satofree
4Jiro Yamadastandard
5Saburo Itofree

Note: users 1 and 3 have completed orders and are excluded. User 2 has only pending orders; user 4 has only cancelled orders; user 5 has no orders, so all three pass.

Model Answer
SELECT
  user_id, name, plan
FROM   users u
WHERE  NOT EXISTS (            -- pass when the subquery returns zero rows (nothing exists)
  SELECT 1
  FROM   orders o
  WHERE  o.user_id = u.user_id -- correlated condition referencing the outer column (required)
    AND  o.status  = 'completed'
)
ORDER BY user_id;

/*
  Execution order (SQL's logical evaluation order):
  1. FROM users u                → process one row at a time
  2. NOT EXISTS(...)             → run the subquery for each user
  3. SELECT user_id, name, plan  → select rows that pass
  4. ORDER BY user_id            → sort by user_id ascending
  */
Explanation (table transitions & key points)
SELECT user_id, name, plan FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND o.status = 'completed' ) ORDER BY user_id;
LEGEND
Rows read / loaded
① Main query: FROM users
FROM users uThese are all 5 rows in users, the target of the main query. The NOT EXISTS subquery runs for each row.
1 / 4
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
5Saburo Itofree
5 rows read
LEARNING POINTS
NOT EXISTS = “pass when no row exists”: It is the opposite of EXISTS. It becomes TRUE only when the subquery returns zero rows. Comparing it with the EXISTS example in Q7 deepens the understanding.
Why NOT EXISTS is safer than NOT IN: A NULL in a NOT IN list makes the evaluation UNKNOWN and excludes every row. NOT EXISTS only evaluates whether a row exists as TRUE or FALSE, so it is not affected by NULL. Make NOT EXISTS your first choice for production data.
Equivalent transformation to LEFT JOIN + IS NULL: LEFT JOIN orders o ON u.user_id=o.user_id AND o.status='completed' WHERE o.user_id IS NULL produces the same result. This pattern is common in queries generated by ORMs.
ANTI-PATTERNS
Zero rows from NULL contamination in NOT IN: If a row with user_id=NULL enters from an external API, WHERE user_id NOT IN (SELECT user_id FROM orders) returns zero rows. Simply changing it to NOT EXISTS makes it correct. This is one of the most common production bugs.
Forgetting the correlated condition: If WHERE o.user_id = u.user_id is omitted inside NOT EXISTS, the presence of even one row in orders excludes every user. The correlated condition is required.
Practical column: Detecting dormant users in a scheduled batch
A batch job that sends a reminder email to “users with no completed orders in the last 30 days” can be implemented by adding the time condition inside the subquery: NOT EXISTS (SELECT 1 FROM orders WHERE user_id = u.user_id AND status='completed' AND ordered_at >= NOW() - INTERVAL '30 days'). NOT EXISTS supports flexible conditions and is a standard pattern for production batch queries.
QUESTION 9
EXISTS with Compound Conditions — Check existence across multiple tables (product × category × order)
EXISTSCorrelated SubqueryMultiple TablesPurchase Analysis
Background

Inside EXISTS, you can JOIN multiple tables to perform a more complex existence check. This pattern uses one EXISTS to check a two-step relationship, such as “users who ordered a product in this category.”

WHERE EXISTS (
  SELECT 1
  FROM   orders o
  JOIN   products p ON o.product_id = p.product_id
  WHERE  o.user_id  = u.user_id           -- correlation with the outer query
    AND  p.category = 'Smartphone'  -- category condition
)
JOIN inside EXISTS: Even when a JOIN is used inside EXISTS, EXISTS checks only whether a row exists. It can remain efficient through short-circuit evaluation even when the row count grows.
Problem

From the users table, retrieve users who have ever ordered at least one product in the 'PC' category. Return user_id, name, plan, ordered by user_id ascending.

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
▸ orders (with product_id)
order_iduser_idproduct_idstatus
10111completed
10212completed
10323pending
10434completed
10542cancelled
▸ products (with category)
product_idnamecategory
1Smartphone XSmartphone
2Smartbook ProPC
3Tablet AirTablet
4Wireless EarbudsAccessories
Expected Output

Expected output (user_id ascending):

user_idnameplan
1Taro Tanakapremium
4Jiro Yamadastandard

Note: order 102 (user 1, product 2: PC) and order 105 (user 4, product 2: PC) match. Users 2 and 3 have no PC orders.

Model Answer
SELECT
  user_id, name, plan
FROM   users u
WHERE  EXISTS (
  SELECT 1
  FROM   orders o
  JOIN   products p ON o.product_id = p.product_id  -- join to product information
  WHERE  o.user_id  = u.user_id               -- correlated condition with the outer users
    AND  p.category = 'PC'                   -- check whether a PC-category product was ordered
)
ORDER BY user_id;

/*
  Execution order (SQL's logical evaluation order):
  1. FROM users u                → process one row at a time
  2. EXISTS(...)                 → check for a PC order for each user
  3. SELECT user_id, name, plan  → select rows that pass
  4. ORDER BY user_id            → sort by user_id ascending
  */
Explanation (table transitions & key points)
SELECT user_id, name, plan FROM users u WHERE EXISTS ( SELECT 1 FROM orders o JOIN products p ON o.product_id = p.product_id WHERE o.user_id = u.user_id AND p.category = 'PC' ) ORDER BY user_id;
LEGEND
Rows read / loaded
① Main query: FROM users
FROM users uThese are the users in the main query.
1 / 5
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
4 rows read
LEARNING POINTS
JOIN inside EXISTS: A normal JOIN can also be used inside an EXISTS subquery. This makes it possible to check existence across two tables through a single EXISTS. It remains efficient through short-circuit evaluation even with many rows.
Equivalent transformation to an IN subquery: This problem can also be written as WHERE user_id IN (SELECT o.user_id FROM orders o JOIN products p ON o.product_id=p.product_id WHERE p.category='PC') with the same result. EXISTS is safer with respect to NULL, however.
Connecting multiple existence checks with AND: Joining multiple EXISTS expressions with AND, as in WHERE EXISTS (...) AND EXISTS (...), retrieves users who ordered both A and B. Reproducing this with IN subqueries requires a complicated INTERSECT, so EXISTS is more natural.
ANTI-PATTERNS
Trying to reference a column selected inside EXISTS from the outer query: Columns selected inside an EXISTS subquery cannot be referenced from the outer query. EXISTS checks existence only. Use a scalar subquery or JOIN when you need to retrieve a value.
Forgetting table aliases in the correlated condition: Writing WHERE user_id = u.user_id can make the join ambiguous. Always give the tables used inside EXISTS aliases and make the relationship explicit, as in o.user_id = u.user_id.
Practical column: An analysis query for “users who bought from a category”
On an e-commerce site, recommending related products to “users who bought from a specific category” is a common use case. The EXISTS + JOIN pattern is typical here. Adding AND NOT EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id=u.user_id AND o2.product_id=X) further narrows the result to “users who bought a PC but have not yet bought product X.” Combining predicates lets you express complex business conditions.
QUESTION 10
Compound Predicates — A practical query combining comparison, IN, and EXISTS
Compound PredicatesEXISTS + INPractical SynthesisPurchase User Analysis
Background

Practical queries express complex conditions by combining multiple predicates. Learn the three-stage filter pattern of narrowing a numeric range with a comparison predicate, selecting specific categories with IN, and checking for an order with EXISTS.

WHERE p.price       >= 50000               -- ① comparison predicate: price range
  AND p.category    IN ('PC', 'Tablet')  -- ② IN predicate: category selection
  AND EXISTS (                               -- ③ EXISTS predicate: check for an order
    SELECT 1 FROM orders o
    WHERE  o.product_id = p.product_id
      AND  o.status = 'completed'
  )
Predicate evaluation order and AND short-circuit evaluation: The database optimizer evaluates conditions in the order of the lowest cost. It may evaluate index-friendly comparison predicates and IN first, then optimize the more expensive EXISTS to run later.
Problem

From the products table, retrieve products where price is at least 50,000 and category is 'PC' or 'Tablet', and where at least one completed order exists. Return product_id, name, category, price, ordered by price descending.

Tables used
▸ products
product_idnamecategorypricestock
1Smartphone XSmartphone8980050
2Smartbook ProPC1280000
3Tablet AirTablet6480030
4Wireless EarbudsAccessories12800100
5USB CableAccessories980200
6Gaming PC ProPC1980005
▸ orders (with product_id)
order_idproduct_idstatus
1011completed
1022completed
1033pending
1046completed
1052cancelled
Expected Output

Expected output (price descending):

product_idnamecategoryprice
6Gaming PC ProPC198000
2Smartbook ProPC128000

Note: product 1 is excluded because it is in the Smartphone category; product 3 is a Tablet but has no completed order (only pending); products 4 and 5 are excluded because they are Accessories and below the price floor.

Model Answer
SELECT
  product_id, name, category, price
FROM   products p
WHERE  p.price    >= 50000              -- ① comparison predicate: at least 50,000 (index-friendly)
  AND  p.category IN ('PC', 'Tablet')    -- ② IN predicate: target categories only
  AND  EXISTS (                         -- ③ EXISTS predicate: check for a completed order
    SELECT 1
    FROM   orders o
    WHERE  o.product_id = p.product_id  -- correlated condition with the outer product
      AND  o.status      = 'completed'  -- check completed orders only
  )
ORDER BY p.price DESC;

/*
  Execution order (SQL's logical evaluation order):
  1. FROM products p                           → read 6 rows
  2. WHERE p.price threshold                   → filter by price
  3. AND p.category IN (...)                   → filter by category
  4. AND EXISTS (...)                           → filter to products with completed orders
  5. SELECT product_id, name, category, price  → select 4 columns
  6. ORDER BY p.price DESC                     → sort by price descending
  */
Explanation (table transitions & key points)
SELECT product_id, name, category, price FROM products p WHERE p.price >= 50000 AND p.category IN ('PC', 'Tablet') AND EXISTS ( SELECT 1 FROM orders o WHERE o.product_id = p.product_id AND o.status = 'completed' ) ORDER BY p.price DESC;
LEGEND
Rows read / loaded
① Main query: FROM products
FROM products pThese are all 6 rows in products, the target of the main query. Three filters will be applied in sequence.
1 / 7
product_idnamecategoryprice
1Smartphone XSmartphone89800
2Smartbook ProPC128000
3Tablet AirTablet64800
4Wireless EarbudsAccessories12800
5USB CableAccessories980
6Gaming PC ProPC198000
6 rows read
LEARNING POINTS
Combining predicates: The staged filter of comparison predicate (price filter) → IN predicate (category selection) → EXISTS predicate (order existence) is one of the most common structures in practical SQL. Separating each predicate's role clearly is the key to readability.
Evaluation order and the optimizer: A database evaluates AND-connected conditions in cost order. It optimizes the plan to evaluate index-friendly comparison predicates and IN first, then the more expensive EXISTS. Use EXPLAIN to inspect the execution plan.
Parentheses and AND / OR precedence: AND has higher precedence than OR. WHERE A OR B AND C is interpreted as WHERE A OR (B AND C). When combining multiple predicates, make a habit of grouping them explicitly with parentheses so the query behaves as intended.
ANTI-PATTERNS
Misunderstanding AND / OR precedence: WHERE price >= 50000 AND category = 'PC' OR category = 'Tablet' is interpreted as WHERE (price >= 50000 AND category = 'PC') OR category = 'Tablet'. The Tablet price condition is then ineffective, producing unintended results. Use parentheses and write category IN ('PC', 'Tablet') to solve the problem.
Forcing a choice between EXISTS and IN instead of using each appropriately: Both are useful, but they fit different situations. In practice, use EXISTS when you only need an existence check and NULLs may be present; use IN when checking membership in a small fixed list.
Practical column: Expressing complex business requirements by combining predicates
The combination of “price-band filter × category selection × purchase history exists” appears in e-commerce product search, recommendations, inventory management, and many other contexts. The essence of SQL predicates is to describe declaratively which rows to keep. By freely combining the basic predicates comparison, IN, EXISTS, IS NULL, and BETWEEN, you can express nearly any business logic in a WHERE clause. This is what it means to read and write SQL at a practical level.