Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Sort Query Results in Supabase

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.

What you'll learn

  • How to sort by a single column using .order()
  • How to sort by multiple columns for tie-breaking
  • How to handle NULL values in sort order
  • How to combine sorting with pagination and filters
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read10 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
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.

Prerequisites

  • 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.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// Sort by created_at descending (newest first)
9const { data: newest } = await supabase
10 .from('posts')
11 .select('*')
12 .order('created_at', { ascending: false })
13
14// Sort by title ascending (A to Z)
15const { data: alphabetical } = await supabase
16 .from('posts')
17 .select('*')
18 .order('title', { ascending: true })
19
20// Sort by price ascending (cheapest first)
21const { data: cheapest } = await supabase
22 .from('products')
23 .select('*')
24 .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.

typescript
1// Primary sort: category ascending, secondary: price descending
2const { data } = await supabase
3 .from('products')
4 .select('*')
5 .order('category', { ascending: true })
6 .order('price', { ascending: false })
7
8// Sort by status, then by created_at within each status
9const { data: tasks } = await supabase
10 .from('tasks')
11 .select('*')
12 .order('status', { ascending: true })
13 .order('created_at', { ascending: false })
14
15// Three-level sort
16const { data: employees } = await supabase
17 .from('employees')
18 .select('*')
19 .order('department', { ascending: true })
20 .order('last_name', { ascending: true })
21 .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.

typescript
1// NULLs at the end (default for ascending)
2const { data: nullsLast } = await supabase
3 .from('products')
4 .select('*')
5 .order('price', { ascending: true, nullsFirst: false })
6
7// NULLs at the beginning (useful to find incomplete data)
8const { data: nullsFirst } = await supabase
9 .from('products')
10 .select('*')
11 .order('price', { ascending: true, nullsFirst: true })
12
13// Descending with NULLs at the end
14const { data: descNullsLast } = await supabase
15 .from('products')
16 .select('*')
17 .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.

typescript
1const PAGE_SIZE = 10
2const page = 0
3
4// Filtered, sorted, paginated query
5const { data, count } = await supabase
6 .from('products')
7 .select('*', { count: 'exact' })
8 .eq('category', 'electronics')
9 .gte('price', 10)
10 .order('price', { ascending: true })
11 .order('id', { ascending: true }) // Tie-breaker for stable pagination
12 .range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
13
14console.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 working example

sorted-product-list.ts
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8interface SortConfig {
9 column: string
10 ascending: boolean
11}
12
13interface QueryOptions {
14 table: string
15 sort: SortConfig[]
16 filters?: Record<string, string | number>
17 page?: number
18 pageSize?: number
19}
20
21async function fetchSorted({
22 table,
23 sort,
24 filters = {},
25 page = 0,
26 pageSize = 20,
27}: QueryOptions) {
28 const from = page * pageSize
29 const to = from + pageSize - 1
30
31 let query = supabase
32 .from(table)
33 .select('*', { count: 'exact' })
34
35 // Apply filters
36 for (const [key, value] of Object.entries(filters)) {
37 query = query.eq(key, value)
38 }
39
40 // Apply sort columns in order
41 for (const { column, ascending } of sort) {
42 query = query.order(column, { ascending })
43 }
44
45 // Apply pagination
46 query = query.range(from, to)
47
48 const { data, error, count } = await query
49
50 return {
51 data: data ?? [],
52 error,
53 totalRows: count ?? 0,
54 totalPages: Math.ceil((count ?? 0) / pageSize),
55 currentPage: page,
56 }
57}
58
59// Example usage
60async function main() {
61 // Cheapest electronics first
62 const result = await fetchSorted({
63 table: 'products',
64 sort: [
65 { column: 'price', ascending: true },
66 { column: 'name', ascending: true },
67 ],
68 filters: { category: 'electronics' },
69 page: 0,
70 pageSize: 10,
71 })
72
73 console.log('Products:', result.data)
74 console.log(`Page ${result.currentPage + 1} of ${result.totalPages}`)
75}
76
77main()

Common mistakes when sorting Query Results in Supabase

Why it's a problem: Not specifying .order() and assuming results come in a predictable order

How to avoid: Always call .order() explicitly. Without it, PostgreSQL returns rows in whatever order is fastest, which can change between queries and break pagination.

Why it's a problem: Using only a non-unique column for sorting, causing rows to swap between pages

How to avoid: 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.

Why it's a problem: Applying .order() after .range(), which has no effect on the query

How to avoid: 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)

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Supabase products table with name, price, category, and created_at columns. Show me how to sort by price ascending, by category then price, handle NULLs in the price column, and combine sorting with pagination using the JavaScript client.

Supabase Prompt

Write a reusable TypeScript function that queries a Supabase table with configurable multi-column sorting, optional filters, and offset pagination. Include total count for page navigation.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.