# How to Sort Query Results in Supabase

- Tool: Supabase
- Difficulty: Beginner
- Time required: 10 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+
- Last updated: March 2026

## TL;DR

Sort query results in Supabase using the .order() modifier on the JavaScript client. Pass the column name and an ascending/descending flag. For multi-column sorting, chain multiple .order() calls — the first is the primary sort, the second breaks ties. Use nullsFirst or nullsLast to control where NULL values appear. In SQL, use standard ORDER BY with ASC/DESC and NULLS FIRST/LAST. Always combine sorting with pagination for large result sets.

## Sorting Query Results with the Supabase JavaScript Client

Displaying data in the right order is fundamental to any application — newest posts first, cheapest products first, alphabetical lists. Supabase exposes PostgreSQL's full sorting capabilities through the .order() method on the JavaScript client. This tutorial covers single-column and multi-column sorting, NULL handling, and combining sort with pagination for production-ready queries.

## Before you start

- A Supabase project with a table containing data
- @supabase/supabase-js v2+ installed in your project
- Basic understanding of SQL ORDER BY concepts

## Step-by-step guide

### 1. Sort by a single column

The .order() method takes a column name and an options object with ascending (boolean). By default, ascending is true (A to Z, 0 to 9, oldest to newest). Set ascending: false for descending order. Always apply .order() before .range() or .limit() to ensure consistent pagination.

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Sort by created_at descending (newest first)
const { data: newest } = await supabase
  .from('posts')
  .select('*')
  .order('created_at', { ascending: false })

// Sort by title ascending (A to Z)
const { data: alphabetical } = await supabase
  .from('posts')
  .select('*')
  .order('title', { ascending: true })

// Sort by price ascending (cheapest first)
const { data: cheapest } = await supabase
  .from('products')
  .select('*')
  .order('price', { ascending: true })
```

**Expected result:** Results are returned in the specified sort order.

### 2. Sort by multiple columns

Chain multiple .order() calls to sort by several columns. The first .order() is the primary sort. When two rows have the same value for the primary column, the second .order() determines their relative position. This is essential for stable pagination — without a tie-breaker, rows with identical primary sort values can appear on different pages between requests.

```
// Primary sort: category ascending, secondary: price descending
const { data } = await supabase
  .from('products')
  .select('*')
  .order('category', { ascending: true })
  .order('price', { ascending: false })

// Sort by status, then by created_at within each status
const { data: tasks } = await supabase
  .from('tasks')
  .select('*')
  .order('status', { ascending: true })
  .order('created_at', { ascending: false })

// Three-level sort
const { data: employees } = await supabase
  .from('employees')
  .select('*')
  .order('department', { ascending: true })
  .order('last_name', { ascending: true })
  .order('first_name', { ascending: true })
```

**Expected result:** Results are sorted by the primary column first, with ties broken by subsequent columns.

### 3. Control NULL value placement

By default, PostgreSQL sorts NULL values last in ascending order and first in descending order. You can override this with the nullsFirst option in the .order() method. This is useful when you want rows with missing data to appear at the end regardless of sort direction, or at the beginning to highlight incomplete records.

```
// NULLs at the end (default for ascending)
const { data: nullsLast } = await supabase
  .from('products')
  .select('*')
  .order('price', { ascending: true, nullsFirst: false })

// NULLs at the beginning (useful to find incomplete data)
const { data: nullsFirst } = await supabase
  .from('products')
  .select('*')
  .order('price', { ascending: true, nullsFirst: true })

// Descending with NULLs at the end
const { data: descNullsLast } = await supabase
  .from('products')
  .select('*')
  .order('price', { ascending: false, nullsFirst: false })
```

**Expected result:** NULL values appear at the position specified by the nullsFirst option.

### 4. Sort with filters and pagination

In real applications, sorting is combined with filters and pagination. Apply filters with .eq(), .gt(), etc., sort with .order(), and paginate with .range(). The order of method calls matters: filters narrow the data, sorting determines order, and range selects the page. The total count from .select('*', { count: 'exact' }) reflects the filtered (not total) row count.

```
const PAGE_SIZE = 10
const page = 0

// Filtered, sorted, paginated query
const { data, count } = await supabase
  .from('products')
  .select('*', { count: 'exact' })
  .eq('category', 'electronics')
  .gte('price', 10)
  .order('price', { ascending: true })
  .order('id', { ascending: true }) // Tie-breaker for stable pagination
  .range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)

console.log(`${count} matching products, showing page ${page + 1}`)
```

**Expected result:** A filtered, sorted page of results with the total matching count for building pagination controls.

## Complete code example

File: `sorted-product-list.ts`

```typescript
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

interface SortConfig {
  column: string
  ascending: boolean
}

interface QueryOptions {
  table: string
  sort: SortConfig[]
  filters?: Record<string, string | number>
  page?: number
  pageSize?: number
}

async function fetchSorted({
  table,
  sort,
  filters = {},
  page = 0,
  pageSize = 20,
}: QueryOptions) {
  const from = page * pageSize
  const to = from + pageSize - 1

  let query = supabase
    .from(table)
    .select('*', { count: 'exact' })

  // Apply filters
  for (const [key, value] of Object.entries(filters)) {
    query = query.eq(key, value)
  }

  // Apply sort columns in order
  for (const { column, ascending } of sort) {
    query = query.order(column, { ascending })
  }

  // Apply pagination
  query = query.range(from, to)

  const { data, error, count } = await query

  return {
    data: data ?? [],
    error,
    totalRows: count ?? 0,
    totalPages: Math.ceil((count ?? 0) / pageSize),
    currentPage: page,
  }
}

// Example usage
async function main() {
  // Cheapest electronics first
  const result = await fetchSorted({
    table: 'products',
    sort: [
      { column: 'price', ascending: true },
      { column: 'name', ascending: true },
    ],
    filters: { category: 'electronics' },
    page: 0,
    pageSize: 10,
  })

  console.log('Products:', result.data)
  console.log(`Page ${result.currentPage + 1} of ${result.totalPages}`)
}

main()
```

## Common mistakes

- **Not specifying .order() and assuming results come in a predictable order** — undefined Fix: Always call .order() explicitly. Without it, PostgreSQL returns rows in whatever order is fastest, which can change between queries and break pagination.
- **Using only a non-unique column for sorting, causing rows to swap between pages** — undefined Fix: Add a unique tie-breaker column (id or created_at) as the final .order() call. This ensures stable sorting when multiple rows share the same value in the primary sort column.
- **Applying .order() after .range(), which has no effect on the query** — undefined Fix: Chain .order() before .range(). The Supabase client builds the query in the order you chain methods, and sort must come before pagination to be meaningful.

## Best practices

- Always specify an explicit sort order with .order() for consistent and predictable results
- Add a unique column as a final tie-breaker in multi-column sorts to guarantee stable pagination
- Use nullsFirst: false when you want NULL values at the end regardless of sort direction
- Combine sorting with { count: 'exact' } and .range() for complete pagination support
- Create database indexes on frequently sorted columns to avoid full table scans
- For composite indexes matching your sort, include columns in the same order: CREATE INDEX idx ON table (col1 ASC, col2 DESC)

## Frequently asked questions

### What is the default sort order if I do not call .order()?

Without .order(), PostgreSQL returns rows in an undefined order — typically the physical storage order. This is not guaranteed and can change after vacuuming, updates, or index changes. Always specify a sort order.

### Can I sort by a column from a related table?

Not directly with .order(). You can sort by columns in the primary table. For sorting by a related table column, create a database view that joins the tables and sort on the view, or use a computed column.

### Does sorting affect query performance?

Sorting without an index requires PostgreSQL to sort all matching rows in memory, which can be slow on large tables. Create an index on columns you frequently sort by: CREATE INDEX idx_products_price ON products (price).

### How do I sort case-insensitively?

The .order() method sorts using PostgreSQL's default collation, which is case-sensitive (uppercase before lowercase). For case-insensitive sorting, create a computed column with lower(name) or use a SQL view, then sort on that column.

### Can I sort by a jsonb field?

Not directly with .order(). Create a generated column that extracts the JSON value: ALTER TABLE products ADD COLUMN color text GENERATED ALWAYS AS (metadata->>'color') STORED; Then sort by the generated column.

### Can RapidDev help optimize my Supabase queries with proper sorting and indexing?

Yes. RapidDev can analyze your query patterns, create optimal indexes for your sort and filter combinations, and implement efficient paginated queries for your application.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-sort-query-results-in-supabase
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-sort-query-results-in-supabase
