Model::orderBy
Sorts query results by a column in ascending or descending order, defaulting to ascending.
public function orderBy(\Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Contracts\Database\Query\Expression|string $column, string $direction = 'asc'): $thisParameters
| Parameter | Type | Description |
|---|---|---|
$column | \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Contracts\Database\Query\Expression|string | The column to sort by. Supports JSON paths via the -> operator, and a Closure or builder becomes a sorting subquery. |
$directionoptional | string= 'asc' | Sort direction, either 'asc' or 'desc'. The comparison is case-insensitive. |
Return value
$thisThe same builder instance, so calls can be chained. Each call appends another ordering, applied in the order the calls were made.
Errors
Throws InvalidArgumentException with the message 'Order direction must be "asc" or "desc".' when $direction is anything else.
orderBy() appends an order by clause. The first argument is the column, the second the direction — asc or desc, ascending by default. Call it repeatedly to sort by several columns; they apply in call order. orderByDesc() is the shorthand for the descending case.
Beyond plain columns
A JSON path works via the arrow operator: orderBy('location->state'). Passing a Closure or a builder compiles it into a sorting subquery, which is how you sort by a value from a related table without a join. reorder() strips every ordering already applied — useful when a scope or a global default got there first.
What trips people up
PDO cannot bind column names. The direction is validated against asc/desc, but the column is interpolated into the SQL string. Laravel's documentation says plainly that you should never let user input dictate column names, including "order by" columns. Validate a sort parameter against an allow-list.
An invalid direction throws InvalidArgumentException rather than silently defaulting.
Ordering by a non-unique column gives no guarantee of row order within ties, and the order can differ between runs. Under pagination that means rows appearing twice or never. Always append a unique tie-break, usually the primary key.
Example
<?php
use App\Models\User;
$users = User::orderBy('name', 'desc')->get();
// select * from users order by name desc
// Several columns - applied in call order.
$users = User::orderBy('name', 'desc')
->orderBy('email', 'asc')
->get();
// orderByDesc() is the shorthand.
$users = User::orderByDesc('verified_at')->get();
foreach ($users as $user) {
echo $user->name, "\n";
}
// A JSON path via the arrow operator.
$corps = Corporation::where('country', 'US')
->orderBy('location->state')
->get();
echo $corps->count(), "\n";When to use it
- Sort an admin table by a user-chosen column validated against an allow-list.
- Order a leaderboard by score descending with the primary key as a tie-break.
- Sort records by a value nested inside a JSON column.
Frequently asked questions
How do I sort by multiple columns in Eloquent?
Call orderBy() once per column; they are applied in the order you call them, so orderBy('status')->orderBy('created_at', 'desc') sorts by status first and breaks ties by date. There is no array form. Always finish a multi-column sort with a unique column such as the primary key if the results will be paginated.
Is it safe to pass a request parameter to orderBy()?
No. PDO cannot bind column names, so the column is interpolated directly into the SQL string — an unvalidated request value is an injection point, and Laravel's documentation warns about order-by columns specifically. Check the value against an allow-list of sortable columns and fall back to a default. The direction argument is safer, since anything other than asc or desc raises InvalidArgumentException.
Why do rows repeat across paginated pages?
Because the sort column has ties and no unique tie-break, so the database may order tied rows differently between the two page queries, letting LIMIT/OFFSET overlap or skip. Append orderBy('id') — or any unique column — to make the ordering total. Cursor pagination has the same requirement and enforces it more strictly.
More examples
Validating a user-supplied sort column
Both request values are narrowed to a known set before reaching the query, which is the only safe pattern since PDO cannot parameterise identifiers. The direction check is separate because an unrecognised string raises InvalidArgumentException rather than falling back to ascending.
<?php
use App\Models\Product;
$sortable = ['name', 'price_cents', 'created_at'];
$column = in_array(request('sort'), $sortable, true)
? request('sort')
: 'name';
$direction = request('dir') === 'desc' ? 'desc' : 'asc';
$products = Product::where('active', 1)
->orderBy($column, $direction)
->orderBy('id') // deterministic tie-break
->paginate(25);
echo $products->total(), "\n";
// Never do this - the column is interpolated, not bound:
// Product::orderBy(request('sort'))->get();
// And this throws InvalidArgumentException:
// Product::orderBy('name', 'ascending')->get();Why pagination needs a tie-break
The first pair of pages is the classic duplicate-rows bug report: the database is free to return tied rows in any order, and LIMIT/OFFSET on two separate queries can overlap or skip. Appending the primary key gives every row a unique position, so the pages partition the set exactly.
<?php
use App\Models\Article;
// Many articles share a status - ties have no defined order.
$page1 = Article::orderBy('status')->paginate(20);
$page2 = Article::orderBy('status')->paginate(20, page: 2);
// The same article can appear on both pages, or on neither.
echo $page1->total(), "\n";
// Adding a unique column makes the sort total and stable.
$stable = Article::orderBy('status')
->orderBy('id')
->paginate(20);
foreach ($stable as $article) {
echo $article->id, ' ', $article->status, "\n";
}Sorting by a subquery and clearing orderings
The subquery ranks users by their most recent login without joining or loading the logins table. The withCount variant shows the same idea for aggregates — the alias is a real column, so the database sorts it. reorder() is the escape hatch when a scope or a default ordering is already in place.
<?php
use App\Models\User;
use App\Models\Login;
// Sort by a value from another table without a join.
$users = User::orderByDesc(
Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->limit(1)
)->get();
foreach ($users as $user) {
echo $user->name, "\n";
}
// Or let the database do the counting and sort by that alias.
$users = User::withCount('logins')
->orderByDesc('logins_count')
->orderBy('id')
->get();
// reorder() drops every ordering applied so far.
$query = User::orderBy('name');
$unordered = $query->reorder()->get();
echo $unordered->count(), "\n";
Discussion