EXISTS and NOT EXISTS
Ask “is there at least one matching row?”: EXISTS and NOT EXISTS, how they compare with IN and joins, and the NULL trap that makes NOT IN return nothing at all.
EXISTS (subquery) is true if the subquery returns at least one row, and false if it returns none. It does not care what the rows contain. Which customers have placed an order?
SELECT c.CustomerId, c.FirstNameFROM dbo.Customers AS cWHERE EXISTS ( SELECT 1 FROM dbo.Orders AS o WHERE o.CustomerId = c.CustomerId)ORDER BY c.CustomerId;CustomerId FirstName
---------- ---------
1 Asha
2 Ben
3 Chloe
4 Dev
6 Farid
8 ZoëThe subquery is correlated: o.CustomerId = c.CustomerIdties it to the customer being tested.SELECT 1is a convention. The select list of an EXISTS subquery is never used, soSELECT *orSELECT 1behave the same.SQL Server can stop looking as soon as it finds the first matching row. Asha has two orders, but one is enough.
NOT EXISTS: rows with no match
NOT EXISTS keeps the rows for which the subquery finds nothing. Customers who have never ordered:
SELECT c.CustomerId, c.FirstName, c.JoinedOnFROM dbo.Customers AS cWHERE NOT EXISTS ( SELECT 1 FROM dbo.Orders AS o WHERE o.CustomerId = c.CustomerId);CustomerId FirstName JoinedOn
---------- --------- ----------
5 Ella 2023-09-14
7 Grace 2024-01-08Conditions inside the subquery
SELECT c.FirstName, c.CityFROM dbo.Customers AS cWHERE EXISTS ( SELECT 1 FROM dbo.Orders AS o JOIN dbo.OrderItems AS oi ON oi.OrderId = o.OrderId JOIN dbo.Products AS p ON p.ProductId = oi.ProductId WHERE o.CustomerId = c.CustomerId AND o.Status = 'Shipped' AND p.CategoryId = 3);FirstName City --------- -------- Dev Bradford Farid York
SELECT c.FirstNameFROM dbo.Customers AS cWHERE EXISTS (SELECT 1 FROM dbo.Orders AS o WHERE o.CustomerId = c.CustomerId) AND NOT EXISTS (SELECT 1 FROM dbo.Orders AS o WHERE o.CustomerId = c.CustomerId AND o.Status = 'Pending')ORDER BY c.FirstName;FirstName --------- Asha Chloe Dev Farid
EXISTS or a join?
SELECT c.FirstNameFROM dbo.Customers AS cJOIN dbo.Orders AS o ON o.CustomerId = c.CustomerIdWHERE o.Status = 'Shipped'ORDER BY c.FirstName; SELECT c.FirstNameFROM dbo.Customers AS cWHERE EXISTS (SELECT 1 FROM dbo.Orders AS o WHERE o.CustomerId = c.CustomerId AND o.Status = 'Shipped')ORDER BY c.FirstName;FirstName --------- Asha Asha Ben Dev Dev Farid Farid FirstName --------- Asha Ben Dev Farid
The NOT IN trap
NOT IN (subquery) looks like it should do the same as NOT EXISTS, and with BuildMart’s data it does. Which categories have no products?
SELECT NameFROM dbo.CategoriesWHERE CategoryId NOT IN (SELECT CategoryId FROM dbo.Products);Name -------- Plumbing
INSERT INTO dbo.Products (Name, CategoryId, Price, Stock, Unit, AddedOn)VALUES (N'Gift voucher', NULL, 25.00, 100, N'each', '2024-07-01'); SELECT NameFROM dbo.CategoriesWHERE CategoryId NOT IN (SELECT CategoryId FROM dbo.Products);Name ----
7 NOT IN (1, 2, 3, 4, 5, 6, NULL) means 7 <> 1 AND 7 <> 2 AND … AND 7 <> NULL. The last test is unknown, and an AND with an unknown part is never true. One NULL in the list makes NOT IN fail for every row.
INSERT INTO dbo.Products (Name, CategoryId, Price, Stock, Unit, AddedOn)VALUES (N'Gift voucher', NULL, 25.00, 100, N'each', '2024-07-01'); SELECT c.NameFROM dbo.Categories AS cWHERE NOT EXISTS (SELECT 1 FROM dbo.Products AS p WHERE p.CategoryId = c.CategoryId);Name -------- Plumbing
Show a hint
Show the solution
SELECT p.Name, p.StockFROM dbo.Products AS pWHERE NOT EXISTS ( SELECT 1 FROM dbo.OrderItems AS oi JOIN dbo.Orders AS o ON o.OrderId = oi.OrderId WHERE oi.ProductId = p.ProductId AND o.Status = 'Shipped')ORDER BY p.Name;Name Stock ---------------- ----- Safety goggles 0 Wall plugs (100) 0
Remember
EXISTS is true if the subquery returns at least one row; its select list is ignored. NOT EXISTS finds rows with no match (an anti-join). EXISTS returns each outer row once; a join repeats it for every match. One NULL in a NOT IN list makes it return no rows at all. Prefer NOT EXISTS, or filter NULLs out of the NOT IN subquery.