SQL SELECT and WHERE: Querying Data the Right Way
Master SQL SELECT statements, column aliases, filtering with WHERE, comparison operators, BETWEEN, IN, LIKE, NULL handling, and logical operators using a real e-commerce database.
SQL SELECT and WHERE: Querying Data the Right Way
SQL is the language of data. Whether you are building a backend API, analyzing business metrics, or debugging a production issue, knowing how to write precise SELECT queries is one of the most valuable skills you can develop. In this article you will learn everything about retrieving data: selecting all columns, selecting specific columns, renaming them with aliases, and filtering rows with the WHERE clause and all its operators.
We will use a realistic e-commerce database throughout so every example feels grounded in something you might actually build.
The E-Commerce Database We Will Use
Before writing any queries, here is the schema we will work with. All examples in this article use these three tables.
-- Products catalog
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
category VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATE NOT NULL
);
-- Registered customers
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(200) UNIQUE NOT NULL,
city VARCHAR(100),
country VARCHAR(100) NOT NULL DEFAULT 'US',
signup_date DATE NOT NULL
);
-- Orders placed by customers
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(customer_id),
product_id INT NOT NULL REFERENCES products(product_id),
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
status VARCHAR(50) NOT NULL, -- 'pending', 'shipped', 'delivered', 'cancelled'
order_date DATE NOT NULL
);Sample data so you can follow along:
INSERT INTO products VALUES
(1, 'Wireless Headphones', 'Electronics', 89.99, 120, TRUE, '2024-01-10'),
(2, 'USB-C Hub', 'Electronics', 34.99, 250, TRUE, '2024-01-15'),
(3, 'Mechanical Keyboard', 'Electronics', 129.99, 80, TRUE, '2024-02-01'),
(4, 'Standing Desk Mat', 'Office', 49.99, 60, TRUE, '2024-02-20'),
(5, 'Notebook (A5)', 'Stationery', 8.99, 500, TRUE, '2024-03-01'),
(6, 'Ballpoint Pen Set', 'Stationery', 5.99, 800, TRUE, '2024-03-05'),
(7, 'Monitor Stand', 'Office', 39.99, 45, FALSE, '2024-03-10'),
(8, 'Webcam HD', 'Electronics', 69.99, 30, TRUE, '2024-03-15'),
(9, 'Laptop Bag', 'Accessories', 59.99, 90, TRUE, '2024-04-01'),
(10, 'Screen Cleaner Kit', 'Accessories', 12.99, 200, TRUE, '2024-04-05');
INSERT INTO customers VALUES
(1, 'Alice', 'Johnson', 'alice@example.com', 'New York', 'US', '2023-06-01'),
(2, 'Bob', 'Smith', 'bob@example.com', 'Los Angeles', 'US', '2023-07-15'),
(3, 'Carol', 'White', 'carol@example.com', 'London', 'GB', '2023-08-20'),
(4, 'David', 'Brown', 'david@example.com', 'Toronto', 'CA', '2023-09-10'),
(5, 'Eve', 'Davis', 'eve@example.com', NULL, 'US', '2023-10-05'),
(6, 'Frank', 'Wilson', 'frank@example.com', 'Sydney', 'AU', '2024-01-12'),
(7, 'Grace', 'Moore', 'grace@example.com', 'New York', 'US', '2024-02-28'),
(8, 'Henry', 'Taylor', 'henry@example.com', 'Chicago', 'US', '2024-03-14');
INSERT INTO orders VALUES
(1, 1, 1, 2, 89.99, 'delivered', '2024-03-01'),
(2, 1, 3, 1, 129.99, 'delivered', '2024-03-10'),
(3, 2, 2, 1, 34.99, 'shipped', '2024-04-01'),
(4, 2, 8, 1, 69.99, 'pending', '2024-04-10'),
(5, 3, 5, 10, 8.99, 'delivered', '2024-02-15'),
(6, 3, 6, 20, 5.99, 'delivered', '2024-02-20'),
(7, 4, 9, 1, 59.99, 'shipped', '2024-04-05'),
(8, 5, 4, 1, 49.99, 'cancelled', '2024-03-20'),
(9, 7, 1, 1, 89.99, 'delivered', '2024-04-12'),
(10, 8, 10, 3, 12.99, 'pending', '2024-04-14');SELECT *: Fetching Every Column
The simplest form of a SELECT statement retrieves every column from a table using the asterisk wildcard.
SELECT * FROM products;This returns all 10 rows with every column: product_id, name, category, price, stock, is_active, created_at.
When SELECT * is acceptable:
- Exploratory queries in a database client where you are just looking around
- Quick debugging during development
- Ad-hoc investigations on small tables
When SELECT * is a problem:
- In application code — if someone adds a column later your app may break or transfer unnecessary data
- Over large tables with wide rows — you are fetching columns you never use
- In views or CTEs that are reused elsewhere
As a rule: write column names explicitly in code that ships to production.
SELECT Specific Columns
Name the columns you want, separated by commas.
SELECT product_id, name, price
FROM products;Result:
| product_id | name | price | |------------|----------------------|--------| | 1 | Wireless Headphones | 89.99 | | 2 | USB-C Hub | 34.99 | | 3 | Mechanical Keyboard | 129.99 | | ... | ... | ... |
You can reorder columns in the output simply by listing them in the order you want:
SELECT price, name, product_id
FROM products;The underlying table is not changed. You are only choosing what the result set looks like.
Column Aliases with AS
An alias renames a column in the output. This is useful when column names are technical or verbose, when you are computing an expression, or when building reports.
SELECT
product_id AS id,
name AS product_name,
price AS unit_price_usd
FROM products;The AS keyword is optional — you can write price unit_price_usd — but using AS makes the intent explicit and is strongly recommended for readability.
Aliases on expressions:
SELECT
name,
price,
price * 1.10 AS price_with_tax,
price * 0.90 AS discounted_price
FROM products;| name | price | price_with_tax | discounted_price | |---------------------|--------|----------------|-----------------| | Wireless Headphones | 89.99 | 98.989 | 80.991 | | USB-C Hub | 34.99 | 38.489 | 31.491 |
Aliases with spaces: if your alias needs spaces, wrap it in double quotes:
SELECT
name AS "Product Name",
price AS "Price (USD)"
FROM products;Note: some databases use backticks (MySQL) or square brackets (SQL Server) instead of double quotes. PostgreSQL uses double quotes.
Can you use aliases in WHERE? No. Aliases defined in SELECT are not visible to WHERE because the database evaluates WHERE before SELECT. This is a common mistake:
-- WRONG: this fails
SELECT price * 1.10 AS price_with_tax
FROM products
WHERE price_with_tax > 100;
-- CORRECT: repeat the expression
SELECT price * 1.10 AS price_with_tax
FROM products
WHERE price * 1.10 > 100;The WHERE Clause
WHERE filters which rows come back. Only rows where the condition evaluates to TRUE are included in the result.
SELECT *
FROM products
WHERE category = 'Electronics';Returns the 4 active electronics products (and the 1 inactive one — we will filter that shortly).
The WHERE clause comes after FROM and before ORDER BY, GROUP BY, and HAVING.
SELECT column1, column2
FROM table_name
WHERE condition
ORDER BY column1;Comparison Operators
SQL gives you six basic comparison operators.
Equal: =
SELECT * FROM products WHERE category = 'Office';Returns Standing Desk Mat and Monitor Stand (both in the Office category).
Not Equal: != or <>
Both mean "not equal". != is more common in modern SQL; <> is ANSI standard.
SELECT name, category FROM products WHERE category != 'Electronics';Returns everything that is not Electronics: Office, Stationery, Accessories.
Less Than and Greater Than: < > <= >=
-- Products under $40
SELECT name, price FROM products WHERE price < 40.00;
-- Products $40 and above
SELECT name, price FROM products WHERE price >= 40.00;
-- Products costing exactly $49.99 to $89.99 (inclusive)
SELECT name, price FROM products WHERE price >= 49.99 AND price <= 89.99;These work on numbers, dates, and strings (strings compare alphabetically).
-- Customers who signed up on or after 2024-01-01
SELECT first_name, last_name, signup_date
FROM customers
WHERE signup_date >= '2024-01-01';BETWEEN
BETWEEN is shorthand for >= lower AND <= upper. Both endpoints are inclusive.
-- Products priced between $30 and $70 (inclusive)
SELECT name, price
FROM products
WHERE price BETWEEN 30.00 AND 70.00;This is equivalent to:
WHERE price >= 30.00 AND price <= 70.00BETWEEN works on dates too:
-- Orders placed in April 2024
SELECT order_id, order_date, status
FROM orders
WHERE order_date BETWEEN '2024-04-01' AND '2024-04-30';NOT BETWEEN excludes the range:
-- Products outside the $30-$70 range
SELECT name, price
FROM products
WHERE price NOT BETWEEN 30.00 AND 70.00;IN
IN lets you test against a list of values. It is cleaner than writing multiple OR conditions.
-- Products in Electronics or Accessories
SELECT name, category
FROM products
WHERE category IN ('Electronics', 'Accessories');Compare to the verbose equivalent:
WHERE category = 'Electronics' OR category = 'Accessories'IN with numbers:
-- Specific orders by ID
SELECT order_id, status, order_date
FROM orders
WHERE order_id IN (1, 3, 5, 7);NOT IN excludes listed values:
-- Everything except Office and Stationery
SELECT name, category
FROM products
WHERE category NOT IN ('Office', 'Stationery');Caution with NOT IN and NULLs: if the list contains a NULL, NOT IN returns no rows at all. This is a subtle SQL trap covered in the NULL section below.
IN with a subquery (a preview of things to come):
-- Customers who have placed at least one order
SELECT first_name, last_name
FROM customers
WHERE customer_id IN (SELECT DISTINCT customer_id FROM orders);LIKE: Pattern Matching
LIKE lets you filter strings by pattern. There are two wildcard characters:
| Wildcard | Meaning |
|----------|-------------------------------|
| % | Zero or more characters |
| _ | Exactly one character |
Starts with a pattern
-- Products whose name starts with 'W'
SELECT name FROM products WHERE name LIKE 'W%';
-- Returns: Wireless Headphones, Webcam HDEnds with a pattern
-- Products whose name ends with 'Kit'
SELECT name FROM products WHERE name LIKE '%Kit';
-- Returns: Screen Cleaner KitContains a pattern
-- Products with 'stand' anywhere in the name (case-sensitive in most DBs)
SELECT name FROM products WHERE name LIKE '%Stand%';
-- Returns: Standing Desk Mat, Monitor StandSingle character wildcard
-- Customers with a 5-letter first name starting with 'A'
SELECT first_name FROM customers WHERE first_name LIKE 'A____';
-- Returns: AliceCase sensitivity
In PostgreSQL, LIKE is case-sensitive. Use ILIKE for case-insensitive matching:
-- PostgreSQL only
SELECT name FROM products WHERE name ILIKE '%keyboard%';In SQL Server, case sensitivity depends on the collation. Most default collations are case-insensitive, so LIKE 'keyboard%' and LIKE 'Keyboard%' return the same results.
NOT LIKE
-- Products not in the 'Stand...' family of names
SELECT name FROM products WHERE name NOT LIKE '%Stand%';Escaping wildcards
If you need to search for a literal % or _, use the ESCAPE clause:
-- Find products where the name literally contains '20%'
SELECT name FROM products WHERE name LIKE '%20!%%' ESCAPE '!';IS NULL and IS NOT NULL
NULL in SQL means "unknown" or "missing". It is not zero, it is not an empty string. It is the absence of a value.
Because NULL represents the unknown, you cannot compare it with = or !=:
-- WRONG: this returns no rows even if city is NULL
SELECT * FROM customers WHERE city = NULL;
-- CORRECT
SELECT * FROM customers WHERE city IS NULL;This is the single most common NULL mistake beginners make. Always use IS NULL or IS NOT NULL.
-- Customers with no city on file
SELECT first_name, last_name, email
FROM customers
WHERE city IS NULL;
-- Returns: Eve Davis (city is NULL)
-- Customers who do have a city
SELECT first_name, last_name, city
FROM customers
WHERE city IS NOT NULL;
-- Returns everyone except EveNULL in expressions: any arithmetic with NULL produces NULL:
SELECT 5 + NULL; -- NULL
SELECT NULL * 100; -- NULL
SELECT NULL = NULL; -- NULL (not TRUE!)This is why NOT IN with a NULL in the list is dangerous:
-- If the subquery returns NULL among its values, NOT IN returns nothing
SELECT * FROM customers
WHERE customer_id NOT IN (1, 2, NULL);
-- Returns 0 rows because NOT IN with NULL is always unknownAND, OR, NOT
Combine multiple conditions with logical operators.
AND: both conditions must be true
-- Active Electronics products priced under $100
SELECT name, price, is_active
FROM products
WHERE category = 'Electronics'
AND is_active = TRUE
AND price < 100.00;Returns: Wireless Headphones (89.99), USB-C Hub (34.99), Webcam HD (69.99).
OR: at least one condition must be true
-- Products in Stationery OR priced under $15
SELECT name, category, price
FROM products
WHERE category = 'Stationery'
OR price < 15.00;Returns: Notebook, Ballpoint Pen Set, Screen Cleaner Kit (because it is under $15).
NOT: negates a condition
-- Products that are NOT active
SELECT name, is_active
FROM products
WHERE NOT is_active;
-- Equivalent to:
WHERE is_active = FALSE
WHERE is_active != TRUECombining all three
-- Orders that are either delivered, or were placed before April 2024 and are not cancelled
SELECT order_id, status, order_date
FROM orders
WHERE status = 'delivered'
OR (order_date < '2024-04-01' AND status != 'cancelled');Operator Precedence
This is where many developers introduce subtle bugs. SQL evaluates operators in a specific order, and AND has higher precedence than OR. This means AND binds tighter.
Consider this query:
SELECT name, category, price
FROM products
WHERE category = 'Electronics'
OR category = 'Office'
AND price > 40.00;What you might intend: "(Electronics OR Office) AND price > 40"
What SQL actually does: "Electronics OR (Office AND price > 40)"
Because AND is evaluated first, this returns ALL Electronics products (regardless of price) PLUS only Office products over $40.
The fix: use parentheses explicitly.
-- This is what you probably meant
SELECT name, category, price
FROM products
WHERE (category = 'Electronics' OR category = 'Office')
AND price > 40.00;Now it means: must be in Electronics or Office, AND must be over $40.
Precedence order (highest to lowest):
NOTANDOR
Another example that trips people up:
-- Intended: active products that are NOT in Electronics or Office
-- Written incorrectly:
WHERE is_active = TRUE AND NOT category = 'Electronics' OR category = 'Office'
-- What it actually means:
WHERE (is_active = TRUE AND (NOT category = 'Electronics')) OR category = 'Office'
-- Which includes ALL Office products regardless of is_active!
-- Correct version:
WHERE is_active = TRUE AND category NOT IN ('Electronics', 'Office')
-- or
WHERE is_active = TRUE AND NOT (category = 'Electronics' OR category = 'Office')Rule of thumb: whenever you mix AND and OR, use parentheses even if you think you know the precedence. It makes the query readable and bug-free.
Practical Query Examples
Let us put everything together with some realistic business queries.
1. Active products available for purchase
SELECT
product_id,
name AS product_name,
category,
price AS unit_price,
stock AS units_in_stock
FROM products
WHERE is_active = TRUE
AND stock > 0
ORDER BY category, price;2. Mid-range electronics
SELECT
name,
price
FROM products
WHERE category = 'Electronics'
AND price BETWEEN 50.00 AND 100.00
AND is_active = TRUE;Returns: Wireless Headphones (89.99), Webcam HD (69.99).
3. UK or Australian customers
SELECT
first_name,
last_name,
email,
country
FROM customers
WHERE country IN ('GB', 'AU');Returns: Carol White (GB), Frank Wilson (AU).
4. Customers with Gmail addresses
SELECT
first_name,
last_name,
email
FROM customers
WHERE email LIKE '%@gmail.com';
-- Returns 0 rows with our sample data — all use @example.com
-- But the pattern is correct for real data5. Orders that need attention (pending or shipped but not recently placed)
SELECT
order_id,
customer_id,
status,
order_date
FROM orders
WHERE status IN ('pending', 'shipped')
AND order_date < '2024-04-10';6. Customers with no city listed who signed up in 2023
SELECT
first_name,
last_name,
email,
signup_date
FROM customers
WHERE city IS NULL
AND signup_date BETWEEN '2023-01-01' AND '2023-12-31';Returns: Eve Davis.
7. Products with suspicious names (contain special patterns for data quality audit)
-- Find products where name contains numbers (data quality check)
SELECT name FROM products WHERE name LIKE '%[0-9]%'; -- SQL Server syntax
SELECT name FROM products WHERE name ~ '[0-9]'; -- PostgreSQL syntaxORDER BY: Sorting Results
Technically ORDER BY is not part of WHERE, but you will use it on almost every SELECT. It sorts the result set.
-- Products sorted by price ascending (cheapest first)
SELECT name, price
FROM products
ORDER BY price ASC;
-- Products sorted by price descending (most expensive first)
SELECT name, price
FROM products
ORDER BY price DESC;Sort by multiple columns:
-- Sort by category, then within each category sort by price
SELECT category, name, price
FROM products
ORDER BY category ASC, price DESC;You can sort by a column that is not in your SELECT list:
SELECT name, price
FROM products
ORDER BY created_at DESC;You can also use the column position number (but avoid this in production code — fragile if columns move):
SELECT name, category, price
FROM products
ORDER BY 3 DESC; -- sorts by price (3rd column)LIMIT and TOP: Restricting Row Count
PostgreSQL / MySQL
-- Top 5 most expensive products
SELECT name, price
FROM products
ORDER BY price DESC
LIMIT 5;
-- Skip first 5, get next 5 (pagination)
SELECT name, price
FROM products
ORDER BY price DESC
LIMIT 5 OFFSET 5;SQL Server
-- Top 5 most expensive products
SELECT TOP 5 name, price
FROM products
ORDER BY price DESC;
-- Pagination with OFFSET FETCH (SQL Server 2012+)
SELECT name, price
FROM products
ORDER BY price DESC
OFFSET 5 ROWS
FETCH NEXT 5 ROWS ONLY;DISTINCT: Removing Duplicates
DISTINCT removes duplicate rows from the result.
-- What categories do we have?
SELECT DISTINCT category
FROM products;Returns: Electronics, Office, Stationery, Accessories (4 unique values instead of 10 rows).
DISTINCT applies to the combination of all selected columns:
-- Unique country + city combinations
SELECT DISTINCT country, city
FROM customers
WHERE city IS NOT NULL;Common Mistakes and How to Avoid Them
Mistake 1: Using = with NULL
-- WRONG
WHERE city = NULL -- always returns 0 rows
-- RIGHT
WHERE city IS NULLMistake 2: Forgetting precedence with AND/OR
-- Ambiguous (AND binds tighter)
WHERE a = 1 OR b = 2 AND c = 3
-- Clear intent
WHERE a = 1 OR (b = 2 AND c = 3)Mistake 3: NOT IN with possible NULLs
-- Dangerous if subquery can return NULL
WHERE id NOT IN (SELECT some_column FROM some_table)
-- Safer alternative
WHERE NOT EXISTS (
SELECT 1 FROM some_table
WHERE some_table.some_column = outer_table.id
)Mistake 4: LIKE with no wildcard behaves like equals
WHERE name LIKE 'Keyboard' -- same as WHERE name = 'Keyboard'
-- If you want "starts with Keyboard", write:
WHERE name LIKE 'Keyboard%'Mistake 5: Comparing strings and numbers without thinking about type
-- If product_id is an INT, do not wrap in quotes
WHERE product_id = 1 -- correct
WHERE product_id = '1' -- works in many DBs but causes implicit cast, potentially slowQuick Reference
| Operator | Example | Meaning |
|-----------------|--------------------------------------|----------------------------------|
| = | price = 49.99 | Equal |
| != or <> | status != 'cancelled' | Not equal |
| lt (<) | price < 50 | Less than |
| gt (>) | stock > 0 | Greater than |
| lte (<=) | price <= 100 | Less than or equal |
| gte (>=) | signup_date >= '2024-01-01' | Greater than or equal |
| BETWEEN | price BETWEEN 30 AND 70 | Inclusive range |
| IN | country IN ('US', 'GB') | Match any value in list |
| LIKE | email LIKE '%@gmail.com' | Pattern match (% = any, _ = one) |
| IS NULL | city IS NULL | Value is missing |
| IS NOT NULL | city IS NOT NULL | Value is present |
| AND | active = TRUE AND price < 100 | Both must be true |
| OR | country = 'US' OR country = 'GB' | Either must be true |
| NOT | NOT is_active | Negation |
Key Takeaways
SELECT *is fine for exploration but always name columns explicitly in production code.- Aliases (
AS) rename columns in the output and can be applied to expressions; they are not available in WHERE. WHEREfilters rows before they are returned; only rows where the condition is TRUE come through.BETWEEN low AND highis inclusive on both ends.IN (list)is cleaner than many OR conditions; avoidNOT INwhen the list might contain NULL.LIKE '%pattern%'uses%for any-length wildcards and_for single-character wildcards.IS NULLandIS NOT NULLare the only correct ways to test for NULL — never use= NULL.ANDhas higher precedence thanOR. Use parentheses to make complex conditions explicit.- Combine
WHEREwithORDER BYandLIMIT/TOPto build precise, paginated result sets.
In the next article we cover INSERT, UPDATE, DELETE, and how transactions keep your data safe when things go wrong.
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.