In this in-depth guide, I will break down everything you need to know about SQL COUNT DISTINCT. We will explore exact syntax patterns, structural mechanics, multi-column workarounds, execution costs, and platform-specific implementations.
SQL COUNT DISTINCT
What Is SQL COUNT DISTINCT and How Does It Work?
In Structured Query Language (SQL), the standard COUNT() function is an aggregate function that returns the total number of rows matching a specific criterion. By default, COUNT(column_name) counts every non-null entry, including duplicates.
When you inject the DISTINCT keyword inside the aggregate function—COUNT(DISTINCT column_name)—you instruct the database query engine to eliminate duplicate values from the evaluation set before computing the final tally.
Key Conceptual Differences
| Aggregate Expression | What It Evaluates | Duplicate Handling | NULL Handling |
COUNT(*) | Total number of rows in the result set | Retains all duplicates | Counts rows containing NULL |
COUNT(column_name) | Total non-null values in the specified column | Retains all duplicates | Ignores / Excludes NULL |
COUNT(DISTINCT column_name) | Total unique non-null values in the column | Deduplicates before counting | Ignores / Excludes NULL |
Basic Syntax and Core Mechanics
The foundational ANSI SQL syntax for COUNT DISTINCT follows this standard pattern:
SQL
SELECT
COUNT(DISTINCT column_name) AS unique_count
FROM
table_name
WHERE
filter_conditions;Example: Retail Customer Orders
To illustrate how the database engine processes this operation, consider a mock transactional table named CustomerOrders representing purchases across various US fulfillment hubs:
| OrderID | CustomerName | State | OrderAmount |
| 1001 | Michael Carter | Texas | $120.00 |
| 1002 | Emily Davis | California | $45.50 |
| 1003 | Michael Carter | Texas | $89.00 |
| 1004 | Sarah Jenkins | New York | $210.00 |
| 1005 | Emily Davis | California | $64.00 |
| 1006 | Robert Taylor | Florida | $150.00 |
| 1007 | Michael Carter | Texas | $35.00 |
Total Orders vs. Unique Customers
If you want to compare the total volume of transactions against the actual unique customer count:
SQL
SELECT
COUNT(OrderID) AS total_transactions,
COUNT(CustomerName) AS non_null_customers,
COUNT(DISTINCT CustomerName) AS unique_customers
FROM
CustomerOrders;Expected Output as shown below:
After executing the above query, I got the expected output as shown in the screenshot below.

Breakdown of Output:
total_transactions(7): Evaluates every individual order record.unique_customers(4): The query engine groupsMichael Carter,Emily Davis,Sarah Jenkins, andRobert Taylor, discarding the repeat transactions from Michael and Emily.
How SQL COUNT DISTINCT Handles NULL Values
A critical point of failure in data reporting involves misunderstandings around how COUNT(DISTINCT column_name) handles NULL records.
Crucial Rule: In standard ANSI SQL,
COUNT(DISTINCT column_name)strictly ignoresNULLvalues. It does not countNULLas a unique distinct entity.
Let us inspect a sample ClientAccounts table where some account managers are not yet assigned:
| AccountID | ClientName | AccountManager | Territory |
| A-101 | Apex Global | David Miller | East |
| A-102 | Summit Logistics | Jennifer White | West |
| A-103 | Horizon Tech | NULL | Midwest |
| A-104 | Beacon Energy | David Miller | East |
| A-105 | Pioneer Media | NULL | South |
If you execute:
SQL
SELECT
COUNT(DISTINCT AccountManager) AS distinct_managers
FROM
ClientAccounts;The result is 2 (David Miller and Jennifer White). The two records with NULL are filtered out during aggregate calculation.
What If You Need to Count NULL as a Distinct Value?
If your business requirements state that an unassigned status (NULL) represents an explicit, distinct classification state, you must transform NULL values into a placeholder using standard functions like COALESCE() or NVL():
SQL
SELECT
COUNT(DISTINCT COALESCE(AccountManager, 'Unassigned')) AS distinct_manager_states
FROM
ClientAccounts;This returns 3 (David Miller, Jennifer White, and 'Unassigned').
Combining COUNT DISTINCT with GROUP BY
In analytical workflows, you rarely count distinct values across an entire table in isolation. More frequently, you slice distinct metrics across categorical dimensions like geographic regions, departments, or calendar quarters.
Scenario: Unique Customers per Region
Using our customer model, suppose we want to determine the number of distinct shoppers per state alongside total spend:
SQL
SELECT
State,
COUNT(OrderID) AS total_orders,
COUNT(DISTINCT CustomerName) AS unique_shoppers,
SUM(OrderAmount) AS gross_revenue
FROM
CustomerOrders
GROUP BY
State
ORDER BY
gross_revenue DESC;
How the Database Processes This Query:
- Partitioning: The engine splits the data into intermediate partitions based on
State. - Aggregation: Within each partition, it sorts/hashes the
CustomerNamevalues to eliminate duplicates. - Computation: It computes the counts and sums independently for each state grouping.
Handling Multiple Columns in COUNT DISTINCT
For example, finding the number of distinct customer-to-state pairs or distinct department-and-role configurations.
The ANSI SQL Multi-Column Syntax
Standard ANSI SQL allows multiple column arguments within COUNT(DISTINCT ...):
SQL
-- Valid in PostgreSQL, MySQL, Oracle, and Snowflake
SELECT
COUNT(DISTINCT CustomerName, State) AS unique_customer_state_pairs
FROM
CustomerOrders;
The Microsoft SQL Server (T-SQL) Limitation and Solution
If you run the query above in Microsoft SQL Server (T-SQL), the database engine will throw an error:
Msg 102: Incorrect syntax near ','.
SQL Server does not natively support multiple column arguments inside a single COUNT(DISTINCT ...) call. To resolve this limitation in T-SQL, you can use two reliable patterns:
Method A: String Concatenation with Delimiters (Fast & Simple)
SQL
SELECT
COUNT(DISTINCT CustomerName + '|#|' + State) AS unique_customer_state_pairs
FROM
CustomerOrders;Note: Always use an unambiguous delimiter (like |#|) to prevent false collision matches between columns (e.g., 'John' + 'Smith' vs. 'JohnS' + 'mith').
Method B: Subquery or Common Table Expression (CTE) (Clean & Robust)
SQL
WITH DistinctPairs AS (
SELECT DISTINCT
CustomerName,
State
FROM
CustomerOrders
)
SELECT
COUNT(*) AS unique_customer_state_pairs
FROM
DistinctPairs;
Method B is my preferred enterprise design pattern: it avoids string concatenation overhead, prevents character encoding issues, and produces self-documenting code that is easy for peer developers to maintain.
Conditional Aggregation with COUNT DISTINCT
In advanced reporting, you often need to calculate distinct values based on conditional filters without filtering out the entire query dataset via a restrictive WHERE clause.
Using CASE Statements Inside COUNT DISTINCT
Because COUNT(DISTINCT ...) ignores NULL, you can pair it with a CASE statement. When the condition evaluates to false, omit the ELSE branch (which defaults to NULL):
SQL
SELECT
COUNT(DISTINCT CustomerName) AS total_unique_customers,
-- Count distinct customers who placed large orders
COUNT(DISTINCT CASE
WHEN OrderAmount >= 100.00 THEN CustomerName
END) AS high_value_customers,
-- Count distinct customers from specific southern states
COUNT(DISTINCT CASE
WHEN State IN ('Texas', 'Florida') THEN CustomerName
END) AS southern_customers
FROM
CustomerOrders;
Why This Works:
- When
OrderAmount < 100.00, theCASEstatement returnsNULL. COUNT(DISTINCT ...)automatically discards allNULLoutputs, leaving only the distinct customers matching the targeted business rule.
Performance Pitfalls and Query Optimization
While COUNT(DISTINCT ...) is simple to write, it is among the most resource-intensive aggregate operations in relational database engines.
Why Is COUNT DISTINCT Expensive?
- Sort and Hash Operations: Unlike standard
COUNT(*), which simply increments a memory counter as it scans rows,COUNT(DISTINCT ...)requires the engine to keep track of every unique value encountered. It must build an in-memory hash table or sort the data stream to identify duplicates. - Memory Spills (TempDB / Disk): When processing tables with hundreds of millions of rows and high cardinality, the hash table often exceeds the available working memory buffer (
work_memin PostgreSQL or query workspace memory in SQL Server), forcing expensive I/O spills to disk. - Multiple COUNT DISTINCT Bottlenecks: Placing multiple distinct aggregations in a single
SELECTlist forces the optimizer to perform multiple distinct sorting passes over the same dataset:
SQL
-- Performance Warning: Requires multiple sorting/hashing passes
SELECT
Department,
COUNT(DISTINCT EmployeeID) AS unique_staff,
COUNT(DISTINCT ProjectCode) AS unique_projects,
COUNT(DISTINCT VendorID) AS unique_vendors
FROM
EnterpriseOperations
GROUP BY
Department;
Proactive Optimization Strategies
- 1. Leverage Composite Indexes:Create covering indexes on the columns involved in the
GROUP BYandCOUNT(DISTINCT ...)clauses. A B-Tree index keeps data pre-sorted, allowing the engine to perform index stream aggregate scans without runtime sorting. - 2. Pre-Aggregate with CTEs or Derived Tables:Deduplicate high-volume datasets early in the execution plan prior to performing complex multi-table joins.
- 3. Use Approximate Counting on Big Data Platforms:When querying massive analytical warehouses (Snowflake, Google BigQuery, AWS Redshift, or Databricks), exact distinct counts can be cost-prohibitive. In scenarios where a ~1% error margin is acceptable (e.g., high-level trend reporting), switch to HyperLogLog approximation functions:
- Snowflake / BigQuery:
APPROX_COUNT_DISTINCT(column_name) - SQL Server:
APPROX_COUNT_DISTINCT(column_name) - AWS Redshift:
COUNT(DISTINCT ...)with HyperLogLog extensions
- Snowflake / BigQuery:
Frequently Asked Questions (FAQ)
What is the difference between SELECT DISTINCT COUNT(col) and SELECT COUNT(DISTINCT col)?
SELECT COUNT(DISTINCT col)counts the number of unique items in that column and returns a single integer.SELECT DISTINCT COUNT(col)first counts all non-null rows in the table (returning a single aggregate total) and then appliesDISTINCTto that single number, which has no practical effect. Always useCOUNT(DISTINCT col).
Can I use COUNT(DISTINCT) with window functions (OVER() clause)?
In standard SQL, most engines (including SQL Server and PostgreSQL) do not support COUNT(DISTINCT col) OVER (PARTITION BY ...). If you need distinct windowed aggregates, use DENSE_RANK() or compute the distinct values inside a Common Table Expression before applying window calculations.
Does COUNT(DISTINCT) count empty strings ('')?
Yes. An empty string ('') is a valid non-null string value in ANSI SQL. If your dataset has three blank strings and two distinct names, COUNT(DISTINCT col) will evaluate the blank string as one distinct entity.
Conclusion and Key Takeaways
The SQL COUNT DISTINCT function is an indispensable component of any data professional’s SQL toolkit. Understanding its internal logic ensures your analytical reporting remains accurate and computationally efficient.
Summary:
- Always account for
NULLvalues: Remember thatCOUNT(DISTINCT ...)naturally ignoresNULLunless wrapped in a fallback function likeCOALESCE. - Mind your dialect constraints: Use CTEs or delimited concatenations when operating within SQL Server environments that restrict multi-column distinct arguments.
- Optimize deliberately: Monitor performance bottlenecks on large tables by indexing properly, deduplicating early in subqueries, and using approximate algorithms (
APPROX_COUNT_DISTINCT) on enterprise big data platforms.
You may also like the following articles:
After working for more than 15 years in the Software field, especially in Microsoft technologies, I have decided to share my expert knowledge of SQL Server. Check out all the SQL Server and related database tutorials I have shared here. Most of the readers are from countries like the United States of America, the United Kingdom, New Zealand, Australia, Canada, etc. I am also a Microsoft MVP. Check out more here.