← SQL on CSV
Lesson 6 of 15 by Arif Aslam 6 minute read

COUNT and DISTINCT

Something new happens in this lesson. Every query so far has returned one output row per input row. COUNT is the first thing that collapses a hundred rows into one, and that idea, called aggregation, carries the next four lessons.

It is also the fastest way to understand a file you have just been handed. How many rows? How many products? Is the date column one row per day, or several? Four short queries and you know more about a file than an hour of scrolling would tell you.

COUNT(*) counts rows

SELECT COUNT(*) FROM csv returns a single row with a single number. The star here does not mean "all columns" the way it does in a plain SELECT; it means "rows, regardless of what is in them". A row of entirely missing values still counts.

Put a WHERE in front of it and you count the matching rows instead, which turns COUNT into the cheapest question in SQL: how many of these are there? It is worth reaching for before you look at any rows at all. If you expect a couple of hundred western sales and the count says three, you have learned something before you have read a single value.

COUNT(column) counts values

COUNT(satisfaction) is not the same thing. Naming a column tells COUNT to count the rows where that column is not null. On a file with no gaps the two numbers agree, which is why the distinction is easy to miss for months and then bites hard.

The gap between them is the useful part. Run SELECT COUNT(*) AS rows, COUNT(score) AS scored and the difference is exactly the number of missing scores, computed without a single IS NULL. You will use precisely that shape on the support-ticket file in lesson 9, where twenty of the ninety tickets have no satisfaction rating.

DISTINCT lists the values

SELECT DISTINCT channel FROM csv returns each channel once. Three rows, on this file. This is the single most useful query for getting to know a category column, and it is how you discover that your data contains both "Online" and "online", or a value with a trailing space that has been silently failing your filters all afternoon.

DISTINCT applies to the whole select list, not to one column. SELECT DISTINCT region, channel gives you each combination that actually occurs, which is a different and often more interesting question. There is no such thing as making one column distinct and leaving another alone; if you want that, you want GROUP BY.

NULL counts as a value for DISTINCT, and all nulls are treated as the same one, so a column with gaps produces one extra row in the list. That is inconsistent with how NULL behaves everywhere else in SQL, and it is a deliberate exception in the standard rather than a quirk of this engine.

COUNT(DISTINCT column)

Put them together and you get the number of unique values: COUNT(DISTINCT product). Unlike plain DISTINCT, this one ignores nulls, in line with COUNT's usual behavior.

Comparing this against the row count is how you check a claimed key. If a file is supposed to have one row per day and COUNT(DISTINCT date) comes back lower than COUNT(*), you have duplicates and every total you compute from that file will be wrong. Two numbers, five seconds, and it has caught more broken exports for me than any validation tool.

One practical note for later: counting distinct values exactly is expensive on very large tables, because the engine has to remember every value it has seen. Warehouses offer approximate versions such as approx_count_distinct, accurate to a percent or so and vastly cheaper. On a hundred rows it makes no difference at all.

Naming the number

An aggregate without an alias comes back with a header like count_star(). Alias it. Three of the four exercises below check your column names for exactly this reason: a number nobody can read the label of is not an answer, it is a trivia question.

Exercises

1. How many products

Return the number of distinct products in the file, in a column headed products.

Hint

DISTINCT goes inside the COUNT brackets, and the alias goes after them.

2. How many western rows

Return the number of rows in the West region, in a column headed west_rows.

Hint

COUNT with a WHERE clause under it. The filter runs before the count.

3. Which channels exist

List each channel that appears in the file, once each.

Hint

No COUNT in this one. DISTINCT goes straight after SELECT.

4. One row per day?

Return the number of distinct dates, headed days. Compare it to the hundred rows in the file and you have proved whether the date column is unique.

Hint

Same shape as exercise 1, different column.

What to look up next

Read about HyperLogLog, the algorithm behind every approximate distinct count in every warehouse. It is a genuinely beautiful piece of engineering: it counts billions of unique values in a few kilobytes of memory, by paying attention to nothing but the longest run of leading zeros it has seen.