SELECT & WHERE.
Read columns. Filter rows.
Two clauses do 80% of the work in real SQL. SELECT picks columns. WHERE filters rows. Together they answer almost every "show me X where Y" question. This is what you'll write every day, in every project, for the rest of your career. Every example below has a Code / Output toggle — click to see exactly what the query returns.
Every SQL query follows the same shape: SELECT columns FROM table WHERE condition. SELECT lists the columns you want (never * in production). FROM names the table. WHERE filters rows — only rows where the condition is true are returned. Every example on this page works this way, and every one has a Code / Output toggle so you can see the result of each query.
Before this lesson: no prior programming experience needed. If you've never written a query before, this is the perfect starting point.
📖 SELECT — read columns
List the columns you want. Get rows. That's the entire concept.
The SELECT clause lists the columns you want to read. FROM names the table. The database returns every row with those columns. Click Code to see the query, Output to see the actual result.
SELECT name, email, country FROM users;
| name | country | |
|---|---|---|
| Alice Chen | alice@example.com | US |
| Bob Smith | bob@example.com | UK |
| Priya Patel | priya@example.com | IN |
| Marcus Lee | marcus@example.com | US |
| Sofia García | sofia@example.com | ES |
Now try the same query with SELECT * — it returns every column in the table. That's useful for exploring a new table, but avoid it in production code:
SELECT * FROM users;
| id | name | country | age | created_at | |
|---|---|---|---|---|---|
| 1 | Alice Chen | alice@example.com | US | 28 | 2026-01-15 |
| 2 | Bob Smith | bob@example.com | UK | 34 | 2026-02-20 |
| 3 | Priya Patel | priya@example.com | IN | 31 | 2026-03-08 |
| 4 | Marcus Lee | marcus@example.com | US | 42 | 2026-04-12 |
| 5 | Sofia García | sofia@example.com | ES | 26 | 2026-05-03 |
SELECT column1, column2
List specific columns. Faster, safer, and works even when the table schema changes. This is what production SQL should use.
SELECT *
Every column. Useful for exploring a new table, but avoid in production — it's slower and returns columns you don't need. Breaks if schema changes.
FROM table_name
Tells the database which table to read from. Every SELECT must be paired with a FROM. Case doesn't matter for keywords (SELECT = select), but consistency helps readability.
No ORDER BY = arbitrary order
Without ORDER BY, SQL doesn't guarantee row order. It might look alphabetical today and be different tomorrow. If order matters, always add ORDER BY.
Why SELECT * is a production bug. It works fine when you're exploring. But two things break it in real code: (1) it returns columns you don't need, which wastes bandwidth and slows queries, and (2) when someone adds a column to the table, every SELECT * query silently returns a new column that downstream code didn't expect. Explicit column lists are self-documenting and break loudly, not silently.
🔍 WHERE — filter rows
Only rows where the condition is true are returned. That's the entire concept.
The WHERE clause filters rows. Only rows where the condition is true are returned. If no rows match, you get an empty result — not an error.
SELECT name, email FROM users WHERE country = 'US';
| name | |
|---|---|
| Alice Chen | alice@example.com |
| Marcus Lee | marcus@example.com |
String values need single quotes. Numbers don't:
SELECT name, age FROM users WHERE age >= 30;
| name | age |
|---|---|
| Bob Smith | 34 |
| Priya Patel | 31 |
| Marcus Lee | 42 |
Strings in single quotes
WHERE country = 'US'. Single quotes, not double. Double quotes are for identifiers (column names) in standard SQL, not string values.
Numbers without quotes
WHERE age >= 30. No quotes. Numbers compare numerically. Adding quotes ('30') may work in some databases via implicit conversion, but it's wrong and slow.
Empty result is not an error
If no rows match, you get zero rows back — not an error. Your application should handle empty results gracefully (e.g., "No users found").
Case sensitivity
'US' and 'us' may or may not match depending on the database. PostgreSQL string comparison is case-sensitive. MySQL depends on the column collation. Use consistent casing.
⚖️ Comparison operators
Six you'll use constantly. Each one has a specific meaning.
-- Equal SELECT name FROM users WHERE country = 'US'; -- Not equal (two syntaxes, both work) SELECT name FROM users WHERE country <> 'US'; SELECT name FROM users WHERE country != 'US'; -- Less than / greater than SELECT name, age FROM users WHERE age < 30; SELECT name, age FROM users WHERE age > 30; -- Less than or equal / greater than or equal SELECT name, age FROM users WHERE age <= 30; SELECT name, age FROM users WHERE age >= 30; -- BETWEEN (inclusive range) SELECT name, age FROM users WHERE age BETWEEN 25 AND 35;
Result of the last query: age BETWEEN 25 AND 35
| name | age |
|---|---|
| Alice Chen | 28 |
| Bob Smith | 34 |
| Priya Patel | 31 |
| Sofia García | 26 |
= (equal)
Not . SQL uses a single equals sign for comparison. is a syntax error. This trips up programmers coming from other languages.
<> or != (not equal)
Both work. <> is the ANSI standard. != is supported by most databases (PostgreSQL, MySQL, SQLite). Use <> for portability.
BETWEEN a AND b
Inclusive on both ends. age BETWEEN 25 AND 35 includes 25 and 35. Equivalent to age >= 25 AND age <= 35. Cleaner to read when a range makes sense.
IN (list)
WHERE country IN ('US', 'UK', 'CA') matches any of the listed values. Cleaner than country = 'US' OR country = 'UK' OR country = 'CA'.
Use BETWEEN and IN — they're clearer than the alternatives. age BETWEEN 25 AND 35 reads more naturally than age >= 25 AND age <= 35. country IN ('US', 'UK') reads more naturally than two OR clauses. Same result, less code, harder to make mistakes. SQL is a small language — use all of it.
🔗 AND / OR — combine conditions
AND requires all conditions. OR requires any. Parentheses decide the order.
-- AND: both conditions must be true SELECT name, age, country FROM users WHERE country = 'US' AND age >= 30; -- OR: at least one condition must be true SELECT name, age, country FROM users WHERE country = 'US' OR country = 'UK'; -- Parentheses matter: mix AND with OR carefully -- Without parentheses, AND binds tighter than OR SELECT name, age, country FROM users WHERE (country = 'US' OR country = 'UK') AND age >= 30; -- Equivalent to: -- (country = 'US' AND age >= 30) -- OR (country = 'UK' AND age >= 30)
Result of the last query: (US OR UK) AND age >= 30
| name | age | country |
|---|---|---|
| Bob Smith | 34 | UK |
| Marcus Lee | 42 | US |
Mixing AND and OR without parentheses is the #1 source of SQL bugs. WHERE country = 'US' OR country = 'UK' AND age >= 30 is not what it looks like. Because AND binds tighter than OR, this is parsed as country = 'US' OR (country = 'UK' AND age >= 30) — which returns all US users of any age, plus UK users over 30. That's probably not what you meant. Always use parentheses when mixing AND and OR.
AND
All conditions must be true. a AND b returns rows where both a and b are true. More conditions = fewer rows returned.
OR
At least one condition must be true. a OR b returns rows where either a or b is true. More conditions = more rows returned.
Precedence
AND binds tighter than OR. a AND b OR c means (a AND b) OR c. Use parentheses to be explicit — it costs nothing and prevents bugs.
NOT
NOT inverts a condition. WHERE NOT country = 'US' is the same as country <> 'US'. Useful for complex conditions: WHERE NOT (age > 18 AND country = 'US').
🔎 LIKE — pattern matching
Search text with wildcards. % for any characters. _ for one character.
-- Starts with a letter SELECT name FROM users WHERE name LIKE 'A%'; -- Returns: names starting with A -- Ends with a pattern SELECT email FROM users WHERE email LIKE '%@gmail.com'; -- Returns: every Gmail address -- Contains a substring SELECT name FROM users WHERE name LIKE '%lee%'; -- Returns: names containing "lee" anywhere -- Exactly N characters (underscore = one character) SELECT name FROM users WHERE name LIKE 'Al___'; -- Returns: 5-character names starting with "Al" -- Combine with AND / OR SELECT name, country FROM users WHERE country = 'US' AND name LIKE 'A%';
Result of the last query: country = 'US' AND name LIKE 'A%'
| name | country |
|---|---|
| Alice Chen | US |
% (any characters)
Matches zero or more of any character. 'A%' = starts with A. '%A' = ends with A. '%A%' = contains A. The most-used wildcard.
_ (one character)
Matches exactly one character. 'A_' = two-character string starting with A. 'A__' = three-character. Rarely used but useful for fixed-length patterns.
Case sensitivity
PostgreSQL: LIKE is case-sensitive, use ILIKE for insensitive. MySQL: LIKE is case-insensitive by default (depends on collation). SQLite: case-insensitive for ASCII.
Performance
'A%' (starts with) can use an index. '%A' (ends with) or '%A%' (contains) usually can't. If you're matching millions of rows, use a full-text index instead.
LIKE 'A%' uses an index; LIKE '%A' does not. A B-tree index can search for "starts with A" by jumping to the first matching entry. But it can't help with "ends with A" or "contains A" — those require scanning every row. If you're doing a lot of contains-searches, use a full-text index or a search engine. This is the most common SQL performance trap.
⚠️ NULL — the trap
NULL means "unknown." It's not equal to anything — not even itself. Use IS NULL.
-- CORRECT: use IS NULL SELECT name, email FROM users WHERE email IS NULL; -- CORRECT: use IS NOT NULL SELECT name, email FROM users WHERE email IS NOT NULL; -- WRONG: comparing with = NULL returns zero rows SELECT name FROM users WHERE email = NULL; -- Always returns 0 rows. NULL is not equal to NULL. -- It's not equal to anything. -- WRONG: comparing with != NULL also fails SELECT name FROM users WHERE email != NULL; -- Also returns 0 rows.
Result of: WHERE email IS NULL
| name | |
|---|---|
| Jake Torres | NULL |
| Emma Wilson | NULL |
The NULL trap. NULL = NULL is not true — it's NULL (unknown). NULL != NULL is also NULL. Any comparison with NULL returns NULL, which behaves as "not true" in a WHERE clause. This is why WHERE email = NULL returns zero rows — it's asking "is the unknown value equal to the unknown value?" The answer is "unknown," so the row doesn't match. Use IS NULL and IS NOT NULL — never = NULL.
NULL = "unknown"
NULL means "we don't know the value." It's not zero. It's not empty string. It's a separate concept. Missing data, not false data.
IS NULL / IS NOT NULL
The only correct way to check for NULL. WHERE email IS NULL matches rows where email has no value. Use IS NOT NULL to find rows where the value exists.
Three-valued logic
SQL has three boolean values: TRUE, FALSE, and NULL. Comparisons with NULL produce NULL (not FALSE). WHERE only keeps rows where the condition is TRUE — so NULL results are filtered out.
COALESCE for defaults
COALESCE(email, 'no email') returns the first non-NULL value. Use when displaying data — replaces NULL with a readable default.
🏆 Challenge
Three tasks. Each exercises a different concept from this lesson.
Task 1 — Write basic queries
Given this products table:
id | name | category | price | stock
Write queries to:
1. Return just the name and price of every product
2. Return only products in the "Electronics" category
3. Return products where price > 100 AND stock > 0
4. Return products where category is "Books" OR "Music"
5. Return products where the name starts with "S"
For each query, predict what the output would look like (how many columns, how many rows).
Task 2 — Spot the bugs
These queries have bugs. What's wrong with each?
1. SELECT name FROM users WHERE email = NULL;
2. SELECT * FROM users WHERE age == 30;
3. SELECT name FROM users WHERE country = US;
4. SELECT name FROM users WHERE country = 'US' OR country = 'UK' AND age > 30;
5. SELECT name, age FROM users WHERE age BETWEEN 30 AND 20;
Rewrite each one correctly.
Task 3 — Multi-condition query
Write a query that answers this question:
"Find all users who are from the US or UK, are between 25 and 40 years old, have an email address (not NULL), and whose name starts with a letter between A and M."
Break it down:
• Country condition: country IN (...)
• Age condition: age BETWEEN ...
• Email condition: email IS ...
• Name condition: name LIKE '...' (use multiple ORs for A-M, or a more creative approach)
Combine everything with AND. Use parentheses if mixing OR and AND. Then imagine what the result would look like — how many columns, how many rows, which users would be included.
⚠️ Common mistakes
The SELECT/WHERE bugs that catch every beginner.
SQL uses a single equals sign for comparison. age == 30 is a syntax error. Fix: age = 30. Trips up everyone coming from other languages.
WHERE email = NULL returns zero rows. NULL is not equal to anything, including itself. Fix: WHERE email IS NULL.
WHERE country = US is a syntax error (or treats US as a column name). Fix: WHERE country = 'US' — single quotes.
a OR b AND c is parsed as a OR (b AND c) because AND binds tighter. Fix: use parentheses to be explicit — (a OR b) AND c.
Fetches every column. Slower, wastes bandwidth, breaks when schema changes. Fix: list the columns you actually need.
Without ORDER BY, row order is not guaranteed. Fix: always add ORDER BY when order matters.
WHERE name = 'A%' looks for the literal string "A%" — not names starting with A. Fix: use LIKE for patterns, = for exact matches.
LIKE 'A%' matches "Alice" in some databases and not in others (depends on collation). Fix: use ILIKE in PostgreSQL for case-insensitive, or LOWER(name) LIKE 'a%' for portability.
➡️ What's next
You can read and filter rows. Next: sort them and pick the top N.
❓ Frequently asked questions
What does SELECT do in SQL?
SELECT reads data from a table. You list the columns you want, and the database returns rows containing those columns. SELECT name, email FROM users returns every row with just the name and email columns. SELECT * returns all columns (avoid this in production).
What does WHERE do in SQL?
WHERE filters rows. Only rows where the condition is true are returned. WHERE age > 18 returns only adult users. WHERE runs before any grouping or sorting, so it filters at the row level.
What are the comparison operators in SQL?
= equal, <> or != not equal, < less than, > greater than, <= less than or equal, >= greater than or equal, BETWEEN range (inclusive), LIKE pattern match, IN list membership, IS NULL for NULL checks. All used in WHERE clauses.
What is the difference between AND and OR in SQL?
AND requires both conditions to be true. OR requires at least one. AND has higher precedence than OR, so a AND b OR c means (a AND b) OR c. Use parentheses to be explicit.
How do I search with LIKE in SQL?
LIKE matches text patterns. '%' matches any number of characters. '_' matches exactly one character. WHERE email LIKE '%@gmail.com' matches every Gmail address. WHERE name LIKE 'A%' matches names starting with A.
How do I check for NULL in SQL?
Use IS NULL and IS NOT NULL — not = NULL or != NULL. WHERE email IS NULL returns rows where email is missing. WHERE email = NULL returns zero rows because NULL isn't equal to anything (including itself).
What is the difference between = and == in SQL?
SQL uses = for equality, not ==. == is not valid SQL syntax (it's from C, JavaScript, and other languages). Writing WHERE age == 18 throws a syntax error. Fix: WHERE age = 18.
Can I use WHERE on multiple columns?
Yes. Combine conditions with AND and OR. WHERE age > 18 AND country = 'US' AND active = true returns only rows where all three are true. Use parentheses when mixing AND and OR.