By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
A Hyper-Practical, Zero-Fluff Guide for Data Analysts & Engineers
You’re handed a dataset—maybe it’s customer orders, sensor readings, or ad impressions—and told to "analyze it." But before you even think about running a regression or building a dashboard, you must answer three questions:
If you skip these checks, you’re flying blind. A single NULL in a critical column can break a model. Duplicate records can inflate revenue numbers by 20%. Stale data can make you optimize for last month’s trends instead of today’s.
NULL
Real-world scenario:You’re a data analyst at an e-commerce company. Your boss asks for a report on "average order value (AOV) by region." You pull the data, calculate AOV, and present it. Later, you find out: - 15% of order_amount values were NULL (skewing the average).- 8% of orders were duplicated (inflating revenue).- The data was 3 days old (missing a major flash sale).
order_amount
Now your report is wrong, your boss is embarrassed, and the marketing team wasted budget on a misguided campaign.
This guide teaches you how to catch these issues before they become disasters.
customer_id
ROW_NUMBER()
information_schema.tables
orders
customers
sensor_readings
SELECT
We’ll use a sample orders table with these columns: - order_id (unique identifier) - customer_id (foreign key to customers) - order_amount (numeric) - order_date (timestamp) - status (e.g., "shipped", "cancelled")
order_id
order_date
status
Goal: Identify columns with high null rates.
-- Null rate per column SELECT COUNT(*) AS total_rows, SUM(CASE WHEN order_id IS NULL THEN 1 ELSE 0 END) AS null_order_id, SUM(CASE WHEN customer_id IS NULL THEN 1 ELSE 0 END) AS null_customer_id, SUM(CASE WHEN order_amount IS NULL THEN 1 ELSE 0 END) AS null_order_amount, SUM(CASE WHEN order_date IS NULL THEN 1 ELSE 0 END) AS null_order_date, SUM(CASE WHEN status IS NULL THEN 1 ELSE 0 END) AS null_status, ROUND(100.0 * SUM(CASE WHEN order_id IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_order_id, ROUND(100.0 * SUM(CASE WHEN customer_id IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_customer_id, ROUND(100.0 * SUM(CASE WHEN order_amount IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_order_amount, ROUND(100.0 * SUM(CASE WHEN order_date IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_order_date, ROUND(100.0 * SUM(CASE WHEN status IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_status FROM orders;
Expected Output:| total_rows | null_order_id | null_customer_id | null_order_amount | ... | pct_null_customer_id | |------------|---------------|------------------|-------------------|-----|----------------------| | 10000 | 0 | 150 | 200 | ... | 1.50 |
What to look for:- order_id and customer_id should never be NULL (they’re primary/foreign keys).- order_amount nulls might be acceptable (e.g., refunds), but flag if >5%.- order_date should almost never be NULL.
Goal: Find exact or partial duplicates.
-- Count exact duplicates SELECT COUNT(*) AS total_duplicates FROM ( SELECT *, COUNT(*) OVER (PARTITION BY order_id, customer_id, order_amount, order_date, status) AS dup_count FROM orders ) t WHERE dup_count > 1;
-- Find duplicates on order_id (should be unique) SELECT order_id, COUNT(*) AS dup_count FROM orders GROUP BY order_id HAVING COUNT(*) > 1;
Expected Output:| order_id | dup_count | |----------|-----------| | 12345 | 2 |
What to look for:- order_id should never have duplicates (it’s a primary key).- If customer_id + order_date has duplicates, investigate (e.g., same customer placing multiple orders in a day).
Goal: Verify the data is up-to-date.
-- Last order date SELECT MAX(order_date) AS last_order_date FROM orders;
-- Last update time (PostgreSQL) SELECT table_name, last_autovacuum, last_autoanalyze FROM information_schema.tables WHERE table_name = 'orders';
-- Daily row count (if you have a date column) SELECT DATE_TRUNC('day', order_date) AS day, COUNT(*) AS row_count FROM orders GROUP BY 1 ORDER BY 1 DESC;
Expected Output:| day | row_count | |-----------|-----------| | 2023-10-15| 1200 | | 2023-10-14| 1100 | | 2023-10-13| 0 | ⚠️ Problem: No data for 10/13
What to look for:- If MAX(order_date) is >24 hours old, your pipeline might be broken.- If row counts drop suddenly, data might be missing.
MAX(order_date)
Goal: Turn these checks into reusable SQL that fails loudly if data is bad.
-- Data quality assertions (PostgreSQL example) DO $$ BEGIN -- Assert: No NULL customer_id IF EXISTS (SELECT 1 FROM orders WHERE customer_id IS NULL) THEN RAISE EXCEPTION 'Data quality check failed: NULL customer_id found'; END IF; -- Assert: No duplicate order_id IF EXISTS ( SELECT order_id, COUNT(*) FROM orders GROUP BY order_id HAVING COUNT(*) > 1 ) THEN RAISE EXCEPTION 'Data quality check failed: Duplicate order_id found'; END IF; -- Assert: Data is fresh (updated in last 24 hours) IF (SELECT MAX(order_date) FROM orders) < NOW() - INTERVAL '24 hours' THEN RAISE EXCEPTION 'Data quality check failed: Data is stale (older than 24h)'; END IF; END $$;
How to use this:- Run this in your ETL pipeline (e.g., Airflow, dbt, or a cron job).- If any assertion fails, the query will error out and alert your team.
UPDATE
DELETE
SELECT *
sql SELECT * FROM orders TABLESAMPLE SYSTEM(1);
dq_
dq_orders_null_rates
data_quality:critical
sql CREATE TABLE data_quality_logs ( check_name TEXT, table_name TEXT, check_time TIMESTAMP, status TEXT, -- "PASS" or "FAIL" details JSONB );
AVG(order_amount)
COALESCE(order_amount, 0)
AVG(COALESCE(order_amount, 0))
COUNT(*)
Answer: SUM(CASE WHEN column IS NULL THEN 1 ELSE 0 END) / COUNT(*).
SUM(CASE WHEN column IS NULL THEN 1 ELSE 0 END) / COUNT(*)
"How do you find duplicate records?"
GROUP BY
HAVING COUNT(*) > 1
Answer: GROUP BY key_columns HAVING COUNT(*) > 1.
GROUP BY key_columns HAVING COUNT(*) > 1
"How do you check if data is fresh?"
MAX(timestamp)
0
''
"You’re analyzing a users table and notice that 30% of email values are NULL. What do you do?"- Bad answer: "Ignore it and proceed with analysis." - Good answer: 1. Check if NULL emails are expected (e.g., guest users). 2. If not, investigate the source (e.g., form submission bug). 3. Document the issue and decide whether to: - Exclude NULL emails from analysis. - Impute missing values (e.g., [email protected]). - Fix the data at the source.
users
email
[email protected]
Challenge:You’re given a transactions table with these columns: - transaction_id (unique) - user_id (foreign key) - amount (numeric) - transaction_date (timestamp)
transactions
transaction_id
user_id
amount
transaction_date
Write a query that: 1. Calculates the null rate for each column.2. Identifies duplicate transaction_id values.3. Checks if the data is fresh (updated in the last 24 hours).
Solution:
-- Null rates SELECT COUNT(*) AS total_rows, ROUND(100.0 * SUM(CASE WHEN transaction_id IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_transaction_id, ROUND(100.0 * SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_user_id, ROUND(100.0 * SUM(CASE WHEN amount IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_amount, ROUND(100.0 * SUM(CASE WHEN transaction_date IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null_transaction_date FROM transactions; -- Duplicates SELECT transaction_id, COUNT(*) AS dup_count FROM transactions GROUP BY transaction_id HAVING COUNT(*) > 1; -- Freshness SELECT MAX(transaction_date) AS last_transaction_date, COUNT(*) AS total_transactions FROM transactions;
Why it works:- Null rates: Uses CASE WHEN to count NULL values and divides by total rows.- Duplicates: Groups by transaction_id and filters for counts > 1.- Freshness: Checks both the latest timestamp and row count.
CASE WHEN
-- Null rate for a single column SELECT COUNT(*) AS total_rows, SUM(CASE WHEN column IS NULL THEN 1 ELSE 0 END) AS null_count, ROUND(100.0 * null_count / total_rows, 2) AS pct_null FROM table;
-- Exact duplicates (all columns) SELECT *, COUNT(*) OVER (PARTITION BY col1, col2, ...) AS dup_count FROM table QUALIFY dup_count > 1; -- Partial duplicates (key columns) SELECT col1, col2, COUNT(*) AS dup_count FROM table GROUP BY col1, col2 HAVING COUNT(*) > 1;
-- Max timestamp SELECT MAX(timestamp_column) AS last_update FROM table; -- Metadata (PostgreSQL) SELECT last_autovacuum FROM information_schema.tables WHERE table_name = 'table'; -- Row count over time SELECT DATE_TRUNC('day', timestamp_column) AS day, COUNT(*) AS row_count FROM table GROUP BY 1 ORDER BY 1 DESC;
DO $$ BEGIN IF EXISTS (SELECT 1 FROM table WHERE key_column IS NULL) THEN RAISE EXCEPTION 'NULL key_column found'; END IF; END $$;
AVG(column)
COALESCE
HAVING
information_schema
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.