SQL Cheat Sheet for Testers
The SQL a tester actually uses, on one page — filtering, NULLs, joins, aggregates, subqueries, window functions and the data validation patterns you'll run after UI and API tests. Every query was run on MariaDB/MySQL against the small sample table below, and the results shown are real. For full explanations, see The Complete SQL for Testers Guide.
Sample Data
| employees | ||||||
|---|---|---|---|---|---|---|
| id | name | dept | salary | manager_id | status | |
| 1 | Naveen | QA | 85000 | NULL | active | naveen@x.com |
| 2 | Asha | QA | 72000 | 1 | active | asha@x.com |
| 3 | Ravi | DEV | 95000 | NULL | active | ravi@x.com |
| 4 | Meera | DEV | 95000 | 3 | inactive | NULL |
| 5 | Nikhil | QA | 60000 | 1 | active | asha@x.com |
| 6 | John | DEV | 70000 | 3 | active | john@x.com |
An orders table (order_id, emp_id, amount, status) holds four orders, one of them for a non-existent employee 9, and one with a NULL amount.
Command Categories
| Category | Commands | Purpose |
|---|---|---|
| DDL | CREATE, ALTER, DROP, TRUNCATE |
Structure |
| DML | SELECT, INSERT, UPDATE, DELETE |
Data (SELECT is sometimes listed separately as DQL) |
| DCL | GRANT, REVOKE |
Permissions |
| TCL | COMMIT, ROLLBACK, SAVEPOINT |
Transactions |
DELETE vs TRUNCATE vs DROP
DELETE FROM t WHERE …— removes chosen rows; can be rolled back inside a transaction.TRUNCATE TABLE t— removes all rows quickly, keeps the table; usually can't be rolled back.DROP TABLE t— removes the table itself.
As a tester, you'll mostly use SELECT — and should never run DELETE or UPDATE without a WHERE clause, or against anything but a test database.
SELECT and Filtering
SELECT DISTINCT name, salary
FROM employees
WHERE dept = 'QA'
AND salary BETWEEN 50000 AND 90000
AND name LIKE 'N%'
AND manager_id IS NOT NULL
ORDER BY salary DESC
LIMIT 10;
-- → Nikhil | 60000 (Naveen has no manager)
| Operator | Purpose |
|---|---|
= != <> < > <= >= |
Comparison |
AND, OR, NOT |
Logic — use brackets when mixing AND and OR |
BETWEEN a AND b |
Inclusive range |
IN (…) |
Any of a list |
LIKE |
Pattern: % any characters, _ one character |
IS NULL / IS NOT NULL |
NULL checks |
DISTINCT |
Removes duplicate rows |
LIMIT n OFFSET m |
Paging (SQL Server: TOP / OFFSET … FETCH) |
The NULL trap ⭐
SELECT COUNT(*) FROM employees WHERE email = NULL; -- → 0 (always — NULL is never "equal")
SELECT COUNT(*) FROM employees WHERE email IS NULL; -- → 1 (Meera)
Replace NULLs in output with COALESCE(email, '(missing)').
Joins ⭐
| Join | Returns |
|---|---|
INNER JOIN |
Only rows that match in both tables |
LEFT JOIN |
All left rows, plus matches (NULLs where none) |
RIGHT JOIN |
All right rows, plus matches |
FULL OUTER JOIN |
All rows from both — in MySQL, LEFT JOIN UNION RIGHT JOIN |
| Self join | A table joined to itself |
-- Self join: each employee with their manager
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- → Naveen NULL, Asha Naveen, Ravi NULL, Meera Ravi, Nikhil Naveen, John Ravi
-- Orphans: orders for employees that don't exist (a data-integrity bug)
SELECT o.*
FROM orders o
LEFT JOIN employees e ON e.id = o.emp_id
WHERE e.id IS NULL;
-- → order 12347, emp_id 9
-- Employees with no orders
SELECT name FROM employees e
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.emp_id = e.id);
-- → John, Meera, Naveen, Nikhil
Watch out: a filter on the right-hand table in WHERE turns a LEFT JOIN into an INNER JOIN — put it in ON instead. Full examples with result tables: SQL Joins Compared.
Aggregates and Grouping
SELECT dept, COUNT(*) AS total, AVG(salary) AS avg_salary
FROM employees
WHERE status = 'active' -- filters rows BEFORE grouping
GROUP BY dept
HAVING COUNT(*) >= 2 -- filters groups AFTER grouping
ORDER BY avg_salary DESC;
-- → DEV | 2 | 82500
-- QA | 3 | 72333.33
| Function | Note |
|---|---|
COUNT(*) |
All rows |
COUNT(column) |
Only non-NULL values |
SUM(), AVG() |
Ignore NULLs |
MIN(), MAX() |
Smallest / largest |
SELECT COUNT(*), COUNT(amount), SUM(amount), AVG(amount) FROM orders;
-- → 4 | 3 | 1848.00 | 616.00 (the NULL amount is skipped — AVG divides by 3, not 4)
Subqueries and Rankings
-- Second-highest salary ⭐
SELECT MAX(salary) FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
-- → 85000 (correct even though two people share the top salary of 95000)
-- Nth highest with DENSE_RANK (N = 2 here)
SELECT DISTINCT salary FROM (
SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) t
WHERE rnk = 2;
-- → 85000
-- Rank within each department
SELECT name, dept, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
FROM employees;
-- DEV: Meera 1, Ravi 1, John 3 ← RANK leaves a gap after a tie
-- QA: Naveen 1, Asha 2, Nikhil 3
RANK vs DENSE_RANK vs ROW_NUMBER: RANK leaves gaps after ties (1, 1, 3), DENSE_RANK doesn't (1, 1, 2), ROW_NUMBER numbers every row uniquely. Window functions need MySQL 8+ or MariaDB 10.2+. More: Subqueries & Nested Queries.
CASE, UNION and Useful Functions
SELECT name,
COALESCE(email, '(missing)') AS email,
CASE WHEN salary >= 90000 THEN 'High'
WHEN salary >= 70000 THEN 'Mid'
ELSE 'Entry' END AS band
FROM employees;
SELECT email FROM employees WHERE dept = 'QA'
UNION -- removes duplicates; UNION ALL keeps them
SELECT email FROM employees WHERE salary > 90000;
Also handy: UPPER(), LOWER(), TRIM(), LENGTH(), CONCAT(), SUBSTRING(), CURRENT_DATE, DATE_ADD(), and date arithmetic like CURRENT_DATE - INTERVAL 7 DAY.
Tester Query Patterns ⭐
-- Duplicates (a unique-email rule is broken)
SELECT email, COUNT(*) AS cnt
FROM employees
WHERE email IS NOT NULL
GROUP BY email
HAVING COUNT(*) > 1;
-- → asha@x.com | 2
-- Verify the record your UI/API test just created
SELECT * FROM orders WHERE order_id = 12345;
-- Recently created data
SELECT COUNT(*) FROM bugs WHERE created_date >= CURRENT_DATE - INTERVAL 7 DAY;
-- Workload per assignee
SELECT assigned_to, COUNT(*) AS bug_count FROM bugs GROUP BY assigned_to ORDER BY bug_count DESC;
-- Pass/fail summary
SELECT status, COUNT(*) FROM test_results GROUP BY status;
-- Users who didn't log in today (condition in ON, not WHERE)
SELECT u.name
FROM users u
LEFT JOIN logins l ON u.id = l.user_id AND l.login_date = CURRENT_DATE
WHERE l.user_id IS NULL;
Typical checks after a UI or API action: the row exists with the right values, status fields changed correctly, no duplicates were created, related child rows exist, and nothing was orphaned by a delete. More: SQL Queries for Testers.
Transactions, ACID and Normalization
START TRANSACTION;
UPDATE employees SET salary = salary + 1000 WHERE id = 2; -- 72000 → 73000 inside the transaction
ROLLBACK; -- back to 72000
Wrapping test-data changes in a transaction and rolling back leaves the database clean. ACID — Atomicity (all or nothing), Consistency (rules hold), Isolation (transactions don't interfere), Durability (committed data survives failures).
| Normal form | Rule |
|---|---|
| 1NF | Atomic values, no repeating groups |
| 2NF | 1NF + no partial dependency on part of a composite key |
| 3NF | 2NF + no transitive dependency between non-key columns |
Practice writing these under time pressure in the SQL Interview Simulator.
From Real Projects
Much of my testing on Apkope and Canolog was about data: prospect and customer data captured from many channels on Apkope, and records flowing between sales, inventory, finance and service on Canolog. Checking what the application shows is only half the job — the data behind it has to be right too, which is where SQL helps testers most. Keep your most-used verification queries in one place for quick reuse.
📚 Official documentation: MySQL Reference Manual
FAQs
How much SQL does a tester need?
SELECT with filtering, NULL handling, joins, GROUP BY/HAVING, subqueries (second-highest salary) and a few validation patterns — enough to verify data after UI and API actions.
WHERE vs HAVING?
WHERE filters rows before grouping; HAVING filters groups after aggregation.
Why does "= NULL" return nothing?
NULL means "unknown", so comparisons with it are never true. Use IS NULL or IS NOT NULL.
How do you find the second-highest salary?
SELECT MAX(salary) FROM employees WHERE salary < (SELECT MAX(salary) FROM employees), or DENSE_RANK for the Nth highest.
How do you find duplicate records?
GROUP BY the columns that should be unique and keep groups with HAVING COUNT(*) > 1.
Go Deeper