Sort a data frame by one or more columns, ascending or descending — base R and dplyr
Programming
Reorder (sort) the rows of a data frame in R with base-R order() and dplyr arrange(). Sort ascending or descending, by several columns at once, and in a custom order — plus how order() differs from sort() and where missing values land. Both engines, side by side.
Published
June 26, 2026
Modified
July 7, 2026
TipKey takeaways
base R:df[order(df$x), ] sorts rows by column x; order(df$x, decreasing = TRUE) for descending.
Multi-key:order(df$x, df$y) and arrange(df, x, y) sort by x, breaking ties with y.
order() returns the positions that sort the data; sort() returns sorted values — you reorder rows with order().
Missing values go last by default in both; control it with na.last (base R) — arrange() always puts NA last.
Introduction
You want your table sorted — biggest scores first, alphabetical by group, or oldest-to-newest by date. Reordering rows makes a data frame easier to read, sets up a “keep the top N” step, or arranges records before deduplication.
There are two routes, and you’ll meet both in real code:
base R — order() returns the row positions that put a column in order; index with them.
dplyr — arrange() sorts in one readable call, with desc() for descending.
This lesson shows both, side by side, for sorting ascending, descending, by several columns, and in a custom order. The two give the same result.
We use the built-in mtcars data set. We’ll keep three columns and a few rows so the sort is easy to see:
Read it as “give me the rows of cars, in the order that sorts mpg.” The lowest mpg is now first. The comma matters: cars[order(cars$mpg), ] reorders rows (and keeps all columns) — don’t forget the trailing comma.
dplyr — arrange()
arrange() sorts ascending by the named column — no indexing, no comma:
Negating (order(-cars$mpg)) gives the same result for numbers and is handy when you want some keys descending and others ascending (next section). decreasing = TRUE flips every key at once.
dplyr — desc()
Wrap the column in desc() to sort it from high to low:
Highest mpg first — the same order as the base-R decreasing = TRUE route.
Sort by several columns
To break ties, give more columns. The first column sorts; the second decides the order within equal values of the first, and so on.
base R — multiple keys, mixed direction
List the keys in order(). Negate a numeric key to sort that one descending while the others stay ascending — here sort by cyl ascending, then hp descending within each cyl:
Same order: cyl ascending, hp descending within each group. arrange() reads almost like the sentence describing the sort.
Sort in a custom order
Sometimes the order you want isn’t alphabetical or numeric — say, low < medium < high. Encode that order in a factor, then sort by it. The factor’s levels define the order.
sizes <-data.frame(item =c("box", "crate", "bag"),size =c("high", "low", "medium"))# Define the order with factor levelssizes$size <-factor(sizes$size, levels =c("low", "medium", "high"))sizes[order(sizes$size), ]
item size
2 crate low
3 bag medium
1 box high
Rows come out low, medium, high — the level order, not alphabetical. arrange(sizes, size) sorts the same factor identically. This is the standard way to impose a logical (non-alphabetical) order on categories.
Try it live
The blocks above ran at build time. Edit this one and press Run to reorder rows on your own — it executes in your browser via webR (no install).
🟢 With an AI agent
Paste your data frame’s names() and ask Prova“sort these rows by date descending, breaking ties by customer name” — it writes the arrange() (or order()) call, and because the runtime is right here you run it to confirm the rows land in the order you meant on your data. Ask Prova →
Working in Python? A pandas sort_values guide is coming to the Python series.
Common issues
sort() returns a single column, not the reordered data frame.sort() sorts a vector of values; it can’t reorder a data frame. To sort rows you need the positions that sort a column — that’s order(): df[order(df$x), ]. Remember the trailing comma, or you’ll subset columns instead of rows.
The sort looks alphabetical when you wanted a logical order. Character columns sort alphabetically, so "high", "low", "medium" come out in that (wrong) order. Convert the column to a factor with explicit levels in the order you want, then sort by it.
Missing values end up where you didn’t expect. By default both order() and arrange() push NA to the bottom. In base R, pass order(df$x, na.last = FALSE) to send NA to the top, or na.last = NA to drop those rows from the result. arrange() always sorts NA last regardless of direction.
Frequently asked questions
NoteHow do I sort a data frame in R?
Base R: df[order(df$x), ] sorts the rows by column x. dplyr: arrange(df, x). Both sort ascending by default and give the same row order.
NoteHow do I sort in descending order in R?
Base R: df[order(df$x, decreasing = TRUE), ], or negate a numeric column with order(-df$x). dplyr: wrap the column in desc() — arrange(df, desc(x)).
NoteHow do I sort by multiple columns in R?
List the keys: df[order(df$a, df$b), ] or arrange(df, a, b). The first column sorts, the second breaks ties. Mix directions by negating a base-R key (order(df$a, -df$b)) or wrapping a dplyr key in desc().
NoteWhat’s the difference between sort() and order() in R?
sort() returns the sorted values of a vector; order() returns the positions that put a vector in order. You reorder data frame rows with order() — df[order(df$x), ] — because you need the positions to index the rows, not the values alone.
NoteHow do I sort rows in a custom (non-alphabetical) order?
Make the column a factor with levels in the order you want — factor(x, levels = c("low", "medium", "high")) — then sort by it with order() or arrange(). The factor’s level order drives the sort instead of alphabetical order.
Test your understanding
ImportantExercise: sort two ways
Sort the data frame below by cylascending, then by mpgdescending within each cylinder group — first with base-R order(), then with dplyr arrange() — and check that both give identical row orders.
# base R: order(cars$cyl, -cars$mpg) -> negate mpg for descending# dplyr: arrange(cars, cyl, desc(mpg))
NoteQuick check: which sorts mpg from high to low?
You want mtcars sorted by mpg descending. Which is correct?
A. sort(mtcars$mpg, decreasing = TRUE) B. mtcars[order(mtcars$mpg, decreasing = TRUE), ] C. arrange(mtcars, mpg)
Show answer
B.order(..., decreasing = TRUE) returns the positions that sort mpg from high to low, and indexing with them reorders the whole data frame. A returns only the sorted mpgvalues (the rest of the table is lost). C sorts ascending — you’d need arrange(mtcars, desc(mpg)).
Conclusion
Reordering rows comes down to two equivalent tools: base-R df[order(df$x), ] (negate or decreasing = TRUE for descending) or dplyr arrange(df, x) (with desc() for descending). List several keys to break ties, encode a factor’s levels for a custom order, and remember that order() returns positions while sort() returns values.
This lesson is reproducible: every result on this page was produced by the code shown — edit any block and press Run to reproduce it in your browser. The runtime is the judge.