SELECT DISTINCT in SQL: Removing Duplicate Rows from Results
On this page 10 sections ▾
DISTINCT removes duplicate rows from a query result. That sentence contains the one thing people get wrong about it: it works on rows, not on columns. Once that clicks, the surprising results stop being surprising.
This page covers the single-column case, the multi-column case that catches everyone, counting distinct values, and the two situations where you should reach for GROUP BY instead. Every example runs against the sample database.
One column: the easy case
The products table has seven rows but fewer categories, because several products share one:
SELECT category FROM Products;
| category |
|---|
| Hardware |
| Accessories |
| Hardware |
| Accessories |
| Software |
| Software |
| Services |
● 7 rows · produced by running this query on the sample database
Add DISTINCT immediately after SELECT and each category appears once:
SELECT DISTINCT category FROM Products;
| category |
|---|
| Hardware |
| Accessories |
| Software |
| Services |
● 4 rows · produced by running this query on the sample database
Note where it goes. DISTINCT is not a function you wrap around a column — there is no DISTINCT(category) in SQL, even though it is often written that way in blog posts. It is a keyword that modifies the whole SELECT list, which is exactly why the next section behaves as it does.
Two columns: the case that trips everyone
Here is the mistake, in the form it usually takes. Someone wants one row per country and writes:
SELECT DISTINCT country, city FROM Customers;
| country | city |
|---|---|
| United Kingdom | London |
| United States | Austin |
| Japan | Osaka |
| Tanzania | Arusha |
| Peru | Lima |
| Peru | Arequipa |
| Lebanon | Beirut |
| Norway | Bergen |
● 8 rows · produced by running this query on the sample database
Every row comes back. Not because DISTINCT failed, but because it did precisely what it says: it removed duplicate rows, and each country/city pair is unique even though the countries are not. Compare it with the single-column version:
SELECT DISTINCT country FROM Customers;
| country |
|---|
| United Kingdom |
| United States |
| Japan |
| Tanzania |
| Peru |
| Lebanon |
| Norway |
● 7 rows · produced by running this query on the sample database
Fewer rows, because now the row is the country. The rule to carry away: DISTINCT de-duplicates the combination of everything in the SELECT list. Adding a column can only ever increase the number of rows it returns, never decrease it.
There is no way to make DISTINCT apply to one column of several
If you need one row per country but also want to see a city, DISTINCT is the wrong tool — the question is really “which city?”, and SQL cannot guess. Group by the country and choose explicitly with an aggregate such as MIN(city), or number the rows with a window function and keep the first. Both are shown below.
Counting distinct values
COUNT(DISTINCT column) counts how many different values exist, which is a different question from how many rows exist:
SELECT COUNT(*) AS customers,
COUNT(DISTINCT country) AS countries
FROM Customers;
| customers | countries |
|---|---|
| 8 | 7 |
● 1 row · produced by running this query on the sample database
This is one of the most useful things in reporting SQL. When a table has one row per event, COUNT(*) counts events and COUNT(DISTINCT customer_id) counts the people behind them — confusing the two is how “active users” numbers get overstated:
SELECT COUNT(*) AS orders,
COUNT(DISTINCT customer_id) AS customers_who_ordered
FROM Orders;
| orders | customers_who_ordered |
|---|---|
| 15 | 8 |
● 1 row · produced by running this query on the sample database
The aggregate functions guide covers the rest of the family, including why COUNT(*) and COUNT(column) disagree when NULLs are involved.
DISTINCT and NULL
For sorting and comparison, NULL is famously not equal to itself — but DISTINCT treats all NULLs as one value, so a column full of missing data yields a single NULL row:
SELECT DISTINCT manager_id FROM Employees;
| manager_id |
|---|
| 1 |
| 4 |
| 7 |
| 9 |
| 11 |
| NULL |
● 6 rows · produced by running this query on the sample database
One of those rows is NULL, standing in for every employee with no manager. That is the standard behaviour and it is what you almost always want, but it is worth knowing because it is inconsistent with =: two NULLs are “the same” to DISTINCT and “unknown” to a comparison. See IS NULL for the comparison side of that story.
DISTINCT with ORDER BY
Sorting works as usual, with one restriction:
SELECT DISTINCT country
FROM Customers
ORDER BY country;
| country |
|---|
| Japan |
| Lebanon |
| Norway |
| Peru |
| Tanzania |
| United Kingdom |
| United States |
● 7 rows · produced by running this query on the sample database
You can only sort by something that survives the de-duplication. ORDER BY signup_date on that query is rejected by most databases, because after the duplicates are removed there may be several signup dates per country and no single value to sort by. If you need that, you need GROUP BY with an aggregate — which brings us to the last section.
DISTINCT or GROUP BY?
These two queries return exactly the same rows:
SELECT DISTINCT category FROM Products;
| category |
|---|
| Hardware |
| Accessories |
| Software |
| Services |
● 4 rows · produced by running this query on the sample database
SELECT category FROM Products GROUP BY category;
| category |
|---|
| Hardware |
| Accessories |
| Software |
| Services |
● 4 rows · produced by running this query on the sample database
On a modern database they usually produce the same plan too, so the choice is about intent. Use DISTINCT when you want a de-duplicated list and nothing else. Use GROUP BY the moment you want anything computed per group:
SELECT category,
COUNT(*) AS products,
MIN(unit_price) AS cheapest,
MAX(unit_price) AS dearest
FROM Products
GROUP BY category
ORDER BY products DESC;
| category | products | cheapest | dearest |
|---|---|---|---|
| Hardware | 2 | 449.99 | 1299 |
| Accessories | 2 | 119 | 189.5 |
| Software | 2 | 899 | 1450 |
| Services | 1 | 400 | 400 |
● 4 rows · produced by running this query on the sample database
That is the query DISTINCT can never write, and it answers the “which city?” problem from earlier too — pick one deliberately with an aggregate rather than hoping DISTINCT guesses:
SELECT country,
COUNT(*) AS customers,
MIN(city) AS first_city_alphabetically
FROM Customers
GROUP BY country
ORDER BY customers DESC, country;
| country | customers | first_city_alphabetically |
|---|---|---|
| Peru | 2 | NULL |
| Japan | 1 | NULL |
| Lebanon | 1 | NULL |
| Norway | 1 | NULL |
| Tanzania | 1 | NULL |
| United Kingdom | 1 | NULL |
| United States | 1 | NULL |
● 7 rows · produced by running this query on the sample database
| You want | Use |
|---|---|
| a list of unique values | DISTINCT |
| unique combinations of several columns | DISTINCT with all of them listed |
| how many different values there are | COUNT(DISTINCT column) |
| one row per group with something computed | GROUP BY with aggregates |
| one row per group with a chosen column | GROUP BY with MIN/MAX, or a window function |
| to remove duplicate rows from a table | not a SELECT at all - see deleting duplicates |
That last row matters: DISTINCT changes what a query returns, never what the table contains. To fix the data itself, see finding and deleting duplicate rows.
Performance, briefly
DISTINCT is not free. To know whether a row is a duplicate the database has to compare it against the rows it has already produced, which means sorting or hashing the whole result. Two practical consequences:
- Select fewer columns. Every extra column makes the comparison wider and duplicates rarer — often the reason a DISTINCT “stopped working” is that someone added a column.
- A DISTINCT that fixes a join is a warning sign. If a query needs DISTINCT to get sensible numbers, the join is probably multiplying rows. Fix the join, or aggregate deliberately; papering over it with DISTINCT hides a bug and will still be wrong once someone selects an extra column.
Practice this topic
Reading explains the idea; producing it yourself is what makes it stick. These exercises use exactly what this page covers, and your answer is checked by running it against the same sample database:
- Which countries do we sell to?Easy
- Look at the whole product catalogueEasy
- How many customers do we have?Easy
- Revenue by product categoryHard
Frequently asked questions
What does SELECT DISTINCT do in SQL?
It removes duplicate rows from the result of a query. It applies to the whole SELECT list, so SELECT DISTINCT country returns each country once, while SELECT DISTINCT country, city returns each unique country-and-city combination.
Why is SELECT DISTINCT returning duplicates?
Almost always because the SELECT list has more columns than you think. DISTINCT de-duplicates entire rows, so if any column differs the row is not a duplicate. Remove columns until only the ones that define uniqueness remain, or switch to GROUP BY.
Can I apply DISTINCT to only one column?
No. There is no DISTINCT(column) in SQL - DISTINCT is a keyword that modifies the whole SELECT list, not a function. If you want one row per value of one column while still showing others, use GROUP BY with an aggregate such as MIN(city), or a window function.
What is the difference between DISTINCT and GROUP BY?
For a plain list of unique values they return the same rows and usually the same query plan, so the choice is about intent. GROUP BY is the tool as soon as you want something computed per group - a count, a total, a minimum - which DISTINCT cannot express at all.
How does DISTINCT treat NULL?
It treats all NULLs as a single value, so a column containing many NULLs contributes exactly one NULL row to the result. This is deliberately different from = , where comparing two NULLs gives unknown rather than true.
Does DISTINCT slow down a query?
It has a real cost: the database must sort or hash the result to detect duplicates. The cost grows with the number of columns and rows. A DISTINCT added to make a joined query return the right numbers is usually hiding a join that multiplies rows, which is worth fixing properly.
Related reading
About the author
SQL Practice
SQL Practice publishes free SQL tutorials, an in-browser playground and 42 practice exercises, built so you can run what you read. Every runnable example in our tutorials is executed against the site’s sample database before it is published. Read how we keep tutorials accurate, or report a mistake.