Correlated Subqueries and EXISTS in SQL Server
A CORRELATED subquery uses a column from the OUTER query.
It is (logically) executed once for EACH row of the outer query.
SELECT ... FROM Employees e
WHERE Salary > (SELECT AVG(Salary) FROM Employees WHERE DepartmentID = e.DepartmentID)
^^^^^^^^^^^^^^
outer column = correlation
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: ProductPriceHistory (price changes over time)
DROP TABLE IF EXISTS dbo.ProductPriceHistory;
CREATE TABLE dbo.ProductPriceHistory
(
HistoryID INT IDENTITY(1,1) PRIMARY KEY,
ProductID INT NOT NULL REFERENCES dbo.Products(ProductID),
Price DECIMAL(10,2) NOT NULL,
EffectiveFrom DATE NOT NULL
);
INSERT INTO dbo.ProductPriceHistory (ProductID, Price, EffectiveFrom) VALUES
(1, 60000, '2024-01-01'), (1, 62000, '2024-07-01'), (1, 65000, '2025-01-01'),
(2, 80000, '2024-01-01'), (2, 85000, '2024-10-01'),
(3, 115000,'2024-01-01'), (3, 118000,'2024-06-01'), (3, 120000,'2025-03-01'),
(6, 27000, '2024-01-01'), (6, 25000, '2024-11-15'),
(7, 72000, '2024-01-01'), (7, 70000, '2025-02-01'),
(9, 1000, '2024-01-01'), (9, 1200, '2024-08-01'),
(13, 11000,'2024-01-01'), (13, 12000,'2025-01-15');
GO
Messages(16 rows affected)
Part A: Correlated Subquery in WHERE
Employees earning more than the average of Their Own department
SELECT e.FirstName, e.DepartmentID, e.Salary
FROM dbo.Employees AS e
WHERE e.Salary > (SELECT AVG(e2.Salary)
FROM dbo.Employees AS e2
WHERE e2.DepartmentID = e.DepartmentID);
Output
| FirstName | DepartmentID | Salary |
| Anita | 2 | 180000.00 |
| Arjun | 2 | 100000.00 |
| Kavya | 3 | 110000.00 |
| Vikram | 4 | 190000.00 |
| Kiran | 4 | 95000.00 |
| Meena | 5 | 120000.00 |
| Suresh | 6 | 130000.00 |
| Deepak | 7 | 105000.00 |
Highest paid employee in each department
SELECT e.DepartmentID, e.FirstName, e.Salary
FROM dbo.Employees AS e
WHERE e.Salary = (SELECT MAX(e2.Salary)
FROM dbo.Employees AS e2
WHERE e2.DepartmentID = e.DepartmentID)
ORDER BY e.DepartmentID;
Output
| DepartmentID | FirstName | Salary |
| 1 | Rajesh | 250000.00 |
| 2 | Anita | 180000.00 |
| 3 | Kavya | 110000.00 |
| 4 | Vikram | 190000.00 |
| 5 | Meena | 120000.00 |
| 6 | Suresh | 130000.00 |
| 7 | Deepak | 105000.00 |
Most expensive product in each category
SELECT p.CategoryID, p.ProductName, p.UnitPrice
FROM dbo.Products AS p
WHERE p.UnitPrice = (SELECT MAX(p2.UnitPrice) FROM dbo.Products p2 WHERE p2.CategoryID = p.CategoryID);
Output
| CategoryID | ProductName | UnitPrice |
| 10 | Desk Organizer | 450.00 |
| 9 | Standing Desk | 22000.00 |
| 8 | Ergonomic Office Chair | 12000.00 |
| 6 | 27 inch Monitor | 18000.00 |
| 5 | iPhone 15 | 70000.00 |
| 4 | Lenovo ThinkCentre | 55000.00 |
| 3 | MacBook Air M3 | 120000.00 |
Each customer's Most Recent order
SELECT o.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Orders AS o
WHERE o.OrderDate = (SELECT MAX(o2.OrderDate) FROM dbo.Orders o2 WHERE o2.CustomerID = o.CustomerID)
ORDER BY o.CustomerID;
Output
| CustomerID | OrderID | OrderDate |
| 1 | 75 | 2025-05-21 |
| 2 | 58 | 2025-01-26 |
| 3 | 79 | 2025-06-15 |
| 4 | 69 | 2025-04-12 |
| 5 | 80 | 2025-06-24 |
| 6 | 60 | 2025-02-09 |
| 7 | 36 | 2024-09-01 |
| 8 | 28 | 2024-07-09 |
| 9 | 40 | 2024-09-28 |
| 10 | 61 | 2025-02-17 |
| 11 | 55 | 2025-01-06 |
| 12 | 49 | 2024-11-26 |
| 13 | 72 | 2025-05-02 |
| 14 | 32 | 2024-08-06 |
| 15 | 78 | 2025-06-10 |
... showing 15 of 17 rows
Students who scored above their Class average in Maths
SELECT s.StudentName, s.ClassName, es.Marks
FROM dbo.Students AS s
JOIN dbo.ExamScores AS es ON es.StudentID = s.StudentID AND es.Subject = 'Maths'
WHERE es.Marks > (SELECT AVG(es2.Marks * 1.0)
FROM dbo.ExamScores es2
JOIN dbo.Students s2 ON s2.StudentID = es2.StudentID
WHERE s2.ClassName = s.ClassName AND es2.Subject = 'Maths');
Output
| StudentName | ClassName | Marks |
| Aarav | 10-A | 95 |
| Vihaan | 10-A | 99 |
| Anaya | 10-B | 72 |
| Ishaan | 10-B | 88 |
Nth highest salary with a correlated subquery (N = 3)
"Count how many DISTINCT salaries are bigger than mine. If 2, I am 3rd."
SELECT DISTINCT e.Salary AS ThirdHighestSalary
FROM dbo.Employees AS e
WHERE 2 = (SELECT COUNT(DISTINCT e2.Salary) FROM dbo.Employees e2 WHERE e2.Salary > e.Salary);
Output
| ThirdHighestSalary |
| 180000.00 |
Part B: EXISTS / NOT EXISTS (always correlated)
Employees who are managers (someone reports to them)
SELECT m.EmployeeID, m.FirstName, m.JobTitle
FROM dbo.Employees AS m
WHERE EXISTS (SELECT 1 FROM dbo.Employees e WHERE e.ManagerID = m.EmployeeID);
Output
| EmployeeID | FirstName | JobTitle |
| 1 | Rajesh | CEO |
| 2 | Anita | VP Sales |
| 3 | Vikram | CTO |
| 4 | Meena | HR Manager |
| 5 | Suresh | Finance Manager |
| 6 | Kavya | Marketing Manager |
| 7 | Arjun | Sales Manager |
| 11 | Kiran | Senior Developer |
| 21 | Deepak | Operations Manager |
Customers who ordered a Laptop (CategoryID = 3)
SELECT c.CustomerName
FROM dbo.Customers AS c
WHERE EXISTS (SELECT 1
FROM dbo.Orders o
JOIN dbo.OrderItems oi ON oi.OrderID = o.OrderID
JOIN dbo.Products p ON p.ProductID = oi.ProductID
WHERE o.CustomerID = c.CustomerID AND p.CategoryID = 3);
Output
| CustomerName |
| Ravi Traders |
| Infosoft Solutions |
| Lakshmi Stores |
| Mumbai Mart |
| Pune Tech Park |
| Capital Electronics |
| Bengal Book House |
| Deccan Systems |
| Pink City Furnishers |
| Kerala Kart |
| Awadh Enterprises |
| Silicon Valley Pvt Ltd |
| Gateway Retail |
| Marina Digital |
| Northern Distributors |
... showing 15 of 16 rows
Sales people who have No order in 2025
SELECT e.FirstName
FROM dbo.Employees AS e
WHERE e.DepartmentID = 2
AND NOT EXISTS (SELECT 1 FROM dbo.Orders o
WHERE o.EmployeeID = e.EmployeeID AND o.OrderDate >= '2025-01-01');
Orders where Every item had a discount (relational division style)
SELECT o.OrderID
FROM dbo.Orders AS o
WHERE NOT EXISTS (SELECT 1 FROM dbo.OrderItems oi WHERE oi.OrderID = o.OrderID AND oi.Discount = 0)
AND EXISTS (SELECT 1 FROM dbo.OrderItems oi WHERE oi.OrderID = o.OrderID);
Output
| OrderID |
| 2 |
| 5 |
| 9 |
| 11 |
| 18 |
| 20 |
| 21 |
| 26 |
| 28 |
| 36 |
| 38 |
| 41 |
| 52 |
| 53 |
| 58 |
... showing 15 of 21 rows
Part C: Correlated Subquery in SELECT
Number of orders and last order date per customer
SELECT c.CustomerName,
(SELECT COUNT(*) FROM dbo.Orders o WHERE o.CustomerID = c.CustomerID) AS TotalOrders,
(SELECT MAX(o.OrderDate) FROM dbo.Orders o WHERE o.CustomerID = c.CustomerID) AS LastOrderDate
FROM dbo.Customers AS c;
Output
| CustomerName | TotalOrders | LastOrderDate |
| Ravi Traders | 11 | 2025-05-21 |
| Infosoft Solutions | 6 | 2025-01-26 |
| Lakshmi Stores | 13 | 2025-06-15 |
| Mumbai Mart | 4 | 2025-04-12 |
| Pune Tech Park | 8 | 2025-06-24 |
| Capital Electronics | 2 | 2025-02-09 |
| Bengal Book House | 2 | 2024-09-01 |
| Deccan Systems | 3 | 2024-07-09 |
| Gujarat Office Supply | 3 | 2024-09-28 |
| Pink City Furnishers | 4 | 2025-02-17 |
| Kerala Kart | 3 | 2025-01-06 |
| Awadh Enterprises | 5 | 2024-11-26 |
| Silicon Valley Pvt Ltd | 3 | 2025-05-02 |
| Gateway Retail | 1 | 2024-08-06 |
| Marina Digital | 6 | 2025-06-10 |
... showing 15 of 20 rows
Number of direct reports for each employee
SELECT e.FirstName, e.JobTitle,
(SELECT COUNT(*) FROM dbo.Employees r WHERE r.ManagerID = e.EmployeeID) AS DirectReports
FROM dbo.Employees AS e
ORDER BY DirectReports DESC;
Output
| FirstName | JobTitle | DirectReports |
| Rajesh | CEO | 5 |
| Arjun | Sales Manager | 4 |
| Kiran | Senior Developer | 4 |
| Vikram | CTO | 3 |
| Anita | VP Sales | 2 |
| Suresh | Finance Manager | 2 |
| Kavya | Marketing Manager | 2 |
| Deepak | Operations Manager | 1 |
| Meena | HR Manager | 1 |
| Pooja | Sales Executive | 0 |
| Rahul | Sales Executive | 0 |
| Sneha | Sales Executive | 0 |
| Divya | Developer | 0 |
| Amit | Developer | 0 |
| Neha | Data Analyst | 0 |
... showing 15 of 25 rows
"Point in time" lookup: what was the product price on the order date?
SELECT o.OrderID, o.OrderDate, oi.ProductID,
(SELECT TOP (1) h.Price
FROM dbo.ProductPriceHistory h
WHERE h.ProductID = oi.ProductID AND h.EffectiveFrom <= o.OrderDate
ORDER BY h.EffectiveFrom DESC) AS PriceOnOrderDate,
oi.UnitPrice AS PriceCharged
FROM dbo.Orders o
JOIN dbo.OrderItems oi ON oi.OrderID = o.OrderID
WHERE oi.ProductID IN (1, 3, 7)
ORDER BY oi.ProductID, o.OrderDate;
Output
| OrderID | OrderDate | ProductID | PriceOnOrderDate | PriceCharged |
| 3 | 2024-01-26 | 1 | 60000.00 | 65000.00 |
| 14 | 2024-04-05 | 1 | 60000.00 | 65000.00 |
| 31 | 2024-07-30 | 1 | 62000.00 | 65000.00 |
| 41 | 2024-10-06 | 1 | 62000.00 | 65000.00 |
| 43 | 2024-10-20 | 1 | 62000.00 | 65000.00 |
| 51 | 2024-12-11 | 1 | 62000.00 | 65000.00 |
| 56 | 2025-01-13 | 1 | 65000.00 | 65000.00 |
| 68 | 2025-04-02 | 1 | 65000.00 | 65000.00 |
| 1 | 2024-01-09 | 3 | 115000.00 | 120000.00 |
| 6 | 2024-02-14 | 3 | 115000.00 | 120000.00 |
| 9 | 2024-03-06 | 3 | 115000.00 | 120000.00 |
| 14 | 2024-04-05 | 3 | 115000.00 | 120000.00 |
| 20 | 2024-05-17 | 3 | 115000.00 | 120000.00 |
| 21 | 2024-05-22 | 3 | 115000.00 | 120000.00 |
| 23 | 2024-06-05 | 3 | 118000.00 | 120000.00 |
... showing 15 of 38 rows
Part D: Correlated UPDATE and DELETE
DROP TABLE IF EXISTS dbo.CustomerSummary;
CREATE TABLE dbo.CustomerSummary
(
CustomerID INT PRIMARY KEY,
TotalOrders INT NULL,
TotalSpent DECIMAL(14,2) NULL
);
INSERT INTO dbo.CustomerSummary (CustomerID) SELECT CustomerID FROM dbo.Customers;
Messages(20 rows affected)
Correlated UPDATE: fill summary columns
UPDATE cs
SET TotalOrders = (SELECT COUNT(*) FROM dbo.Orders o WHERE o.CustomerID = cs.CustomerID),
TotalSpent = (SELECT ISNULL(SUM(oi.LineTotal), 0)
FROM dbo.Orders o JOIN dbo.OrderItems oi ON oi.OrderID = o.OrderID
WHERE o.CustomerID = cs.CustomerID)
FROM dbo.CustomerSummary AS cs;
SELECT * FROM dbo.CustomerSummary ORDER BY TotalSpent DESC;
Messages(20 rows affected)
Output
| CustomerID | TotalOrders | TotalSpent |
| 3 | 13 | 1925592.50 |
| 5 | 8 | 1649975.00 |
| 1 | 11 | 1527910.00 |
| 15 | 6 | 1326609.00 |
| 2 | 6 | 1163225.00 |
| 12 | 5 | 673885.00 |
| 11 | 3 | 644925.00 |
| 13 | 3 | 574857.00 |
| 17 | 4 | 548650.00 |
| 10 | 4 | 536100.00 |
| 14 | 1 | 476850.00 |
| 4 | 4 | 365735.00 |
| 16 | 2 | 275200.00 |
| 7 | 2 | 259075.00 |
| 8 | 3 | 220310.00 |
... showing 15 of 20 rows
Correlated DELETE: remove summary rows of customers with no orders
DELETE cs
FROM dbo.CustomerSummary AS cs
WHERE NOT EXISTS (SELECT 1 FROM dbo.Orders o WHERE o.CustomerID = cs.CustomerID);
SELECT COUNT(*) AS RemainingRows FROM dbo.CustomerSummary;
Messages(3 rows affected)
Performance Note
- Correlated subqueries in SELECT run per row -> can be slow on big tables.
- The optimizer often rewrites EXISTS into a semi join (fast).
- Alternatives: JOIN + GROUP BY, window functions (file 08), or OUTER APPLY (file 13).
PRACTICE QUESTIONS 1. Find products priced below the average price of their own category. 2. For each department show the newest employee (latest HireDate). 3. Find customers whose every order is 'Delivered'. 4. Show each order with the number of items in it, using a correlated subquery. 5. Find employees whose salary is the lowest in their city.