Skip to content
Good engineers know 3 min read · Window 6 practice problems ↓

row_number / rank

Number and rank rows inside a window, and how the three ranking functions treat ties.

You will learn

  • What a window is, and how it differs from groupBy
  • How row_number, rank and dense_rank treat ties
  • The top-N-per-group pattern
  • What a window costs: shuffle plus sort

Read first

Comfortable with these? Read on.

TL;DR A window function computes a value for every row from a group of related rows, without collapsing them. row_number gives 1, 2, 3 with no ties; rank gives tied rows the same number and leaves gaps; dense_rank leaves no gaps.

What a window is

groupBy turns each group into one row. A window keeps every row and adds a column computed over that row's group, called its partition, in a given order. You define it once with Window.partitionBy(...).orderBy(...) and apply functions with .over(w).

groupBy + agg

Many rows in, one row per group out.

Window function

Many rows in, the same rows out, each with a value computed from its partition.

Three ways to number rows

Ranking players by points within each team. Bo and Cy tie on 85:

playerpointsrow_numberrankdense_rank
Ana90111
Bo85222
Cy85322
Di70443
  • row_number always counts 1, 2, 3, 4. For the tie it picks an order arbitrarily: Bo could be 3 tomorrow.
  • rank gives ties the same rank and skips the next numbers, like a sports ranking: two second places, then fourth.
  • dense_rank gives ties the same rank without gaps: 1, 2, 2, 3.

Run the example

PySparkSpark SQL
from pyspark.sql import functions as F
from pyspark.sql.window import Window

w = Window.partitionBy("team").orderBy(F.col("points").desc())
result = scores.select(
    "team", "player", "points",
    F.row_number().over(w).alias("row_number"),
    F.rank().over(w).alias("rank"),
    F.dense_rank().over(w).alias("dense_rank"),
)
SELECT team, player, points,
       ROW_NUMBER() OVER w AS row_number,
       RANK()       OVER w AS rank,
       DENSE_RANK() OVER w AS dense_rank
FROM scores
WINDOW w AS (PARTITION BY team ORDER BY points DESC)

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

Top N per group

The classic interview pattern: rank inside each group, then filter. The ranking function you choose decides what happens at the cut-off.

PySparkSpark SQL · Top 2 per team
from pyspark.sql import functions as F
from pyspark.sql.window import Window

w = Window.partitionBy("team").orderBy(F.col("points").desc())
result = (scores
    .withColumn("r", F.dense_rank().over(w))
    .filter(F.col("r") <= 2))
SELECT * FROM (
  SELECT *, DENSE_RANK() OVER (
    PARTITION BY team ORDER BY points DESC) AS r
  FROM scores)
WHERE r <= 2

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

Question asks forUse
Exactly N rows per grouprow_number, with a tiebreaker in the order
Everyone in the top N positions, ties includedrank
The top N distinct valuesdense_rank

Under the hood

A window needs all rows of a partition together and sorted. Spark shuffles by the partition keys, then sorts each partition by the partition and order keys, then streams through the sorted rows. In the plan: Exchange hashpartitioning(team) → Sort → Window.

Watch out: a window with no partitionBy moves every row to a single partition, and one task does all the work. Spark warns "No Partition Defined for Window operation!". On big data that is a guaranteed slow job or out-of-memory error.

Several window functions over the same window spec share one shuffle and one sort, so computing row_number, rank and lag together is cheap. A different partitionBy means another shuffle.

Common mistakes

Using row_number when ties matter

One of the tied rows is dropped arbitrarily at the cut-off. Use rank or dense_rank, or add a tiebreaker.

A window without partitionBy

All rows go to one task. Partition by something, or compute a global rank another way.

Expecting the window order to order the output

The orderBy inside the window only orders rows within each partition for the function. Sort the result separately if you need it.

Key takeaways

  • Window functions keep every row and add a value computed over its partition.
  • row_number has no ties, rank leaves gaps, dense_rank does not.
  • Top N per group is rank-then-filter; choose the function by how ties should behave.
  • A window costs a shuffle and a sort; always partition it.

Check yourself

3 questions

1. Values in order are 100, 90, 90, 80. What does rank() give the 80?

Show the answer

4. rank gives the two 90s rank 2 and skips 3, so 80 is 4th. dense_rank would give 3.

2. You need exactly one row per customer: their latest order. Which function?

Show the answer

row_number with a tiebreaker. row_number guarantees one row per partition; a tiebreaker in the order makes the choice deterministic.

3. What happens with Window.orderBy("ts") and no partitionBy on a 1 TB table?

Show the answer

All rows are moved to one partition and processed by one task. Without partition keys the whole dataset is one window partition, so one task processes everything.

Practice it

Interview problems that use row_number / rank: write the PySpark, run it, and get graded on hidden tests.

Solve: Latest Order per Customer →
See all 6 problems →

Go deeper

Primary sources: functions.row_number · Window functions (SQL)