SQL EXISTS Use Cases and Examples

Problem

I’ve seen the SQL EXISTS keyword in Microsoft SQL Server T-SQL code and don’t understand it well. What does it do? How do I use it? Are there best practices around SQL IF EXISTS?

This SQL tutorial will explain what the keyword EXISTS does and show several different use cases.

Solution

The EXISTS keyword is a Boolean function that returns either true or false. Since it is a function, it expects a parameter within a set of parentheses (…). The general syntax is: EXISTS ( subquery ).

The subquery can be a restricted SELECT statement, where the INTO keyword is not allowed. The single parameter accepted by EXISTS is a SELECT statement. The function returns TRUE if the SELECT statement parameter returns at least 1 row and FALSE if exactly 0 rows are returned.

EXISTS is used as an argument in IF statements, WHILE loops, and WHERE clauses. While it can be used in JOIN predicates, this is exceedingly rare.

The demos in this tip utilize the WideWorldImporters sample SQL database, which can be downloaded for free from Github. All demos are shown using SQL Server Management Studio (SSMS) and SQL Server 2022, but the information in this tip is valid going back multiple versions of SQL Server as well as Azure SQL Database and Managed Instance, Azure Synapse Analytics, SQL analytics endpoint in Microsoft Fabric, Warehouse in Microsoft Fabric, and SQL database in Microsoft Fabric.

Calling the EXISTS Function

Consider this SELECT statement. It should return at least four rows on most SQL Server installations and perhaps two rows on Azure DBaaS instances. It should never return 0 rows.

SELECT * FROM sys.databases

SQL EXISTS Logic

Since we know that statement contains several rows, what happens if we paste that query within the parameter section of an EXISTS function? Notice that since the EXISTS function naturally returns a Boolean value (true or false), there is no need for a comparison operator. There is no reason to include an “= 1” or “= TRUE” in the argument.

IF EXISTS(SELECT 1 FROM sys.databases) 
  PRINT 'EXISTS evaluated to true' 
ELSE 
  PRINT 'EXISTS evaluated to false'

We would expect the output to be something like this in every SQL query.

SQL EXISTS output as True

What if the SELECT statement returned exactly one row? This script should return the same as above since the EXISTS function considers one row or 1 million rows as TRUE. Only queries that produce 0 rows produce a FALSE.

IF EXISTS(SELECT 1 FROM sys.databases WHERE name = 'master') 
  PRINT 'EXISTS evaluated to true' 
ELSE 
  PRINT 'EXISTS evaluated to false'

This is an example of EXISTS with a query that returns 0 rows. It is a perfectly valid query that produces an empty result set. For the first time, we see the FALSE output.

IF EXISTS(SELECT 1 FROM sys.databases WHERE database_id = -1) 
  PRINT 'EXISTS evaluated to true' 
ELSE 
  PRINT 'EXISTS evaluated to false'
SQL EXISTS output as False

SQL EXISTS in a Query

Now consider this query. On my system, this returns exactly one row and exactly one column that has a NULL value.

SELECT source_database_id FROM sys.databases WHERE name = 'master';

What will SQL IF EXISTS do with this? SQL IF EXISTS is not concerned with the number of columns or the value of any column. It is only interested in the existence or lack thereof of any rows. As such, this will also be evaluated to be true.

IF EXISTS(SELECT 1 FROM sys.databases WHERE name = 'master') 
  PRINT 'EXISTS evaluated to true' 
ELSE 
  PRINT 'EXISTS evaluated to false'

EXISTS makes it possible to use the following query to return customers who have at least one order:

SELECT CustomerID, CustomerName
FROM Sales.Customers c
WHERE EXISTS (
    SELECT 1 
    FROM Sales.Orders o 
    WHERE o.CustomerID = c.CustomerID
);

Therefore, these queries in the WHERE clause are equivalent to one another and will produce the same result:

WHERE EXISTS (SELECT 1 FROM Sales.Orders o WHERE o.CustomerID = c.CustomerID) 
WHERE EXISTS (SELECT NULL FROM Sales.Orders o WHERE o.CustomerID = c.CustomerID) 
WHERE EXISTS (SELECT * FROM Sales.Orders o WHERE o.CustomerID = c.CustomerID)

The SELECT 1 variant is most common because it clearly represents the intent of checking for general existence, not so much return specifics rows and/or columns.

Next, we will look at a stored procedure. This may not evaluate on Azure DBaaS but should work on any full instance of SQL Server.

exec msdb.dbo.sp_help_job;

This returns a long list of SQL Server Agent jobs on my laptop.

query output

What will SQL IF EXISTS think of this? In short, it won’t like this at all. Attempting to run this query will result in a syntax error. EXISTS expects a SELECT statement, not just a T-SQL statement, that could potentially return rows.

SQL EXISTS with IF and ELSE logic with error message

Finally, what about a variable? Once again, this produces a syntax error. You cannot simply send a variable to the SQL IF EXISTS function.

SQL EXISTS with IF logic error message

I’m not sure what the use case would be here, but putting SELECT in front of the variable returns a single-row result set and causes this script to return TRUE.

DECLARE @MSSQLTips INT;
IF EXISTS(SELECT @MSSQLTips) 
  PRINT 'EXISTS evaluated to true' 
ELSE 
  PRINT 'EXISTS evaluated to false'

Using EXISTS instead of INNER JOIN

One of the great benefits of EXISTS is using it for existence checks, which is often more efficient than using JOIN. Let us consider these couple of queries that will produce an identical result set:

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT 
       SI.InvoiceID
     , SI.CustomerID
FROM Sales.Invoices SI
WHERE EXISTS (
    SELECT 1
      FROM Sales.Customers SC
     WHERE SC.CustomerID = SI.CustomerID
       AND SC.CustomerCategoryID = 4
);
SELECT DISTINCT
           SI.InvoiceID
           , SI.CustomerID
      FROM Sales.Invoices SI
INNER JOIN Sales.Customers SC ON SI.CustomerID = SC.CustomerID
     WHERE SC.CustomerCategoryID = 4;

Running the queries together with the execution plan enabled, we can see the difference clearly. The first query with EXISTS is more efficient having a query cost of 46% vs 54 % for the JOIN query:

query and execution plan

If you examine the execution plans in detail you will also see a heavier index utilization from the EXISTS subquery. Therefore, it is important to have your tables properly indexed to fully benefit from the EXISTS statement. 

Using NOT EXISTS instead of LEFT JOIN

In a typical LEFT JOIN scenario, we want to find records where the related records don’t exist. Therefore, using NOT EXISTS is a particularly suitable alternative in cases where we are not interested in showing the non-existing records. Let us consider the following couple of queries. We are interested in customers without invoices:

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT 
        C.CustomerID
      , C.CustomerName
      , C.PhoneNumber
      , C.CreditLimit
 FROM Sales.Customers C
WHERE NOT EXISTS (
   SELECT 1
     FROM Sales.Invoices I
    WHERE I.CustomerID = C.CustomerID
);
SELECT 
          C.CustomerID
        , C.CustomerName
        , C.PhoneNumber
        , C.CreditLimit
     FROM Sales.Customers C
LEFT JOIN Sales.Invoices I ON I.CustomerID = C.CustomerID
    WHERE I.CustomerID IS NULL;

While both queries produce an empty result set, the performance gain from is noticeable: 26% relative query cost for NOT EXISTS vs 74% relative query cost for LEFT JOIN:

query and execution plan

In both cases when comparing (NOT) EXISTS to (LEFT) JOIN the EXISTS clause wins. In general, that happens because the evaluation stops as soon as a match is found. The JOIN should be used mainly for cases when aggregation results with zero must be shown, or data from all joined tables must be shown.

SQL EXISTS in WHILE Loops

Each of the prior examples shows EXISTS as part of an IF statement. It is also common to use them in WHILE loops. This method is an easy way to perform one task per row in a query without using a cursor. While this code is easy to follow and understand, it should not replace set-based coding. If the action to be completed per row can be done with an insert/update/delete statement, then that would be preferred.

This example calls a stored procedure, sp_send_dbmail, from MSDB once per row.

--!!!! Be careful running this script!!!! 
-- It does attempt to call sp_send_dbmail. --
 
--From MSSQLTips.com
DECLARE @CustomerID INT;
DECLARE @Email NVARCHAR(256);
 
WHILE EXISTS(SELECT 1
             FROM Sales.Customers
             WHERE IsStatementSent = 0)
             
BEGIN 
  SELECT TOP 1 
    @CustomerID = CustomerID
  , @Email = EmailAddress
  FROM 
    Sales.Customers SC
      INNER JOIN [Application].People P -- SQL JOIN
    ON SC.PrimaryContactPersonID = P.PersonID
  WHERE 
    IsStatementSent = 0;
 
  EXEC msdb.dbo.sp_send_dbmail @recipients = @Email, @subject = 'Please pay us';
 
  UPDATE Sales.Customers -- UPDATE statement
  SET IsStatementSent = 1
  WHERE CustomerID = @CustomerID;
 
END;

EXISTS in a WHERE Clause

Using an EXISTS function call in a WHERE clause is probably the most common use case. The function will work exactly the same as in each earlier example, but there is one noticeable change. The subquery will almost always reference a column in a table that is otherwise out of the scope of the subquery. This can be difficult to understand, so we will jump right into an example.

SELECT *
FROM Sales.Invoices SI
WHERE EXISTS(SELECT 1
             FROM Sales.Customers SC
             WHERE SC.CustomerCategoryID = 4
              AND SC.CustomerID = SI.CustomerID);

In this example, the subquery has only one table listed in the FROM clause, Sales.Customers, aliased as SC. Yet the WHERE clause of that subquery references a column, SI.CustomerID, that could only come from the main query outside of the parentheses. This can initially seem counter-intuitive, but this is exactly how it should work.

While the optimizer treats it differently, you can think of it as calling that subquery repeatedly — once for every row in the main query. An EXISTS function call in a WHERE clause that did not include a reference to the primary query would only have to run once and would either evaluate as true or false for the entire rowset of the primary query.

When to use EXISTS in WHERE Clauses

The example query above could easily have been written with an IN statement, as seen below. In fact, modern SQL Server engines will usually compile a query like this (with IN) and the one above (with EXISTS) exactly the same way. Deciding between the two is largely a style-based decision.

SELECT *
FROM Sales.Invoices SI
WHERE SI.CustomerID IN (SELECT SC.CustomerID
             FROM Sales.Customers SC
             WHERE SC.CustomerCategoryID = 4);

In that demo query, there is only one matching column between the primary and subqueries, so replacing EXISTS with an IN statement is pretty easy. However, if the comparison includes more than one shared column, the IN statement won’t work, as seen in the following query.

SELECT *
FROM Sales.Invoices SI
WHERE EXISTS(SELECT *
             FROM Sales.Customers SC
             WHERE SC.CustomerCategoryID = 4
              AND SC.CustomerID = SI.CustomerID
              AND SC.DeliveryMethodID = SI.DeliveryMethodID);

A query such as this is a great example where EXISTS makes sense.

Additionally, we would prefer using EXISTS over COUNT(*) for straightforward existence checks:

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT 
        C.CustomerID
      , C.CustomerName
 FROM Sales.Customers C
WHERE (
    SELECT COUNT(*) 
      FROM Sales.Orders O 
     WHERE O.CustomerID = C.CustomerID) > 0;
SELECT 
     C.CustomerID
     , C.CustomerName
FROM Sales.Customers C
WHERE EXISTS (
   SELECT 1 
     FROM Sales.Orders O 
    WHERE O.CustomerID = C.CustomerID
);

However, closely examining the query plan will indicate an equal cost of both queries, as well as identical query plans:

query and execution plan

What happens is that the query optimizer is smart enough to prevent the COUNT operation in this case. While the “Stream Aggregate” operation associated with the COUNT clause exists in the plan, its cost is zero percent. Therefore, the benefit of using EXISTS over COUNT(*) may be more apparent with tables containing more data (the Sales.Orders table contains only around seventy thousand rows).

Good practices when using (NOT) EXISTS

To wrap it up, here are some good practices to keep in mind when using EXISTS:

  • Use SELECT 1 or SELECT NULL in the subquery. The actual selected value is irrelevant because we are doing an existence check and do not want any specific result set retrieved.
  • Ensure proper indexing on the correlated columns. The columns in your WHERE clause that correlate the outer and inner queries (e.g., WHERE o.CustomerID = c.CustomerID) should be indexed. Without proper indexes, EXISTS queries can perform full table scans, and you will forego any performance benefits.
  • Prefer EXISTS/NOT EXISTS over IN/NOT IN for correlated subqueries. EXISTS stops the evaluation at the first match and often generates better execution plans.
  • Use NOT EXISTS instead of LEFT JOIN…IS NULL for anti-joins when you only need to filter rows, not retrieve data from both tables.
  • Avoid chaining too many EXISTS statements. While there is no explicit limit, code readability should be a priority.
  • Consider using EXISTS instead of COUNT in scenarios where the result from COUNT is irrelevant and you are only checking if there exists more than one row matching a condition.

Frequently Asked Questions

Does the EXISTS function short-circuit the queries if possible?

Yes!  The EXISTS function will stop searching when a row is found (Learn how to get read counts for SQL Server queries).  You can prove that to yourself by running this query.  On my laptop, the first query takes 44 reads.  When that identical query is run again as part of an EXISTS, it takes only 2 reads – indicating that SQL Server stopped looking for data after the first row was returned.

SET STATISTICS IO ON;
 
SELECT CustomerID FROM Sales.Orders WHERE CustomerID > 900;
 
IF EXISTS (SELECT CustomerID FROM Sales.Orders WHERE CustomerID > 900)
BEGIN
  SELECT ‘EXISTS RETURNS TRUE’;
END;

Does EXISTS work in other RDBMS such as Oracle, PostgreSQL, or MySQL?

Yes.  EXISTS is a standard function in all of these platforms.

What if the query inside the EXISTS returns only a NULL value?

Exists only cares if there is a row, not the value(s) in that row.  Even if the only value returned is NULL, that will still evaluate to true.  This very simple query will prove that point.

IF EXISTS (SELECT NULL)
BEGIN
  SELECT ‘EXISTS RETURNS TRUE’;
END;

Can a subquery in an EXISTS reference columns from the main query?

Yes.  This is a very common use case.  In fact, most of your EXISTS function calls that are part of a query will use this pattern.  There are several examples of this in the tip above.  The concept of a subquery referencing columns from outside of the parentheses can be difficult for new SQL professionals to understand. 

Learn more about subqueries on MSSQLTips.com!
SQL Server Subquery Examples
SQL Server Subquery Use Cases

What if my subquery in an EXISTS joins on a column that isn’t indexed?

SQL Server will still run this query but expect poor performance – especially if the table is sizable. 

Learn more about how to determine if this is happening by reviewing the query plan.
How to view your query plan
How to read your query plan
How to use a plan to tune a query

If I am concerned about creating an infinite loop with a WHILE EXISTS(), how can I mitigate that?

The most common method is to declare a counter variable and increment it with every execution.  Then change the WHILE statement to break when either the EXISTS fails to find a row or the counter hits a maximum number of executions.  Here is an example.

DECLARE @Counter INT = 0;
 
WHILE EXISTS (SELECT 1 FROM dbo.MyQueue WHERE ProcessedFlag = 0) AND @Counter < 50
BEGIN
  SELECT ‘Do Some Work’;
  SET @Counter += 1;
END;

What if the query in my EXISTS returns a lot of rows?  Will that affect performance?

Upon determining the existence of the first row, the EXISTS function will stop looking for more rows and return a TRUE value.  It doesn’t matter if the query would eventually return only that 1 row or 1,000,000 rows.

Final Thoughts

EXISTS is a more advanced function that not every T-SQL writer uses. But knowing what it is and how it works is still an important step in the journey to becoming an expert in the field.

Next Steps

Updates also added by Hristo Hristov.

Leave a Reply

Your email address will not be published. Required fields are marked *