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
Sort by a single column
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.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// Sort by created_at descending (newest first)9const { data: newest } = await supabase10 .from('posts')11 .select('*')12 .order('created_at', { ascending: false })1314// Sort by title ascending (A to Z)15const { data: alphabetical } = await supabase16 .from('posts')17 .select('*')18 .order('title', { ascending: true })1920// Sort by price ascending (cheapest first)21const { data: cheapest } = await supabase22 .from('products')23 .select('*')24 .order('price', { ascending: true })Expected result: Results are returned in the specified sort order.
Sort by multiple columns
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.
1// Primary sort: category ascending, secondary: price descending2const { data } = await supabase3 .from('products')4 .select('*')5 .order('category', { ascending: true })6 .order('price', { ascending: false })78// Sort by status, then by created_at within each status9const { data: tasks } = await supabase10 .from('tasks')11 .select('*')12 .order('status', { ascending: true })13 .order('created_at', { ascending: false })1415// Three-level sort16const { data: employees } = await supabase17 .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.
Control NULL value placement
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.
1// NULLs at the end (default for ascending)2const { data: nullsLast } = await supabase3 .from('products')4 .select('*')5 .order('price', { ascending: true, nullsFirst: false })67// NULLs at the beginning (useful to find incomplete data)8const { data: nullsFirst } = await supabase9 .from('products')10 .select('*')11 .order('price', { ascending: true, nullsFirst: true })1213// Descending with NULLs at the end14const { data: descNullsLast } = await supabase15 .from('products')16 .select('*')17 .order('price', { ascending: false, nullsFirst: false })Expected result: NULL values appear at the position specified by the nullsFirst option.
Sort with filters and pagination
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.
1const PAGE_SIZE = 102const page = 034// Filtered, sorted, paginated query5const { data, count } = await supabase6 .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 pagination12 .range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)1314console.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
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78interface SortConfig {9 column: string10 ascending: boolean11}1213interface QueryOptions {14 table: string15 sort: SortConfig[]16 filters?: Record<string, string | number>17 page?: number18 pageSize?: number19}2021async function fetchSorted({22 table,23 sort,24 filters = {},25 page = 0,26 pageSize = 20,27}: QueryOptions) {28 const from = page * pageSize29 const to = from + pageSize - 13031 let query = supabase32 .from(table)33 .select('*', { count: 'exact' })3435 // Apply filters36 for (const [key, value] of Object.entries(filters)) {37 query = query.eq(key, value)38 }3940 // Apply sort columns in order41 for (const { column, ascending } of sort) {42 query = query.order(column, { ascending })43 }4445 // Apply pagination46 query = query.range(from, to)4748 const { data, error, count } = await query4950 return {51 data: data ?? [],52 error,53 totalRows: count ?? 0,54 totalPages: Math.ceil((count ?? 0) / pageSize),55 currentPage: page,56 }57}5859// Example usage60async function main() {61 // Cheapest electronics first62 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 })7273 console.log('Products:', result.data)74 console.log(`Page ${result.currentPage + 1} of ${result.totalPages}`)75}7677main()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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation