PySpark: keep the newest version of each row with row_number()
To keep the newest version of each row in PySpark, number each key's rows from newest to oldest with row_number() over a window partitioned by the key and ordered by the update time, descending, then keep the rows numbered 1. Order by the column that changes when the row changes, such as updated_at, so that 2 versions of a key never tie.
Module 2: PySpark DataFrames
At the end of this part you can:
- Keep 1 current row per key from a file of events, with a window and row_number()
- Say why row_number() and not rank(), and why the order column must change with the row
- Remove exact repeats with dropDuplicates() and say when it is needed
Take this module into your AI
Open in ChatGPT (new tab)Open in Claude (new tab)Source systems rarely send you a clean table. They send events: an order was placed, then shipped, then perhaps sent again by a retry. Turning those into 1 row per order, the order as it is now, is the most common job in data engineering, and this part does it with a window function.
Part 1 ended with a wrong answer. Asked for the orders that are still placed, Spark returned 1001 and 1002, which have since been shipped and cancelled, and 1004 twice. Nothing was wrong with the filter. The data holds every version of each order, and the filter matched the old ones.
Start from the saved orders
This part reads the bronze copy part 1 saved. If you are starting here, run part 1's setup and its read and save first, then:
from pyspark.sql import SparkSession, Window
from pyspark.sql import functions as F
spark = SparkSession.builder.appName("orders").getOrCreate()
LAKE = "lake"
good = spark.read.parquet(f"{LAKE}/bronze_orders").filter(F.col("_bad").isNull()).drop("_bad")
On Databricks, read the table instead: spark.table(f"{catalog}.{schema}.bronze_orders"), with the same filter.
Number the versions, keep the newest
For each order, number its versions from newest to oldest and keep number 1. dropDuplicates() first removes exact repeats, such as the 2 identical rows of order 1004:
newest_first = Window.partitionBy("order_id").orderBy(F.col("updated_at").desc())
current = (
good.dropDuplicates()
.withColumn("version", F.row_number().over(newest_first))
.filter(F.col("version") == 1)
.drop("version")
)
current.orderBy("order_id").show()
+--------+-----------+---------+------+--------+-------------------+-------------------+
|order_id|customer_id| status|amount|currency| ordered_at| updated_at|
+--------+-----------+---------+------+--------+-------------------+-------------------+
| 1001| C01| shipped| 25.50| EUR|2026-10-01 09:15:00|2026-10-02 08:00:00|
| 1002| C02|cancelled| 12.00| EUR|2026-10-01 10:02:00|2026-10-03 09:00:00|
| 1004| C01| placed| 40.00| EUR|2026-10-02 12:45:00|2026-10-02 12:45:00|
| 1005| C09| placed| 8.75| EUR|2026-10-03 14:10:00|2026-10-03 14:10:00|
+--------+-----------+---------+------+--------+-------------------+-------------------+
4 rows, 1 per order, each in its current state. A window is a group of rows that a function looks across without collapsing them, unlike groupBy, which returns 1 row per group. partitionBy("order_id") makes 1 window per order, orderBy sorts inside it, and row_number() counts 1, 2, 3 down the sorted rows.
2 choices decide the answer
The first is the function. row_number() gives every row its own number, even when 2 rows tie on the order column. rank() gives tied rows the same number, so when 2 versions share a time, both are number 1 and you keep both. To keep exactly 1 row per key, use row_number().
The second is the column you order by: updated_at, when the row changed, not ordered_at, when the order was made. An order keeps its ordered_at when its status changes, so ordered by that column the placed and shipped versions of 1001 tie. row_number() still numbers one of them 1, but which one depends on the order Spark meets the rows in, and that can change with the files and how they are split. The same code can then report a shipped order as placed on one run and get it right on the next, which is the worst kind of wrong: it passes a test once.
Try both in the demo, and the repeats too.
newest_first = Window.partitionBy("order_id").orderBy(F.col("updated_at").desc())
good.dropDuplicates().withColumn("version", F.row_number().over(newest_first))
.filter(F.col("version") == 1)| order_id | status | updated_at | version | |
|---|---|---|---|---|
| 1001 | shipped | 2026-10-02 08:00 | 1 | Kept |
| placed | 2026-10-01 09:15 | 2 | ||
| 1002 | cancelled | 2026-10-03 09:00 | 1 | Kept |
| placed | 2026-10-01 10:02 | 2 | ||
| 1004 | placed | 2026-10-02 12:45 | 1 | Kept |
| 1005 | placed | 2026-10-03 14:10 | 1 | Kept |
rows for 4 orders
4 rows, 1 per order, each in its newest state.
The 7 rows of the module's orders that parsed. Each result is what Spark 4.2 kept for that choice; where it shows a ? Spark kept one of the tied rows, and which one is not guaranteed.
With row_number() the window keeps 1 of the identical 1004 rows even without dropDuplicates(), so here it changes nothing. Keep it anyway: it costs little, and it protects the next person who switches to rank(). Do not reach for dropDuplicates(["order_id"]) to keep 1 row per order, either. It keeps 1 row for each order, but not one you can choose.
This pattern, 1 current row per key that is overwritten as the key changes, is the heart of slowly changing dimensionSlowly Changing DimensionsThe ways a warehouse handles a change to a descriptive record, such as a customer's address: type 1 overwrites it, type 2 adds a new dated version, and type 3 keeps the previous value in an extra column. type 1, which module 1 covers in SQL. Type 2, which keeps every version with the dates it was true, builds on the same window. Part 3 joins the current orders to the customers and adds them up.
Check yourself
1 questions, each in a new situation
1. You keep the newest row of each order with row_number() over a window ordered by ordered_at, descending. When an order is shipped, the shop sends a new row with the same ordered_at and a later updated_at. What does the step keep for that order?
Not quite. row_number() always gives each row its own number, so ties do not keep both rows; rank() does that. When 2 rows tie on the order column, which one row_number() numbers 1 depends on the order Spark meets them in, which can change with the files and the partitions. Order by updated_at, which changes when the row changes, and the tie is gone.
Questions
- How do I keep the latest record for each key in PySpark?
- Make a window with Window.partitionBy("key").orderBy(F.col("updated_at").desc()), add a column with F.row_number().over(window), keep the rows where it is 1 and drop the column. Each key keeps exactly 1 row, its newest. Order by a column that changes whenever the row changes, so 2 versions of a key never tie.
- What is the difference between row_number and rank in PySpark?
- row_number() numbers the rows of a window 1, 2, 3 with no repeats, even when rows tie on the order column. rank() gives tied rows the same number and leaves a gap after them, so 2 rows can both be number 1. To keep exactly 1 row per key, use row_number().
- What is the difference between dropDuplicates and distinct in PySpark?
- With no columns, dropDuplicates() and distinct() both remove rows that are the same in every column. dropDuplicates also takes a list of columns, such as dropDuplicates(["order_id"]), and keeps 1 row for each value of them, but it does not let you say which row, so it is no way to keep the newest version. Use a window for that.
Sources