Select_queries
Chapter 8: SELECT - Querying Data
Section titled “Chapter 8: SELECT - Querying Data”Mastering Data Retrieval in PostgreSQL
Section titled “Mastering Data Retrieval in PostgreSQL”8.1 SELECT Fundamentals
Section titled “8.1 SELECT Fundamentals”The SELECT statement is the primary means of retrieving data from PostgreSQL. It allows you to specify exactly what data you want and how it should be presented.
SELECT Query Flow ========================================================================
┌─────────────────────────────────────────────────────────────────────┐ │ SELECT Query Processing │ │ │ │ SELECT columns ← Projection (what columns) │ │ FROM table ← Source (which table) │ │ WHERE conditions ← Selection (which rows) │ │ GROUP BY grouping ← Grouping (aggregation) │ │ HAVING group_conditions← Filter groups │ │ ORDER BY sorting ← Sorting (output order) │ │ LIMIT n; ← Limiting (how many) │ │ │ └─────────────────────────────────────────────────────────────────────┘
Processing Order: =================
1. FROM → Identify source tables 2. WHERE → Filter rows 3. GROUP BY → Group rows 4. HAVING → Filter groups 5. SELECT → Project columns 6. ORDER BY → Sort results 7. LIMIT → Limit results ========================================================================8.2 Basic SELECT Syntax
Section titled “8.2 Basic SELECT Syntax”-- Create sample tables for examplesCREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL, email VARCHAR(255) NOT NULL, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), total_amount DECIMAL(10,2), status VARCHAR(20) DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- Insert sample dataINSERT INTO users (username, email) VALUES ('john_doe', 'john@example.com'), ('jane_smith', 'jane@example.com'), ('bob_wilson', 'bob@example.com');
INSERT INTO orders (user_id, total_amount, status) VALUES (1, 150.00, 'completed'), (1, 75.50, 'pending'), (2, 200.00, 'completed'), (3, 50.00, 'cancelled');
-- Basic SELECT statements
-- Select all columnsSELECT * FROM users;
-- Select specific columnsSELECT username, email FROM users;
-- Select with WHERE clauseSELECT * FROM users WHERE is_active = TRUE;
-- Select with multiple conditionsSELECT * FROM ordersWHERE status = 'completed' AND total_amount > 100;8.3 Column Expressions
Section titled “8.3 Column Expressions”-- Arithmetic expressionsSELECT id, total_amount, total_amount * 1.10 AS with_tax, total_amount / 2 AS half_priceFROM orders;
-- String concatenationSELECT username || ' <' || email || '>' AS display_nameFROM users;
-- Using functionsSELECT UPPER(username) AS username_upper, LOWER(email) AS email_lower, INITCAP(username) AS username_properFROM users;
-- Column aliasesSELECT u.username AS "User Name", u.email AS "Email Address"FROM users u; -- Table alias
-- DISTINCT - remove duplicatesSELECT DISTINCT status FROM orders;SELECT DISTINCT user_id, status FROM orders; -- Unique combinations
-- Count with DISTINCTSELECT COUNT(DISTINCT user_id) AS unique_customers FROM orders;8.4 WHERE Clause
Section titled “8.4 WHERE Clause”The WHERE clause filters rows based on conditions.
-- Basic comparisonsSELECT * FROM users WHERE id = 1;SELECT * FROM users WHERE id != 1;SELECT * FROM users WHERE id <> 1; -- Not equal alternative
-- Comparison operatorsSELECT * FROM orders WHERE total_amount > 100;SELECT * FROM orders WHERE total_amount >= 100;SELECT * FROM orders WHERE total_amount < 100;SELECT * FROM orders WHERE total_amount <= 100;
-- BETWEEN (inclusive)SELECT * FROM ordersWHERE total_amount BETWEEN 50 AND 150;
-- IN (multiple values)SELECT * FROM users WHERE id IN (1, 2, 3);SELECT * FROM orders WHERE status IN ('pending', 'completed');
-- LIKE (pattern matching)SELECT * FROM users WHERE username LIKE 'j%'; -- Starts with jSELECT * FROM users WHERE username LIKE '%ohn%'; -- Contains ohnSELECT * FROM users WHERE username LIKE 'j_n%'; -- j + any char + n
-- ILIKE (case-insensitive)SELECT * FROM users WHERE username ILIKE 'JOHN%';
-- REGEXP (regular expressions)SELECT * FROM users WHERE username ~ '^[a-z]+$';SELECT * FROM users WHERE username ~* 'john'; -- Case-insensitive
-- NULL handlingSELECT * FROM users WHERE email IS NULL;SELECT * FROM users WHERE email IS NOT NULL;
-- Combining conditionsSELECT * FROM ordersWHERE status = 'completed' AND total_amount > 100 AND created_at > '2024-01-01';
-- OR conditionsSELECT * FROM ordersWHERE status = 'completed' OR status = 'pending';
-- IN with subquerySELECT * FROM usersWHERE id IN (SELECT user_id FROM orders WHERE status = 'completed');8.5 ORDER BY
Section titled “8.5 ORDER BY”Sort query results using ORDER BY.
-- Basic sorting (ascending by default)SELECT * FROM users ORDER BY username;SELECT * FROM users ORDER BY username ASC; -- Explicit
-- Descending orderSELECT * FROM users ORDER BY created_at DESC;
-- Multiple columnsSELECT * FROM ordersORDER BY user_id ASC, total_amount DESC;
-- NULLS FIRST/LASTSELECT * FROM users ORDER BY email NULLS FIRST;SELECT * FROM users ORDER BY email NULLS LAST;
-- By expressionSELECT username, LENGTH(username) AS name_lengthFROM usersORDER BY LENGTH(username) DESC;
-- By column position (1-based)SELECT id, username, emailFROM usersORDER BY 3; -- Order by 3rd column (email)
-- Random orderSELECT * FROM users ORDER BY RANDOM();
-- Order by with LIMIT (useful for top-N queries)SELECT * FROM ordersORDER BY total_amount DESCLIMIT 3;8.6 LIMIT, OFFSET, and FETCH
Section titled “8.6 LIMIT, OFFSET, and FETCH”Control the number of rows returned.
-- LIMITSELECT * FROM users LIMIT 10;
-- OFFSET (skip rows)SELECT * FROM users OFFSET 5;
-- LIMIT with OFFSET (pagination)SELECT * FROM usersORDER BY idLIMIT 10 OFFSET 20; -- Page 3 (21-30)
-- FETCH (SQL standard, similar to LIMIT)SELECT * FROM users FETCH FIRST 10 ROWS ONLY;SELECT * FROM users OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY;
-- Practical pagination pattern-- Page 1SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 0;-- Page 2SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 10;-- Page 3SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 20;
-- Keyset pagination (more efficient for large offsets)-- Instead of OFFSET, use WHERE clauseSELECT * FROM ordersWHERE id > 10ORDER BY idLIMIT 10;
-- Get total count with LIMITSELECT (SELECT COUNT(*) FROM orders) AS total_count, *FROM ordersORDER BY idLIMIT 10;8.7 CASE Expressions
Section titled “8.7 CASE Expressions”Add conditional logic to queries.
-- Simple CASESELECT username, CASE status WHEN 'completed' THEN 'Done' WHEN 'pending' THEN 'In Progress' WHEN 'cancelled' THEN 'Cancelled' ELSE 'Unknown' END AS status_textFROM orders;
-- Searched CASESELECT total_amount, CASE WHEN total_amount < 50 THEN 'Small Order' WHEN total_amount >= 50 AND total_amount < 150 THEN 'Medium Order' WHEN total_amount >= 150 THEN 'Large Order' END AS order_sizeFROM orders;
-- CASE in aggregationSELECT COUNT(*) AS total_orders, COUNT(CASE WHEN status = 'completed' THEN 1 END) AS completed, COUNT(CASE WHEN status = 'pending' THEN 1 END) AS pending, COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelledFROM orders;
-- CASE with NULL handlingSELECT username, COALESCE(email, 'No email provided') AS emailFROM users;
-- NULLIF for division by zeroSELECT total_amount, order_count, CASE WHEN order_count > 0 THEN total_amount / order_count ELSE 0 END AS avg_order_valueFROM ( SELECT SUM(total_amount) AS total_amount, COUNT(*) AS order_count FROM orders) sub;8.8 JOINs in SELECT
Section titled “8.8 JOINs in SELECT”Combine data from multiple tables.
-- Create additional sample tablesCREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(200), price DECIMAL(10,2));
CREATE TABLE order_items ( id SERIAL PRIMARY KEY, order_id INTEGER REFERENCES orders(id), product_id INTEGER REFERENCES products(id), quantity INTEGER, price DECIMAL(10,2));
-- INNER JOINSELECT o.id AS order_id, u.username, o.total_amount, o.statusFROM orders oINNER JOIN users u ON o.user_id = u.id;
-- Equivalent to (INNER is default)SELECT o.id, u.usernameFROM orders oJOIN users u ON o.user_id = u.id;
-- LEFT JOIN (include all from left table)SELECT u.username, o.id AS order_id, o.total_amountFROM users uLEFT JOIN orders o ON u.id = o.user_id;
-- RIGHT JOIN (include all from right table)SELECT u.username, o.id AS order_idFROM users uRIGHT JOIN orders o ON u.id = o.user_id;
-- FULL OUTER JOIN (include all from both)SELECT u.username, o.id AS order_idFROM users uFULL OUTER JOIN orders o ON u.id = o.user_id;
-- Multiple JOINsSELECT o.id AS order_id, u.username, p.name AS product_name, oi.quantity, oi.priceFROM orders oJOIN users u ON o.user_id = u.idJOIN order_items oi ON o.id = oi.order_idJOIN products p ON oi.product_id = p.id;8.9 Subqueries in SELECT
Section titled “8.9 Subqueries in SELECT”Use subqueries for complex data retrieval.
-- Subquery in WHERESELECT * FROM ordersWHERE user_id IN ( SELECT id FROM users WHERE is_active = TRUE);
-- Subquery in FROM (derived table)SELECT order_count, COUNT(*) AS user_countFROM ( SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id) user_ordersGROUP BY order_count;
-- Subquery in SELECT (scalar subquery)SELECT u.username, (SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS order_countFROM users u;
-- Correlated subquerySELECT o.*, (SELECT AVG(total_amount) FROM orders WHERE user_id = o.user_id) AS avg_order_valueFROM orders o;
-- EXISTSSELECT * FROM users uWHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- NOT EXISTSSELECT * FROM users uWHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- Subquery with ALL/ANYSELECT * FROM ordersWHERE total_amount > ALL ( SELECT total_amount FROM orders WHERE user_id = 1);
SELECT * FROM ordersWHERE total_amount > ANY ( SELECT total_amount FROM orders WHERE user_id = 1);8.10 Practical Examples
Section titled “8.10 Practical Examples”-- E-commerce: Get top spending customersSELECT u.id, u.username, u.email, SUM(o.total_amount) AS total_spent, COUNT(o.id) AS order_count, AVG(o.total_amount) AS avg_order_valueFROM users uJOIN orders o ON u.id = o.user_idWHERE o.status = 'completed'GROUP BY u.id, u.username, u.emailORDER BY total_spent DESCLIMIT 10;
-- Get month-over-month revenue growthWITH monthly_revenue AS ( SELECT DATE_TRUNC('month', created_at) AS month, SUM(total_amount) AS revenue FROM orders WHERE status = 'completed' GROUP BY DATE_TRUNC('month', created_at))SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS prev_month, revenue - LAG(revenue) OVER (ORDER BY month) AS growth, ROUND( (revenue - LAG(revenue) OVER (ORDER BY month)) / NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 2 ) AS growth_percentFROM monthly_revenueORDER BY month;
-- Find users with no ordersSELECT * FROM users uWHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id);
-- Alternative using LEFT JOINSELECT u.*FROM users uLEFT JOIN orders o ON u.id = o.user_idWHERE o.id IS NULL;8.11 Best Practices
Section titled “8.11 Best Practices” SELECT Query Best Practices ========================================================================
1. Specify Columns Explicitly ┌──────────────────────────────────────────────────────────────┐ │ SELECT * FROM users; │ │ │ │ ❌ Retrieves all columns │ │ ❌ Returns changeable data (password_hash exposed) │ │ ❌ No control over column order │ │ │ │ SELECT id, username, email FROM users; │ │ │ │ ✓ Only retrieves needed columns │ │ ✓ Better performance │ │ ✓ Explicit about data being used │ └──────────────────────────────────────────────────────────────┘
2. Use Proper Indexing ┌──────────────────────────────────────────────────────────────┐ │ • Index columns used in WHERE clauses │ │ • Index foreign key columns │ │ • Use EXPLAIN to analyze query plans │ └──────────────────────────────────────────────────────────────┘
3. Avoid SELECT in Loops ┌──────────────────────────────────────────────────────────────┐ │ ❌ N+1 Query Problem: │ │ FOR user IN (SELECT * FROM users) LOOP │ │ SELECT COUNT(*) FROM orders WHERE user_id = user.id; │ │ END LOOP; │ │ │ │ ✓ Use JOIN or subquery instead: │ │ SELECT u.*, COUNT(o.id) FROM users u │ │ LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.* │ └──────────────────────────────────────────────────────────────┘
4. Use Keyset Pagination for Large Tables ┌──────────────────────────────────────────────────────────────┐ │ ❌ OFFSET for deep pagination: │ │ SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 1000000 │ │ │ │ ✓ Keyset pagination: │ │ SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT │ └──────────────────────────────────────────────────────────────┘
5. Use EXPLAIN ANALYZE ┌──────────────────────────────────────────────────────────────┐ │ EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1; │ │ │ │ • Shows actual execution time │ │ • Reveals sequential scans vs index scans │ │ • Identifies missing indexes │ └──────────────────────────────────────────────────────────────┘ ========================================================================Next Chapter
Section titled “Next Chapter”Chapter 9: INSERT - Adding Data
Last Updated: February 2026