SELECT¶
Basic syntax¶
SELECT [DISTINCT] column_list
FROM table_name [AS alias]
[JOIN ...]
[WHERE condition]
[GROUP BY column_list]
[HAVING condition]
[ORDER BY column_list [ASC|DESC]]
[LIMIT n]
[OFFSET m]
Column selection¶
-- All columns
SELECT * FROM users;
-- Named columns
SELECT id, name, email FROM users;
-- Aliases
SELECT id, UPPER(name) AS display_name FROM users;
-- Expressions
SELECT id, price * quantity AS total FROM order_lines;
WHERE¶
See Operators & WHERE for the full operator reference.
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE age >= 18 AND active = true;
SELECT * FROM users WHERE email LIKE '%@example.com';
SELECT * FROM users WHERE id IN (1, 2, 3);
SELECT * FROM users WHERE score BETWEEN 80 AND 100;
SELECT * FROM users WHERE name IS NOT NULL;
DISTINCT¶
Remove duplicate rows from the result:
ORDER BY¶
-- Single column ascending (default)
SELECT * FROM users ORDER BY created;
-- Descending
SELECT * FROM users ORDER BY created DESC;
-- Multiple columns
SELECT * FROM users ORDER BY department ASC, salary DESC;
LIMIT and OFFSET¶
-- First 10 rows
SELECT * FROM users LIMIT 10;
-- Pagination: rows 21–30
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;
GROUP BY and HAVING¶
-- Count per department
SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department;
-- Only departments with more than 5 members
SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_sal
FROM employees
GROUP BY department
HAVING COUNT(*) > 5;
-- Sum per user with filter
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 1000;
Aggregate functions¶
SELECT COUNT(*) FROM orders;
SELECT COUNT(DISTINCT user_id) FROM orders;
SELECT SUM(amount) FROM orders;
SELECT AVG(amount) FROM orders;
SELECT MIN(amount), MAX(amount) FROM orders;
See Aggregate Functions.
JOINs¶
INNER JOIN¶
Returns rows matching in both tables:
LEFT JOIN¶
All rows from the left table; NULL for unmatched right rows:
SELECT u.id, u.name, COALESCE(o.amount, 0) AS total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
RIGHT JOIN¶
All rows from the right table; NULL for unmatched left rows:
Multi-table chain¶
SELECT u.name, p.name AS product, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN products p ON o.product_id = p.id
LEFT JOIN reviews r ON p.id = r.product_id;
Subqueries¶
Scalar subquery in SELECT¶
IN with subquery¶
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM banned_users);
Derived table in FROM¶
Common Table Expressions (CTEs)¶
-- Single CTE
WITH active_users AS (
SELECT id, name FROM users WHERE score > 80
)
SELECT * FROM active_users;
-- Multiple CTEs
WITH
high_scorers AS (SELECT id FROM users WHERE score > 80),
user_orders AS (SELECT user_id, amount FROM orders)
SELECT uo.amount
FROM user_orders uo
WHERE uo.user_id IN (SELECT id FROM high_scorers);
-- CTE joined with real table
WITH active AS (SELECT id FROM users WHERE score > 80)
SELECT u.name
FROM users AS u
INNER JOIN active AS a ON u.id = a.id;
-- CTE with aggregation
WITH totals AS (
SELECT user_id, COUNT(*) AS cnt, SUM(amount) AS total
FROM orders
GROUP BY user_id
)
SELECT * FROM totals WHERE total > 500;
Note
Recursive CTEs (WITH RECURSIVE) are not yet implemented.
UNION and UNION ALL¶
-- UNION deduplicates rows
SELECT id, name FROM active_users
UNION
SELECT id, name FROM archived_users;
-- UNION ALL keeps duplicates (faster)
SELECT id, name FROM users
UNION ALL
SELECT id, name FROM former_users;
-- Chain of three
SELECT id FROM users
UNION ALL
SELECT id FROM archived_users
UNION
SELECT id FROM deleted_users;
CASE WHEN¶
Searched form (arbitrary conditions):
SELECT id, name,
CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
WHEN score >= 70 THEN 'C'
ELSE 'F'
END AS grade
FROM students;
Simple form (equality on one expression):
SELECT id,
CASE status
WHEN 'active' THEN 'Active'
WHEN 'inactive' THEN 'Inactive'
ELSE 'Unknown'
END AS status_label
FROM accounts;
CAST¶
SELECT CAST(price AS INT8) FROM products;
SELECT CAST(id AS TEXT) FROM users;
SELECT CAST(n AS DOUBLE) FROM numbers;
SELECT CAST(name AS VARCHAR(50)) FROM users;
SELECT CAST('2024-01-01 00:00:00' AS TIMESTAMP);
SELECT CAST('550e8400-e29b-41d4-a716-446655440000' AS UUID);
SELECT CAST('{"a": 1}' AS JSON);
Window functions¶
-- Row number
SELECT name, ROW_NUMBER() OVER (ORDER BY score DESC) AS rn
FROM students;
-- Partition by department
SELECT name, dept,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY score DESC) AS dept_rank,
DENSE_RANK() OVER (PARTITION BY dept ORDER BY score DESC) AS dense_rk,
SUM(score) OVER (PARTITION BY dept) AS dept_total,
AVG(score) OVER (PARTITION BY dept) AS dept_avg
FROM employees;
-- Running total
SELECT name, score,
SUM(score) OVER (ORDER BY score) AS running_sum
FROM students;
-- LAG / LEAD
SELECT name, score,
LAG(score, 1) OVER (ORDER BY score) AS prev_score,
LEAD(score, 1) OVER (ORDER BY score) AS next_score
FROM students;
See Window Functions.
Full-text search¶
SELECT id, TS_RANK(body, 'database storage') AS score
FROM articles
WHERE MATCH(body, 'database storage')
ORDER BY score DESC
LIMIT 10;
See Full-text Search.
Vector nearest-neighbour search¶
See Vector Search.