SQL SELECT DISTINCT: What It Actually Deduplicates, and When It Hides a Bug

Understand SQL DISTINCT across full rows, NULLs, counts, groups, latest-row selection, and join fanout using verified PostgreSQL outputs.

Author: PracHub

Published: 8/14/2026

SQL SELECT DISTINCT: What It Actually Deduplicates, and When It Hides a Bug

By PracHub
August 14, 2026
20 min read
0
SQL SELECT DISTINCT: What It Actually Deduplicates, and When It Hides a Bug

Quick Overview

A grain-first Data Analyst guide to SELECT DISTINCT, COUNT(DISTINCT), GROUP BY, PostgreSQL DISTINCT ON, and join fanout. Every concept shows visible inputs, an operation-only query, an accessible row-flow diagram, and exact output.

Data AnalystFree

SELECT DISTINCT removes duplicate projected rows. It does not pick one row based on a single column, repair a multiplied join, or guarantee which full row survives.

The select list defines the deduplication key. Add a unique column such as order_id, and every projected row becomes distinct again. Start by naming the grain you want, then choose the tool that produces that grain explicitly.

DISTINCT works on the complete select list

This query asks for unique customer-region combinations. Acme appears twice because it ordered in two regions. The two Hooli rows collapse because both projected values match, including the NULL region.

Input: orders

order_idcustomerregionstatusamountorder_date
1acmeeastshipped120.002026-03-01
2acmeeastshipped80.002026-03-04
3acmewestrefunded120.002026-03-09
4globexeastshipped45.002026-03-02
5globexeastpending45.002026-03-07
6initechwestshipped200.002026-03-03
7hooliNULLpending60.002026-03-05
8hooliNULLpending60.002026-03-05
SELECT DISTINCT customer, region
FROM orders
ORDER BY customer, region NULLS LAST;
Row flow for deduplicating customer-region rows Eight order rows project to customer and region, repeated projected pairs collapse, and five unique combinations remain. 8 order rows6 columns eachProject 2 columnsdedupe whole pairs5 unique pairsone NULL pair
Parentheses around one selected column would not change the key; ordinary DISTINCT still applies to every projected column.

Output

customerregion
acmeeast
acmewest
globexeast
hooliNULL
initechwest

Adding order_id would return all eight rows because that column is unique. Also note the NULL behavior: duplicate NULL values collapse in a distinct result even though NULL = NULL is not true in a predicate. The SQL order-of-operations guide explains where projection, deduplication, and sorting sit in one query block.

COUNT(DISTINCT) and GROUP BY answer different questions

COUNT(DISTINCT column) counts unique non-NULL values. That differs from counting rows returned by SELECT DISTINCT column, which can include one NULL row.

Input: orders

order_idcustomerregionstatusamountorder_date
1acmeeastshipped120.002026-03-01
2acmeeastshipped80.002026-03-04
3acmewestrefunded120.002026-03-09
4globexeastshipped45.002026-03-02
5globexeastpending45.002026-03-07
6initechwestshipped200.002026-03-03
7hooliNULLpending60.002026-03-05
8hooliNULLpending60.002026-03-05
SELECT
  COUNT(*) AS order_rows,
  COUNT(region) AS rows_with_region,
  COUNT(DISTINCT region) AS distinct_known_regions,
  COUNT(DISTINCT customer) AS distinct_customers
FROM orders;
Row flow for row, known-value, and distinct counts Eight order rows remain eight for count star, become six when null regions are removed, and reduce to two known regions and four customers under distinct counting. 8 order rows2 NULL regionsChoose count unitrows, known, unique8, 6, 2, 4four definitions
The NULL region forms a row in a distinct key list but contributes nothing to COUNT(DISTINCT region).

Output

order_rowsrows_with_regiondistinct_known_regionsdistinct_customers
8624

For more count-specific edge cases, see SQL COUNT.

Use GROUP BY when the unique keys need measures or a group filter. The result has one row per region, just as a distinct region list would, but each row remains an addressable group for its counts.

Input: orders

order_idcustomerregionstatusamountorder_date
1acmeeastshipped120.002026-03-01
2acmeeastshipped80.002026-03-04
3acmewestrefunded120.002026-03-09
4globexeastshipped45.002026-03-02
5globexeastpending45.002026-03-07
6initechwestshipped200.002026-03-03
7hooliNULLpending60.002026-03-05
8hooliNULLpending60.002026-03-05
SELECT
  region,
  COUNT(*) AS order_rows,
  COUNT(DISTINCT customer) AS customer_count
FROM orders
GROUP BY region
ORDER BY region NULLS LAST;
Row flow for grouping distinct region keys with measures Eight orders form east, west, and null-region groups, and each group reports its order rows and distinct customers. 8 orders3 region keysGROUP BY regioncount rows and customers3 region rowsNULL group retained
Choose GROUP BY because the output needs calculations beside each unique key, not because one spelling is assumed faster.

Output

regionorder_rowscustomer_count
east42
west22
NULL21

The GROUP BY guide covers aggregate and HAVING decisions. Do not make a blanket performance claim between DISTINCT and GROUP BY; PostgreSQL can choose different plans as data, indexes, and projections change.

DISTINCT ON chooses one complete row per key

PostgreSQL's DISTINCT ON keeps the first row in each ordered key group. The ORDER BY must start with the distinct key; later expressions define the winner. Here the latest order wins, and order_id DESC resolves same-date ties.

Input: orders

order_idcustomerregionstatusamountorder_date
1acmeeastshipped120.002026-03-01
2acmeeastshipped80.002026-03-04
3acmewestrefunded120.002026-03-09
4globexeastshipped45.002026-03-02
5globexeastpending45.002026-03-07
6initechwestshipped200.002026-03-03
7hooliNULLpending60.002026-03-05
8hooliNULLpending60.002026-03-05
SELECT DISTINCT ON (customer)
  customer,
  order_id,
  order_date,
  amount
FROM orders
ORDER BY customer, order_date DESC, order_id DESC;
Row flow for selecting the latest order per customer Eight orders sort within four customer groups by date and order ID descending, then the first row from each group produces four latest-order rows. 8 order rows4 customersSort each key grouplatest, then largest ID4 winning rowsone per customer
The two Hooli orders share a date, so the order-ID tie-breaker makes order 8 the stable winner.

Output

customerorder_idorder_dateamount
acme32026-03-09120.00
globex52026-03-0745.00
hooli82026-03-0560.00
initech62026-03-03200.00

DISTINCT ON is PostgreSQL-specific. A ROW_NUMBER() partition with the same ordering expresses the same choice when another engine is required. Ordinary SELECT DISTINCT customer, order_id, ... cannot solve this because every order ID is already different.

DISTINCT cannot repair join fanout

A one-to-many join repeats the parent order amount for every item. This audit displays both common patches: summing the repeated amount inflates revenue, while SUM(DISTINCT amount) collapses two legitimate 100.00 orders for customer 1.

Input: customer_orders

order_idcustomer_idamount
1011100.00
1021100.00
103275.00

Input: order_items

item_idorder_idsku
1001101A
1002101B
1003102C
1004103D
1005103E
SELECT
  o.customer_id,
  COUNT(*) AS joined_rows,
  COUNT(DISTINCT o.order_id) AS distinct_orders,
  SUM(o.amount) AS duplicated_amount,
  SUM(DISTINCT o.amount) AS distinct_amount
FROM customer_orders AS o
JOIN order_items AS i
  ON i.order_id = o.order_id
GROUP BY o.customer_id
ORDER BY o.customer_id;
Row flow exposing incorrect distinct repairs after fanout Three orders join five items and become five rows; distinct order counts remain correct, ordinary sums duplicate parent amounts, and distinct sums collapse equal legitimate amounts. 3 orders + 5 itemsone-to-many join5 joined rowsparent amounts repeatTwo wrong sumsinflated or collapsed
Distinct order IDs answer an entity-count question; distinct amount values do not restore order grain.

Output

customer_idjoined_rowsdistinct_ordersduplicated_amountdistinct_amount
132300.00100.00
221150.0075.00

Customer 1's correct order revenue is 200.00, which appears in neither sum. If the item table is used only to require that an order has at least one item, EXISTS preserves one row per order and prevents fanout.

Input: customer_orders

order_idcustomer_idamount
1011100.00
1021100.00
103275.00

Input: order_items

item_idorder_idsku
1001101A
1002101B
1003102C
1004103D
1005103E
SELECT
  o.customer_id,
  COUNT(*) AS order_count,
  SUM(o.amount) AS order_revenue
FROM customer_orders AS o
WHERE EXISTS (
  SELECT 1
  FROM order_items AS i
  WHERE i.order_id = o.order_id
)
GROUP BY o.customer_id
ORDER BY o.customer_id;
Row flow for preventing fanout with EXISTS Three order rows are tested for matching item existence without joining item rows, remain at order grain, and aggregate into two correct customer totals. 3 order rowscorrect revenue grainEXISTS item matchno child rows emitted2 customer totals200.00 and 75.00
The predicate checks membership while leaving the parent rows at order grain.

Output

customer_idorder_countorder_revenue
12200.00
2175.00

If child measures are also needed, aggregate the child table to one row per order before joining. The SQL joins guide covers outer joins, membership tests, and fanout decisions.

A practical decision guide

NeedPreferReason
Unique projected keysSELECT DISTINCTThe select list is exactly the deduplication key.
Measures or filters per keyGROUP BYThe unique key remains a group that can be aggregated.
Unique-entity countCOUNT(DISTINCT entity_id)Repeated event rows should count once per entity.
One complete winner per key in PostgreSQLDISTINCT ON with deterministic orderingOrdering states which row survives.
One complete winner per key across enginesROW_NUMBER in a subqueryPartitioning and tie-breakers are explicit.
Membership without child columnsEXISTSIt cannot multiply parent rows.

Treat an unexplained final DISTINCT as a review signal. It may be correct, but first ask which columns define the intended grain and which earlier operation created duplicates. Removing visible duplicates is not the same as fixing their cause.

FAQ

Does DISTINCT apply to one column or every selected column?

It applies to the complete projected row. SELECT DISTINCT a, b returns unique (a, b) combinations. Parentheses around a do not make it a one-column distinct operation.

Does DISTINCT keep one NULL?

Yes. Duplicate projected rows containing NULL collapse together. This is separate from predicate equality, where NULL = NULL is unknown.

What is the difference between UNIQUE and DISTINCT?

UNIQUE is a schema constraint that controls stored data. DISTINCT is a query operation that removes duplicate rows from one result. Their NULL behavior can also differ by engine and constraint definition.

Is DISTINCT slower than GROUP BY?

There is no useful universal answer. The projected columns, aggregates, indexes, data distribution, memory, and selected plan matter. Use the operation that states the query's job, then inspect the plan for the real workload.

When is COUNT(DISTINCT) appropriate after a join?

It is appropriate when the metric is genuinely unique entities across the joined rows, such as unique customers per product. It is not a general repair for duplicated sums or an unclear join grain.


Comments (0)