Subqueries in SQL Server
A subquery is a query inside another query.
You will learn:
- Scalar subquery (returns 1 value)
- Multi-row subquery with IN / NOT IN / ANY / ALL
- Subquery in SELECT, FROM (derived table), WHERE, HAVING
- EXISTS vs IN, and the NOT IN + NULL trap
Run the sample database script first. Every query below was run on that data, and the Output under each query is the real result.
USE SqlLearningDb;
GO
Topic table: Students and Exams (a second small dataset)
DROP TABLE IF EXISTS dbo.ExamScores;
DROP TABLE IF EXISTS dbo.Students;
CREATE TABLE dbo.Students
(
StudentID INT PRIMARY KEY,
StudentName VARCHAR(50) NOT NULL,
ClassName VARCHAR(10) NOT NULL,
MentorID INT NULL -- refers to Employees.EmployeeID, NULL for some
);
INSERT INTO dbo.Students (StudentID, StudentName, ClassName, MentorID) VALUES
(1, 'Aarav', '10-A', 14),
(2, 'Diya', '10-A', 14),
(3, 'Ishaan', '10-B', 15),
(4, 'Kiara', '10-B', NULL),
(5, 'Vihaan', '10-A', 11),
(6, 'Anaya', '10-B', 15),
(7, 'Reyansh', '10-A', NULL),
(8, 'Myra', '10-B', 11);
CREATE TABLE dbo.ExamScores
(
StudentID INT NOT NULL REFERENCES dbo.Students(StudentID),
Subject VARCHAR(20) NOT NULL,
Marks INT NOT NULL,
PRIMARY KEY (StudentID, Subject)
);
INSERT INTO dbo.ExamScores (StudentID, Subject, Marks) VALUES
(1,'Maths',95),(1,'Science',88),(1,'English',76),
(2,'Maths',67),(2,'Science',72),(2,'English',91),
(3,'Maths',88),(3,'Science',95),(3,'English',64),
(4,'Maths',45),(4,'Science',58),(4,'English',70),
(5,'Maths',99),(5,'Science',81),(5,'English',85),
(6,'Maths',72),(6,'Science',66),(6,'English',88),
(7,'Maths',55),(7,'Science',49),(7,'English',62);
-- Student 8 (Myra) has no exam scores
GO
Messages(8 rows affected)
(21 rows affected)
Part A: Scalar Subqueries (return exactly one value)
Employees earning more than the company average salary
SELECT FirstName, Salary
FROM dbo.Employees
WHERE Salary > (SELECT AVG(Salary) FROM dbo.Employees);
Output
| FirstName | Salary |
| Rajesh | 250000.00 |
| Anita | 180000.00 |
| Vikram | 190000.00 |
| Meena | 120000.00 |
| Suresh | 130000.00 |
| Kavya | 110000.00 |
| Arjun | 100000.00 |
| Kiran | 95000.00 |
| Sanjay | 90000.00 |
| Deepak | 105000.00 |
Show each employee's salary next to the company average and the difference
SELECT FirstName, Salary,
(SELECT AVG(Salary) FROM dbo.Employees) AS CompanyAvg,
Salary - (SELECT AVG(Salary) FROM dbo.Employees) AS DiffFromAvg
FROM dbo.Employees;
Output
| FirstName | Salary | CompanyAvg | DiffFromAvg |
| Rajesh | 250000.00 | 87680.000000 | 162320.000000 |
| Anita | 180000.00 | 87680.000000 | 92320.000000 |
| Vikram | 190000.00 | 87680.000000 | 102320.000000 |
| Meena | 120000.00 | 87680.000000 | 32320.000000 |
| Suresh | 130000.00 | 87680.000000 | 42320.000000 |
| Kavya | 110000.00 | 87680.000000 | 22320.000000 |
| Arjun | 100000.00 | 87680.000000 | 12320.000000 |
| Pooja | 55000.00 | 87680.000000 | -32680.000000 |
| Rahul | 52000.00 | 87680.000000 | -35680.000000 |
| Sneha | 58000.00 | 87680.000000 | -29680.000000 |
| Kiran | 95000.00 | 87680.000000 | 7320.000000 |
| Divya | 70000.00 | 87680.000000 | -17680.000000 |
| Amit | 68000.00 | 87680.000000 | -19680.000000 |
| Neha | 75000.00 | 87680.000000 | -12680.000000 |
| Sanjay | 90000.00 | 87680.000000 | 2320.000000 |
... showing 15 of 25 rows
The most expensive product
SELECT ProductName, UnitPrice
FROM dbo.Products
WHERE UnitPrice = (SELECT MAX(UnitPrice) FROM dbo.Products);
Output
| ProductName | UnitPrice |
| MacBook Air M3 | 120000.00 |
Second highest salary (classic interview question)
SELECT MAX(Salary) AS SecondHighestSalary
FROM dbo.Employees
WHERE Salary < (SELECT MAX(Salary) FROM dbo.Employees);
Output
| SecondHighestSalary |
| 190000.00 |
Latest order date and the order(s) placed on that day
SELECT OrderID, CustomerID, OrderDate
FROM dbo.Orders
WHERE OrderDate = (SELECT MAX(OrderDate) FROM dbo.Orders);
Output
| OrderID | CustomerID | OrderDate |
| 80 | 5 | 2025-06-24 |
Part B: Multi-row Subqueries (in, NOT IN, Any, All)
In: employees working in departments located in Hyderabad
SELECT FirstName, DepartmentID
FROM dbo.Employees
WHERE DepartmentID IN (SELECT DepartmentID FROM dbo.Departments WHERE Location = 'Hyderabad');
Output
| FirstName | DepartmentID |
| Rajesh | 1 |
| Meena | 5 |
| Suresh | 6 |
| Priya | 5 |
| Manoj | 6 |
| Lakshmi | 6 |
In: products that were ever ordered
SELECT ProductName
FROM dbo.Products
WHERE ProductID IN (SELECT ProductID FROM dbo.OrderItems);
Output
| ProductName |
| Dell Inspiron 15 |
| HP Pavilion 14 |
| MacBook Air M3 |
| Lenovo ThinkCentre |
| Acer Aspire Desktop |
| Samsung Galaxy A55 |
| iPhone 15 |
| Redmi Note 13 |
| Wireless Mouse |
| Mechanical Keyboard |
| 27 inch Monitor |
| USB-C Hub |
| Ergonomic Office Chair |
| Basic Office Chair |
| Standing Desk |
... showing 15 of 18 rows
NOT IN: customers who never ordered
SELECT CustomerName
FROM dbo.Customers
WHERE CustomerID NOT IN (SELECT CustomerID FROM dbo.Orders);
Output
| CustomerName |
| Nizam Bakers |
| Coastal Exports |
| Himalaya Ventures |
The NOT IN + NULL Trap !!!
Students.MentorID has NULLs. This returns ZERO rows (surprise!) because "x NOT IN (1, 2, NULL)" is UNKNOWN, never TRUE.
SELECT FirstName
FROM dbo.Employees
WHERE EmployeeID NOT IN (SELECT MentorID FROM dbo.Students);
-- Fix 1 : filter NULLs inside the subquery
SELECT FirstName
FROM dbo.Employees
WHERE EmployeeID NOT IN (SELECT MentorID FROM dbo.Students WHERE MentorID IS NOT NULL);
-- Fix 2 (recommended) : use NOT EXISTS, it is NULL-safe
SELECT e.FirstName
FROM dbo.Employees AS e
WHERE NOT EXISTS (SELECT 1 FROM dbo.Students AS s WHERE s.MentorID = e.EmployeeID);
Output
| FirstName |
| Rajesh |
| Anita |
| Vikram |
| Meena |
| Suresh |
| Kavya |
| Arjun |
| Pooja |
| Rahul |
| Sneha |
| Divya |
| Amit |
| Priya |
| Manoj |
| Lakshmi |
... showing 15 of 22 rows
Output
| FirstName |
| Rajesh |
| Anita |
| Vikram |
| Meena |
| Suresh |
| Kavya |
| Arjun |
| Pooja |
| Rahul |
| Sneha |
| Divya |
| Amit |
| Priya |
| Manoj |
| Lakshmi |
... showing 15 of 22 rows
> All: salary greater than Every salary in the Sales department
SELECT FirstName, Salary
FROM dbo.Employees
WHERE Salary > ALL (SELECT Salary FROM dbo.Employees WHERE DepartmentID = 2);
Output
| FirstName | Salary |
| Rajesh | 250000.00 |
| Vikram | 190000.00 |
> Any: salary greater than at Least One salary in the It department
SELECT FirstName, Salary
FROM dbo.Employees
WHERE Salary > ANY (SELECT Salary FROM dbo.Employees WHERE DepartmentID = 4)
AND DepartmentID <> 4;
Output
| FirstName | Salary |
| Rajesh | 250000.00 |
| Anita | 180000.00 |
| Meena | 120000.00 |
| Suresh | 130000.00 |
| Kavya | 110000.00 |
| Arjun | 100000.00 |
| Deepak | 105000.00 |
Part C: Subquery in from (Derived Table)
Average of department averages
SELECT AVG(DeptAvg) AS AvgOfDeptAverages
FROM (
SELECT DepartmentID, AVG(Salary) AS DeptAvg
FROM dbo.Employees
WHERE DepartmentID IS NOT NULL
GROUP BY DepartmentID
) AS d; -- a derived table MUST have an alias
Output
| AvgOfDeptAverages |
| 104809.523809 |
Order totals, then pick orders above 1 lakh
SELECT t.OrderID, t.OrderTotal
FROM (
SELECT OrderID, SUM(LineTotal) AS OrderTotal
FROM dbo.OrderItems
GROUP BY OrderID
) AS t
WHERE t.OrderTotal > 100000
ORDER BY t.OrderTotal DESC;
Output
| OrderID | OrderTotal |
| 32 | 476850.00 |
| 6 | 407400.00 |
| 4 | 393600.00 |
| 57 | 377750.00 |
| 23 | 376000.00 |
| 25 | 367000.00 |
| 74 | 366650.00 |
| 31 | 350400.00 |
| 15 | 349142.50 |
| 9 | 348225.00 |
| 21 | 324000.00 |
| 65 | 312450.00 |
| 80 | 294500.00 |
| 71 | 293775.00 |
| 1 | 285000.00 |
... showing 15 of 46 rows
Student average marks with class average side by side
SELECT s.StudentName, s.ClassName, sa.AvgMarks, ca.ClassAvg
FROM dbo.Students AS s
JOIN (SELECT StudentID, AVG(Marks * 1.0) AS AvgMarks FROM dbo.ExamScores GROUP BY StudentID) AS sa
ON sa.StudentID = s.StudentID
JOIN (SELECT st.ClassName, AVG(es.Marks * 1.0) AS ClassAvg
FROM dbo.ExamScores es JOIN dbo.Students st ON st.StudentID = es.StudentID
GROUP BY st.ClassName) AS ca
ON ca.ClassName = s.ClassName;
Output
| StudentName | ClassName | AvgMarks | ClassAvg |
| Aarav | 10-A | 86.333333 | 76.666666 |
| Diya | 10-A | 76.666666 | 76.666666 |
| Ishaan | 10-B | 82.333333 | 71.777777 |
| Kiara | 10-B | 57.666666 | 71.777777 |
| Vihaan | 10-A | 88.333333 | 76.666666 |
| Anaya | 10-B | 75.333333 | 71.777777 |
| Reyansh | 10-A | 55.333333 | 76.666666 |
Part D: Subquery in HAVING
Departments whose average salary is above the company average
SELECT DepartmentID, AVG(Salary) AS DeptAvg
FROM dbo.Employees
GROUP BY DepartmentID
HAVING AVG(Salary) > (SELECT AVG(Salary) FROM dbo.Employees);
Output
| DepartmentID | DeptAvg |
| 1 | 250000.000000 |
| 4 | 94000.000000 |
Customers who placed more orders than the average customer
SELECT CustomerID, COUNT(*) AS OrderCount
FROM dbo.Orders
GROUP BY CustomerID
HAVING COUNT(*) > (SELECT AVG(Cnt * 1.0)
FROM (SELECT COUNT(*) AS Cnt FROM dbo.Orders GROUP BY CustomerID) AS x);
Output
| CustomerID | OrderCount |
| 1 | 11 |
| 2 | 6 |
| 3 | 13 |
| 5 | 8 |
| 12 | 5 |
| 15 | 6 |
Part E: Subqueries in INSERT / UPDATE / DELETE
DROP TABLE IF EXISTS dbo.HighValueCustomers;
CREATE TABLE dbo.HighValueCustomers (CustomerID INT PRIMARY KEY, CustomerName VARCHAR(100), TotalSpent DECIMAL(14,2));
INSERT ... SELECT with subquery
INSERT INTO dbo.HighValueCustomers (CustomerID, CustomerName, TotalSpent)
SELECT c.CustomerID, c.CustomerName,
(SELECT SUM(oi.LineTotal)
FROM dbo.Orders o JOIN dbo.OrderItems oi ON oi.OrderID = o.OrderID
WHERE o.CustomerID = c.CustomerID)
FROM dbo.Customers AS c
WHERE c.CustomerID IN (SELECT CustomerID FROM dbo.Orders GROUP BY CustomerID HAVING COUNT(*) >= 5);
SELECT * FROM dbo.HighValueCustomers ORDER BY TotalSpent DESC;
Messages(6 rows affected)
Output
| CustomerID | CustomerName | TotalSpent |
| 3 | Lakshmi Stores | 1925592.50 |
| 5 | Pune Tech Park | 1649975.00 |
| 1 | Ravi Traders | 1527910.00 |
| 15 | Marina Digital | 1326609.00 |
| 2 | Infosoft Solutions | 1163225.00 |
| 12 | Awadh Enterprises | 673885.00 |
UPDATE with subquery (inside a transaction so we can undo it)
BEGIN TRAN;
UPDATE dbo.Products
SET UnitPrice = UnitPrice * 0.90 -- 10% off for products never ordered
WHERE ProductID NOT IN (SELECT ProductID FROM dbo.OrderItems);
SELECT ProductID, ProductName, UnitPrice FROM dbo.Products WHERE ProductID IN (19, 20);
ROLLBACK TRAN; -- undo, keep data clean
Messages(2 rows affected)
Output
| ProductID | ProductName | UnitPrice |
| 19 | Old Model Tablet | 16200.00 |
| 20 | Laser Printer | 14400.00 |
Practice Questions
- Find the student(s) with the highest Maths marks.
- List products whose price is above the average price of their category (try with derived table).
- Find employees who are managers (their ID appears in ManagerID).
- Find the third highest salary using subqueries only.
- List customers whose total order count equals the maximum order count of any customer.