SQL de Interview Prep
SQL de Interview Prep
8
32 7 New DE 3
Total Questions Categories Questions Difficulty Levels
■ Aggregation 4 questions
■ Joins 4 questions
PROBLEM
Find all duplicate rows in the employees table based on name and department. Return the duplicated
values and their occurrence count.
INPUT SCHEMA
employees
emp_id INTEGER
name VARCHAR
department VARCHAR
salary INTEGER
EXPECTED OUTPUT
■ TIP: GROUP BY the columns that should be unique, then HAVING COUNT(*) > 1.
■ HINT
SELECT name, department, COUNT(*) FROM employees GROUP BY name, department HAVING COUNT(*) > 1
■ SOLUTION
1 SELECT name, department, COUNT(*) AS count 2 FROM employees 3 GROUP BY name, department 4
HAVING COUNT(*) > 1;
■ EXPLANATION
GROUP BY clusters matching rows. HAVING COUNT(*) > 1 keeps only groups appearing more than
once.
PROBLEM
Find the second highest salary from the employees table. Return NULL if no second exists.
INPUT SCHEMA
employees
emp_id INTEGER
name VARCHAR
salary INTEGER
EXPECTED OUTPUT
■ TIP: Two approaches — subquery excluding the max, or DISTINCT + LIMIT 1 OFFSET 1.
■ HINT
SELECT MAX(salary) WHERE salary < (SELECT MAX(salary)...) OR use DISTINCT + ORDER BY DESC LIMIT 1
OFFSET 1
■ SOLUTION
1 -- Option 1: Subquery 2 SELECT MAX(salary) 3 FROM employees 4 WHERE salary < (SELECT
MAX(salary) FROM employees); 5 6 -- Option 2: DISTINCT + OFFSET 7 SELECT DISTINCT salary 8
FROM employees 9 ORDER BY salary DESC 10 LIMIT 1 OFFSET 1;
■ EXPLANATION
Subquery finds the highest, outer MAX finds highest below it. OFFSET 1 skips the first result.
PROBLEM
What is the difference between WHERE and HAVING? Write a query demonstrating both: find
departments with more than 5 active employees, showing only departments where avg salary > 50000.
INPUT SCHEMA
employees
emp_id INTEGER
name VARCHAR
department VARCHAR
salary INTEGER
status VARCHAR
EXPECTED OUTPUT
■ WHERE filters individual rows BEFORE grouping. HAVING filters groups AFTER aggregation.
■ HINT
WHERE status = active (row filter). GROUP BY department. HAVING COUNT > 5 AND AVG(salary) > 50000 (group
filter).
■ SOLUTION
■ EXPLANATION
WHERE pre-filters rows before aggregation. HAVING filters the resulting groups. Both can coexist in a
single query.
PROBLEM
Explain the difference between DELETE, TRUNCATE, and DROP. Write example SQL for each operation
on the orders table.
INPUT SCHEMA
orders
order_id INTEGER
customer_id INTEGER
amount DECIMAL
status VARCHAR
EXPECTED OUTPUT
■ DELETE: row-level with WHERE. TRUNCATE: all rows fast. DROP: removes the entire table structure.
■ HINT
DELETE uses WHERE, is logged, rollback-safe. TRUNCATE is fast, resets identity. DROP removes the table
entirely.
■ SOLUTION
1 -- Delete specific rows (can use WHERE, logged, rollbackable) 2 DELETE FROM orders WHERE
status = 'cancelled'; 3 4 -- Remove all rows fast (no WHERE, resets identity) 5 TRUNCATE
TABLE orders; 6 7 -- Remove the entire table structure + data 8 DROP TABLE orders;
■ EXPLANATION
DELETE is logged row-by-row and supports WHERE. TRUNCATE is faster and resets auto-increment.
DROP removes the table entirely — irreversible without backup.
PROBLEM
Demonstrate all three JOIN types using an employees and departments table. Show which
employees/departments appear in each result.
INPUT SCHEMA
employees
emp_id INTEGER
name VARCHAR
dept_id INTEGER
departments
dept_name VARCHAR
EXPECTED OUTPUT
1 INNER: matched rows only 2 LEFT: all employees + dept info (NULL if no dept) 3 FULL: all
from both sides
■ INNER JOIN = intersection. LEFT JOIN = all left + matches. FULL OUTER JOIN = all from both tables.
■ HINT
Use ON e.dept_id = d.dept_id. Switch JOIN type to get different results.
■ SOLUTION
1 -- INNER JOIN: only matching rows 2 SELECT [Link], d.dept_name 3 FROM employees e 4 INNER
JOIN departments d ON e.dept_id = d.dept_id; 5 6 -- LEFT JOIN: all employees, NULL if no
dept 7 SELECT [Link], d.dept_name 8 FROM employees e 9 LEFT JOIN departments d ON e.dept_id
= d.dept_id; 10 11 -- FULL OUTER JOIN: all from both tables 12 SELECT [Link], d.dept_name
13 FROM employees e 14 FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;
■ EXPLANATION
INNER: only matched rows. LEFT: all left table rows, NULLs where no match. FULL OUTER: all rows from
both, NULLs where no match on either side.
PROBLEM
Find the maximum total earnings (salary × months) for all employees, and the count of employees who
have that maximum. Print as space-separated integers.
INPUT SCHEMA
Employee
employee_id INTEGER
name VARCHAR
months INTEGER
salary INTEGER
EXPECTED OUTPUT
1 69952 1
■ TIP: GROUP BY the computed earnings expression, then ORDER BY DESC LIMIT 1.
■ HINT
GROUP BY (salary * months), ORDER BY DESC LIMIT 1, COUNT(*) gives employees with that max.
■ SOLUTION
■ EXPLANATION
salary × months computed inline. Grouping clusters identical totals. COUNT(*) per group. LIMIT 1 picks
the maximum group.
PROBLEM
A keyboard's 0 key was broken so all zeros were stripped from salary values. Find CEIL(actual_avg -
broken_avg).
INPUT SCHEMA
EMPLOYEES
ID INTEGER
Name VARCHAR
Salary INTEGER
1 2061
■ HINT
CEIL(AVG(Salary) - AVG(REPLACE(Salary, 0, empty string)))
■ SOLUTION
■ EXPLANATION
REPLACE strips zero digits. Difference between true and broken averages gives the error. CEIL rounds
up.
PROBLEM
Find departments whose total salary exceeds the average total salary across all departments. Return
department_name, total_salary, employee_count. Order by total_salary DESC.
INPUT SCHEMA
Departments
department_id INTEGER
department_name VARCHAR
Employees
employee_id INTEGER
name VARCHAR
department_id INTEGER
salary INTEGER
EXPECTED OUTPUT
■ TIP: Nested subquery in HAVING computes the global average of department totals.
■ HINT
HAVING SUM(salary) > (SELECT AVG(dt) FROM (SELECT SUM(salary) AS dt FROM Employees GROUP BY
dept_id) t)
■ SOLUTION
■ EXPLANATION
Nested subquery sums per dept then takes AVG. HAVING filters departments above that average.
PROBLEM
Query the median of LAT_N from STATION. Round to 4 decimal places. Median = value where equal
counts exist above and below.
INPUT SCHEMA
STATION
ID INTEGER
CITY VARCHAR
LAT_N DECIMAL
LONG_W DECIMAL
EXPECTED OUTPUT
1 83.8913
■ HINT
Correlated subqueries: WHERE (SELECT COUNT WHERE LAT_N < S.LAT_N) = (SELECT COUNT WHERE
LAT_N > S.LAT_N)
■ SOLUTION
1 SELECT ROUND(S.LAT_N, 4) 2 FROM STATION S 3 WHERE (SELECT COUNT(LAT_N) FROM STATION WHERE
LAT_N < S.LAT_N) = 4 (SELECT COUNT(LAT_N) FROM STATION WHERE LAT_N > S.LAT_N);
■ EXPLANATION
For each row correlated subqueries count values above and below. Equal counts = median position.
PROBLEM
Find employees whose salary is greater than their manager salary. Return employee_name, salary,
manager_name, manager_salary.
INPUT SCHEMA
employees
emp_id INTEGER
name VARCHAR
salary INTEGER
manager_id INTEGER
EXPECTED OUTPUT
■ TIP: Self-join the table twice — once as employee (e), once as manager (m).
■ HINT
FROM employees e JOIN employees m ON e.manager_id = m.emp_id WHERE [Link] > [Link]
■ SOLUTION
■ EXPLANATION
Self-join aliases the same table as employee and manager. JOIN links manager_id to emp_id. WHERE
filters the comparison.
PROBLEM
Find all products that have never appeared in any order. Return all product columns.
INPUT SCHEMA
products
id INTEGER
product_name VARCHAR
price DECIMAL
item_id INTEGER
order_id INTEGER
product_id INTEGER
EXPECTED OUTPUT
■ TIP: LEFT JOIN + WHERE right side IS NULL is the classic anti-join pattern.
■ HINT
LEFT JOIN order_items ON [Link] = o.product_id. WHERE o.product_id IS NULL means no match.
■ SOLUTION
1 SELECT p.* 2 FROM products p 3 LEFT JOIN order_items o ON [Link] = o.product_id 4 WHERE
o.product_id IS NULL;
■ EXPLANATION
LEFT JOIN keeps all products. NULL on right side means no order exists. WHERE IS NULL filters to
unmatched products only.
PROBLEM
Map students to grades using mark ranges. Show NULL for grade < 8, else show Name. Order by Grade
DESC, then by Name (grade>=8) or Marks (grade<8).
INPUT SCHEMA
Students
ID INTEGER
Name VARCHAR
Marks INTEGER
Grades
Grade INTEGER
Min_Mark INTEGER
Max_Mark INTEGER
EXPECTED OUTPUT
■ TIP: Join using BETWEEN — no shared key column. Grade is determined by mark range.
■ HINT
JOIN ON Marks BETWEEN Min_Mark AND Max_Mark. CASE WHEN Grade < 8 THEN NULL ELSE Name END.
1 SELECT 2 CASE WHEN [Link] < 8 THEN NULL ELSE [Link] END AS Name, 3 [Link], [Link] 4
FROM Students s 5 JOIN Grades g ON [Link] BETWEEN g.Min_Mark AND g.Max_Mark 6 ORDER BY
[Link] DESC, 7 CASE WHEN [Link] >= 8 THEN [Link] END ASC, 8 CASE WHEN [Link] < 8 THEN
[Link] END ASC;
■ EXPLANATION
BETWEEN join maps marks to grade range. Two CASE expressions in ORDER BY activate different sort
logic per tier.
PROBLEM
Find all customers who have placed at least one order. Show the difference between using INNER JOIN
and EXISTS. Return customer_name.
INPUT SCHEMA
customers
id INTEGER
name VARCHAR
orders
order_id INTEGER
customer_id INTEGER
amount DECIMAL
EXPECTED OUTPUT
■ TIP: EXISTS stops at the first match — more efficient for large datasets than INNER JOIN which may produce
duplicates.
■ HINT
INNER JOIN produces duplicate customers if they have multiple orders. EXISTS returns each customer only once.
■ SOLUTION
1 -- INNER JOIN (may produce duplicates) 2 SELECT DISTINCT [Link] 3 FROM customers c 4
INNER JOIN orders o ON [Link] = o.customer_id; 5 6 -- EXISTS (cleaner, stops at first match)
7 SELECT name 8 FROM customers c 9 WHERE EXISTS ( 10 SELECT 1 FROM orders o 11 WHERE
o.customer_id = [Link] 12 );
■ EXPLANATION
INNER JOIN may duplicate rows if multiple orders exist per customer — requires DISTINCT. EXISTS
stops scanning at the first match, making it faster for large datasets.
PROBLEM
Explain CTEs with an example. Write a CTE to find top-paid employees (salary > 100000), then select
from it. Show why a CTE is better than a subquery here.
INPUT SCHEMA
employees
emp_id INTEGER
name VARCHAR
salary INTEGER
department VARCHAR
EXPECTED OUTPUT
■ TIP: CTEs (WITH clause) create named temporary result sets — readable and reusable within the same query.
■ HINT
WITH cte_name AS (SELECT ... WHERE salary > 100000) SELECT * FROM cte_name
■ SOLUTION
1 WITH HighEarners AS ( 2 SELECT emp_id, name, salary, department 3 FROM employees 4 WHERE
salary > 100000 5 ) 6 SELECT name, department, salary 7 FROM HighEarners 8 ORDER BY salary
DESC;
■ EXPLANATION
CTE creates a named temporary result set reusable in the same query. Benefits: improves readability,
avoids nested subqueries, allows recursion. Requires MySQL 8.0+.
PROBLEM
Find all employees whose salary is above their own department average. Use a correlated subquery.
INPUT SCHEMA
employees
emp_id INTEGER
name VARCHAR
department VARCHAR
salary INTEGER
EXPECTED OUTPUT
■ TIP: A correlated subquery references the outer query — here it recalculates avg for each employee own
department.
■ HINT
WHERE salary > (SELECT AVG(salary) FROM employees e2 WHERE [Link] = [Link])
■ SOLUTION
■ EXPLANATION
Correlated subquery runs once per outer row, computing avg for that employee department. Rows above
their department avg are returned.
PROBLEM
Use a recursive CTE to traverse the employee-manager hierarchy. Return each employee id, parent_id,
and their depth level starting from 1 at the root.
INPUT SCHEMA
employees
id INTEGER
name VARCHAR
EXPECTED OUTPUT
1 1 NULL 1 2 2 1 2 3 3 1 2 4 4 2 3
■ HINT
Anchor: WHERE parent_id IS NULL. Recursive: JOIN org_chart ON e.parent_id = [Link], increment level.
■ SOLUTION
1 WITH RECURSIVE org_chart AS ( 2 SELECT id, parent_id, 1 AS level 3 FROM employees 4 WHERE
parent_id IS NULL 5 UNION ALL 6 SELECT [Link], e.parent_id, [Link] + 1 7 FROM employees e 8
JOIN org_chart oc ON e.parent_id = [Link] 9 ) 10 SELECT * FROM org_chart;
■ EXPLANATION
Anchor selects the root. Recursive member joins children incrementing level. UNION ALL accumulates all
levels until no more children.
PROBLEM
Each hacker total score = sum of their best score per challenge. Print hacker_id, name, total. Exclude
zero-total hackers. Order by total DESC.
INPUT SCHEMA
Hackers
hacker_id INTEGER
name VARCHAR
Submissions
submission_id INTEGER
hacker_id INTEGER
challenge_id INTEGER
score INTEGER
EXPECTED OUTPUT
■ TIP: A hacker may submit many times per challenge — only their MAX per challenge counts toward the total.
■ HINT
CTE: MAX(score) GROUP BY hacker_id, challenge_id. Then SUM those maxes per hacker.
■ SOLUTION
■ EXPLANATION
PROBLEM
Show the difference between ROW_NUMBER(), RANK(), and DENSE_RANK() in a single query. Rank all
employees by salary descending.
INPUT SCHEMA
employees
emp_id INTEGER
name VARCHAR
salary INTEGER
EXPECTED OUTPUT
■ ROW_NUMBER: always unique. RANK: skips after ties (1,1,3). DENSE_RANK: no skips (1,1,2).
■ HINT
All three use OVER (ORDER BY salary DESC). The difference shows when two rows have the same salary.
■ SOLUTION
1 SELECT name, salary, 2 ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num, 3 RANK() OVER
(ORDER BY salary DESC) AS rank_num, 4 DENSE_RANK() OVER (ORDER BY salary DESC) AS
dense_rank 5 FROM employees;
■ EXPLANATION
ROW_NUMBER: unique always. RANK: ties share a rank but next rank is skipped. DENSE_RANK: ties
share a rank, next rank is consecutive.
PROBLEM
Calculate a running total of sales that resets each month. Return sale_date, daily_amount, and
running_total.
INPUT SCHEMA
Sales
sale_id INTEGER
sale_date DATE
amount DECIMAL
■ TIP: PARTITION BY YEAR+MONTH resets the window. ORDER BY date inside OVER makes SUM cumulative.
■ HINT
SUM(amount) OVER (PARTITION BY YEAR(sale_date), MONTH(sale_date) ORDER BY sale_date)
■ SOLUTION
■ EXPLANATION
PARTITION BY YEAR, MONTH resets the window each month. ORDER BY inside OVER makes SUM
accumulate chronologically.
PROBLEM
Find the top 3 selling products by revenue within each category. Include tied products at rank 3.
INPUT SCHEMA
sales
sale_id INTEGER
category VARCHAR
product VARCHAR
revenue DECIMAL
EXPECTED OUTPUT
■ TIP: Use RANK() not ROW_NUMBER() when ties should both appear.
■ HINT
Inner: SUM(revenue) + RANK() OVER (PARTITION BY category ORDER BY SUM(revenue) DESC). Outer:
WHERE rnk <= 3.
■ SOLUTION
■ EXPLANATION
RANK() allows tied products at rank 3 to both appear. PARTITION BY category resets ranking per
category. Subquery aggregates first, then ranks.
PROBLEM
Pivot OCCUPATIONS so each occupation becomes a column: Doctor, Professor, Singer, Actor. Names
alphabetical within each occupation. Print NULL when a column runs out.
INPUT SCHEMA
OCCUPATIONS
Name VARCHAR
Occupation VARCHAR
EXPECTED OUTPUT
1 Aamina Ashley Christeen Eve 2 Julia Britney Jane Jennifer 3 NULL Meera Jenny Ketty
■ TIP: MySQL has no native PIVOT. Use ROW_NUMBER() partitioned by Occupation, then MAX(CASE WHEN).
■ HINT
CTE: ROW_NUMBER PARTITION BY Occupation. Outer: GROUP BY rn, MAX(CASE WHEN Occupation=X THEN
Name END).
■ SOLUTION
■ EXPLANATION
ROW_NUMBER assigns 1,2,3... alphabetically within each occupation. GROUP BY rn aligns all 1st names
into one row. MAX(CASE WHEN) extracts the name per column, NULL when no more names.
PROBLEM
You have a source orders table and a target data warehouse table. Write a query to load ONLY new or
updated records since the last load, using a watermark (last_loaded_at timestamp).
INPUT SCHEMA
orders_source
order_id INTEGER
customer_id INTEGER
amount DECIMAL
updated_at TIMESTAMP
load_metadata
table_name VARCHAR
last_loaded_at TIMESTAMP
EXPECTED OUTPUT
■ TIP: This is the core of incremental ETL — only process changed rows using a high-watermark timestamp.
■ HINT
WHERE updated_at > (SELECT last_loaded_at FROM load_metadata WHERE table_name = orders)
■ SOLUTION
■ EXPLANATION
Incremental load uses a watermark (last loaded timestamp) to identify only new/changed rows. This avoids
full table scans and is fundamental to ETL pipelines.
PROBLEM
Your pipeline loaded duplicate records into the staging table. Keep only the LATEST record per order_id
(based on updated_at). Delete or exclude older duplicates.
INPUT SCHEMA
orders_staging
id INTEGER (auto)
order_id INTEGER
amount DECIMAL
status VARCHAR
updated_at TIMESTAMP
EXPECTED OUTPUT
■ TIP: ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC) assigns 1 to the latest
row.
■ HINT
CTE with ROW_NUMBER partitioned by order_id ordered by updated_at DESC. WHERE rn = 1 keeps the latest.
■ SOLUTION
■ EXPLANATION
ROW_NUMBER partitioned by order_id assigns rank 1 to the most recent record. Filtering WHERE rn = 1
keeps only the latest. The DELETE version removes all but the first occurrence.
PROBLEM
As a data engineer you need to validate incoming data. Write queries to: (1) count NULLs per column, (2)
find rows where critical fields are NULL, (3) report the NULL percentage per column.
INPUT SCHEMA
customer_data
customer_id INTEGER
name VARCHAR
email VARCHAR
phone VARCHAR
created_at TIMESTAMP
EXPECTED OUTPUT
■ TIP: NULL checks are essential data quality validation steps in any ETL pipeline.
■ HINT
COUNT(*) - COUNT(column) counts NULLs. ROUND(100.0 * SUM(CASE WHEN col IS NULL...)/COUNT(*)) gives
percentage.
■ SOLUTION
■ EXPLANATION
COUNT(column) ignores NULLs, so COUNT(*) - COUNT(col) = NULL count. CASE WHEN with SUM
gives percentage. These checks catch data quality issues before they reach downstream tables.
PROBLEM
Implement SCD Type 2 logic: when a customer updates their address, keep the old record with an
end_date and insert a new record with is_current = TRUE. Write the SQL to handle an address change.
INPUT SCHEMA
customer_dim
customer_id INTEGER
name VARCHAR
address VARCHAR
start_date DATE
is_current BOOLEAN
EXPECTED OUTPUT
1 (Old row updated with end_date. New row inserted with is_current=TRUE)
■ TIP: SCD Type 2 preserves history by closing old records and inserting new ones. Widely used in data
warehouses.
■ HINT
Step 1: UPDATE old row SET end_date = today, is_current = FALSE. Step 2: INSERT new row with start_date =
today.
■ SOLUTION
1 -- Step 1: Close the old record 2 UPDATE customer_dim 3 SET 4 end_date = CURRENT_DATE -
INTERVAL '1 day', 5 is_current = FALSE 6 WHERE customer_id = 101 7 AND is_current = TRUE; 8
9 -- Step 2: Insert the new current record 10 INSERT INTO customer_dim 11 (customer_id,
name, address, start_date, end_date, is_current) 12 VALUES 13 (101, 'Alice', '456 New St',
CURRENT_DATE, NULL, TRUE);
■ EXPLANATION
SCD Type 2 preserves full history. Old record is closed with end_date. New record inserted with
is_current=TRUE. Enables point-in-time reporting — critical for data warehouses.
PROBLEM
Write an UPSERT: if a record with the same order_id exists in the target table, UPDATE it; otherwise
INSERT it. This is a common ETL pattern called MERGE or INSERT ON DUPLICATE KEY.
INPUT SCHEMA
orders_target
amount DECIMAL
status VARCHAR
updated_at TIMESTAMP
orders_staging
order_id INTEGER
amount DECIMAL
status VARCHAR
updated_at TIMESTAMP
EXPECTED OUTPUT
■ TIP: MySQL uses INSERT ... ON DUPLICATE KEY UPDATE. PostgreSQL uses INSERT ... ON CONFLICT DO
UPDATE.
■ HINT
MySQL: INSERT INTO target SELECT * FROM staging ON DUPLICATE KEY UPDATE col=VALUES(col). Or use
MERGE in SQL Server.
■ SOLUTION
1 -- MySQL: INSERT ... ON DUPLICATE KEY 2 INSERT INTO orders_target (order_id, amount,
status, updated_at) 3 SELECT order_id, amount, status, updated_at 4 FROM orders_staging 5
ON DUPLICATE KEY UPDATE 6 amount = VALUES(amount), 7 status = VALUES(status), 8 updated_at
= VALUES(updated_at); 9 10 -- PostgreSQL: INSERT ... ON CONFLICT 11 INSERT INTO
orders_target (order_id, amount, status, updated_at) 12 SELECT order_id, amount, status,
updated_at 13 FROM orders_staging 14 ON CONFLICT (order_id) DO UPDATE SET 15 amount =
[Link], 16 status = [Link], 17 updated_at = EXCLUDED.updated_at;
■ EXPLANATION
UPSERT combines insert and update atomically. MySQL uses ON DUPLICATE KEY. PostgreSQL uses
ON CONFLICT. SQL Server uses MERGE. This pattern is fundamental to idempotent ETL pipelines.
PROBLEM
Calculate Day-1 retention: for users who first appeared on a given date, what percentage came back the
NEXT day? Return cohort_date, total_users, retained_users, retention_pct.
INPUT SCHEMA
user_activity
user_id INTEGER
activity_date DATE
EXPECTED OUTPUT
■ TIP: Find each user first_seen date (MIN). Then LEFT JOIN on user_id AND first_seen + 1 = activity_date.
■ HINT
CTE1: MIN(activity_date) per user = first_seen. CTE2: join back to check if user appeared on first_seen+1.
■ SOLUTION
■ EXPLANATION
First CTE finds each user cohort date (first appearance). Second CTE left joins to check day-1 return.
NULL from left join = did not return. Retention % = retained / total.
PROBLEM
You have a massive orders table partitioned by year. Write a query that efficiently uses partition pruning to
get 2024 orders over $500, and explain why you filter on the partition column.
INPUT SCHEMA
orders
order_id INTEGER
customer_id INTEGER
amount DECIMAL
order_date DATE
INTEGER (partition
year key)
EXPECTED OUTPUT
■ TIP: Always filter on the partition column directly to enable partition pruning — skips scanning other partitions
entirely.
■ HINT
WHERE year = 2024 AND amount > 500. If using DATE, use WHERE order_date BETWEEN 2024-01-01 AND
2024-12-31.
■ SOLUTION
■ EXPLANATION
Partition pruning skips entire partitions not matching the filter — huge performance gain. Applying
functions to the partition column (like YEAR(order_date)) disables pruning. Always filter directly on the
partition key.
PROBLEM
You load data from multiple source systems. Add metadata columns to track where each row came from
and when it was loaded. Write the INSERT with audit columns.
INPUT SCHEMA
orders_raw
order_id INTEGER
amount DECIMAL
source_system VARCHAR
load_timestamp TIMESTAMP
batch_id VARCHAR
is_active BOOLEAN
EXPECTED OUTPUT
■ TIP: Audit columns (source_system, load_timestamp, batch_id) are essential for debugging pipelines and tracing
data issues.
■ HINT
INSERT with hardcoded metadata: source_system = system_name, load_timestamp = NOW(), batch_id = unique
run ID.
■ SOLUTION
1 -- Insert with full lineage metadata 2 INSERT INTO orders_raw 3 (order_id, amount,
source_system, load_timestamp, batch_id, is_active) 4 SELECT 5 order_id, 6 amount, 7
'CRM_SYSTEM' AS source_system, 8 NOW() AS load_timestamp, 9 'BATCH_2024_01_15_001' AS
batch_id, 10 TRUE AS is_active 11 FROM orders_staging_crm; 12 13 -- Query lineage: which
batches loaded today? 14 SELECT batch_id, source_system, 15 MIN(load_timestamp) AS
batch_start, 16 MAX(load_timestamp) AS batch_end, 17 COUNT(*) AS rows_loaded 18 FROM
orders_raw 19 WHERE DATE(load_timestamp) = CURRENT_DATE 20 GROUP BY batch_id,
source_system;
■ EXPLANATION
Audit columns answer: where did this data come from? When? Which pipeline run? Essential for
debugging, reprocessing, and compliance. batch_id lets you roll back a specific load.
PROBLEM
Find all missing IDs in the employees table. Assuming IDs should be consecutive integers, identify any
gaps in the sequence.
INPUT SCHEMA
employees
id INTEGER
name VARCHAR
EXPECTED OUTPUT
1 3 2 7 3 8
■ TIP: Self-join or NOT EXISTS — find IDs where (id + 1) does not exist in the table.
■ HINT
SELECT [Link] + 1 FROM employees curr WHERE NOT EXISTS (SELECT 1 FROM employees WHERE id =
[Link] + 1) AND [Link] < (SELECT MAX(id))
■ SOLUTION
1 SELECT [Link] + 1 AS missing_id 2 FROM employees curr 3 WHERE NOT EXISTS ( 4 SELECT 1
FROM employees 5 WHERE id = [Link] + 1 6 ) 7 AND [Link] < (SELECT MAX(id) FROM employees)
8 ORDER BY missing_id;
■ EXPLANATION
For each row, NOT EXISTS checks if the next sequential ID exists. The max ID check prevents false gaps
beyond the last real ID.
PROBLEM
Query the two cities in STATION with the shortest and longest CITY name lengths, including the lengths. If
tied, choose alphabetically first.
INPUT SCHEMA
STATION
ID INTEGER
CITY VARCHAR
STATE VARCHAR
LAT_N DECIMAL
LONG_W DECIMAL
EXPECTED OUTPUT
■ TIP: UNION two queries — one sorted by LENGTH ASC, one by LENGTH DESC, each with LIMIT 1.
■ HINT
ORDER BY LENGTH(CITY) ASC, CITY ASC LIMIT 1. Change ASC to DESC for longest.
■ SOLUTION
1 (SELECT CITY, LENGTH(CITY) AS len 2 FROM STATION 3 ORDER BY LENGTH(CITY) ASC, CITY ASC 4
LIMIT 1) 5 UNION 6 (SELECT CITY, LENGTH(CITY) AS len 7 FROM STATION 8 ORDER BY LENGTH(CITY)
DESC, CITY ASC 9 LIMIT 1);
■ EXPLANATION
Two queries combined with UNION. Secondary sort by CITY handles ties alphabetically. Parentheses
allow ORDER BY inside UNION subqueries in MySQL.
PROBLEM
Print company_code, founder, and total distinct counts of lead managers, senior managers, managers,
and employees. Order by company_code (string sort).
INPUT SCHEMA
Company
company_code VARCHAR
founder VARCHAR
Lead_Manager
lead_manager_code VARCHAR
company_code VARCHAR
Employee
employee_code VARCHAR
manager_code VARCHAR
company_code VARCHAR
EXPECTED OUTPUT
■ TIP: Tables may contain duplicates. Use COUNT(DISTINCT ...) to avoid overcounting. String ORDER BY sorts
C_1, C_10, C_2.
■ HINT
LEFT JOIN all tables to Company via company_code. COUNT(DISTINCT) each level code.
■ SOLUTION
■ EXPLANATION
COUNT(DISTINCT) deduplicates potentially duplicate records. LEFT JOIN keeps companies with no staff.
String ORDER BY sorts lexicographically not numerically.
COUNT(*) vs COUNT(col) COUNT(*) counts all rows including NULLs. COUNT(col) ignores NULLs.
RANK vs DENSE_RANK RANK skips numbers after ties (1,1,3). DENSE_RANK does not (1,1,2).
ROW_NUMBER Always unique sequential number. Use for deduplication (WHERE rn = 1).
Self JOIN JOIN a table to itself using two aliases. Used for manager/employee
comparisons.
Anti-JOIN LEFT JOIN + WHERE right IS NULL finds rows with no match in the right table.
EXISTS vs IN EXISTS stops at first match — more efficient. IN checks all values.
Correlated Subquery References outer query. Runs once per row. Slower but flexible.
CTE (WITH) Named temporary result. More readable than nested subqueries. MySQL 8.0+.
Recursive CTE Traverses hierarchies. Anchor query + UNION ALL recursive member.
PARTITION BY Divides window function into groups. Resets running totals per group.
Incremental Load Load only rows WHERE updated_at > last_watermark. Core ETL pattern.
SCD Type 2 Track history: close old row (end_date), insert new row (is_current=TRUE).
Partition Pruning Filter directly on partition column. Avoid functions on partition keys.