<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. SQL
  3. Grouping and subqueries
All lessons in this course

Lesson 27 of 54 14 minIntermediate

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?

Query.sql
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;
Output
CustomerId  FirstName
----------  ---------
         1  Asha
         2  Ben
         3  Chloe
         4  Dev
         6  Farid
         8  Zoë
  • The subquery is correlated: o.CustomerId = c.CustomerId ties it to the customer being tested.
  • SELECT 1 is a convention. The select list of an EXISTS subquery is never used, so SELECT * or SELECT 1 behave the same.
  • SQL Server can stop looking as soon as it finds the first matching row. Asha has two orders, but one is enough.
Interactive

Is there a matching product?

SELECT c.Name
FROM dbo.Categories AS c
WHERE EXISTS (
    SELECT 1 FROM dbo.Products AS p
    WHERE p.CategoryId = c.CategoryId
);
Timber: scanning Products for CategoryId = 3
1 cat 12 cat 13 cat 14 cat 25 cat 26 cat 37 cat 38 cat 49 cat 410 cat 511 cat 512 cat 6
Found a match at product 6 after checking 6 rows, and can stop there. EXISTS is true: Timber is returned.
Name
1Tools
2Building
3Timber
4Fixings
5Paint
6Safety
EXISTS keeps every category with at least one product. Each category is returned once, however many products match, and the scan can stop at the first match.

NOT EXISTS: rows with no match

NOT EXISTS keeps the rows for which the subquery finds nothing. Customers who have never ordered:

Query.sql
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);
Output
CustomerId  FirstName  JoinedOn
----------  ---------  ----------
         5  Ella       2023-09-14
         7  Grace      2024-01-08

This is called an anti-join. It gives the same answer as the LEFT JOIN … WHERE key IS NULL pattern from the outer joins lesson, and says what you mean more directly.

Conditions inside the subquery

The subquery can be as detailed as you need. Customers who have a shipped order containing Timber:

Query.sql
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);
Output
FirstName  City
---------  --------
Dev        Bradford
Farid      York

And EXISTS and NOT EXISTS combine with AND and OR like any other test. Customers who have ordered, but have nothing pending:

Query.sql
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;
Output
FirstName
---------
Asha
Chloe
Dev
Farid

EXISTS or a join?

A join returns one row for every match, so a customer with two matching orders appears twice. EXISTS (a semi-join) returns each outer row at most once:

Query.sql
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;
Output
FirstName
---------
Asha
Asha
Ben
Dev
Dev
Farid
Farid

FirstName
---------
Asha
Ben
Dev
Farid

Adding DISTINCT to the join would hide the duplicates, but it does extra work to remove rows that EXISTS never makes. Use a join when you need columns from the other table; use EXISTS when you only need to know whether a match exists.

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?

Query.sql
SELECT NameFROM dbo.CategoriesWHERE CategoryId NOT IN (SELECT CategoryId FROM dbo.Products);
Output
Name
--------
Plumbing

Now add a product with no category (a gift voucher, CategoryId NULL) and run the same query again:

Query.sql
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);
Output
Name
----

Nothing at all. 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.

Query.sql
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);
Output
Name
--------
Plumbing

NOT EXISTS only asks whether a matching row exists. The voucher’s NULL matches no category, so it simply does not count.

Quick check

The list returned by a subquery is (1, 2, NULL). Which outer value makes x NOT IN (subquery) true?

Quick check

Why is SELECT 1 used inside EXISTS?

Practice

List the products (name and stock) that have never been sold on a shipped order. Use NOT EXISTS.

Show a hint

Inside NOT EXISTS, join OrderItems to Orders and match oi.ProductId = p.ProductId AND o.Status = 'Shipped'.

Show the solution
Solution.sql
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;
Output
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.
0 of 54 lessons completed