SQL for QA β
The UI and the API tell you what the system claims to have done; the database tells you what it has actually done. A QA who can query the database can verify persistence, prepare test data and diagnose bugs without depending on anyone.
The 20 % of SQL that solves 80 % of the job β
You don't need to be a DBA. This covers almost all the day-to-day:
| I need to⦠| SQL pattern |
|---|---|
| Verify a record was created | SELECT with WHERE |
| Verify a state change | SELECT status FROM β¦ WHERE id = β¦ |
| Count how many there are | COUNT(*) with GROUP BY |
| Detect duplicates | GROUP BY β¦ HAVING COUNT(*) > 1 |
| Detect orphans (integrity) | LEFT JOIN β¦ WHERE child.id IS NULL |
| Find data for a test | SELECT β¦ WHERE <criteria> LIMIT 5 |
Examples over a minimal schema β
With two example tables, customers and service_orders (a customer has many service orders, and each order moves through created β validated β provisioning β active):
-- Was the customer created when they signed up for the 1 Gbps fibre plan?
SELECT id, document, status, created_at
FROM customers
WHERE document = '12345678A';
-- Did the service order reach 'active' after the full provisioning flow?
SELECT status FROM service_orders WHERE id = 10442;
-- Duplicate customers that validation should have prevented
SELECT document, COUNT(*)
FROM customers
GROUP BY document
HAVING COUNT(*) > 1;
-- Orphan service orders: they point to a customer that no longer exists
SELECT o.id
FROM service_orders o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE c.id IS NULL;
-- Test data: active customers with orders still in provisioning
SELECT c.id, c.document
FROM customers c
JOIN service_orders o ON o.customer_id = c.id
WHERE c.status = 'active' AND o.status = 'provisioning'
LIMIT 5;The NULL trap β
NULL equals nothing, not even another NULL. Two consequences that produce false greens:
WHERE column = NULLnever returns anything: you writeWHERE column IS NULL.COUNT(column)ignoresNULLs;COUNT(*)counts every row. If you validate totals, choose deliberately.
Safety rules in real environments β
- Read-only access whenever possible. An
UPDATEwithout aWHEREin a shared environment is a classic horror story for a reason. LIMITby default on big tables: a heavy query on a shared environment can degrade performance for the whole team.- Careful with personal data: the result of a query with real emails or names doesn't get pasted into a ticket or a chat. Anonymize, or reference by id.
How it fits automation β
In API tests, the most complete assertion validates both sides: the response (contract, status, body) and the persistence (what ended up in the database). It's also the setup-and-teardown tool: locating data that meets the test's conditions, and verifying the initial state before executing.
Common mistakes β
- Validating only the response. A 200 with a correct body doesn't guarantee it was persisted correctly (or that it wasn't persisted twice).
- Tests that depend on live data. If the test's query assumes "customer 42 exists and has 3 service orders", the test breaks the moment someone touches that data. Create or locate your data, don't assume it.
SELECT *on huge tables, out of habit. Ask for the columns you need.- Copying queries without understanding the JOIN. An
INNER JOINwhere aLEFT JOINwas needed hides exactly the rows you were looking for (the ones without a match).
Key idea
The API tells you what the system claims; the database, what it actually did. When the two versions don't match, there's a bug β and a good one.
References β
- SQLBolt β the best interactive tutorial to get fluent with SELECT and JOINs.
- PostgreSQL documentation β queries