SQL queries every QA engineer should know

The UI can look perfect while the data underneath is wrong. A handful of SQL patterns lets you verify what actually reached the database — no DBA badge required.

The UI can look perfectly fine while the data underneath is wrong. That is why SQL is one of the highest-leverage skills a QA engineer can pick up: it lets you verify what actually got written to the database, not just what the screen claims. You do not need to be a database administrator — a handful of query patterns covers most day-to-day validation. All examples below use a generic schema with a users table and an orders table.

Spot checks with SELECT and WHERE

The bread and butter. After submitting an order through the app, confirm the row exists and holds the right values:

SELECT id, status, total, created_at
FROM orders
WHERE user_id = 4021
ORDER BY created_at DESC
LIMIT 5;

I run queries like this constantly during exploratory sessions. They catch silent failures — the app showed a success message, but the row never landed, or landed with a null total.

JOINs to verify relationships

Plenty of bugs are relationship bugs: the order exists, but it points at the wrong user, or at no user at all. A JOIN shows the connected picture in one result:

SELECT o.id, o.status, u.email
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.id = 98213;

One row, and you can see at a glance whether the order is attached to the account you actually tested with.

COUNT and GROUP BY for reconciliation

When a dashboard says there are 1,240 completed orders and an export says 1,236, someone has to find the truth. GROUP BY gives you the database's own answer:

SELECT status, COUNT(*) AS orders_count
FROM orders
GROUP BY status
ORDER BY orders_count DESC;

Compare these numbers against what the UI or the report claims. Mismatches usually mean a filter bug, a timezone boundary, or state that is not being updated where it should be.

Finding orphans with LEFT JOIN and IS NULL

Orphaned rows — orders whose user was deleted, for example — cause some of the ugliest production bugs, because the app only crashes for the affected records:

SELECT o.id, o.user_id
FROM orders o
LEFT JOIN users u ON u.id = o.user_id
WHERE u.id IS NULL;

An empty result is what healthy referential integrity looks like. Anything else is a bug report waiting to be written, even if no user has hit it yet.

Before/after SQL checks around a bug fix

When developers fix a data bug, the fix usually includes a migration or a backfill. Verifying it means measuring the same thing twice. Before the fix ships, capture the extent of the damage:

SELECT COUNT(*)
FROM orders
WHERE status = 'pending'
  AND created_at < '2026-01-01';

Run the identical query after deployment. If the count did not move the way the fix promised, the ticket is not done — and you have the exact number to prove it, which ends the "works on my machine" conversation very quickly.

Takeaway: read-only access to a staging database plus these five patterns will catch a class of bugs that UI testing never can. Save your queries next to your test notes — reconciliation queries in particular get reused for years.

None of this requires write access — and you should not want write access to shared environments anyway. SELECT-only credentials, a schema diagram, and curiosity are enough. The first time you close a bug with a query attached as evidence, developers start reading your reports differently.