Query operations interviewers love — WHERE vs HAVING, UNION, subqueries, CASE, IN vs EXISTS and more.
WHERE is used to filter rows before grouping, while HAVING is used to filter groups after the GROUP BY operation. WHERE cannot be used with aggregate functions, but HAVING can.
SELECT * FROM employees WHERE salary > 50000;
SELECT department, AVG(salary) as avg_salary FROM employees GROUP BY department HAVING AVG(salary) > 60000;
UNION and UNION ALL are both used to combine results from multiple SELECT queries, but they differ in how they handle duplicates and performance.
SELECT employee_name FROM employees_ny UNION SELECT employee_name FROM employees_sf;
SELECT employee_name FROM employees_ny UNION ALL SELECT employee_name FROM employees_sf;
These three commands are used for removing data, but they work differently in terms of scope, speed, and reversibility.
-- Removes specific rows, can be rolled back DELETE FROM employees WHERE department = 'HR'; -- Removes all rows but table structure remains DELETE FROM employees;
-- Removes all rows, cannot be rolled back, faster than DELETE TRUNCATE TABLE employees;
-- Removes entire table including structure DROP TABLE employees;
Both IN and EXISTS are used for subqueries, but they work differently in terms of performance and use cases.
-- Find employees in specific departments
SELECT name, department
FROM employees
WHERE department IN ('Sales', 'Marketing', 'Engineering');
-- Using subquery with IN
SELECT name
FROM employees
WHERE department_id IN (
SELECT department_id
FROM departments
WHERE location = 'New York'
);
-- Find customers who have placed orders
SELECT customer_name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
-- Using NOT EXISTS to find customers without orders
SELECT customer_name
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
The CASE statement provides conditional logic in SQL queries, similar to if-else statements in programming languages.
SELECT
name,
department,
CASE department
WHEN 'Sales' THEN 'Revenue Department'
WHEN 'Marketing' THEN 'Brand Department'
WHEN 'Engineering' THEN 'Tech Department'
ELSE 'Other Department'
END as dept_category,
CASE
WHEN salary >= 100000 THEN 'High'
WHEN salary >= 60000 THEN 'Medium'
WHEN salary >= 30000 THEN 'Low'
ELSE 'Very Low'
END as salary_grade
FROM employees;
SELECT
order_id,
amount,
CASE
WHEN amount > 1000 THEN 'Large Order'
WHEN amount > 500 THEN 'Medium Order'
WHEN amount > 100 THEN 'Small Order'
ELSE 'Mini Order'
END as order_size,
CASE
WHEN DATEDIFF(day, order_date, shipped_date) > 7 THEN 'Delayed'
WHEN DATEDIFF(day, order_date, shipped_date) <= 2 THEN 'Express'
ELSE 'Standard'
END as shipping_status
FROM orders;
-- Count by category
SELECT
COUNT(CASE WHEN department = 'Sales' THEN 1 END) as sales_count,
COUNT(CASE WHEN department = 'Engineering' THEN 1 END) as eng_count,
COUNT(CASE WHEN salary > 80000 THEN 1 END) as high_earners,
SUM(CASE WHEN status = 'active' THEN salary END) as active_payroll,
AVG(CASE WHEN department = 'Marketing' THEN salary END) as marketing_avg
FROM employees;
-- PIVOT-like functionality
SELECT
EXTRACT(YEAR FROM hire_date) as hire_year,
COUNT(CASE WHEN department = 'Sales' THEN 1 END) as sales,
COUNT(CASE WHEN department = 'Engineering' THEN 1 END) as engineering,
COUNT(CASE WHEN department = 'Marketing' THEN 1 END) as marketing
FROM employees
GROUP BY EXTRACT(YEAR FROM hire_date);
UNION combines rows from multiple queries vertically, while JOIN combines columns from multiple tables horizontally.
-- Combine employees from different cities SELECT name, 'New York' as location FROM employees_ny UNION ALL SELECT name, 'San Francisco' as location FROM employees_sf UNION ALL SELECT name, 'Chicago' as location FROM employees_chi; -- UNION vs UNION ALL SELECT product_id FROM current_products UNION -- Removes duplicates SELECT product_id FROM discontinued_products; SELECT product_id FROM current_products UNION ALL -- Keeps duplicates SELECT product_id FROM popular_products;
-- Combine employee info with department info
SELECT
e.name,
e.salary,
d.department_name,
m.name as manager_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
LEFT JOIN employees m ON e.manager_id = m.employee_id;
-- Multiple JOIN types
SELECT
c.customer_name,
o.order_date,
p.product_name,
oi.quantity
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id;
Recursive queries use Common Table Expressions (CTEs) to query hierarchical or graph-like data, such as organizational charts or bill of materials.
-- Employees table with manager hierarchy
CREATE TABLE employees (
employee_id INT PRIMARY KEY,
name VARCHAR(100),
manager_id INT REFERENCES employees(employee_id)
);
-- Recursive CTE to get hierarchy
WITH RECURSIVE org_chart AS (
-- Anchor member: top-level employees (no manager)
SELECT
employee_id,
name,
manager_id,
1 as level,
name as path
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive member: join with previous level
SELECT
e.employee_id,
e.name,
e.manager_id,
oc.level + 1 as level,
oc.path || ' -> ' || e.name as path
FROM employees e
INNER JOIN org_chart oc ON e.manager_id = oc.employee_id
)
SELECT
employee_id,
name,
level,
path
FROM org_chart
ORDER BY path;
-- Parts explosion (component hierarchy)
CREATE TABLE bom (
assembly_id INT,
component_id INT,
quantity INT
);
WITH RECURSIVE parts_explosion AS (
-- Anchor: top-level assemblies
SELECT
assembly_id,
component_id,
quantity,
1 as level,
CAST(component_id AS VARCHAR) as path
FROM bom
WHERE assembly_id = 1000 -- Starting assembly
UNION ALL
-- Recursive: explode components
SELECT
b.assembly_id,
b.component_id,
pe.quantity * b.quantity as total_qty,
pe.level + 1 as level,
pe.path || '.' || CAST(b.component_id AS VARCHAR)
FROM bom b
INNER JOIN parts_explosion pe ON b.assembly_id = pe.component_id
)
SELECT * FROM parts_explosion
ORDER BY level, path;
-- Find all managers above an employee
WITH RECURSIVE manager_chain AS (
-- Start with specific employee
SELECT
employee_id,
name,
manager_id,
0 as level
FROM employees
WHERE employee_id = 123 -- Starting employee
UNION ALL
-- Move up to manager
SELECT
e.employee_id,
e.name,
e.manager_id,
mc.level + 1
FROM employees e
INNER JOIN manager_chain mc ON e.employee_id = mc.manager_id
)
SELECT * FROM manager_chain
ORDER BY level DESC;
These operators compare a value to each value in a list or subquery, with different comparison semantics.
-- Find employees earning more than ANY manager
SELECT name, salary
FROM employees
WHERE salary > ANY (
SELECT salary
FROM employees
WHERE title = 'Manager'
);
-- Equivalent to: salary > MIN(manager_salaries)
-- Using SOME (synonym for ANY)
SELECT name, salary
FROM employees
WHERE salary > SOME (
SELECT salary
FROM employees
WHERE title = 'Manager'
);
-- ANY with IN-like behavior
SELECT product_name
FROM products
WHERE category_id = ANY (SELECT category_id FROM popular_categories);
-- Equivalent to: category_id IN (...)
-- Find employees earning more than ALL managers
SELECT name, salary
FROM employees
WHERE salary > ALL (
SELECT salary
FROM employees
WHERE title = 'Manager'
);
-- Equivalent to: salary > MAX(manager_salaries)
-- ALL with comparison
SELECT product_name, price
FROM products
WHERE price <= ALL (
SELECT price
FROM products
WHERE category_id = 5
);
-- Finds cheapest product in category 5
-- ALL with NOT
SELECT customer_name
FROM customers
WHERE customer_id != ALL (
SELECT customer_id
FROM orders
WHERE order_date > '2024-01-01'
);
-- Customers with no recent orders
-- Find departments where ALL employees earn > 50000
SELECT department
FROM employees e1
WHERE 50000 < ALL (
SELECT salary
FROM employees e2
WHERE e2.department = e1.department
)
GROUP BY department;
-- Using EXISTS alternative for ALL
SELECT department
FROM employees e1
WHERE NOT EXISTS (
SELECT 1
FROM employees e2
WHERE e2.department = e1.department
AND e2.salary <= 50000
)
GROUP BY department;