SQL • QUERYING DATA

DISTINCT — Use DISTINCT and understand when it changes results

Eliminate duplicate rows from query results and learn precisely when deduplication changes your output.

Historical Context & Motivation

Relational databases emerged from a simple but powerful insight: data stored as mathematical relations—tables of rows and columns—could be queried with a declarative language rather than procedural navigation. Edgar F. Codd's foundational 1970 paper introduced the relational model, which treats a relation as a set of tuples. In pure set theory, sets contain no duplicate elements; however, SQL's practical design chose to work with multisets (also called bags) by default, meaning a query's result can contain duplicate rows. The DISTINCT keyword was introduced to bridge this gap, allowing programmers to request true set semantics when needed.

1970
Codd's Relational Model
Edgar F. Codd publishes "A Relational Model of Data for Large Shared Data Banks," defining relations as sets of tuples with no duplicates by definition.
1974
SEQUEL at IBM
IBM researchers Chamberlin and Boyce create SEQUEL (later SQL), choosing multiset semantics for performance reasons and adding DISTINCT as an explicit deduplication operator.
1986
SQL-86 Standard
ANSI adopts the first SQL standard, formally specifying SELECT DISTINCT as part of the language grammar along with its interaction with ORDER BY and aggregate functions.
1992
SQL-92 Enhancements
The SQL-92 standard clarifies DISTINCT behavior inside aggregate functions (e.g., COUNT(DISTINCT col)) and codifies ALL as the default qualifier.
2003+
Modern Optimizers
Query optimizers in PostgreSQL, MySQL, SQL Server, and Oracle implement hash-based and sort-based distinct elimination strategies, making DISTINCT performance a key consideration in query tuning.

The fundamental question DISTINCT addresses is deceptively simple: when does a SELECT statement produce duplicate rows, and how should a programmer decide whether to eliminate them? Understanding the answer requires grasping how projections, joins, and the absence of primary key columns in a SELECT list conspire to create duplicates—and recognizing the performance cost of removing them.

Core Principles & Definitions

Before diving into syntax, it is essential to distinguish between set semantics and bag (multiset) semantics. SQL defaults to bag semantics: a SELECT projection may return the same combination of values many times. The DISTINCT keyword switches the query to set semantics for the result, collapsing all identical rows into a single representative. This distinction is not merely academic—it affects correctness, performance, and even the logical meaning of aggregate calculations.

1

Bag vs. Set Semantics

SQL SELECT returns a multiset (bag) by default. Adding DISTINCT converts the output to a set, removing duplicate rows. The implicit qualifier is ALL, which preserves duplicates.
2

Row-Level Comparison

DISTINCT compares entire rows across all columns in the SELECT list. Two rows are duplicates if and only if every corresponding column value is equal (with NULLs treated as equal to each other for this purpose).
3

Projection Creates Duplicates

Duplicates typically arise when the SELECT list omits columns that differentiate rows—especially primary keys or unique identifiers. A projection onto non-key columns collapses distinct tuples into identical-looking rows.
4

DISTINCT Inside Aggregates

Aggregate functions like COUNT, SUM, and AVG accept DISTINCT as a modifier: COUNT(DISTINCT department) counts unique departments rather than all rows. This is orthogonal to SELECT DISTINCT.
5

Performance Implications

DISTINCT requires the engine to sort or hash all result rows to detect duplicates. On large result sets, this adds significant CPU and memory overhead. Use DISTINCT only when duplicates are genuinely possible and undesirable.
KEY TAKEAWAY
Think of DISTINCT like a mail sorter removing duplicate copies of the same letter before delivering the stack. If the original stack has no duplicates—say, each letter has a unique tracking number included in your view—then DISTINCT does nothing except waste time sorting through the pile. Only apply it when you know the projection might produce repeated rows.

Visual Explanation

The left panel shows the original employees table with six unique rows. The center panel shows the result of SELECT department without DISTINCT—six rows, three of which are duplicates. The right panel applies SELECT DISTINCT department, collapsing the result to three unique department values.

The diagram illustrates the most common scenario that produces duplicates: projecting away the primary key. When you select only the department column from a table where multiple employees share the same department, the engine faithfully returns one row per original tuple—resulting in repeated values. DISTINCT instructs the engine to perform a deduplication pass, comparing every column in the SELECT list across all result rows and retaining only one copy of each unique combination. Notice that if you had instead written SELECT DISTINCT id, department, no rows would be eliminated because id is unique—every row is already distinct, and DISTINCT becomes a no-op.

How DISTINCT Works Internally

Understanding how the database engine implements DISTINCT helps you reason about when it is necessary and what it costs. The SQL logical query processing order places DISTINCT after SELECT and before ORDER BY, meaning the engine first evaluates the FROM, WHERE, GROUP BY, HAVING, and SELECT clauses to produce a multiset of rows, then applies DISTINCT to eliminate duplicates, and finally sorts the result if ORDER BY is specified.

Logical Query Processing Order

  1. FROM — Identify source tables and perform joins
  2. WHERE — Filter rows
  3. GROUP BY — Aggregate rows into groups
  4. HAVING — Filter groups
  5. SELECT — Evaluate expressions and project columns
  6. DISTINCT — Remove duplicate rows from the projected result
  7. ORDER BY — Sort the final output
  8. LIMIT / OFFSET — Restrict the number of rows returned

Physical Implementation Strategies

Database engines typically choose between two physical strategies to implement DISTINCT. The sort-based approach sorts the entire result set, then performs a linear scan to collapse adjacent identical rows. This runs in O(n log n) time and is favorable when the output is already partially sorted or when the query also requires ORDER BY. The hash-based approach builds an in-memory hash table of seen rows, inserting each new row only if its hash bucket is empty. This achieves O(n) average-case time complexity but requires O(k) memory, where k is the number of distinct rows. Modern query optimizers choose between these strategies based on estimated cardinality and available memory.

SORT-BASED DISTINCT
T(n) = O(n log n) + O(n) linear scan
Where n is the number of rows in the pre-DISTINCT result set. The sort dominates cost.
HASH-BASED DISTINCT
T(n) = O(n) Space = O(k)
Where k is the number of distinct rows. When k is small relative to n, hashing is highly efficient; when k ≈ n, the overhead provides no benefit over omitting DISTINCT entirely.
NULL Handling
For the purposes of DISTINCT, two NULLs are considered equal. This is an exception to the general SQL rule that NULL ≠ NULL in comparison predicates. If a column contains multiple NULL values, DISTINCT collapses them into a single NULL row. This behavior is mandated by the SQL standard (ISO/IEC 9075) and is consistent across major database engines.

When DISTINCT Changes (and Doesn't Change) Results

A critical skill is recognizing when DISTINCT actually affects the output. Blindly adding DISTINCT to every query is a common antipattern that masks data model misunderstandings and introduces unnecessary overhead. The decision tree below formalizes the reasoning: DISTINCT changes results only when the columns in the SELECT list do not form a superkey of the result set.

This decision tree guides you through the key questions: does the SELECT list contain a unique column? Is GROUP BY already in play? Are JOINs multiplying rows? Following the branches helps determine whether DISTINCT is necessary, redundant, or a sign of a deeper query design issue.

Common Scenarios

Common query patterns and whether DISTINCT affects the output
ScenarioDISTINCT Changes Result?Explanation
SELECT id, name FROM usersNoid is a primary key; every row is already unique.
SELECT city FROM usersYes (likely)Multiple users can share the same city; projection onto a non-key column produces duplicates.
SELECT u.name FROM users u JOIN orders o ON u.id = o.user_idYes (likely)A one-to-many join produces one row per order; a user with 5 orders appears 5 times.
SELECT department, COUNT(*) FROM employees GROUP BY departmentNoGROUP BY already produces one row per group; DISTINCT is redundant here.
SELECT DISTINCT status FROM orders WHERE amount > 100Yesstatus is a low-cardinality column; many orders share the same status value.

Worked Example

Consider an e-commerce database with two tables: customers(id, name, city) and orders(id, customer_id, product, amount). We want to find all distinct cities from which customers have placed orders exceeding $50. This requires a JOIN, a filter, a projection, and DISTINCT.

Finding Distinct Cities of High-Value Customers
1
Step 1 — Identify the Tables and Join ConditionWe need data from both customers (for the city) and orders (for the amount filter). The join condition is customers.id = orders.customer_id. Because this is a one-to-many relationship (one customer can have many orders), the join will produce multiple rows per customer.
2
Step 2 — Write the Base Query Without DISTINCTStart with: SELECT c.city FROM customers c JOIN orders o ON c.id = o.customer_id WHERE o.amount > 50. If a customer in New York has placed three qualifying orders, 'New York' appears three times in the result.
Result may contain duplicate cities due to the one-to-many join.
3
Step 3 — Analyze Whether DISTINCT Is NeededThe SELECT list contains only c.city, which is not a key of the joined result. Multiple customers may also share the same city. Therefore, duplicates are both possible and undesirable for our use case (we want the list of unique cities, not a count of qualifying orders per city).
4
Step 4 — Add DISTINCTThe final query becomes: SELECT DISTINCT c.city FROM customers c JOIN orders o ON c.id = o.customer_id WHERE o.amount > 50. The engine performs the join and filter first, then projects the city column, and finally deduplicates the result.
Each city appears exactly once in the output, regardless of how many qualifying orders originated from it.
5
Step 5 — Consider AlternativesAn equivalent approach uses a subquery: SELECT DISTINCT city FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE amount > 50). This avoids the join-induced row multiplication entirely; the DISTINCT here handles the case where multiple customers in the same city qualify. Another option is EXISTS, which can be more efficient because it short-circuits after finding the first matching order.
When duplicates come from JOINs, restructuring the query with subqueries or EXISTS can eliminate the need for DISTINCT and improve performance.

Strengths, Limitations & Common Pitfalls

DISTINCT: strengths vs. limitations
AspectStrengthLimitation / Pitfall
SimplicityA single keyword eliminates duplicates with no code changes to the rest of the query.Over-reliance masks incorrect joins or missing WHERE conditions that inflate row counts.
CorrectnessGuarantees set semantics when the business question demands unique values.If used with aggregates naively (e.g., SELECT DISTINCT with a SUM), it can silently drop valid data that happens to look identical.
PerformanceWhen the distinct count is very low (high duplication ratio), DISTINCT significantly reduces data transferred to the client.Requires a sort or hash of the full result set. On millions of rows, this can dominate query execution time.
ComposabilityWorks cleanly with ORDER BY, LIMIT, and in subqueries.Cannot be combined with SELECT * in a meaningful way if you only want uniqueness on a subset of columns—use GROUP BY instead.
Debugging SignalComparing row counts with and without DISTINCT can reveal data model issues.Habitual DISTINCT is a 'code smell'—if you need it frequently, revisit your schema or join logic.
KEY TAKEAWAY
DISTINCT is like a lint checker for your query output—it cleans up visible duplicates, but it doesn't fix the underlying issue. If you find yourself routinely adding DISTINCT, treat it as a diagnostic signal. Investigate whether the root cause is a faulty join condition (producing a Cartesian product), a missing WHERE clause, or a data model that lacks proper normalization. Fixing the root cause is almost always preferable to masking it with DISTINCT.

Connection to GROUP BY, Window Functions & Set Operations

DISTINCT is closely related to several other SQL constructs that also manipulate duplicate rows. Understanding these connections helps you choose the most appropriate tool for each situation and prepares you for more advanced query design patterns.

DISTINCT in the context of related SQL features
FeatureWhat It DoesRelationship to DISTINCT
GROUP BYPartitions rows into groups and collapses each group into a single output row via aggregate functions.SELECT DISTINCT a, b is logically equivalent to SELECT a, b GROUP BY a, b when no aggregates are used. GROUP BY is more powerful because it allows aggregation.
UNIONCombines results of two queries and removes duplicates across both result sets.UNION implicitly applies DISTINCT. Use UNION ALL to preserve duplicates (better performance when dedup is unnecessary).
ROW_NUMBER()Assigns a unique sequential integer to rows within a partition, enabling more nuanced deduplication.For complex dedup (e.g., keep the most recent row per group), use ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) with a WHERE rn = 1 filter—DISTINCT cannot handle this.
EXISTS / IN subqueryTests for the existence of related rows without joining them into the result.Avoids the join-induced duplication that often necessitates DISTINCT. Preferred when you only need columns from the outer table.
DISTINCT ON (PostgreSQL)Returns the first row for each unique combination of specified columns, respecting ORDER BY.A PostgreSQL extension that goes beyond standard DISTINCT by allowing you to pick which row to keep from each group—similar to ROW_NUMBER() + filter but more concise.

As you progress to advanced SQL, you will find that DISTINCT is often the simplest tool in a family of deduplication strategies. Window functions like ROW_NUMBER(), RANK(), and DENSE_RANK() provide fine-grained control over which representative row survives from a group of duplicates, which DISTINCT alone cannot achieve. Similarly, Common Table Expressions (CTEs) combined with ROW_NUMBER() are the standard idiom for deduplicating tables during data cleaning pipelines—a scenario far more complex than simple SELECT DISTINCT.

Practice Problems

PROBLEM 1CONCEPTUAL
Consider the query SELECT DISTINCT id, email FROM users where id is the primary key. Will DISTINCT ever remove any rows from this result? Explain why or why not.
PROBLEM 2BASIC CALCULATION
Given a table products(id, category, price) with 10,000 rows and 25 unique categories, how many rows does SELECT DISTINCT category FROM products return? What about SELECT DISTINCT category, price FROM products if there are 8,500 unique (category, price) combinations?
PROBLEM 3INTERMEDIATE
Write a query to find all unique product categories that have been ordered at least once, given tables products(id, name, category) and order_items(id, order_id, product_id, quantity). Provide two versions: one using DISTINCT and one that avoids DISTINCT entirely.
PROBLEM 4APPLIED
A colleague writes SELECT DISTINCT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id. They say the DISTINCT is needed 'for safety.' Is the DISTINCT doing anything here? What would you tell them during a code review?
PROBLEM 5CRITICAL THINKING
Consider two queries: (A) SELECT COUNT(DISTINCT department) FROM employees and (B) SELECT COUNT(*) FROM (SELECT DISTINCT department FROM employees) AS sub. Are these guaranteed to return the same result? What happens if there are NULL values in the department column? Discuss any edge cases.

Summary

The DISTINCT keyword transforms SQL's default multiset (bag) semantics into set semantics, eliminating duplicate rows from a query's result. Duplicates arise primarily when the SELECT list projects away unique key columns or when one-to-many JOINs multiply rows. DISTINCT operates on the entire tuple of selected columns, treating two NULLs as equal. Internally, the engine uses either a sort-based (O(n log n)) or hash-based (O(n)) strategy, both of which add overhead.

Use DISTINCT deliberately: when the SELECT list includes a primary key or unique column, DISTINCT is a no-op and should be omitted. When GROUP BY is already present, it typically makes DISTINCT redundant. For complex deduplication—such as keeping one representative row per group—consider ROW_NUMBER() window functions or restructuring with EXISTS / IN subqueries. Habitual use of DISTINCT without understanding why duplicates appear is a code smell that may mask join errors or schema issues.

Varsity Tutors • SQL • DISTINCT — Use DISTINCT and understand when it changes results