Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

SQL Subqueries and Advanced Commands
DBMS

SQL Subqueries and Advanced Commands

Master subqueries (correlated and non-correlated), EXISTS, IN, ANY, ALL, CTEs (WITH clause), UNION, INTERSECT, EXCEPT, and window functions.

SQL Subqueries and Advanced Commands

A subquery is a query nested inside another query. Subqueries enable operations that cannot be done with simple joins or aggregations.

This chapter covers subqueries, set operations (UNION, INTERSECT, EXCEPT), Common Table Expressions (CTEs), and window functions.


Learning Objectives

After completing this chapter, you will be able to:

  • Write scalar, row, and table subqueries.
  • Differentiate between correlated and non-correlated subqueries.
  • Use EXISTS, IN, ANY, and ALL with subqueries.
  • Write CTEs with the WITH clause.
  • Use UNION, INTERSECT, and EXCEPT.
  • Write window functions (ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD).
  • Answer advanced SQL interview questions.

Sample Data

CREATE TABLE Employees (
    Emp_ID INT PRIMARY KEY,
    Name VARCHAR(50),
    Dept_ID INT,
    Salary DECIMAL(10,2)
);

CREATE TABLE Departments (
    Dept_ID INT PRIMARY KEY,
    Dept_Name VARCHAR(50)
);

Employees

Emp_IDNameDept_IDSalary
1Rahul1060000
2Priya1075000
3Amit2050000
4Sneha2055000
5Vikram3080000
6NehaNULL45000

Departments

Dept_IDDept_Name
10Engineering
20Marketing
40Finance

Types of Subqueries

Subqueries can return different types of results.

TypeReturnsUsed In
ScalarSingle value (one column, one row)SELECT, WHERE, SET
RowMultiple columns, one rowWHERE comparisons
TableMultiple rows and columnsFROM, IN, EXISTS, joins
ColumnSingle column, multiple rowsIN, ANY, ALL

Scalar Subquery

Returns a single value. Used anywhere a single value is needed.

-- Get employees earning more than the average salary
SELECT Name, Salary
FROM Employees
WHERE Salary > (SELECT AVG(Salary) FROM Employees);
NameSalary
Rahul60000
Priya75000
Vikram80000

The subquery (SELECT AVG(Salary) FROM Employees) returns 60833.33 (a single value).

-- Show each employee's salary as a percentage of the total
SELECT Name, Salary,
       ROUND(Salary * 100.0 / (SELECT SUM(Salary) FROM Employees), 2) AS Pct
FROM Employees;

Row Subquery

Returns one row with multiple columns.

-- Find the employee(s) with the highest salary in each department
-- This requires a correlated subquery (covered next)

Row subqueries are less common but useful for comparing multiple columns at once.

-- Find employee whose salary matches the max in dept 10
SELECT Name, Salary
FROM Employees
WHERE (Dept_ID, Salary) = (SELECT Dept_ID, MAX(Salary)
                            FROM Employees
                            WHERE Dept_ID = 10
                            GROUP BY Dept_ID);

Table Subquery

Returns a full result set. Used in the FROM clause (derived table) or with IN/EXISTS.

-- Find departments with above-average salaries
SELECT d.Dept_Name, avg_data.Avg_Salary
FROM Departments d
JOIN (
    SELECT Dept_ID, AVG(Salary) AS Avg_Salary
    FROM Employees
    GROUP BY Dept_ID
) avg_data ON d.Dept_ID = avg_data.Dept_ID
WHERE avg_data.Avg_Salary > (SELECT AVG(Salary) FROM Employees);
Dept_NameAvg_Salary
Engineering67500

Correlated vs Non-Correlated Subqueries

Non-Correlated

The inner query runs independently of the outer query. It runs once.

SELECT Name FROM Employees
WHERE Salary > (SELECT AVG(Salary) FROM Employees);

The subquery runs once → gets the average → the outer query uses that value.

Correlated

The inner query references a column from the outer query. It runs once for each row of the outer query.

-- Find employees who earn more than the average of their OWN department
SELECT e1.Name, e1.Salary, e1.Dept_ID
FROM Employees e1
WHERE e1.Salary > (
    SELECT AVG(e2.Salary)
    FROM Employees e2
    WHERE e2.Dept_ID = e1.Dept_ID  -- Reference to outer query
);
NameSalaryDept_ID
Priya7500010
Sneha5500020
Vikram8000030

Dept 10 average: 67500 → Priya (75000) is above. Dept 20 average: 52500 → Sneha (55000) is above. Dept 30 average: 80000 → Vikram (80000) is equal (not above). Neha (Dept_ID NULL): ignored.

Performance

Correlated subqueries are slower — they execute once per outer row. For large tables, consider rewriting as a JOIN or using window functions.


EXISTS

EXISTS returns TRUE if the subquery returns at least one row.

-- Find departments that have at least one employee
SELECT d.Dept_Name
FROM Departments d
WHERE EXISTS (
    SELECT 1 FROM Employees e WHERE e.Dept_ID = d.Dept_ID
);
Dept_Name
Engineering
Marketing

NOT EXISTS

-- Find departments with NO employees
SELECT d.Dept_Name
FROM Departments d
WHERE NOT EXISTS (
    SELECT 1 FROM Employees e WHERE e.Dept_ID = d.Dept_ID
);
Dept_Name
Finance

EXISTS vs IN

AspectEXISTSIN
NULL handlingNot affected by NULLsAffected — IN (NULL, 1, 2) has unexpected behavior
PerformanceBetter for large subquery resultsBetter for small, static lists
ReadabilityClear for “exists” semanticsClear for “value is in list”

IN, ANY, ALL

IN

Checks if a value matches any value in a list.

SELECT Name FROM Employees
WHERE Dept_ID IN (10, 20);

With subquery:

SELECT Name FROM Employees
WHERE Dept_ID IN (SELECT Dept_ID FROM Departments);

ANY

Compares a value to each value returned by the subquery.

-- Find employees earning more than any employee in dept 20
SELECT Name, Salary
FROM Employees
WHERE Salary > ANY (SELECT Salary FROM Employees WHERE Dept_ID = 20);

Dept 20 salaries: 50000, 55000. ANY means “greater than at least one” — so employees with salary > 50000.

ALL

Compares a value to all values returned by the subquery.

-- Find employees earning more than ALL employees in dept 20
SELECT Name, Salary
FROM Employees
WHERE Salary > ALL (SELECT Salary FROM Employees WHERE Dept_ID = 20);

Dept 20 salaries: 50000, 55000. ALL means “greater than every one” — so employees with salary > 55000.


CTE (Common Table Expression)

A CTE (WITH clause) creates a temporary named result set that you can reference in the main query.

WITH DeptAvg AS (
    SELECT Dept_ID, AVG(Salary) AS Avg_Salary
    FROM Employees
    GROUP BY Dept_ID
)
SELECT d.Dept_Name, da.Avg_Salary
FROM Departments d
JOIN DeptAvg da ON d.Dept_ID = da.Dept_ID
WHERE da.Avg_Salary > 50000;

Recursive CTE

Used for hierarchical data (org charts, category trees).

WITH RECURSIVE OrgChart AS (
    -- Anchor: top-level managers
    SELECT Emp_ID, Name, Manager_ID, 1 AS Level
    FROM Employees WHERE Manager_ID IS NULL

    UNION ALL

    -- Recursive: employees reporting to those in the CTE
    SELECT e.Emp_ID, e.Name, e.Manager_ID, oc.Level + 1
    FROM Employees e
    JOIN OrgChart oc ON e.Manager_ID = oc.Emp_ID
)
SELECT * FROM OrgChart;

CTE vs Subquery

AspectCTESubquery
ReadabilityBetter (named, separate)Can get nested and confusing
ReusabilityCan reference the CTE multiple timesMust repeat the subquery
RecursionSupports recursive queriesCannot do recursion

UNION, INTERSECT, EXCEPT

Set operations combine results from two queries.

UNION

Combines results, removing duplicates.

SELECT Name FROM Employees
UNION
SELECT Dept_Name FROM Departments;

UNION ALL keeps duplicates (faster).

SELECT Dept_ID FROM Employees
UNION ALL
SELECT Dept_ID FROM Departments;

INTERSECT

Returns rows that appear in both queries.

SELECT Dept_ID FROM Employees
INTERSECT
SELECT Dept_ID FROM Departments;

Dept_IDs present in both tables: 10, 20.

EXCEPT (MINUS in Oracle)

Returns rows from the first query that do NOT appear in the second.

SELECT Dept_ID FROM Departments
EXCEPT
SELECT Dept_ID FROM Employees;

Dept_IDs in Departments but not in Employees: 40.

Rules

  • Both queries must have the same number of columns.
  • Corresponding columns must have compatible data types.
  • ORDER BY applies to the final result (at the end).

Window Functions

Window functions perform calculations across a set of rows related to the current row, without collapsing them into a single output row.

ROW_NUMBER

Assigns a unique sequential number to each row within a partition.

SELECT Name, Dept_ID, Salary,
       ROW_NUMBER() OVER (ORDER BY Salary DESC) AS Rank
FROM Employees;

RANK

Assigns ranks with gaps for ties.

SELECT Name, Dept_ID, Salary,
       RANK() OVER (ORDER BY Salary DESC) AS Rank
FROM Employees;

DENSE_RANK

Assigns ranks without gaps for ties.

SELECT Name, Dept_ID, Salary,
       DENSE_RANK() OVER (ORDER BY Salary DESC) AS Rank
FROM Employees;
NameSalaryROW_NUMBERRANKDENSE_RANK
Vikram80000111
Priya75000222
Rahul60000333
Sneha55000444
Amit50000555

(No ties in this dataset, but if multiple employees had the same salary, RANK would skip numbers while DENSE_RANK would not.)

Rank Within Each Department (PARTITION BY)

SELECT Name, Dept_ID, Salary,
       RANK() OVER (PARTITION BY Dept_ID ORDER BY Salary DESC) AS Dept_Rank
FROM Employees
WHERE Dept_ID IS NOT NULL;

LAG and LEAD

Access data from previous or next rows.

SELECT Name, Salary,
       LAG(Salary, 1) OVER (ORDER BY Salary) AS Previous_Salary,
       LEAD(Salary, 1) OVER (ORDER BY Salary) AS Next_Salary
FROM Employees;
NameSalaryPrevious_SalaryNext_Salary
Amit50000NULL55000
Sneha550005000060000
Rahul600005500075000
Priya750006000080000
Vikram8000075000NULL

LAG/LEAD are useful for calculating differences between consecutive rows.


Interview Deep Dive

Q: What is a correlated subquery? Give an example.

A: A correlated subquery references a column from the outer query and executes once per row of the outer query. Example: “Find employees earning more than the average of their own department.” The inner query must reference e1.Dept_ID from the outer query to calculate the department-specific average.

Q: When would you choose a CTE over a subquery?

A: Choose a CTE when: (1) the same subquery result is needed multiple times — the CTE is defined once and referenced many times; (2) the query is deeply nested and hard to read — CTEs flatten the structure; (3) you need recursion (hierarchical data like org charts). Choose a subquery for simple, one-off cases.

Q: What is the difference between RANK and DENSE_RANK?

A: Both assign rankings with ties. RANK skips numbers after ties (1, 2, 2, 4). DENSE_RANK does not skip numbers (1, 2, 2, 3). Example: If three people tie for first, RANK gives 1, 1, 1, 4; DENSE_RANK gives 1, 1, 1, 2.

Q: What is the difference between UNION and JOIN?

A: UNION combines results vertically (adds rows from two queries) and requires both queries to have the same columns. JOIN combines results horizontally (adds columns by matching rows from two tables). UNION gives more rows; JOIN gives wider rows.


Key Takeaways

  • Scalar subqueries return a single value; table subqueries return a result set.
  • Correlated subqueries reference the outer query and run once per row.
  • EXISTS checks for existence (faster than IN for large subqueries).
  • IN, ANY, ALL compare values against subquery results.
  • CTEs make complex queries readable and support recursion.
  • UNION, INTERSECT, EXCEPT operate on result sets (not individual rows).
  • Window functions (ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD) compute across rows without collapsing them.
  • PARTITION BY creates groups within window functions.

My Private Notes

Notes are auto-saved locally to this device.