SQL Data Query Language (DQL)
DQL is the subset of SQL used to retrieve data from a database. It consists of one statement: SELECT. Everything from simple column retrieval to complex multi-table analytical queries uses SELECT.
DQL is read-only — it never modifies data.
The SELECT Statement
At its simplest, SELECT retrieves columns from a table:
SELECT column1, column2FROM table_name;A fully featured SELECT can include filtering, sorting, grouping, joining, and windowing:
SELECT c.name, COUNT(o.order_id) AS order_count, SUM(o.amount) AS total_spentFROM customers cJOIN orders o ON c.customer_id = o.customer_idWHERE o.status = 'completed' AND o.created_at >= '2025-01-01'GROUP BY c.customer_id, c.nameHAVING SUM(o.amount) > 500ORDER BY total_spent DESCLIMIT 20;SQL Clause Execution Order
SQL clauses are written in this order:
SELECT → FROM → JOIN → WHERE → GROUP BY → HAVING → ORDER BY → LIMITBut they are executed in a different order:
1. FROM / JOIN — identify and join source tables2. WHERE — filter rows3. GROUP BY — group rows into aggregates4. HAVING — filter groups5. SELECT — compute output columns6. DISTINCT — remove duplicates7. ORDER BY — sort8. LIMIT / OFFSET — restrict row countUnderstanding execution order explains why you can’t use a SELECT alias in WHERE (WHERE runs before SELECT computes aliases), but can reference it in ORDER BY (ORDER BY runs after SELECT).
Column Selection and Expressions
-- Select all columns (use only for exploration)SELECT * FROM products;
-- Select specific columnsSELECT product_id, name, price FROM products;
-- Compute expressionsSELECT name, price, price * 0.1 AS tax, price * 1.1 AS price_with_taxFROM products;
-- String expressionsSELECT CONCAT(first_name, ' ', last_name) AS full_name, UPPER(email) AS email_upper, LENGTH(bio) AS bio_lengthFROM users;
-- Conditional valuesSELECT name, price, CASE WHEN price >= 100 THEN 'Expensive' WHEN price >= 50 THEN 'Mid-range' ELSE 'Budget' END AS price_tierFROM products;Filtering with WHERE
SELECT name, price FROM productsWHERE category = 'electronics' AND price < 300 AND is_active = TRUE;
-- Pattern matchingWHERE name LIKE 'Pro%'WHERE email ILIKE '%@gmail.com' -- PostgreSQL: case-insensitive
-- RangeWHERE price BETWEEN 50 AND 200
-- ListWHERE status IN ('pending', 'processing', 'shipped')
-- NULL checkWHERE phone IS NULLWHERE discount IS NOT NULLJoining Tables
-- INNER JOIN: rows in both tablesSELECT o.order_id, c.name, o.amountFROM orders oJOIN customers c ON o.customer_id = c.customer_id;
-- LEFT JOIN: all rows from left, NULLs where no right matchSELECT c.name, COUNT(o.order_id) AS order_countFROM customers cLEFT JOIN orders o ON c.customer_id = o.customer_idGROUP BY c.customer_id, c.name;
-- Multiple joinsSELECT c.name, p.name AS product, oi.quantityFROM orders oJOIN customers c ON o.customer_id = c.customer_idJOIN order_items oi ON o.order_id = oi.order_idJOIN products p ON oi.product_id = p.product_id;Grouping and Aggregation
-- GROUP BY: one result row per groupSELECT category, COUNT(*) AS product_count, AVG(price) AS avg_price, SUM(stock) AS total_stockFROM productsGROUP BY categoryORDER BY product_count DESC;
-- HAVING: filter groups (not individual rows)SELECT category, COUNT(*) AS cntFROM productsGROUP BY categoryHAVING COUNT(*) > 10;Aggregate functions: COUNT(), SUM(), AVG(), MIN(), MAX(), STRING_AGG(), ARRAY_AGG()
Subqueries in DQL
-- Scalar subquery in SELECTSELECT name, price, (SELECT AVG(price) FROM products) AS overall_avgFROM products;
-- Subquery in WHERE (IN)SELECT name FROM productsWHERE category_id IN ( SELECT category_id FROM categories WHERE is_featured = TRUE);
-- Derived table in FROMSELECT * FROM ( SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id) tWHERE total > 1000;Common Table Expressions (CTEs)
CTEs name intermediate result sets for readability and reuse:
WITH monthly_totals AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(amount) AS revenue FROM orders WHERE status = 'completed' GROUP BY 1),ranked_months AS ( SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS prev_revenue FROM monthly_totals)SELECT month, revenue, revenue - prev_revenue AS change, ROUND(100.0 * (revenue - prev_revenue) / NULLIF(prev_revenue, 0), 1) AS pct_changeFROM ranked_monthsORDER BY month DESC;Window Functions
Window functions compute a value across a set of rows without collapsing them — you keep all original rows plus the computed value:
SELECT name, department, salary, AVG(salary) OVER (PARTITION BY department) AS dept_avg_salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_salary_rank, SUM(salary) OVER (ORDER BY hire_date) AS running_payrollFROM employees;Common window functions: ROW_NUMBER(), RANK(), DENSE_RANK(), LAG(), LEAD(), FIRST_VALUE(), LAST_VALUE(), NTILE(), SUM() OVER, AVG() OVER, COUNT() OVER
DISTINCT: Remove Duplicate Rows
-- All unique city valuesSELECT DISTINCT city FROM customers;
-- Unique city + country combinationsSELECT DISTINCT city, country FROM customers ORDER BY country, city;UNION: Combine Multiple Queries
-- Stack rows from two queries (removes duplicates)SELECT name, email FROM customersUNIONSELECT company_name, contact_email FROM suppliers;
-- UNION ALL: keeps all rows including duplicates (faster)SELECT product_id FROM featured_productsUNION ALLSELECT product_id FROM promoted_products;Practical DQL Query Template
SELECT t1.column_a, t2.column_b, SUM(t1.amount) AS total, COUNT(DISTINCT t1.id) AS unique_countFROM table1 t1JOIN table2 t2 ON t1.fk = t2.pkWHERE t1.status = 'active' AND t1.created_at >= '2025-01-01'GROUP BY t1.column_a, t2.column_bHAVING SUM(t1.amount) > 100ORDER BY total DESCLIMIT 50;