Every essential SQL command in one place — SELECT, JOINs, aggregates, subqueries, window functions, indexes, stored procedures and more. Curated for beginners to advanced developers.
The SELECT statement is the foundation of every SQL query — retrieve, sort and limit records from any table.
-- Select all columns SELECT * FROM employees; -- Select specific columns SELECT first_name, last_name, salary FROM employees; -- Column aliases SELECT first_name AS "First Name", salary * 12 AS annual_salary, UPPER(department) AS dept FROM employees; -- Distinct values SELECT DISTINCT department FROM employees; -- Limit results SELECT * FROM employees LIMIT 10; SELECT * FROM employees LIMIT 10 OFFSET 20; -- skip 20
-- Sort ascending (default) SELECT * FROM employees ORDER BY last_name ASC; -- Sort descending SELECT * FROM employees ORDER BY salary DESC; -- Multi-column sort SELECT * FROM employees ORDER BY department ASC, salary DESC; -- SQL Execution Order: -- 1. FROM / JOIN 2. WHERE -- 3. GROUP BY 4. HAVING -- 5. SELECT 6. DISTINCT -- 7. ORDER BY 8. LIMIT/OFFSET
| Category | Data Type | Description | Example |
|---|---|---|---|
| Numeric | INT / INTEGER |
Whole numbers (4 bytes) | age INT |
| Numeric | BIGINT |
Large whole numbers (8 bytes) | user_id BIGINT |
| Numeric | DECIMAL(p,s) |
Exact fixed-point number | price DECIMAL(10,2) |
| Numeric | FLOAT / DOUBLE |
Approximate floating point | rate FLOAT |
| Text | VARCHAR(n) |
Variable-length string | name VARCHAR(100) |
| Text | CHAR(n) |
Fixed-length string | code CHAR(3) |
| Text | TEXT |
Unlimited text content | bio TEXT |
| Date/Time | DATE |
Date only: YYYY-MM-DD | dob DATE |
| Date/Time | DATETIME / TIMESTAMP |
Date and time combined | created_at TIMESTAMP |
| Other | BOOLEAN |
TRUE / FALSE | is_active BOOLEAN |
| Other | JSON |
JSON document (MySQL 5.7+) | metadata JSON |
Narrow down results with WHERE clauses — comparison operators, logical operators, LIKE, BETWEEN, IN and NULL checks.
-- Comparison operators SELECT * FROM employees WHERE salary > 50000; WHERE age >= 30 AND age <= 50; WHERE department = 'Sales'; WHERE department != 'HR'; -- or <> -- Logical operators WHERE salary > 60000 AND dept = 'IT'; WHERE dept = 'Sales' OR dept = 'IT'; WHERE NOT is_active = 0; -- NULL checks WHERE manager_id IS NULL; WHERE phone IS NOT NULL;
-- IN: match list of values WHERE department IN ('Sales', 'IT', 'HR'); WHERE id NOT IN (1, 2, 3); -- BETWEEN: inclusive range WHERE salary BETWEEN 40000 AND 80000; WHERE hire_date BETWEEN '2020-01-01' AND '2023-12-31'; -- LIKE: pattern matching WHERE name LIKE 'A%'; -- starts with A WHERE name LIKE '%son'; -- ends with son WHERE email LIKE '%@gmail%'; WHERE code LIKE 'A_C'; -- _ = one char -- Case-insensitive (MySQL) WHERE LOWER(name) LIKE '%john%';
-- Simple CASE SELECT name, CASE department WHEN 'IT' THEN 'Tech' WHEN 'HR' THEN 'People' ELSE 'Other' END AS dept_group FROM employees; -- Searched CASE SELECT name, salary, CASE WHEN salary < 40000 THEN 'Junior' WHEN salary < 80000 THEN 'Mid' ELSE 'Senior' END AS level FROM employees; -- CASE in ORDER BY ORDER BY CASE status WHEN 'urgent' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END;
Combine rows from two or more tables based on a related column — the heart of relational databases.
-- INNER JOIN: only matched rows SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id = d.id; -- LEFT JOIN: all from left table SELECT e.name, d.dept_name FROM employees e LEFT JOIN departments d ON e.dept_id = d.id; -- NULLs where no dept match -- Find unmatched rows SELECT e.* FROM employees e LEFT JOIN departments d ON e.dept_id = d.id WHERE d.id IS NULL;
-- RIGHT JOIN: all from right table SELECT e.name, d.dept_name FROM employees e RIGHT JOIN departments d ON e.dept_id = d.id; -- FULL OUTER JOIN: all rows, both SELECT * FROM employees e FULL OUTER JOIN departments d ON e.dept_id = d.id; -- CROSS JOIN: cartesian product SELECT a.color, b.size FROM colors a CROSS JOIN sizes b; -- SELF JOIN: table joins itself SELECT e.name, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.id;
-- Three-table join SELECT o.order_id, c.name AS customer, p.product_name FROM orders o JOIN customers c ON o.cust_id = c.id JOIN products p ON o.product_id = p.id; -- JOIN with multiple conditions JOIN prices pr ON p.id = pr.product_id AND pr.region = 'US'; -- JOIN with subquery JOIN ( SELECT dept_id, AVG(salary) avg_sal FROM employees GROUP BY dept_id ) ds ON e.dept_id = ds.dept_id;
| JOIN Type | Returns | Use When |
|---|---|---|
INNER JOIN |
Only rows with matches in BOTH tables | You only want complete data |
LEFT JOIN |
All left + matched right (NULLs for no match) | Keep all left rows, optional right |
RIGHT JOIN |
All right + matched left (NULLs for no match) | Keep all right rows, optional left |
FULL OUTER JOIN |
All rows from both, NULLs where no match | Find all records regardless of match |
CROSS JOIN |
Cartesian product of both tables | Generate all combinations |
SELF JOIN |
Table joined to itself | Hierarchical / parent-child data |
Summarise data with COUNT, SUM, AVG, MIN, MAX — and filter groups with HAVING.
-- COUNT SELECT COUNT(*) FROM employees; SELECT COUNT(manager_id) FROM employees; -- non-NULL SELECT COUNT(DISTINCT dept) FROM employees; -- SUM / AVG / MIN / MAX SELECT SUM(salary) AS total_payroll, AVG(salary) AS avg_salary, MIN(salary) AS min_salary, MAX(salary) AS max_salary, ROUND(AVG(salary),2) AS avg_rounded FROM employees; -- GROUP_CONCAT (MySQL) SELECT dept, GROUP_CONCAT(name ORDER BY name SEPARATOR ', ') AS members FROM employees GROUP BY dept;
-- GROUP BY: summarise per group SELECT department, COUNT(*) AS headcount, AVG(salary) AS avg_salary, MAX(salary) AS top_salary FROM employees GROUP BY department ORDER BY headcount DESC; -- HAVING: filter on aggregated values -- (use HAVING not WHERE for aggregates) SELECT department, COUNT(*) AS cnt FROM employees GROUP BY department HAVING COUNT(*) > 5; -- WHERE + GROUP BY + HAVING SELECT dept, SUM(sales) AS total FROM orders WHERE year = 2024 GROUP BY dept HAVING SUM(sales) > 100000 ORDER BY total DESC;
Nest queries inside queries or use Common Table Expressions (WITH) for readable, reusable logic.
-- Scalar subquery (single value) SELECT * FROM employees WHERE salary > ( SELECT AVG(salary) FROM employees ); -- IN subquery (list) SELECT * FROM employees WHERE dept_id IN ( SELECT id FROM departments WHERE location = 'NYC' ); -- Derived table (FROM subquery) SELECT dept, avg_sal FROM ( SELECT department dept, AVG(salary) avg_sal FROM employees GROUP BY department ) dept_summary WHERE avg_sal > 60000;
-- EXISTS: check if any rows exist SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.cust_id = c.id ); -- NOT EXISTS WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.cust_id = c.id ); -- ANY / ALL comparisons WHERE salary > ANY ( SELECT salary FROM managers ); WHERE salary > ALL ( SELECT salary FROM managers );
-- Basic CTE WITH dept_avg AS ( SELECT dept_id, AVG(salary) avg_sal FROM employees GROUP BY dept_id ) SELECT e.*, da.avg_sal FROM employees e JOIN dept_avg da ON e.dept_id = da.dept_id; -- Multiple CTEs WITH top_earners AS ( SELECT * FROM employees WHERE salary > 100000 ), their_depts AS ( SELECT DISTINCT dept_id FROM top_earners ) SELECT * FROM their_depts;
CREATE, ALTER and DROP tables, columns, constraints and relationships that define your database schema.
CREATE TABLE employees ( id INT PRIMARY KEY AUTO_INCREMENT, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, salary DECIMAL(10,2) DEFAULT 0.00, dept_id INT, hire_date DATE DEFAULT (CURDATE()), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW(), -- Foreign key constraint FOREIGN KEY (dept_id) REFERENCES departments(id) ON DELETE SET NULL ON UPDATE CASCADE );
-- Add a column ALTER TABLE employees ADD COLUMN phone VARCHAR(20); -- Modify column type / constraint ALTER TABLE employees MODIFY COLUMN salary DECIMAL(12,2) NOT NULL; -- Rename a column ALTER TABLE employees RENAME COLUMN phone TO mobile; -- Drop a column ALTER TABLE employees DROP COLUMN mobile; -- Add a constraint ALTER TABLE employees ADD CONSTRAINT chk_salary CHECK (salary >= 0); -- Rename the table ALTER TABLE employees RENAME TO staff;
-- DROP TABLE (permanent!) DROP TABLE employees; DROP TABLE IF EXISTS employees; -- TRUNCATE: delete all rows fast TRUNCATE TABLE employees; -- Resets AUTO_INCREMENT; faster than DELETE -- Drop database DROP DATABASE mydb; -- Constraint types summary -- PRIMARY KEY — unique, not null identifier -- FOREIGN KEY — reference to another table -- UNIQUE — no duplicate values -- NOT NULL — value required -- DEFAULT — fallback value -- CHECK — custom validation rule -- Create table from SELECT CREATE TABLE emp_backup AS SELECT * FROM employees;
INSERT, UPDATE and DELETE rows — the bread-and-butter of day-to-day database operations.
-- Single row INSERT INTO employees (first_name, last_name, salary, dept_id) VALUES ('Alice', 'Brown', 72000, 3); -- Multiple rows INSERT INTO employees (first_name, last_name, salary) VALUES ('Bob', 'Smith', 68000), ('Carol', 'Jones', 75000), ('Dave', 'Lee', 80000); -- Insert from SELECT INSERT INTO archive SELECT * FROM employees WHERE is_active = 0; -- UPSERT: insert or update on duplicate INSERT INTO employees (id, salary) VALUES (1, 85000) ON DUPLICATE KEY UPDATE salary = VALUES(salary);
-- Update a single column UPDATE employees SET salary = 90000 WHERE id = 42; -- Update multiple columns UPDATE employees SET salary = salary * 1.10, -- 10% raise is_active = TRUE WHERE department = 'Sales'; -- Update using subquery UPDATE employees SET salary = ( SELECT AVG(salary) FROM benchmarks WHERE role = 'dev' ) WHERE title = 'Developer'; -- ⚠️ Update ALL rows (no WHERE) UPDATE products SET tax_rate = 0.18; -- every row!
-- Delete specific rows DELETE FROM employees WHERE id = 42; -- Delete with condition DELETE FROM employees WHERE hire_date < '2010-01-01' AND is_active = 0; -- Delete using subquery DELETE FROM orders WHERE customer_id IN ( SELECT id FROM customers WHERE country = 'Deleted' ); -- ⚠️ Delete ALL rows (dangerous) DELETE FROM logs; -- no WHERE = all rows -- Safe delete: check count first SELECT COUNT(*) FROM employees WHERE dept_id = 7; -- then DELETE
Perform calculations across a set of rows related to the current row without collapsing them like GROUP BY.
-- Aggregate over entire result set SELECT name, salary, AVG(salary) OVER() AS company_avg FROM employees; -- PARTITION BY: per-group window SELECT name, department, salary, AVG(salary) OVER (PARTITION BY department) AS dept_avg, MAX(salary) OVER (PARTITION BY department) AS dept_max FROM employees; -- Running total with ORDER BY in OVER SELECT order_date, amount, SUM(amount) OVER ( ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total FROM orders;
SELECT name, department, salary, -- ROW_NUMBER: unique 1,2,3... ROW_NUMBER() OVER ( PARTITION BY department ORDER BY salary DESC ) AS row_num, -- RANK: gaps after ties (1,2,2,4) RANK() OVER ( PARTITION BY dept ORDER BY salary DESC ) AS rank_num, -- DENSE_RANK: no gaps (1,2,2,3) DENSE_RANK() OVER ( ORDER BY salary DESC ) AS dense, -- LAG/LEAD: previous/next row value LAG(salary, 1, 0) OVER (ORDER BY id) AS prev_sal, LEAD(salary, 1) OVER (ORDER BY id) AS next_sal FROM employees;
Speed up queries dramatically with indexes — and understand EXPLAIN to diagnose slow queries.
-- Create a regular index CREATE INDEX idx_email ON employees(email); -- Unique index CREATE UNIQUE INDEX idx_emp_email ON employees(email); -- Composite (multi-column) index CREATE INDEX idx_dept_salary ON employees(department, salary); -- Full-text index (MySQL) CREATE FULLTEXT INDEX idx_bio ON employees(bio); -- List indexes on a table SHOW INDEX FROM employees; -- Drop an index DROP INDEX idx_email ON employees; -- EXPLAIN: see query execution plan EXPLAIN SELECT * FROM employees WHERE email = 'a@b.com';
-- CREATE VIEW: saved query as virtual table CREATE VIEW v_active_employees AS SELECT id, name, department, salary FROM employees WHERE is_active = 1; -- Query a view like a table SELECT * FROM v_active_employees; -- Drop a view DROP VIEW IF EXISTS v_active_employees; -- Performance tips: -- ✓ Index WHERE, JOIN, ORDER BY columns -- ✓ Use LIMIT to avoid full scans -- ✓ Avoid SELECT * in production -- ✓ Use EXISTS over IN for large sets -- ✓ Avoid functions on indexed columns -- WHERE YEAR(date) = 2024 ← slow -- WHERE date >= '2024-01-01' ← fast -- ✓ Use covering indexes for hot queries
Encapsulate reusable SQL logic on the server — procedures for actions, functions for return values.
-- Create a stored procedure DELIMITER // CREATE PROCEDURE GetEmpByDept( IN p_dept VARCHAR(50), OUT p_count INT ) BEGIN SELECT * FROM employees WHERE department = p_dept; SELECT COUNT(*) INTO p_count FROM employees WHERE department = p_dept; END // DELIMITER ; -- Call a procedure CALL GetEmpByDept('Engineering', @cnt); SELECT @cnt; -- Drop a procedure DROP PROCEDURE IF EXISTS GetEmpByDept;
-- User-defined function DELIMITER // CREATE FUNCTION CalcBonus(p_salary DECIMAL(10,2)) RETURNS DECIMAL(10,2) DETERMINISTIC BEGIN RETURN p_salary * 0.15; END // DELIMITER ; -- Use function in query SELECT name, salary, CalcBonus(salary) AS bonus FROM employees; -- CREATE TRIGGER CREATE TRIGGER before_emp_update BEFORE UPDATE ON employees FOR EACH ROW BEGIN SET NEW.updated_at = NOW(); END;
Group multiple statements into atomic units that either fully succeed or fully roll back — ACID compliance.
-- Basic transaction START TRANSACTION; -- or BEGIN; UPDATE accounts SET balance = balance - 500 WHERE id = 1; UPDATE accounts SET balance = balance + 500 WHERE id = 2; COMMIT; -- save both or neither -- Rollback on error START TRANSACTION; DELETE FROM orders WHERE id = 5; ROLLBACK; -- undo everything -- SAVEPOINT: partial rollback SAVEPOINT sp1; -- do some work ROLLBACK TO SAVEPOINT sp1; RELEASE SAVEPOINT sp1;
| ACID Property | Meaning |
|---|---|
| Atomicity | All or nothing — no partial commits |
| Consistency | DB always goes from valid state to valid state |
| Isolation | Concurrent txns don't interfere |
| Durability | Committed data survives crashes |
-- Set isolation level SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- lowest READ COMMITTED; REPEATABLE READ; -- MySQL default SERIALIZABLE; -- highest -- Row locking SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- exclusive lock SELECT * FROM accounts FOR SHARE; -- shared/read lock
String & date functions, UNION, INTERSECT, EXCEPT, recursive CTEs and useful built-in functions.
UPPER('hello') -- HELLO LOWER('WORLD') -- world LENGTH('SQL') -- 3 TRIM(' hi ') -- 'hi' LTRIM / RTRIM -- left/right trim SUBSTRING('hello', 2, 3) -- 'ell' LEFT('hello', 3) -- 'hel' RIGHT('hello', 2) -- 'lo' CONCAT('a', 'b', 'c') -- 'abc' CONCAT_WS('-', 'a', 'b') -- 'a-b' REPLACE('ab', 'a', 'x') -- 'xb' INSTR('hello', 'll') -- 3 LPAD('5', 3, '0') -- '005' REVERSE('abc') -- 'cba' FORMAT(1234567.8, 2) -- '1,234,567.80'
NOW() -- 2024-06-01 12:30:00 CURDATE() -- 2024-06-01 CURTIME() -- 12:30:00 DATE(NOW()) -- extract date part YEAR(hire_date) -- 2022 MONTH(hire_date) -- 6 DAY(hire_date) -- 15 DAYNAME(hire_date) -- Monday DATEDIFF(NOW(), hire_date) -- days diff TIMESTAMPDIFF(MONTH, hire_date, NOW()) -- months diff DATE_ADD(NOW(), INTERVAL 30 DAY) -- add 30 days DATE_FORMAT(NOW(), '%d/%m/%Y') -- 01/06/2024
-- UNION: combine, remove duplicates SELECT id, name FROM customers UNION SELECT id, name FROM suppliers; -- UNION ALL: keep duplicates (faster) SELECT city FROM customers UNION ALL SELECT city FROM employees; -- INTERSECT: rows in BOTH queries SELECT email FROM customers INTERSECT SELECT email FROM newsletter; -- EXCEPT / MINUS: in first but not second SELECT email FROM customers EXCEPT -- use MINUS in Oracle SELECT email FROM unsubscribed; -- Rules: same # columns, compatible types
Level up further with our other free developer reference guides and learning resources.