SQL for QA: What They Actually Ask in Interviews
SQL for QA: What They Actually Ask in Interviews
The minimum SQL every tester needs — with real interview tasks and traps

Hi there! This is a continuation of the QA interview series. We’ve already covered test design, API & Security testing, and System Design. Now — SQL.
Honestly: SQL comes up in QA interviews more often than people expect. Not DBA-level, but definitely beyond SELECT * FROM users. Usually they give you a table and ask you to write a query on a whiteboard or in Google Docs. If you freeze at the word JOIN — this article is for you.
Why Does QA Even Need SQL?
Because testing through the UI is like looking at an iceberg from above. Bugs live underwater — in the database.
Here’s a real situation. A tester created an order through the UI, checked it — everything looked great: status “Paid”, amount $50. Test passed. Then in production, a customer complains about being charged twice. Turns out, the database had two records in the payments table — a backend bug. The UI only showed the latest one. If the tester had checked the database after creating the order — caught instantly.
QA needs SQL for three things:
- Data verification. UI shows one thing — the database might have another. A
SELECTafter every action is a good tester’s habit. - Test data preparation. Need 100 users for a load test? Through UI — a day’s work. One
INSERT— one second. - Root cause analysis. “User can’t see their orders” — is it a UI problem, an API issue, or is the data wrong in the database? One query — and you know where to dig.
SELECT, WHERE, and Basic Syntax
Let’s start with what everyone knows. But I’ve seen people get confused even here, so let’s run through it quickly.
Say we have a users table:
| id | name | age | city | created_at | |
|---|---|---|---|---|---|
| 1 | Atajan | atajan@mail.com | 30 | Ashgabat | 2025-01-15 |
| 2 | Maria | maria@mail.com | 25 | Moscow | 2025-03-20 |
| 3 | John | john@mail.com | 35 | Istanbul | 2025-06-10 |
| 4 | Anna | anna@mail.com | 22 | Moscow | 2025-09-01 |
| 5 | Kemal | kemal@mail.com | 28 | Ashgabat | 2026-01-05 |
Basic queries:
-- All users
SELECT * FROM users;
-- Only name and email
SELECT name, email FROM users;
-- Users from Moscow
SELECT * FROM users WHERE city = 'Moscow';
-- Users older than 25
SELECT * FROM users WHERE age > 25;
-- Combining conditions
SELECT * FROM users WHERE city = 'Moscow' AND age > 23;
-- IN — instead of multiple ORs
SELECT * FROM users WHERE city IN ('Moscow', 'Istanbul');
-- BETWEEN — range
SELECT * FROM users WHERE age BETWEEN 25 AND 35;
-- LIKE — pattern matching
SELECT * FROM users WHERE email LIKE '%@mail.com';
-- NULL — a special case
SELECT * FROM users WHERE city IS NULL; -- not = NULL!
Interview trap: WHERE city = NULL doesn’t work. NULL isn’t a value — it’s the absence of a value. You must use IS NULL / IS NOT NULL. Someone will inevitably write = NULL — and the interviewer will notice.
ORDER BY, LIMIT, DISTINCT
-- Sorting (default ASC — ascending)
SELECT * FROM users ORDER BY age DESC;
-- First 3 youngest
SELECT * FROM users ORDER BY age ASC LIMIT 3;
-- Unique cities
SELECT DISTINCT city FROM users;
-- How many unique cities
SELECT COUNT(DISTINCT city) FROM users;
JOIN — The #1 Interview Question
JOIN is asked in 80% of cases. Not because it’s hard, but because candidates confuse the types.
Let’s add a second table orders:
| id | user_id | product | amount | status |
|---|---|---|---|---|
| 1 | 1 | Laptop | 50000 | paid |
| 2 | 1 | Mouse | 2000 | paid |
| 3 | 2 | Keyboard | 3000 | cancelled |
| 4 | 3 | Monitor | 25000 | paid |
| 5 | 99 | Headphones | 5000 | paid |
Notice: user_id = 99 doesn’t exist in the users table, and users Anna (4) and Kemal (5) have no orders.
INNER JOIN — Only Matches
SELECT users.name, orders.product, orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;
| name | product | amount |
|---|---|---|
| Atajan | Laptop | 50000 |
| Atajan | Mouse | 2000 |
| Maria | Keyboard | 3000 |
| John | Monitor | 25000 |
Anna and Kemal didn’t make it (no orders). The order with user_id = 99 also didn’t make it (no such user).
LEFT JOIN — Everything From Left + Matches From Right
SELECT users.name, orders.product
FROM users
LEFT JOIN orders ON users.id = orders.user_id;
| name | product |
|---|---|
| Atajan | Laptop |
| Atajan | Mouse |
| Maria | Keyboard |
| John | Monitor |
| Anna | NULL |
| Kemal | NULL |
Anna and Kemal appear with NULL — they exist in users but have no orders.
RIGHT JOIN — Everything From Right + Matches From Left
SELECT users.name, orders.product
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;
| name | product |
|---|---|
| Atajan | Laptop |
| Atajan | Mouse |
| Maria | Keyboard |
| John | Monitor |
| NULL | Headphones |
Headphones appears with NULL — the order exists but user_id = 99 doesn’t.
FULL JOIN — Everything From Both Tables
Combination of LEFT and RIGHT: all users + all orders, even without matches.
Cheat Sheet
INNER JOIN = A ∩ B (only matches)
LEFT JOIN = A + (A ∩ B) (everything from left)
RIGHT JOIN = B + (A ∩ B) (everything from right)
FULL JOIN = A ∪ B (everything from both)
Interview task: “Find users who haven’t placed any orders.”
SELECT users.name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.id IS NULL;
Result: Anna, Kemal. LEFT JOIN gives NULL for those without orders, and WHERE filters only them. This task comes up constantly — learn it by heart.
Aggregate Functions
| Function | What It Does | Example |
|---|---|---|
| COUNT() | Counts rows | SELECT COUNT(*) FROM users; → 5 |
| SUM() | Sum | SELECT SUM(amount) FROM orders; |
| AVG() | Average | SELECT AVG(age) FROM users; |
| MAX() | Maximum | SELECT MAX(amount) FROM orders; |
| MIN() | Minimum | SELECT MIN(age) FROM users; |
Trap: COUNT(*) counts all rows including NULL. COUNT(column) counts only non-NULL values. They might ask about the difference.
-- How many orders per user
SELECT users.name, COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.name;
| name | order_count |
|---|---|
| Atajan | 2 |
| Maria | 1 |
| John | 1 |
| Anna | 0 |
| Kemal | 0 |
GROUP BY and HAVING
GROUP BY groups rows, HAVING filters groups. Key difference from WHERE: WHERE filters rows before grouping, HAVING — after.
-- Cities with more than one user
SELECT city, COUNT(*) AS user_count
FROM users
GROUP BY city
HAVING COUNT(*) > 1;
| city | user_count |
|---|---|
| Ashgabat | 2 |
| Moscow | 2 |
Interview task: “Find users who spent more than $100.”
SELECT users.name, SUM(orders.amount) AS total_spent
FROM users
INNER JOIN orders ON users.id = orders.user_id
WHERE orders.status = 'paid'
GROUP BY users.name
HAVING SUM(orders.amount) > 10000;
| name | total_spent |
|---|---|
| Atajan | 52000 |
| John | 25000 |
Notice: Maria was filtered out because her order was cancelled, and WHERE removed it before grouping.
Subqueries
A subquery is a query inside a query. Sometimes you can’t avoid them.
-- Users whose age is above average
SELECT name, age
FROM users
WHERE age > (SELECT AVG(age) FROM users);
| name | age |
|---|---|
| Atajan | 30 |
| John | 35 |
Average age = 28. Atajan (30) and John (35) are above.
-- Users who have at least one paid order
SELECT name
FROM users
WHERE id IN (
SELECT DISTINCT user_id
FROM orders
WHERE status = 'paid'
);
Result: Atajan, John.
Interview task: “Find the product with the highest order amount.”
-- Method 1: subquery
SELECT product, amount
FROM orders
WHERE amount = (SELECT MAX(amount) FROM orders);
-- Method 2: ORDER BY + LIMIT
SELECT product, amount
FROM orders
ORDER BY amount DESC
LIMIT 1;
Both return: Laptop, 50000. But if the maximum isn’t unique — the first method returns all records, the second — only one.
Real Interview Tasks
Task 1: “Duplicate Emails”
Find emails that appear more than once.
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
Why does QA care? We’re checking that the system correctly blocks registration with duplicate emails. If this query returns rows — it’s a bug.
Task 2: “Second Highest Order”
Find the amount of the second highest order.
SELECT DISTINCT amount
FROM orders
ORDER BY amount DESC
LIMIT 1 OFFSET 1;
Result: 25000 (after Laptop at 50000). OFFSET 1 skips the first row.
Task 3: “Users Without Orders in the Last Month”
Find users who registered more than a month ago but haven’t placed any orders.
SELECT u.name, u.email, u.created_at
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL
AND u.created_at < NOW() - INTERVAL '1 month';
Why does QA care? For example, testing a “You haven’t ordered in a while” email campaign — need to make sure it goes to exactly these users.
Task 4: “Top 3 Buyers”
Find the 3 buyers with the highest total paid order amounts.
SELECT u.name, SUM(o.amount) AS total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid'
GROUP BY u.name
ORDER BY total DESC
LIMIT 3;
Task 5: “Conversion by City”
For each city, count: total users, how many placed at least one order, and the conversion rate.
SELECT
u.city,
COUNT(DISTINCT u.id) AS total_users,
COUNT(DISTINCT o.user_id) AS buyers,
ROUND(
COUNT(DISTINCT o.user_id) * 100.0 / COUNT(DISTINCT u.id), 1
) AS conversion_pct
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.city;
| city | total_users | buyers | conversion_pct |
|---|---|---|---|
| Ashgabat | 2 | 1 | 50.0 |
| Moscow | 2 | 1 | 50.0 |
| Istanbul | 1 | 1 | 100.0 |
This is middle+ level. Solve it in an interview — you’ll impress.
UPDATE and DELETE — With Caution
Interviews sometimes ask you to write UPDATE or DELETE. The golden rule: always WHERE. DELETE FROM users without WHERE deletes everyone. In production, that’s a disaster.
-- Update a user's city
UPDATE users SET city = 'Istanbul' WHERE id = 1;
-- Delete cancelled orders older than a year
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
Interview tip: before DELETE/UPDATE, first write a SELECT with the same WHERE — make sure you’re targeting the right rows.
-- First verify
SELECT * FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
-- Verified — now delete
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
5 Traps Candidates Fall Into
1. NULL is not a value — it’s the absence of a value.
-- Wrong
SELECT * FROM users WHERE city = NULL;
-- Correct
SELECT * FROM users WHERE city IS NULL;
NULL = NULL is not TRUE. It’s NULL. Any operation with NULL yields NULL.
2. COUNT(*) vs COUNT(column).
COUNT(*) counts all rows. COUNT(city) counts only those where city is not NULL.
3. GROUP BY — all non-aggregated columns.
-- Error: name is not in GROUP BY and not in an aggregate function
SELECT name, city, COUNT(*)
FROM users
GROUP BY city;
-- Correct
SELECT city, COUNT(*)
FROM users
GROUP BY city;
4. WHERE vs HAVING.
WHERE filters rows before grouping. HAVING — after. You can’t use aggregate functions in WHERE.
-- Error
SELECT city, COUNT(*) FROM users WHERE COUNT(*) > 1 GROUP BY city;
-- Correct
SELECT city, COUNT(*) FROM users GROUP BY city HAVING COUNT(*) > 1;
5. SQL execution order is not the same as writing order.
You write: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
It executes: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
That’s why you can’t use SELECT aliases in WHERE — they don’t exist yet.
Interview Checklist
- SELECT, WHERE, AND/OR, IN, BETWEEN, LIKE, IS NULL — basic syntax
- ORDER BY, LIMIT, OFFSET — sorting and pagination
- DISTINCT — unique values
- JOIN — INNER, LEFT, RIGHT, FULL (and when to use each)
- “Users without orders” — LEFT JOIN + WHERE IS NULL
- COUNT, SUM, AVG, MAX, MIN — aggregate functions
- GROUP BY + HAVING — grouping and group filtering
- Subqueries — WHERE IN (SELECT …) and scalar
- UPDATE and DELETE — always with WHERE, SELECT first
- NULL — IS NULL, not = NULL
- COUNT(*) vs COUNT(column)
- SQL execution order
This covers 90% of QA interviews. No need to know window functions, CTEs, or stored procedures — that’s DBA territory.
How to Practice
- SQLBolt — Interactive lessons from scratch. Free. 15 minutes a day — master the basics in a week.
- LeetCode (Database) — Interview-level problems. Start with Easy.
- HackerRank SQL — Problems with verification. Great collection for beginners.
- Your work project. If you have access to a test database — write queries against real data. Best practice there is.
Free QA course with hands-on exercises at annayev.com (English, Russian, Turkish).
Found this useful? Let me know what SQL questions you’ve been asked in interviews!