Mastering SELECT Queries
Learning Objectives
- Master basic SELECT statement structure
- Use WHERE clauses for filtering data
- Implement sorting with ORDER BY
- Limit and paginate results
- Select distinct values and handle NULL
- Use operators and wildcards effectively
- Apply column aliases and expressions
- Understand query execution order
SELECT Statement Anatomy
The SELECT statement is the most used SQL command. It retrieves data from one or more tables. Let's master it! 🔍
Query Execution Order
Basic SELECT Statements
graph TB
A[SELECT] --> B[All Columns *]
A --> C[Specific Columns]
A --> D[Expressions]
A --> E[Functions]
A --> F[DISTINCT]
A --> G[Aliases]
style A fill:#dcfce7
style B fill:#dbeafe
style C fill:#dbeafe
style D fill:#fef3c7
style E fill:#fee2e2
style F fill:#e9d5ff
style G fill:#e0f2fe
Basic SELECT Examples
-- ========================================
-- SELECTING COLUMNS
-- ========================================
-- Select all columns (avoid in production)
SELECT * FROM users;
-- Select specific columns (recommended)
SELECT username, email, created_at
FROM users;
-- Select with table prefix (useful for joins)
SELECT
users.username,
users.email,
users.created_at
FROM users;
-- Using table alias
SELECT
u.username,
u.email,
u.created_at
FROM users u;
-- ========================================
-- COLUMN ALIASES
-- ========================================
-- Simple alias
SELECT
username AS user_name,
email AS email_address
FROM users;
-- Alias without AS (valid but less clear)
SELECT
username user_name,
email email_address
FROM users;
-- Alias with spaces (use backticks)
SELECT
username AS `User Name`,
email AS `Email Address`,
created_at AS `Registration Date`
FROM users;
-- ========================================
-- EXPRESSIONS AND CALCULATIONS
-- ========================================
-- Arithmetic expressions
SELECT
product_name,
price,
price * 1.1 AS price_with_tax,
price * quantity AS total_value
FROM products;
-- String concatenation
SELECT
CONCAT(first_name, ' ', last_name) AS full_name,
CONCAT(city, ', ', country) AS location,
UPPER(username) AS username_upper,
LENGTH(bio) AS bio_length
FROM users;
-- Date calculations
SELECT
username,
created_at,
DATEDIFF(NOW(), created_at) AS days_since_joined,
DATE_ADD(created_at, INTERVAL 1 YEAR) AS anniversary_date,
YEAR(created_at) AS join_year
FROM users;
-- Conditional expressions
SELECT
product_name,
price,
CASE
WHEN price < 50 THEN 'Budget'
WHEN price < 200 THEN 'Mid-range'
ELSE 'Premium'
END AS price_category,
IF(stock_quantity > 0, 'In Stock', 'Out of Stock') AS availability
FROM products;
-- ========================================
-- DISTINCT VALUES
-- ========================================
-- Get unique values
SELECT DISTINCT status FROM orders;
-- Distinct combinations
SELECT DISTINCT country, city FROM customers;
-- Count distinct values
SELECT
COUNT(DISTINCT customer_id) AS unique_customers,
COUNT(DISTINCT product_id) AS unique_products,
COUNT(*) AS total_orders
FROM orders;
-- ========================================
-- LIMITING RESULTS
-- ========================================
-- Get first 10 rows
SELECT * FROM products LIMIT 10;
-- Get rows 11-20 (pagination)
SELECT * FROM products LIMIT 10 OFFSET 10;
-- Or alternative syntax
SELECT * FROM products LIMIT 10, 10;
-- Get top 5 expensive products
SELECT product_name, price
FROM products
ORDER BY price DESC
LIMIT 5;
-- ========================================
-- SELECTING FROM MULTIPLE DATABASES
-- ========================================
-- Fully qualified table name
SELECT * FROM database_name.table_name;
-- Cross-database query
SELECT
db1.users.username,
db2.profiles.bio
FROM
database1.users db1,
database2.profiles db2
WHERE db1.user_id = db2.user_id;
-- ========================================
-- LITERAL VALUES AND CONSTANTS
-- ========================================
-- Select literal values
SELECT
'Hello World' AS greeting,
42 AS answer,
NOW() AS current_time,
PI() AS pi_value;
-- Combine literals with data
SELECT
username,
'Active' AS status,
100 AS default_score,
NOW() AS query_time
FROM users;
-- Using DUAL (dummy table)
SELECT 'Test' FROM DUAL;
SELECT 1 + 1 FROM DUAL;
SELECT NOW() FROM DUAL;
WHERE Clause - Filtering Data
WHERE Clause Examples
-- ========================================
-- COMPARISON OPERATORS
-- ========================================
-- Basic equality
SELECT * FROM users WHERE status = 'active';
-- Not equal (two syntaxes)
SELECT * FROM products WHERE category != 'Electronics';
SELECT * FROM products WHERE category <> 'Electronics';
-- Numeric comparisons
SELECT * FROM products WHERE price > 100;
SELECT * FROM orders WHERE total_amount <= 500;
SELECT * FROM users WHERE age >= 18;
-- Date comparisons
SELECT * FROM orders WHERE order_date > '2024-01-01';
SELECT * FROM events WHERE event_date <= NOW();
-- ========================================
-- LOGICAL OPERATORS
-- ========================================
-- AND operator (all conditions must be true)
SELECT * FROM products
WHERE price > 50
AND stock_quantity > 0
AND status = 'active';
-- OR operator (any condition can be true)
SELECT * FROM users
WHERE role = 'admin'
OR role = 'moderator';
-- Combining AND and OR (use parentheses!)
SELECT * FROM products
WHERE (category = 'Electronics' OR category = 'Computers')
AND price < 1000
AND stock_quantity > 0;
-- NOT operator
SELECT * FROM users WHERE NOT status = 'banned';
SELECT * FROM products WHERE NOT (price > 100 AND category = 'Luxury');
-- ========================================
-- IN and NOT IN
-- ========================================
-- IN operator (match any value in list)
SELECT * FROM orders
WHERE status IN ('pending', 'processing', 'shipped');
-- NOT IN
SELECT * FROM products
WHERE category NOT IN ('Discontinued', 'Test');
-- IN with subquery
SELECT * FROM users
WHERE user_id IN (
SELECT DISTINCT user_id
FROM orders
WHERE total_amount > 1000
);
-- ========================================
-- BETWEEN
-- ========================================
-- Numeric range
SELECT * FROM products
WHERE price BETWEEN 50 AND 200; -- Inclusive
-- Date range
SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31';
-- NOT BETWEEN
SELECT * FROM users
WHERE age NOT BETWEEN 13 AND 17;
-- ========================================
-- NULL HANDLING
-- ========================================
-- Check for NULL
SELECT * FROM users WHERE phone IS NULL;
SELECT * FROM products WHERE description IS NOT NULL;
-- NEVER use = NULL (wrong!)
-- SELECT * FROM users WHERE phone = NULL; -- This won't work!
-- Coalesce NULL values
SELECT * FROM products
WHERE COALESCE(discount, 0) > 10;
-- ========================================
-- LIKE and Pattern Matching
-- ========================================
-- Starts with
SELECT * FROM users WHERE username LIKE 'john%';
-- Ends with
SELECT * FROM users WHERE email LIKE '%@gmail.com';
-- Contains
SELECT * FROM products WHERE name LIKE '%laptop%';
-- Specific pattern (_ for single character)
SELECT * FROM codes WHERE code LIKE 'ABC___'; -- ABC followed by 3 chars
-- Case-insensitive by default
SELECT * FROM users WHERE name LIKE 'john%'; -- Matches 'John', 'JOHN', etc.
-- Escape special characters
SELECT * FROM products WHERE name LIKE '%\_%' ESCAPE '\'; -- Contains underscore
-- NOT LIKE
SELECT * FROM users WHERE email NOT LIKE '%@test.com';
-- ========================================
-- REGULAR EXPRESSIONS
-- ========================================
-- Basic regex
SELECT * FROM users WHERE username REGEXP '^[a-z]+$'; -- Lowercase only
-- Email validation
SELECT * FROM users
WHERE email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';
-- Phone number pattern
SELECT * FROM contacts
WHERE phone REGEXP '^[0-9]{3}-[0-9]{3}-[0-9]{4}$';
-- Word boundaries
SELECT * FROM posts
WHERE content REGEXP '[[:<:]]MySQL[[:>:]]'; -- Whole word 'MySQL'
-- Case-sensitive regex
SELECT * FROM users
WHERE BINARY username REGEXP '^[A-Z]'; -- Must start with uppercase
-- ========================================
-- COMPLEX CONDITIONS
-- ========================================
-- Multiple conditions with grouping
SELECT * FROM products
WHERE
(category = 'Electronics' AND price < 500)
OR (category = 'Books' AND price < 50)
OR (is_featured = TRUE);
-- Date and status combination
SELECT * FROM orders
WHERE
status IN ('pending', 'processing')
AND order_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
AND total_amount > 100;
-- Complex NULL handling
SELECT * FROM users
WHERE
(email IS NOT NULL AND email != '')
AND (phone IS NULL OR phone = '')
AND status = 'active';
-- Using CASE in WHERE
SELECT * FROM products
WHERE price >
CASE category
WHEN 'Luxury' THEN 1000
WHEN 'Standard' THEN 100
ELSE 10
END;
ORDER BY - Sorting Results
Sorting Data with ORDER BY
-- ========================================
-- BASIC SORTING
-- ========================================
-- Sort ascending (default)
SELECT * FROM users ORDER BY username;
SELECT * FROM users ORDER BY username ASC;
-- Sort descending
SELECT * FROM products ORDER BY price DESC;
-- Multiple columns
SELECT * FROM users
ORDER BY last_name, first_name;
-- Different directions for each column
SELECT * FROM products
ORDER BY category ASC, price DESC;
-- ========================================
-- SORTING BY EXPRESSIONS
-- ========================================
-- Sort by calculated field
SELECT
product_name,
price,
discount,
price - discount AS final_price
FROM products
ORDER BY final_price DESC;
-- Sort by function result
SELECT * FROM users
ORDER BY LENGTH(username) DESC;
-- Sort by CASE expression
SELECT * FROM orders
ORDER BY
CASE status
WHEN 'urgent' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END;
-- ========================================
-- SORTING WITH NULL VALUES
-- ========================================
-- NULLs first (MySQL default for ASC)
SELECT * FROM users
ORDER BY last_login ASC;
-- NULLs last
SELECT * FROM users
ORDER BY last_login IS NULL, last_login DESC;
-- Custom NULL handling
SELECT * FROM products
ORDER BY
CASE
WHEN discount IS NULL THEN 1
ELSE 0
END,
discount DESC;
-- ========================================
-- FIELD() FUNCTION FOR CUSTOM ORDER
-- ========================================
-- Specific order for values
SELECT * FROM orders
ORDER BY FIELD(status, 'pending', 'processing', 'shipped', 'delivered');
-- Custom priority ordering
SELECT * FROM tickets
ORDER BY
FIELD(priority, 'critical', 'high', 'medium', 'low'),
created_at ASC;
-- ========================================
-- RANDOM ORDERING
-- ========================================
-- Random order (be careful with large tables!)
SELECT * FROM products
ORDER BY RAND()
LIMIT 10;
-- Better random selection for large tables
SELECT * FROM products
WHERE RAND() < 0.01 -- ~1% sample
ORDER BY RAND()
LIMIT 10;
-- ========================================
-- COMBINING WITH LIMIT
-- ========================================
-- Top 10 highest prices
SELECT product_name, price
FROM products
ORDER BY price DESC
LIMIT 10;
-- Latest 5 orders
SELECT * FROM orders
ORDER BY order_date DESC
LIMIT 5;
-- Pagination with ORDER BY
SELECT * FROM users
ORDER BY user_id
LIMIT 20 OFFSET 40; -- Page 3, 20 per page
Advanced SELECT Techniques
Pro Tips
-- ========================================
-- WORKING WITH DATES
-- ========================================
-- Current date/time
SELECT
NOW() AS current_datetime,
CURDATE() AS current_date,
CURTIME() AS current_time;
-- Date filtering
SELECT * FROM orders
WHERE DATE(order_datetime) = CURDATE(); -- Today's orders
SELECT * FROM orders
WHERE YEAR(order_date) = 2024
AND MONTH(order_date) = 12; -- December 2024
-- Date ranges
SELECT * FROM users
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY); -- Last 30 days
SELECT * FROM events
WHERE event_date BETWEEN
DATE_SUB(NOW(), INTERVAL 1 WEEK)
AND DATE_ADD(NOW(), INTERVAL 1 WEEK); -- ±1 week
-- ========================================
-- STRING MANIPULATION
-- ========================================
SELECT
UPPER(name) AS name_upper,
LOWER(email) AS email_lower,
CONCAT(first_name, ' ', last_name) AS full_name,
SUBSTRING(description, 1, 100) AS short_desc,
REPLACE(phone, '-', '') AS phone_clean,
TRIM(address) AS address_trimmed,
LENGTH(bio) AS bio_length,
REVERSE(username) AS username_reversed
FROM users;
-- ========================================
-- CONDITIONAL LOGIC
-- ========================================
-- IF statement
SELECT
product_name,
IF(stock > 0, 'Available', 'Out of Stock') AS status
FROM products;
-- CASE statement
SELECT
order_id,
total_amount,
CASE
WHEN total_amount < 100 THEN 'Small'
WHEN total_amount < 500 THEN 'Medium'
WHEN total_amount < 1000 THEN 'Large'
ELSE 'Extra Large'
END AS order_size
FROM orders;
-- IFNULL and COALESCE
SELECT
username,
IFNULL(phone, 'No phone') AS phone,
COALESCE(email, phone, 'No contact') AS primary_contact
FROM users;
-- ========================================
-- QUERY OPTIMIZATION
-- ========================================
-- Use EXPLAIN to analyze query
EXPLAIN SELECT * FROM orders WHERE customer_id = 123;
-- Use index hints
SELECT * FROM users USE INDEX (idx_email)
WHERE email = 'user@example.com';
-- Avoid functions on indexed columns
-- Bad:
SELECT * FROM orders WHERE YEAR(order_date) = 2024;
-- Good:
SELECT * FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01';
-- ========================================
-- UNION - Combining Results
-- ========================================
-- UNION (removes duplicates)
SELECT username, email FROM customers
UNION
SELECT username, email FROM vendors;
-- UNION ALL (keeps duplicates, faster)
SELECT product_name, 'In Stock' AS status FROM products WHERE stock > 0
UNION ALL
SELECT product_name, 'Out of Stock' FROM products WHERE stock = 0;
-- ========================================
-- EXISTS vs IN
-- ========================================
-- EXISTS (usually faster for large datasets)
SELECT * FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id
);
-- IN (simpler syntax)
SELECT * FROM customers
WHERE customer_id IN (
SELECT DISTINCT customer_id FROM orders
);
Practice Exercise
E-Commerce Query Challenge
💡 Click for Solutions
-- 1. Find all active products under $50
SELECT
product_id,
product_name,
price,
stock_quantity
FROM products
WHERE status = 'active'
AND price < 50
ORDER BY price ASC;
-- 2. List customers who joined in the last 30 days
SELECT
customer_id,
first_name,
last_name,
email,
created_at
FROM customers
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY created_at DESC;
-- 3. Show top 5 best-selling products
SELECT
p.product_id,
p.product_name,
COUNT(oi.order_id) AS times_ordered,
SUM(oi.quantity) AS total_quantity_sold,
SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM products p
INNER JOIN order_items oi ON p.product_id = oi.product_id
GROUP BY p.product_id
ORDER BY total_quantity_sold DESC
LIMIT 5;
-- 4. Find orders with total over $500
SELECT
order_id,
customer_id,
order_date,
total_amount,
status
FROM orders
WHERE total_amount > 500
ORDER BY total_amount DESC;
-- 5. List products with "laptop" in the name
SELECT
product_id,
product_name,
category,
price,
stock_quantity
FROM products
WHERE LOWER(product_name) LIKE '%laptop%'
OR LOWER(description) LIKE '%laptop%'
ORDER BY price DESC;
-- 6. Show customers with no orders
SELECT
c.customer_id,
c.first_name,
c.last_name,
c.email,
c.created_at
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL
ORDER BY c.created_at DESC;
-- Alternative using NOT EXISTS
SELECT
customer_id,
first_name,
last_name,
email
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
-- 7. Find products low in stock (less than 10)
SELECT
product_id,
product_name,
category,
stock_quantity,
CASE
WHEN stock_quantity = 0 THEN 'Out of Stock'
WHEN stock_quantity < 5 THEN 'Critical'
ELSE 'Low Stock'
END AS stock_status
FROM products
WHERE stock_quantity < 10
AND status = 'active'
ORDER BY stock_quantity ASC;
-- 8. Calculate age of each customer
SELECT
customer_id,
first_name,
last_name,
birth_date,
TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age,
CASE
WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) < 18 THEN 'Minor'
WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) < 30 THEN 'Young Adult'
WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) < 50 THEN 'Adult'
WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) < 65 THEN 'Middle Age'
ELSE 'Senior'
END AS age_group
FROM customers
WHERE birth_date IS NOT NULL
ORDER BY age DESC;