Skip to content

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
FirstNameDepartmentIDSalary
Anita2180000.00
Arjun2100000.00
Kavya3110000.00
Vikram4190000.00
Kiran495000.00
Meena5120000.00
Suresh6130000.00
Deepak7105000.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
DepartmentIDFirstNameSalary
1Rajesh250000.00
2Anita180000.00
3Kavya110000.00
4Vikram190000.00
5Meena120000.00
6Suresh130000.00
7Deepak105000.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
CategoryIDProductNameUnitPrice
10Desk Organizer450.00
9Standing Desk22000.00
8Ergonomic Office Chair12000.00
627 inch Monitor18000.00
5iPhone 1570000.00
4Lenovo ThinkCentre55000.00
3MacBook Air M3120000.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
CustomerIDOrderIDOrderDate
1752025-05-21
2582025-01-26
3792025-06-15
4692025-04-12
5802025-06-24
6602025-02-09
7362024-09-01
8282024-07-09
9402024-09-28
10612025-02-17
11552025-01-06
12492024-11-26
13722025-05-02
14322024-08-06
15782025-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
StudentNameClassNameMarks
Aarav10-A95
Vihaan10-A99
Anaya10-B72
Ishaan10-B88

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
EmployeeIDFirstNameJobTitle
1RajeshCEO
2AnitaVP Sales
3VikramCTO
4MeenaHR Manager
5SureshFinance Manager
6KavyaMarketing Manager
7ArjunSales Manager
11KiranSenior Developer
21DeepakOperations 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');
Output
FirstName
Anita

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
CustomerNameTotalOrdersLastOrderDate
Ravi Traders112025-05-21
Infosoft Solutions62025-01-26
Lakshmi Stores132025-06-15
Mumbai Mart42025-04-12
Pune Tech Park82025-06-24
Capital Electronics22025-02-09
Bengal Book House22024-09-01
Deccan Systems32024-07-09
Gujarat Office Supply32024-09-28
Pink City Furnishers42025-02-17
Kerala Kart32025-01-06
Awadh Enterprises52024-11-26
Silicon Valley Pvt Ltd32025-05-02
Gateway Retail12024-08-06
Marina Digital62025-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
FirstNameJobTitleDirectReports
RajeshCEO5
ArjunSales Manager4
KiranSenior Developer4
VikramCTO3
AnitaVP Sales2
SureshFinance Manager2
KavyaMarketing Manager2
DeepakOperations Manager1
MeenaHR Manager1
PoojaSales Executive0
RahulSales Executive0
SnehaSales Executive0
DivyaDeveloper0
AmitDeveloper0
NehaData Analyst0
... 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
OrderIDOrderDateProductIDPriceOnOrderDatePriceCharged
32024-01-26160000.0065000.00
142024-04-05160000.0065000.00
312024-07-30162000.0065000.00
412024-10-06162000.0065000.00
432024-10-20162000.0065000.00
512024-12-11162000.0065000.00
562025-01-13165000.0065000.00
682025-04-02165000.0065000.00
12024-01-093115000.00120000.00
62024-02-143115000.00120000.00
92024-03-063115000.00120000.00
142024-04-053115000.00120000.00
202024-05-173115000.00120000.00
212024-05-223115000.00120000.00
232024-06-053118000.00120000.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
CustomerIDTotalOrdersTotalSpent
3131925592.50
581649975.00
1111527910.00
1561326609.00
261163225.00
125673885.00
113644925.00
133574857.00
174548650.00
104536100.00
141476850.00
44365735.00
162275200.00
72259075.00
83220310.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)
Output
RemainingRows
17

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.