Skip to content
Everyone knows 3 min read · Aggregation 28 practice problems ↓

groupBy + agg

Aggregate rows per group: counts, sums, averages, and how count(*) differs from count(col).

You will learn

  • How groupBy and agg turn many rows into one per group
  • The difference between count(*), count(col) and countDistinct
  • How to filter on an aggregate (HAVING)
  • Why aggregations are cheaper than they look: partial aggregation

Read first

Comfortable with these? Read on.

TL;DR groupBy(keys).agg(...) returns one row per distinct key combination, with each aggregate computed over that group's rows. Aggregate functions skip nulls, except count("*").

What it does

groupBy on its own returns a GroupedData object, not a DataFrame. Calling agg (or a shortcut like count()) finishes the job and returns a DataFrame with the grouping columns followed by the aggregates.

Step by step

employees

namedeptsalary
AshaEngineering72000
ChitraEngineering95000
BenSales48000
EshaSales50000
DevHR51000

Output

deptpeopleavg_salary
Engineering283500
Sales249000
HR151000
  1. 1
    Rows are split into groups by the key, here dept.
  2. 2
    Each aggregate is computed per group: count counts rows, avg averages non-null salaries.
  3. 3
    The output has one row per group. Output order is not guaranteed: add orderBy if you need one.

Run the example

PySparkSpark SQL
from pyspark.sql import functions as F
result = (employees
    .groupBy("dept")
    .agg(
        F.count("*").alias("people"),
        F.round(F.avg("salary")).alias("avg_salary"),
        F.max("salary").alias("top_salary"),
    )
    .orderBy("dept"))
SELECT dept,
       COUNT(*)            AS people,
       ROUND(AVG(salary))  AS avg_salary,
       MAX(salary)         AS top_salary
FROM employees
GROUP BY dept
ORDER BY dept

Switch to PySpark to edit and run this example in your browser.

Farid's department is null, and he still gets a group: groupBy treats all nulls in a key as one group. In ascending order that null group sorts first.

Three ways to count

ExpressionCountsWith nulls
F.count("*") / COUNT(*)Rows in the groupCounted
F.count("manager_id")Non-null values of that columnSkipped
F.countDistinct("dept")Distinct non-null valuesSkipped

For a quick estimate on huge data, F.approx_count_distinct uses HyperLogLog++ and is far cheaper than an exact distinct count, with a default relative error of about 5%.

Filtering on an aggregate

SQL has HAVING for conditions on aggregates. In PySpark you just filter after the aggregation; Catalyst produces the same plan.

PySparkSpark SQL
(employees.groupBy("dept")
    .agg(F.count("*").alias("n"))
    .filter(F.col("n") >= 2))
SELECT dept, COUNT(*) AS n
FROM employees
GROUP BY dept
HAVING COUNT(*) >= 2

Under the hood: partial aggregation

An aggregation needs all rows of a key on one executor, which means a shuffle. But Spark does not ship every row. It aggregates in two phases:

  1. Map side

    Each partition computes partial results per key

  2. Shuffle

    Only one partial row per key per partition moves

  3. Reduce side

    Partials are merged into final results

For avg, the partial result is a (sum, count) pair; the final step divides. That is why the plan shows two HashAggregate nodes around an Exchange. With few distinct keys, this shuffles almost nothing.

Why this matters: functions like collect_list cannot shrink their input in the map phase, so they shuffle every value. They are the aggregations to watch on big data.

Common mistakes

Selecting a column that is neither grouped nor aggregated

In SQL, SELECT dept, name ... GROUP BY dept fails. In PySpark there is no way to express it: you must aggregate name somehow, for example with max_by or collect_list.

Using count(col) when you mean rows

F.count("manager_id") skips the null manager and undercounts people.

Relying on output order

groupBy output order depends on hashing and partitioning. Always orderBy when order matters.

Key takeaways

  • groupBy(...).agg(...) returns one row per key combination.
  • count("*") counts rows; count(col) skips nulls.
  • Filter after agg to get SQL's HAVING.
  • Spark aggregates before and after the shuffle, so most aggregations move little data.

Check yourself

3 questions

1. A group has 5 rows; 2 have a null bonus. What does F.count("bonus") return?

Show the answer

3. count(col) counts non-null values only. count("*") would return 5.

2. What is the PySpark equivalent of SQL HAVING?

Show the answer

A filter after agg. Filter on the aggregated column after agg. Catalyst treats it exactly like HAVING.

3. Why does the physical plan of an aggregation show two HashAggregate nodes?

Show the answer

Partial aggregation before the shuffle, final aggregation after. Spark pre-aggregates on each partition so only partial results are shuffled, then merges them.

Practice it

Interview problems that use groupBy + agg: write the PySpark, run it, and get graded on hidden tests.

Solve: Find Repeat Customers →
See all 28 problems →

Go deeper

Primary sources: DataFrame.groupBy · GROUP BY clause