Comparison predicates (=, <>, <, >, <=, >=) are SQL's most basic predicates. Multiple conditions are connected with AND (all true) or OR (at least one true).
SELECT * FROM products WHERE price >= 10000 -- lower bound (inclusive) AND price <= 100000 -- upper bound (inclusive) AND stock >= 1; -- in stock
From the products table, retrieve products where price is at least 10,000 and at most 100,000 and stock is at least 1 (in stock). Return product_id, name, category, price, stock, ordered by price descending.
| product_id | name | category | price | stock |
|---|---|---|---|---|
| 1 | Smartphone X | Smartphone | 89800 | 50 |
| 2 | Smartbook Pro | PC | 128000 | 0 |
| 3 | Tablet Air | Tablet | 64800 | 30 |
| 4 | Wireless Earbuds | Accessories | 12800 | 100 |
| 5 | USB Cable | Accessories | 980 | 200 |
| 6 | Gaming PC Pro | PC | 198000 | 5 |
Expected output (price descending):
| product_id | name | category | price | stock |
|---|---|---|---|---|
| 1 | Smartphone X | Smartphone | 89800 | 50 |
| 3 | Tablet Air | Tablet | 64800 | 30 |
| 4 | Wireless Earbuds | Accessories | 12800 | 100 |
Note: product 2 is excluded because stock=0; product 5 because price=980 (below the lower bound); and product 6 because price=198000 (above the upper bound).
SELECT product_id, name, category, price, stock FROM products WHERE price >= 10000 -- lower bound: at least 10,000 (inclusive) AND price <= 100000 -- upper bound: at most 100,000 (inclusive) AND stock >= 1 -- in stock (stock is greater than 0) ORDER BY price DESC; /* Execution order (SQL's logical evaluation order): 1. FROM products → read 6 rows 2. WHERE price / stock ranges → filter down to 3 rows 3. SELECT product_id, name, ... → select 5 columns 4. ORDER BY price DESC → sort by price descending */
LEGEND
① FROM products
FROM productsRead all 6 rows from the products table. These are the input data evaluated by the WHERE clause.| product_id | name | price | stock |
|---|---|---|---|
| 1 | Smartphone X | 89800 | 50 |
| 2 | Smartbook Pro | 128000 | 0 |
| 3 | Tablet Air | 64800 | 30 |
| 4 | Wireless Earbuds | 12800 | 100 |
| 5 | USB Cable | 980 | 200 |
| 6 | Gaming PC Pro | 198000 | 5 |
>= includes the boundary value (at least 10000 means 10000 is included), while > excludes it (greater than 10000 means 10001 or more). Read specifications such as "at least," "greater than," "at most," and "less than" precisely.price >= 10000 AND price <= 100000 is equivalent to price BETWEEN 10000 AND 100000. BETWEEN includes both endpoints (covered in detail in Q2).WHERE category = 'PC' AND category = 'Tablet' means "PC and Tablet" and always returns 0 rows because one column cannot hold two values at once. Use OR or IN for multiple category values (see Q5).WHERE stock = NULL evaluates to UNKNOWN and always returns 0 rows. Always use IS NULL to check for NULL (covered in detail in Q4).stock >= 1, which is TRUE for most rows, has limited index benefit, and the optimizer may choose a full scan.BETWEEN a AND b is equivalent to col >= a AND col <= b and specifies a range that includes both boundary values. It works with numbers, dates, and strings.
SELECT * FROM orders WHERE ordered_at BETWEEN '2024-01-01' AND '2024-03-31'; -- ↑ equivalent to ordered_at >= '2024-01-01' AND ordered_at <= '2024-03-31'
'2024-03-31' is interpreted as '2024-03-31 00:00:00'. Rows after 00:01 on March 31 are excluded, so ordered_at < '2024-04-01' is safer for TIMESTAMP columns.From the orders table, retrieve orders where ordered_at falls from January 1 through March 31, 2024 (Q1). Return order_id, user_id, amount, status, ordered_at, ordered by ordered_at ascending.
| order_id | user_id | amount | status | ordered_at |
|---|---|---|---|---|
| 101 | 1 | 8000 | completed | 2024-01-15 |
| 102 | 2 | 3500 | completed | 2024-02-20 |
| 103 | 1 | 12000 | pending | 2024-04-05 |
| 104 | 3 | 9500 | completed | 2023-12-01 |
| 105 | 4 | 6000 | cancelled | 2024-03-10 |
| 106 | 3 | 4500 | completed | 2024-01-28 |
Expected output (ordered_at ascending):
| order_id | user_id | amount | status | ordered_at |
|---|---|---|---|---|
| 101 | 1 | 8000 | completed | 2024-01-15 |
| 106 | 3 | 4500 | completed | 2024-01-28 |
| 102 | 2 | 3500 | completed | 2024-02-20 |
| 105 | 4 | 6000 | cancelled | 2024-03-10 |
Note: order 103 (2024-04-05: after the range) and order 104 (2023-12-01: before the range) are excluded.
SELECT order_id, user_id, amount, status, ordered_at FROM orders WHERE ordered_at BETWEEN '2024-01-01' AND '2024-03-31' -- ↑ equivalent to >= '2024-01-01' AND <= '2024-03-31' -- DATE includes both endpoints. For TIMESTAMP, note that '2024-03-31 00:00:00' is the upper bound. ORDER BY ordered_at; /* Execution order (SQL's logical evaluation order): 1. FROM orders → read 6 rows 2. WHERE ordered_at BETWEEN ... → filter down to 4 rows 3. SELECT order_id, user_id, amount, ... → select 5 columns 4. ORDER BY ordered_at → sort by date ascending */
LEGEND
① FROM orders
FROM ordersRead all 6 rows from the orders table. Notice the ordered_at values: 2023, Q1 2024, and Q2 2024 are mixed together.| order_id | user_id | amount | status | ordered_at |
|---|---|---|---|---|
| 101 | 1 | 8000 | completed | 2024-01-15 |
| 102 | 2 | 3500 | completed | 2024-02-20 |
| 103 | 1 | 12000 | pending | 2024-04-05 |
| 104 | 3 | 9500 | completed | 2023-12-01 |
| 105 | 4 | 6000 | cancelled | 2024-03-10 |
| 106 | 3 | 4500 | completed | 2024-01-28 |
BETWEEN a AND b is exactly equivalent to col >= a AND col <= b. Always remember that the boundary values a and b also match.'2024-03-31' is interpreted as '2024-03-31 00:00:00'. Orders after 00:01 on March 31 can be silently missed. Use the ordered_at < '2024-04-01' pattern for TIMESTAMP columns.NOT BETWEEN a AND b is equivalent to col < a OR col > b, so it expresses the reverse condition, "retrieve rows outside the range."BETWEEN 100000 AND 10000 is mathematically impossible and always returns 0 rows. Always write smaller value AND larger value.BETWEEN '2024-01-01' AND '2024-03-31' frequently misses March 31 rows when the column is TIMESTAMP. Changing the upper bound to < '2024-04-01' solves the problem.>= 'month start' AND < 'next month start' (for example, February uses >= '2024-02-01' AND < '2024-03-01'). This automatically handles leap years and avoids truncating times, making it the safest pattern. It is best to limit BETWEEN to DATE columns.The LIKE predicate is used for string pattern matching. % (percent) matches any string of zero or more characters, while _ (underscore) matches exactly one character.
WHERE name LIKE 'Smart%' -- prefix match: starts with "Smart" OR name LIKE '%Pro' -- suffix match: ends with "Pro" OR name LIKE '%Air%' -- infix match: contains "Air" (cannot use an index) OR name LIKE '__blet'; -- _ is one arbitrary character (two characters + blet)
'prefix%' can use a B-tree index. A suffix match '%suffix' and an infix match '%word%' cannot use an index and require a full scan. Be especially careful with large tables.From the products table, retrieve products whose name starts with "Smart" or whose name ends with "Pro". Return product_id, name, category, price, ordered by price ascending.
| product_id | name | category | price |
|---|---|---|---|
| 1 | Smartphone X | Smartphone | 89800 |
| 2 | Smartbook Pro | PC | 128000 |
| 3 | Tablet Air | Tablet | 64800 |
| 4 | Wireless Earbuds | Accessories | 12800 |
| 5 | USB Cable | Accessories | 980 |
| 6 | Gaming PC Pro | PC | 198000 |
Expected output (price ascending):
| product_id | name | category | price |
|---|---|---|---|
| 1 | Smartphone X | Smartphone | 89800 |
| 2 | Smartbook Pro | PC | 128000 |
| 6 | Gaming PC Pro | PC | 198000 |
Note: product 2 matches both "Smart%" and "%Pro". Because this is an OR condition, it is output once without duplication.
SELECT product_id, name, category, price FROM products WHERE name LIKE 'Smart%' -- prefix match: starts with "Smart" OR name LIKE '%Pro' -- suffix match: ends with "Pro" ORDER BY price; /* Execution order (SQL's logical evaluation order): 1. FROM products → read 6 rows 2. WHERE name LIKE ... OR ... → filter down to 3 rows 3. SELECT product_id, name, ... → select 4 columns 4. ORDER BY price → sort by price ascending */
LEGEND
① FROM products
FROM productsRead all 6 rows from the products table. Focus on the patterns in the name column.| product_id | name | category | price |
|---|---|---|---|
| 1 | Smartphone X | Smartphone | 89800 |
| 2 | Smartbook Pro | PC | 128000 |
| 3 | Tablet Air | Tablet | 64800 |
| 4 | Wireless Earbuds | Accessories | 12800 |
| 5 | USB Cable | Accessories | 980 |
| 6 | Gaming PC Pro | PC | 198000 |
% (any string of zero or more characters) and _ (any one character). For example, 'A_C' matches "ABC" and "AXC", but not "AC" or "ABBC".'prefix%' can use a B-tree index. A suffix match '%suffix' and an infix match '%word%' cannot use an index and require a full scan. For full-text search, consider pg_trgm (PostgreSQL) or FULLTEXT INDEX (MySQL).WHERE name NOT LIKE '%Pro' extracts products that do not end with "Pro". To exclude multiple patterns, connect NOT LIKE '...' conditions with AND.LIKE 'Smartphone X' is equivalent to = 'Smartphone X', but index behavior differs and the optimizer may have a harder time optimizing it. Use = for exact matches.LIKE '%keyword%' requires a full scan on a table with millions of rows. Check with EXPLAIN before running it on a production table and consider a full-text search index if needed.% or _ characters can unintentionally broaden the pattern. Escape % to % and _ to _ in the application, then use the ESCAPE clause as in LIKE :q ESCAPE '\'. Concatenating strings directly instead of using placeholders creates a risk of SQL injection.SQL uses TRUE / FALSE / UNKNOWN three-valued logic rather than two-valued TRUE / FALSE logic. NULL means "an unknown value": both NULL = NULL and NULL <> NULL evaluate to UNKNOWN. Always use IS NULL or IS NOT NULL to check for NULL.
-- ✗ Incorrect: = cannot be used with NULL (always UNKNOWN → 0 rows) WHERE coupon_code = NULL -- ✓ Correct: check with IS NULL WHERE coupon_code IS NULL -- ✓ Retrieve non-NULL rows WHERE coupon_code IS NOT NULL
From the orders table, retrieve orders where coupon_code is NULL (no coupon used). Return order_id, user_id, amount, coupon_code, ordered_at, ordered by order_id ascending.
| order_id | user_id | amount | coupon_code | ordered_at |
|---|---|---|---|---|
| 101 | 1 | 8000 | SUMMER10 | 2024-06-01 |
| 102 | 2 | 12000 | NULL | 2024-06-03 |
| 103 | 3 | 5000 | NULL | 2024-06-05 |
| 104 | 1 | 9800 | WINTER20 | 2024-06-07 |
| 105 | 4 | 3200 | NULL | 2024-06-10 |
Expected output (order_id ascending):
| order_id | user_id | amount | coupon_code | ordered_at |
|---|---|---|---|---|
| 102 | 2 | 12000 | NULL | 2024-06-03 |
| 103 | 3 | 5000 | NULL | 2024-06-05 |
| 105 | 4 | 3200 | NULL | 2024-06-10 |
Note: orders 101 (SUMMER10) and 104 (WINTER20) are excluded because their coupons were used.
SELECT order_id, user_id, amount, coupon_code, ordered_at FROM orders WHERE coupon_code IS NULL -- Always use IS NULL for a NULL check (= NULL is always UNKNOWN) ORDER BY order_id; /* Execution order (SQL's logical evaluation order): 1. FROM orders → read 5 rows 2. WHERE coupon_code IS NULL → filter down to 3 rows 3. SELECT order_id, user_id, ... → select 5 columns 4. ORDER BY order_id → sort by order_id ascending ▸ Three-valued logic reminder: 'SUMMER10' IS NULL → FALSE → excluded NULL IS NULL → TRUE → passes NULL = NULL → UNKNOWN → excluded (a common mistake) */
LEGEND
① FROM orders
FROM ordersRead all 5 rows from the orders table. The coupon_code column contains both NULL and strings.| order_id | user_id | amount | coupon_code | ordered_at |
|---|---|---|---|---|
| 101 | 1 | 8000 | SUMMER10 | 2024-06-01 |
| 102 | 2 | 12000 | NULL | 2024-06-03 |
| 103 | 3 | 5000 | NULL | 2024-06-05 |
| 104 | 1 | 9800 | WINTER20 | 2024-06-07 |
| 105 | 4 | 3200 | NULL | 2024-06-10 |
IS NULL is the only predicate that correctly detects NULL. IS NOT NULL retrieves rows where a value exists. A common pattern is to combine COALESCE with it, such as COALESCE(coupon_code, 'none'), to convert NULL to a default value.WHERE col IS NOT NULL inside the subquery.col = NULL always becomes UNKNOWN, so 0 rows pass. Always use IS NULL.IS NOT NULL.WHERE col IN (v1, v2, …) is shorthand for col = v1 OR col = v2 OR …. It expresses multiple matches cleanly by listing the accepted values.
WHERE category IN ('PC', 'Tablet') -- ↑ equivalent to category = 'PC' OR category = 'Tablet'
From the products table, retrieve products whose category is 'Smartphone', 'Tablet', or 'PC'. Return product_id, name, category, price, stock, ordered by category ascending and then price ascending.
| product_id | name | category | price | stock |
|---|---|---|---|---|
| 1 | Smartphone X | Smartphone | 89800 | 50 |
| 2 | Smartbook Pro | PC | 128000 | 0 |
| 3 | Tablet Air | Tablet | 64800 | 30 |
| 4 | Wireless Earbuds | Accessories | 12800 | 100 |
| 5 | USB Cable | Accessories | 980 | 200 |
| 6 | Gaming PC Pro | PC | 198000 | 5 |
Expected output (category ascending → price ascending):
| product_id | name | category | price | stock |
|---|---|---|---|---|
| 2 | Smartbook Pro | PC | 128000 | 0 |
| 6 | Gaming PC Pro | PC | 198000 | 5 |
| 1 | Smartphone X | Smartphone | 89800 | 50 |
| 3 | Tablet Air | Tablet | 64800 | 30 |
Note: products 4 and 5 are excluded because their category is 'Accessories', which is outside the IN list.
SELECT product_id, name, category, price, stock FROM products WHERE category IN ('Smartphone', 'Tablet', 'PC') ORDER BY category, price; -- ↑ equivalent to category = 'Smartphone' OR category = 'Tablet' OR category = 'PC' /* Execution order (SQL's logical evaluation order): 1. FROM products → read 6 rows 2. WHERE category IN (...) → filter down to 4 rows 3. SELECT product_id, name, ... → select 5 columns 4. ORDER BY category, price → category ascending → price ascending */
LEGEND
① FROM products
FROM productsRead all 6 rows from the products table. Focus on the different values in the category column.| product_id | name | category | price |
|---|---|---|---|
| 1 | Smartphone X | Smartphone | 89800 |
| 2 | Smartbook Pro | PC | 128000 |
| 3 | Tablet Air | Tablet | 64800 |
| 4 | Wireless Earbuds | Accessories | 12800 |
| 5 | USB Cable | Accessories | 980 |
| 6 | Gaming PC Pro | PC | 198000 |
IN (v1, v2, v3) is equivalent to = v1 OR = v2 OR = v3. Even when the list grows, you only need to add values to the IN list, which is more readable than repeating OR.WHERE category NOT IN ('Accessories') retrieves products other than Accessories. However, if the NOT IN list contains NULL, a trap causes 0 rows to be returned, so take care (covered in Q6).IN (SELECT category FROM ... to generate a list dynamically based on the state of a table (covered in Q6).IN (id1, id2, ... id50000) generated by the application greatly increases parsing and evaluation costs. Change the design to use a temporary table or JOIN.IN (col1, col2) is a value list, not a list of columns. For matching combinations of multiple columns, use the row-value constructor syntax (col1, col2) IN ((v1, v2), (v3, v4)) (supported by PostgreSQL).WHERE category = ANY($1) (PostgreSQL) or WHERE category IN (?) (multiple bound parameters). This safely handles a variable number of values.