Part 4: Grouping
References: - ClickHouse GROUP BY clause - ClickHouse HAVING clause
Welcome to the intermediate questions. From here on you will be given fewer clues, and you will often need to work out which columns you need from the shape of the result rather than from the question.
Another way to reduce the amount of output, and to produce a summary of records rather than a list of them, is a GROUP BY clause. At a basic level this creates one row for each matching value and tells you how many times it appeared, instead of returning every individual record.
Grouping rule: every column in the
SELECTclause that is not wrapped in an aggregate function must also appear in theGROUP BYclause, as a comma separated list in the same style as theSELECT.
4a. Group basics
Write a query that returns a count of the top 15 entries containing Microsoft in the name column of the endpoint_program table, across all hosts, regardless of case, ordered by the highest count, in the past 4 days.
Hint - ignoring case
You met `lower()` in [Part 2](hunt_lab_training_2.md#2b-case-sensitivity). It works the same way here.Answer
SELECT
name,
COUNT(*) AS Installs
FROM endpoint_program
WHERE
_time_observed >= toUnixTimestamp(subtractDays(now(), 4)) * 1000
AND lower(name) LIKE '%microsoft%'
GROUP BY name
ORDER BY Installs desc
LIMIT 15
Notice how large those numbers are. That is because COUNT(*) counts every individual record in the table, and the telemetry is scanned and added to hourly, so the same program on the same host is counted many times over.
uniqExact gives us a value that counts distinct entries instead.
Modify the query above so it returns the top 15 Microsoft programs showing the exact number of hosts each one is installed on, in descending order.
Answer
SELECT
name,
uniqExact(_hostname) AS Host_Count
FROM endpoint_program
WHERE
_time_observed >= toUnixTimestamp(subtractDays(now(), 4)) * 1000
AND lower(name) LIKE '%microsoft%'
GROUP BY name
ORDER BY Host_Count desc
LIMIT 15
![]()
uniqExactagainstuniq: ClickHouse also has auniq()function, which is faster but approximate. UseuniqExact()when the number has to be right, which for host counts it usually does.
4b. Filtering grouped results
A WHERE clause filters records before they are grouped, so it cannot filter on a count that does not exist yet. To filter the results a GROUP BY produces, use a HAVING clause. It goes after the GROUP BY and behaves much like a WHERE.
Using the query from before, add a HAVING clause so that only programs installed on 45 or fewer hosts are returned.
Answer
SELECT
name,
uniqExact(_hostname) AS Host_Count
FROM endpoint_program
WHERE
_time_observed >= toUnixTimestamp(subtractDays(now(), 4)) * 1000
AND lower(name) LIKE '%microsoft%'
GROUP BY name
HAVING Host_Count <= 45
ORDER BY Host_Count desc
LIMIT 15
Why this matters for hunting: rare is interesting. A
HAVINGclause that keeps only the low counts is the simplest form of frequency analysis, and it is how you surface the one host running something the other nine hundred are not.
Great! Now you can move onto Part 5 of the Hunt Lab training.