Topic 1 of 120%
🗄️ Database Testing

SQL Database
Testing Tutorial

Master SQL from a tester's perspective. Learn to validate backend data, catch data integrity bugs, write queries for CRUD verification, understand JOINs for E-commerce testing, and confidently answer every SQL interview question.

⏱️ ~4 hrs 🗄️ MySQL / Any RDBMS 🎯 12 Topics 🛒 E-commerce Examples 🧪 Quiz each section
01
Introduction
What is Database Testing?
Database Testing (also called Backend Testing or Data Testing) is the process of verifying that the data stored in a database is accurate, consistent, and complete. While frontend testing checks what users see on the UI, database testing directly queries the database to confirm the actual data matches expected results.
🧠 Real-World Analogy: Imagine a customer places an order on Flipkart for ₹2,999. The UI shows "Order placed successfully!" — but is the data actually stored correctly in the database? Was the right price saved? Was the user ID linked? Was the product quantity reduced by 1? Was the payment status set to "pending"? Database testing answers all these questions by querying the database directly — independently of what the UI shows.

Frontend Testing vs Database Testing:

🖥️ Frontend / UI Testing

  • Tests what user sees (UI)
  • Uses Selenium / Appium
  • Checks buttons, forms, text
  • No SQL knowledge needed
  • Also called: Black-box testing

🗄️ Database / Backend Testing

  • Tests hidden data storage
  • Uses SQL queries directly
  • Checks tables, rows, columns
  • SQL knowledge required
  • Also called: White-box testing

What do testers verify in database testing?

  • Data Accuracy — Does the UI show exactly what's stored? Example: User enters email "priya@gmail.com" in a form. Tester queries the DB to confirm SELECT email FROM users WHERE user_id = 101 returns "priya@gmail.com" — not something else.
  • Data Integrity — Are relationships between tables correct? Example: An order has user_id = 999, but user 999 doesn't exist in the users table — this is an orphaned record bug.
  • CRUD Operations — Do Create, Read, Update, Delete operations work correctly? Example: After clicking "Delete Account", does the user's row actually disappear from the users table?
  • Business Rule Validation — Are calculated fields correct? Example: Order total = sum of all line items × quantity + taxes. Tester verifies this math in the DB independently of UI.
  • Duplicate Data — Are there unwanted duplicate records? Example: After clicking "Submit" twice quickly, were two orders created? SELECT COUNT(*) FROM orders WHERE session_id = 'xyz' should return 1, not 2.
  • NULL Values — Are required fields stored as NULL when they shouldn't be? Example: A mandatory phone number field stored as NULL in the database — a sign that validation is broken.
💡
Why every QA tester must know SQL: In every real project, the QA team is expected to verify data at the backend after testing UI flows. When a Selenium test finds that the order confirmation page shows wrong data, the tester must immediately query the DB to determine whether the bug is in the UI layer (frontend), API layer, or the database itself. Without SQL, you can only say "something is wrong" — with SQL, you can pinpoint exactly where.
🧪 Quiz: A user registers on an app. The UI shows "Registration successful!" but the user cannot log in later. As a tester, what should you do first?
02
SQL Fundamentals
SQL Basics — DDL, DML, DQL, DCL, TCL
SQL (Structured Query Language) is the standard language used to communicate with Relational Databases (MySQL, PostgreSQL, Oracle, SQL Server, SQLite). SQL commands are categorized into different types based on their purpose. As a tester, you primarily use DQL (querying data) but understanding all categories is essential for interviews.
DDL — Data Definition Language
Commands that define the structure of the database.
Commands: CREATE (create table), ALTER (modify table structure), DROP (delete table), TRUNCATE (remove all rows but keep table structure).
As tester: You rarely run DDL, but you verify that the schema (table structure, column types, constraints) matches requirements.
DML — Data Manipulation Language
Commands that modify data inside tables.
Commands: INSERT (add rows), UPDATE (modify rows), DELETE (remove rows).
As tester: Used to set up test data and to verify that application correctly performs these operations in the DB.
DQL — Data Query Language
Commands that retrieve data.
Command: SELECT (read data from tables).
As tester: This is what you'll use 90% of the time. After a UI action, you SELECT from the DB to verify the data was stored correctly.
DCL — Data Control Language
Commands that control access permissions.
Commands: GRANT (give permission), REVOKE (remove permission).
As tester: Used in security testing — verifying that certain users cannot access certain data.
TCL — Transaction Control Language
Commands that manage transactions (groups of operations that must all succeed or all fail).
Commands: COMMIT (save permanently), ROLLBACK (undo), SAVEPOINT.
As tester: Verify that if a payment fails mid-flow, ROLLBACK undoes all partial data — no orphaned order records.

3 most important differences for interviews:

DROP vs TRUNCATE vs DELETE

  • DROP — removes entire TABLE (structure + data)
  • TRUNCATE — removes ALL rows, keeps structure, cannot be rolled back
  • DELETE — removes specific rows with WHERE clause, can be rolled back

Key Interview Point

  • DELETE is DML → can ROLLBACK
  • TRUNCATE is DDL → cannot ROLLBACK
  • DROP is DDL → cannot ROLLBACK
  • DELETE is slower (logs each row)
  • TRUNCATE is faster (no row logging)
🧪 Quiz: A developer accidentally deletes all rows from the "orders" table using TRUNCATE. Can the DBA recover the data using ROLLBACK?
03
Core Skill
SELECT Queries — The Foundation
The SELECT statement is the most used SQL command for testers. It retrieves data from one or more tables. As a QA tester, after every UI action you will write a SELECT query to verify the data was stored correctly in the backend.

Sample E-commerce Database — we'll use this throughout the tutorial:

users
PK user_id
name
email
phone
city
created_at
orders
PK order_id
FK user_id
total_amount
status
order_date
products
PK product_id
product_name
price
stock_qty
category
order_items
PK item_id
FK order_id
FK product_id
quantity
unit_price

SELECT syntax — from basic to practical:

SELECT — All forms with tester examples
-- Select ALL columns from a table (* = all)
SELECT * FROM users;

-- Tester use: After registering a user, verify all columns were saved

-- Select SPECIFIC columns (best practice — don't use * in production)
SELECT user_id, name, email, city
FROM users;

-- DISTINCT — get unique values only (no duplicates)
SELECT DISTINCT city
FROM users;
-- Tester use: Check how many unique cities have registered users

-- ORDER BY — sort results ASC (default) or DESC
SELECT order_id, total_amount, order_date
FROM orders
ORDER BY order_date DESC;  -- newest orders first

-- LIMIT — show only first N rows (MySQL syntax)
SELECT * FROM orders
ORDER BY order_date DESC
LIMIT 5;  -- show only the 5 most recent orders

-- AS — give columns a readable alias in results
SELECT
    user_id   AS ID,
    name      AS "Customer Name",
    email     AS "Email Address"
FROM users;

Real-world tester scenario — after user registration:

Real tester queries — after form submission on UI
-- SCENARIO: User filled registration form. UI shows "Account created!"
-- TESTER QUERY: Verify the row was actually inserted in DB
SELECT * FROM users
WHERE email = 'priya@gmail.com';

-- Expected result: 1 row with correct name, email, phone
-- user_id | name         | email            | phone      | city
-- --------|--------------|------------------|------------|-------
-- 101     | Priya Sharma | priya@gmail.com  | 9876543210 | Mumbai

-- SCENARIO: User placed an order. Verify order was created
SELECT order_id, user_id, total_amount, status
FROM orders
WHERE user_id = 101
ORDER BY order_date DESC
LIMIT 1;

-- Expected: status='pending', total_amount = 2999
🧪 Quiz: You want to see all unique product categories from the products table — without duplicates. Which query is correct?
04
Filtering Data
WHERE, AND, OR, LIKE, IN, BETWEEN, NOT
The WHERE clause filters rows based on conditions. It is the most essential filtering tool for testers. Without WHERE, every SELECT returns the entire table — in a real database with millions of rows, you always use WHERE to find specific records.
WHERE clause — All operators with tester scenarios
-- Basic WHERE with = operator
SELECT * FROM users WHERE city = 'Mumbai';

-- Comparison operators: =  !=  <  >  <=  >=
SELECT * FROM orders WHERE total_amount >= 1000;
SELECT * FROM orders WHERE status != 'delivered';

-- AND — BOTH conditions must be true
SELECT * FROM users
WHERE city = 'Mumbai' AND created_at > '2024-01-01';
-- Tester: Users from Mumbai who registered after Jan 2024

-- OR — ANY ONE condition must be true
SELECT * FROM orders
WHERE status = 'failed' OR status = 'cancelled';
-- Tester: Find all failed or cancelled orders for validation

-- IN — match any value from a list (cleaner than multiple ORs)
SELECT * FROM orders
WHERE status IN ('failed', 'cancelled', 'refunded');
-- Same as above but cleaner. IN is equivalent to multiple ORs.

-- NOT IN — exclude specific values
SELECT * FROM orders
WHERE status NOT IN ('delivered', 'shipped');

-- BETWEEN — range check (inclusive of both ends)
SELECT * FROM orders
WHERE total_amount BETWEEN 500 AND 5000;
-- Tester: Orders with amount between ₹500 and ₹5000

SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31';
-- Tester: All orders placed in year 2024

-- LIKE — pattern matching (% = any chars, _ = one char)
SELECT * FROM users WHERE email LIKE '%@gmail.com';
-- Tester: Find all users with Gmail addresses

SELECT * FROM users WHERE name LIKE 'Priya%';
-- Names starting with "Priya"

SELECT * FROM users WHERE name LIKE '%Kumar%';
-- Names containing "Kumar" anywhere

SELECT * FROM users WHERE phone LIKE '98_______';
-- 10-digit phone starting with "98" (_ = exactly 1 character)
💡
LIKE wildcards: % matches zero or more characters. _ (underscore) matches exactly one character. So LIKE 'A%' matches "Amit", "Anjali", "A". And LIKE 'A_it' matches "Amit" but not "Ankit" (that's 5 chars between A and t).
🧪 Quiz: You want to find all products with price between ₹100 and ₹500. Which is correct?
05
Data Manipulation
CRUD — INSERT, UPDATE, DELETE
CRUD stands for Create, Read, Update, Delete — the four fundamental database operations. In testing, testers use INSERT to create test data, UPDATE to modify it, and DELETE to clean up after tests. They also verify that the application correctly performs these operations when users interact with the UI.
INSERT, UPDATE, DELETE — with tester context
-- ═══ INSERT ═══ Create new rows ═══════════════════════════════════

-- Insert one user (specify column names — best practice)
INSERT INTO users (name, email, phone, city)
VALUES ('Priya Sharma', 'priya@gmail.com', '9876543210', 'Mumbai');

-- Tester use: Create test data before running test cases

-- Insert multiple rows at once
INSERT INTO products (product_name, price, stock_qty, category)
VALUES
    ('iPhone 15',        79999, 50, 'Mobiles'),
    ('Samsung S24',      59999, 30, 'Mobiles'),
    ('Sony Headphones',  4999,  100, 'Electronics');

-- ═══ UPDATE ═══ Modify existing rows ════════════════════════════

-- Update a single user's city
UPDATE users
SET city = 'Delhi'
WHERE user_id = 101;
-- CRITICAL: Always use WHERE in UPDATE! Without it, ALL rows get updated!

-- Update multiple columns at once
UPDATE orders
SET status = 'delivered'
WHERE order_id = 5001;

-- Tester: After marking order as delivered in UI, verify in DB:
-- SELECT status FROM orders WHERE order_id = 5001;
-- Expected: 'delivered'

-- ═══ DELETE ═══ Remove specific rows ════════════════════════════

-- Delete a specific test user (cleanup)
DELETE FROM users
WHERE email = 'testuser@test.com';

-- CRITICAL: Always use WHERE in DELETE! Without it, ALL rows get deleted!

-- Delete all test orders older than a date (test data cleanup)
DELETE FROM orders
WHERE order_date < '2020-01-01'
  AND status = 'test';
Most dangerous SQL mistake for testers: Running UPDATE or DELETE without a WHERE clause. UPDATE orders SET status = 'delivered' (without WHERE) sets EVERY order in the database to "delivered". DELETE FROM users deletes ALL users. Always double-check your WHERE clause before running UPDATE or DELETE on a real database. Best practice: Run the equivalent SELECT first to see which rows will be affected.
🧪 Quiz: Before running UPDATE users SET city='Delhi' WHERE user_id=101 — what is the best safe practice?
06
Multi-Table Queries
JOINs — INNER, LEFT, RIGHT, FULL OUTER
Real-world data is spread across multiple related tables. JOIN is the SQL command that combines rows from two or more tables based on a related column (usually a Foreign Key). JOINs are the most important SQL concept for testers — most validation scenarios require combining data from multiple tables. They are the most asked SQL topic in QA interviews.
🧠 Real-World Analogy: The orders table stores order_id, user_id, and total_amount — but not the user's name. To see "Which user placed which order?", you must JOIN the orders table with the users table on user_id. Without JOINs, you can only see tables in isolation — with JOINs, you see the complete picture.
INNER JOIN
🔵∩🔵
Returns only rows where there is a match in BOTH tables. Unmatched rows are excluded.
LEFT JOIN
🔵⊃🔵
Returns ALL rows from LEFT table + matched rows from right. Unmatched right = NULL.
RIGHT JOIN
🔵⊂🔵
Returns matched left rows + ALL rows from RIGHT table. Unmatched left = NULL.
FULL OUTER JOIN
🔵∪🔵
Returns ALL rows from BOTH tables. Unmatched sides show NULL.
All JOINs — E-commerce database testing examples
-- ═══ INNER JOIN ═══ Only matched rows ═══════════════════════════
-- Find all orders with the customer name (orders + users)
SELECT
    o.order_id,
    u.name         AS customer_name,
    u.email,
    o.total_amount,
    o.status
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id;

-- Result: Only orders where user exists in users table
-- order_id | customer_name | email           | total_amount | status
-- ---------|---------------|-----------------|--------------|--------
-- 5001     | Priya Sharma  | priya@gmail.com | 2999.00      | pending
-- 5002     | Rahul Mehta   | rahul@yahoo.com | 599.00       | delivered

-- ═══ LEFT JOIN ═══ All left rows + matched right ═════════════════
-- Find ALL users — including those who NEVER placed an order
SELECT
    u.user_id,
    u.name,
    o.order_id   -- will be NULL if user has no orders
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;

-- user_id | name         | order_id
-- --------|--------------|----------
-- 101     | Priya Sharma | 5001
-- 102     | Rahul Mehta  | 5002
-- 103     | Ankit Singh  | NULL      ← user exists but has NO orders

-- Tester use case: Find users who registered but never ordered
SELECT u.user_id, u.name, u.email
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
-- Returns ONLY users with no orders (order_id = NULL)

-- ═══ 3-TABLE JOIN ═══ orders + users + products ═════════════════
-- Full order details: customer name, product name, qty, price
SELECT
    u.name            AS customer,
    p.product_name,
    oi.quantity,
    oi.unit_price,
    (oi.quantity * oi.unit_price) AS line_total,
    o.status
FROM orders o
INNER JOIN users u         ON o.user_id    = u.user_id
INNER JOIN order_items oi  ON o.order_id   = oi.order_id
INNER JOIN products p      ON oi.product_id = p.product_id
WHERE o.order_id = 5001;
-- Tester: Verify complete order details match what UI displays
🧪 Quiz: You want to find all users who have NEVER placed an order. Which JOIN type and condition should you use?
07
Calculations
Aggregate Functions & GROUP BY
Aggregate functions perform calculations on a set of rows and return a single value. They are essential for testers to validate totals, counts, averages, and summaries. GROUP BY groups rows by a column value so aggregate functions are applied per group — not to the whole table.
COUNT(*)
Counts the number of rows. COUNT(*) counts all rows including NULLs. COUNT(column) counts only non-NULL values in that column.
SUM(column)
Adds up all numeric values in a column. Tester use: Verify total revenue = sum of all order amounts.
AVG(column)
Calculates the average of numeric values. Tester use: Check average order value matches what analytics dashboard shows.
MAX(column)
Returns the highest value. Tester use: Verify the highest-priced order is correctly identified in reports.
MIN(column)
Returns the lowest value. Tester use: Verify minimum order amount in a date range.
Aggregate functions — Real tester validation queries
-- COUNT: How many users have registered?
SELECT COUNT(*) AS total_users
FROM users;
-- Result: 1250

-- SUM: Total revenue from all delivered orders
SELECT SUM(total_amount) AS total_revenue
FROM orders
WHERE status = 'delivered';
-- Result: 4,56,78,900.00  ← verify this matches dashboard

-- AVG: Average order amount
SELECT AVG(total_amount) AS avg_order_value
FROM orders;
-- Result: 1850.50

-- MAX and MIN: Price range of products
SELECT
    MAX(price) AS highest_price,
    MIN(price) AS lowest_price
FROM products;
-- Result: highest_price=79999 | lowest_price=99

-- ═══ GROUP BY ═══ Aggregate per group ═══════════════════════════

-- Count orders per status (verify order distribution)
SELECT
    status,
    COUNT(*) AS order_count
FROM orders
GROUP BY status
ORDER BY order_count DESC;
-- status     | order_count
-- -----------|------------
-- delivered  | 8920
-- pending    | 1230
-- cancelled  | 450
-- failed     | 180

-- Total orders and revenue per user (top customers)
SELECT
    u.name,
    COUNT(o.order_id) AS total_orders,
    SUM(o.total_amount) AS total_spent
FROM users u
JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name
ORDER BY total_spent DESC
LIMIT 5;
-- Tester: Verify top customers shown in dashboard match DB

-- Count products per category
SELECT
    category,
    COUNT(*) AS product_count,
    AVG(price) AS avg_price
FROM products
GROUP BY category;
🧪 Quiz: You run: SELECT city, COUNT(*) FROM users GROUP BY city; — What does this query return?
08
Critical Difference
WHERE vs HAVING — Most Asked Interview Question
The difference between WHERE and HAVING is the single most asked SQL question in QA interviews. Both filter data, but at completely different stages of query execution. Getting this wrong or right in an interview immediately signals your SQL knowledge level.

WHERE clause

  • Filters ROWS before grouping
  • Works on individual row data
  • Cannot use aggregate functions
  • Used with: SELECT, UPDATE, DELETE
  • Executes BEFORE GROUP BY

HAVING clause

  • Filters GROUPS after grouping
  • Works on aggregated/group data
  • CAN use aggregate functions
  • Only used with GROUP BY
  • Executes AFTER GROUP BY
WHERE vs HAVING — Side by side comparison
-- WHERE: Filter individual orders with amount > 1000 BEFORE grouping
SELECT status, COUNT(*) AS count
FROM orders
WHERE total_amount > 1000   -- filter rows FIRST (before grouping)
GROUP BY status;
-- This counts orders with amount>1000, grouped by status

-- HAVING: Filter groups (statuses) where order COUNT > 100
SELECT status, COUNT(*) AS count
FROM orders
GROUP BY status
HAVING COUNT(*) > 100;      -- filter GROUPS after grouping
-- This shows only status groups that have MORE THAN 100 orders

-- USING BOTH TOGETHER: Real tester scenario
-- Find cities that have MORE THAN 50 users who registered in 2024
SELECT
    city,
    COUNT(*) AS user_count
FROM users
WHERE created_at >= '2024-01-01'   -- WHERE filters 2024 users FIRST
GROUP BY city                          -- GROUP remaining rows by city
HAVING COUNT(*) > 50                  -- HAVING filters groups with >50
ORDER BY user_count DESC;

-- WRONG: Cannot use aggregate function in WHERE
SELECT city, COUNT(*)
FROM users
WHERE COUNT(*) > 50  -- ❌ ERROR! Cannot use COUNT in WHERE
GROUP BY city;
💡 Memory trick for interviews
"WHERE filters Rows, HAVING filters Groups"

Another way to remember: HAVING always comes after GROUP BY in the query. WHERE comes before GROUP BY. If you need to filter using an aggregate function (COUNT, SUM, AVG), you MUST use HAVING — WHERE will give a syntax error with aggregate functions.
🧪 Quiz: You want to find users who have placed MORE THAN 5 orders. Which clause do you use for the "more than 5" condition?
09
Advanced Queries
Subqueries — Query Inside a Query
A subquery (also called inner query or nested query) is a SQL query written inside another query. The subquery executes first, and its result is used by the outer query. Subqueries are used when you need to answer a question like "find all X where X matches the result of another query".
Subqueries — Practical tester examples
-- ═══ Subquery in WHERE ═══════════════════════════════════════════

-- Find all orders placed by users from 'Mumbai'
-- Without subquery: you'd need a JOIN
-- With subquery:
SELECT * FROM orders
WHERE user_id IN (
    SELECT user_id          -- inner query runs first
    FROM users
    WHERE city = 'Mumbai'  -- returns list: (101, 105, 118...)
);
-- Outer query: WHERE user_id IN (101, 105, 118...)

-- Find products that have NEVER been ordered
SELECT * FROM products
WHERE product_id NOT IN (
    SELECT DISTINCT product_id
    FROM order_items
);
-- Tester: Check for stale/unused products in catalog

-- Find orders with amount ABOVE average order value
SELECT order_id, total_amount
FROM orders
WHERE total_amount > (
    SELECT AVG(total_amount)
    FROM orders       -- inner query returns single value: 1850.50
);
-- Returns orders where amount > 1850.50

-- ═══ Subquery in SELECT (Scalar subquery) ════════════════════════
-- Show each order along with the user's total order count
SELECT
    order_id,
    total_amount,
    (SELECT COUNT(*)
     FROM orders o2
     WHERE o2.user_id = o1.user_id
    ) AS user_total_orders
FROM orders o1;
-- Each row shows the order + how many total orders that user has

-- ═══ EXISTS — check if subquery returns any row ════════════════
-- Find users who HAVE placed at least one order
SELECT name, email
FROM users u
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.user_id = u.user_id
);
-- EXISTS returns true if the subquery returns at least one row
🧪 Quiz: You want to find all products whose price is HIGHER than the average price of all products. How do you write this?
10
Special Cases
NULL Values — Handling & Common Bugs
NULL in SQL means "no value" or "unknown". It is NOT zero, NOT empty string, NOT false — it is the absence of any value. NULL is special: you cannot compare it with = or !=. For NULL comparisons, you must use IS NULL or IS NOT NULL. Misunderstanding NULL is one of the most common sources of data bugs.
NULL — How to detect, handle, and replace
-- WRONG: Cannot use = or != to check for NULL
SELECT * FROM users WHERE phone = NULL;      -- ❌ Returns 0 rows (wrong!)
SELECT * FROM users WHERE phone != NULL;     -- ❌ Also returns 0 rows (wrong!)

-- CORRECT: Use IS NULL / IS NOT NULL
SELECT * FROM users WHERE phone IS NULL;     -- ✅ Users with no phone number
SELECT * FROM users WHERE phone IS NOT NULL; -- ✅ Users who have a phone number

-- Tester use: Find incomplete user profiles (NULL in required fields)
SELECT user_id, name, email, phone
FROM users
WHERE phone IS NULL OR city IS NULL;
-- Bug: If phone/city are mandatory fields, these rows show a validation bug

-- COALESCE: Replace NULL with a default value
SELECT
    name,
    COALESCE(phone, 'Not provided') AS phone,
    COALESCE(city,  'Unknown')      AS city
FROM users;
-- COALESCE returns the first non-NULL value from its arguments

-- IFNULL (MySQL): Replace NULL with a value
SELECT IFNULL(phone, 'Not provided') FROM users;

-- NULL in aggregate functions: NULL values are IGNORED by COUNT(col)
SELECT
    COUNT(*)         AS total_rows,    -- counts ALL rows including nulls
    COUNT(phone)    AS rows_with_phone -- counts ONLY non-NULL phone rows
FROM users;
-- total_rows=1250 | rows_with_phone=1180 (70 users have NULL phone)

-- NULL in math: Any calculation with NULL = NULL
SELECT 100 + NULL;  -- Result: NULL (not 100!)
-- Tester: If price or quantity is NULL, order total = NULL — verify this bug!
⚠️
Common NULL-related bugs to test for: (1) User submits form without optional fields — verify they are stored as NULL, not empty string '' or "0". (2) Business calculations: if any component is NULL, the total is NULL — a ₹5000 order might show NULL if one product price is NULL. (3) ORDER BY NULL: NULL values appear first in ASC order and last in DESC order — verify report sorting matches expectations.
🧪 Quiz: SELECT * FROM users WHERE phone = NULL — how many rows does this return even if 50 users have no phone number (NULL)?
11
Data Quality
Data Integrity & Constraints
Constraints are rules applied to table columns to ensure the accuracy and consistency of data. They automatically reject data that violates defined rules. Testing constraints is an important part of database testing — you verify that invalid data cannot be inserted and that referential integrity is maintained.
PRIMARY KEY
Uniquely identifies each row. Cannot be NULL. Cannot be duplicate. Each table can have only ONE primary key. Testing: Try inserting two rows with the same primary key — must be rejected.
FOREIGN KEY
Links two tables together. Ensures a value in one table matches an existing value in another table's primary key. Testing: Try inserting an order with a user_id that doesn't exist in users table — must be rejected (referential integrity).
UNIQUE
All values in a column must be different. Unlike PRIMARY KEY, UNIQUE columns can contain NULL. Testing: Try inserting two users with the same email address — must be rejected.
NOT NULL
Column cannot contain NULL values. Testing: Try inserting a user without an email (required field) — must be rejected.
CHECK
Ensures column values meet a specific condition. Testing: Try inserting a product with price = -500 when CHECK(price > 0) is set — must be rejected.
DEFAULT
Provides a default value when no value is specified. Testing: Insert a user without specifying created_at — verify it is auto-populated with current timestamp.
Tester queries — Validating data integrity bugs
-- Check for DUPLICATE email addresses (UNIQUE constraint violation)
SELECT email, COUNT(*) AS count
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
-- Bug: If rows returned → duplicate emails exist = constraint not enforced

-- Check for ORPHANED RECORDS (Foreign Key violation)
-- Orders with user_id that doesn't exist in users table
SELECT o.order_id, o.user_id
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE u.user_id IS NULL;
-- Bug: If rows returned → orphaned orders (user was deleted but orders remain)

-- Check for NEGATIVE PRICES (CHECK constraint violation)
SELECT * FROM products
WHERE price <= 0;
-- Bug: Products with 0 or negative price — business rule violation

-- Check for NULL in mandatory fields (NOT NULL violation)
SELECT user_id, name
FROM users
WHERE email IS NULL OR name IS NULL;
-- Bug: Email and name are required fields — should never be NULL

-- Verify order total = sum of order items (business rule)
SELECT
    o.order_id,
    o.total_amount           AS stored_total,
    SUM(oi.quantity * oi.unit_price) AS calculated_total
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY o.order_id, o.total_amount
HAVING o.total_amount != SUM(oi.quantity * oi.unit_price);
-- Bug: Orders where stored total doesn't match calculated total
-- (pricing bug — possible rounding error or discount not applied)
🧪 Quiz: PRIMARY KEY vs UNIQUE constraint — what is the key difference?
12
Professional Standards
Database Testing Checklist & Interview Preparation

Complete Database Testing Checklist for QA Testers:

  • 1
    Schema Verification — Verify table names, column names, data types, and column lengths match the design document. Example: The email column should be VARCHAR(255), not VARCHAR(50). If it's too short, long emails won't save and the app will break silently.
  • 2
    CRUD Validation — After every UI form submission (Create), verify the row exists in DB. After editing (Update), verify the column changed. After delete, verify the row is gone. After read operations, verify the data matches what's shown.
  • 3
    Constraint Testing — Test that PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, and CHECK constraints work. Try to insert invalid data from the backend — the DB should reject it.
  • 4
    Data Integrity — Orphan Record Check — Use LEFT JOIN + IS NULL to find orphaned records. Example: Order items pointing to products that were deleted — this breaks the order summary display.
  • 5
    Duplicate Data Check — Use GROUP BY + HAVING COUNT(*) > 1 to find duplicates. Example: User submitted payment form twice — were two identical orders created?
  • 6
    Business Rule Validation — Verify calculated fields. Order total = sum of (qty × unit_price). Discount percentage applied correctly. Tax calculated on correct base amount. These calculations are in the application code — verify them independently in SQL.
  • 7
    NULL Validation — Verify mandatory fields are never NULL. Verify optional fields are NULL (not empty string) when not filled. Verify app handles NULL values gracefully (no NullPointerException).
  • 8
    Transaction Testing (ACID) — Verify Atomicity: if a payment fails mid-flow, NO partial data is saved. Verify Consistency: DB constraints hold before and after every transaction. Verify Isolation: Parallel orders don't interfere with each other.
  • 9
    Data Mapping Verification — Each UI form field maps to a specific DB column. Verify: First Name field → first_name column (not last_name). Shipping address → shipping_address column (not billing_address). Wrong mapping = UI shows correct data but DB stores it in wrong columns.
  • 10
    Soft Delete Verification — Many apps use "soft delete" (set is_deleted=1 instead of DELETE). Verify deleted items don't appear in UI, verify is_deleted=1 in DB, verify they aren't counted in statistics.

Top SQL interview answers — Q&A format:

What is database testing?
Database testing (backend/data testing) verifies that data stored in the database is accurate, consistent, and complete. Unlike UI testing, it directly queries the database to validate that CRUD operations, business rules, constraints, and data integrity are working correctly.
WHERE vs HAVING?
WHERE filters individual rows before GROUP BY. HAVING filters groups after GROUP BY. WHERE cannot use aggregate functions. HAVING can. Example: WHERE city='Mumbai' filters rows; HAVING COUNT(*)>50 filters groups.
INNER JOIN vs LEFT JOIN?
INNER JOIN returns only rows with matching records in BOTH tables — unmatched rows are excluded. LEFT JOIN returns ALL rows from the left table + matched rows from right; unmatched right rows show NULL. Use LEFT JOIN to find orphaned or missing records.
DELETE vs TRUNCATE vs DROP?
DELETE (DML) removes specific rows with WHERE, can be rolled back. TRUNCATE (DDL) removes all rows, keeps table structure, cannot be rolled back. DROP (DDL) removes the entire table including structure, cannot be rolled back.
What is NULL in SQL?
NULL means "no value" or "unknown". It is NOT zero or empty string. NULL comparisons require IS NULL / IS NOT NULL — using = NULL always returns no rows. Any arithmetic with NULL results in NULL. Use COALESCE() or IFNULL() to provide defaults for NULL values.
What is a subquery?
A subquery is a SQL query nested inside another query. The inner (sub)query executes first and its result is used by the outer query. Used in WHERE clause (filtered by subquery result), SELECT clause (scalar value), or FROM clause (as a derived table).
Primary Key vs Foreign Key?
Primary Key uniquely identifies each row in its own table — cannot be NULL or duplicate. Foreign Key is a column in one table that references the Primary Key of another table — it enforces referential integrity (you can't have an order for a user who doesn't exist).
🎯
Interview-ready answer — "How do you use SQL in your testing work?":

"I use SQL extensively for backend validation in every testing cycle. After a UI action — like a user placing an order — I query the database directly to verify the data was stored correctly. My typical workflow is: 1) Identify the affected tables and expected data changes. 2) Write SELECT queries to verify data accuracy — checking that all fields are correctly populated, not NULL, and match what's shown on the UI. 3) Use JOIN queries to verify data integrity across related tables — for example, confirming that an order_item correctly references both the order_id and product_id. 4) Use aggregate queries (COUNT, SUM) to validate totals and statistics shown in dashboards. 5) Use GROUP BY + HAVING to find duplicate records or constraint violations. I've found several data bugs that Selenium tests missed — including an orphaned order record after a user deletion, and a NULL being stored in a mandatory field when the form accepted an empty input."
🧪 Final Quiz: You want to find all orders where the stored total_amount does NOT match the sum of (quantity × unit_price) from order_items. This would indicate a pricing/calculation bug. Which approach is correct?

Ready to Master SQL for Real QA Projects?

STAD Solution's QA Automation course covers SQL for backend testing, data integrity validation, complete Selenium + TestNG framework, CI/CD integration — with 100% placement support.

Explore Courses at STAD Solution →