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

How to Implement Data Sorting in Retool Tables

Enable sorting on a Retool table by toggling 'Allow sort' in the Table Inspector. Set a default sort in the Inspector's Sort section. For programmatic sorting, use table1.setSort([{ columnId: 'created_at', desc: true }]) in a JS Query. For large datasets, add SQL ORDER BY {{ table1.sortColumn }} {{ table1.sortDirection }} for server-side sorting.

What you'll learn

  • How to enable and configure default column sorting in the Table Inspector
  • How to use table1.setSort() programmatically from a JS Query or event handler
  • How to implement multi-column sort with Shift+click
  • How to use SQL ORDER BY for server-side sorting on large datasets
  • The difference between client-side and server-side sorting and when to use each
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read15-20 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Enable sorting on a Retool table by toggling 'Allow sort' in the Table Inspector. Set a default sort in the Inspector's Sort section. For programmatic sorting, use table1.setSort([{ columnId: 'created_at', desc: true }]) in a JS Query. For large datasets, add SQL ORDER BY {{ table1.sortColumn }} {{ table1.sortDirection }} for server-side sorting.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required15-20 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

Client-Side and Server-Side Sorting in Retool Tables

Retool tables support several sorting approaches. Client-side sorting (the default) sorts already-loaded data in the browser — it works well for tables with up to a few thousand rows. Server-side sorting delegates ORDER BY to your SQL query, which is necessary for large datasets or when you need sorted pagination.

This tutorial covers both approaches, plus programmatic sorting via table1.setSort() for scenarios where you want buttons or event handlers to control the sort order, and multi-column sorting for complex sort requirements.

Prerequisites

  • A Retool app with a Table component connected to a query
  • For server-side sorting: a SQL database resource
  • Basic familiarity with the Table Inspector panel

Step-by-step guide

1

Enable Column Sorting in the Table Inspector

Select your Table component. In the Inspector, scroll to the Interaction section and ensure 'Allow sort' is enabled (it is on by default). Click the gear icon on individual columns to enable or disable sorting per column. Columns with sorting enabled show a sort arrow icon in the header — users click it to toggle ascending/descending. For columns where sorting doesn't make sense (e.g. action buttons, image columns), disable sorting on those columns individually.

Expected result: Sort arrow icons appear in column headers, and clicking them toggles ascending/descending sort.

2

Set a Default Sort

To sort the table by a specific column when the app loads, configure the default sort in the Table Inspector. Find the Sort section (or Default sort settings) and specify the column and direction. This applies immediately when the app loads without requiring user interaction. In the Inspector, look for 'Default sort column' and 'Default sort direction' fields. Enter the column key (e.g. 'created_at') and choose 'descending' or 'ascending'.

typescript
1// Alternative: set default sort via table1.setSort() in a Page Load query
2// (JS Query with 'Run this query on page load' enabled)
3await table1.setSort([{ columnId: 'created_at', desc: true }]);
4
5// Multiple default sorts (multi-column):
6await table1.setSort([
7 { columnId: 'department', desc: false },
8 { columnId: 'last_name', desc: false }
9]);

Expected result: The table loads already sorted by the configured default column.

3

Use table1.setSort() for Programmatic Sorting

Use table1.setSort() in JS Queries or event handlers to programmatically control the table sort. This is useful for 'Sort by date' and 'Sort by name' buttons, or for resetting sort to a known state after data operations. table1.setSort() takes an array of sort objects, each with columnId (the column key, not the display name) and desc (boolean).

typescript
1// In a JS Query or event handler:
2// Sort by created_at descending (newest first):
3await table1.setSort([{ columnId: 'created_at', desc: true }]);
4
5// Sort by last_name ascending (A-Z):
6await table1.setSort([{ columnId: 'last_name', desc: false }]);
7
8// Reset sort (no sort applied):
9await table1.setSort([]);
10
11// Toggle sort direction based on current state:
12const currentSort = table1.sort;
13const isDesc = currentSort?.[0]?.desc ?? false;
14await table1.setSort([{ columnId: 'created_at', desc: !isDesc }]);
15
16// Note: table1.setSort() is async — await it before reading table1.sort

Expected result: Clicking programmatic sort buttons immediately reorders the table rows.

4

Implement Multi-Column Sorting

Users can apply multi-column sorting by holding Shift while clicking a column header. The first click sets the primary sort; Shift+click on another column adds a secondary sort. For programmatic multi-column sort, pass multiple objects in the setSort() array. The array order determines sort priority — the first element is the primary sort.

typescript
1// Multi-column sort: department ascending, then last_name ascending:
2await table1.setSort([
3 { columnId: 'department', desc: false },
4 { columnId: 'last_name', desc: false },
5 { columnId: 'first_name', desc: false }
6]);
7
8// Business-logic sort: priority descending, then due_date ascending:
9await table1.setSort([
10 { columnId: 'priority', desc: true },
11 { columnId: 'due_date', desc: false }
12]);

Expected result: The table applies multi-column sorting, showing a sort indicator on each sorted column.

5

Implement Server-Side Sorting with SQL ORDER BY

For large datasets (10,000+ rows), client-side sorting is impractical since all data must be loaded first. Implement server-side sorting by adding ORDER BY to your SQL query, driven by table1.sortColumn and table1.sortDirection. Important: you must sanitize the column name to prevent SQL injection since it cannot be parameterized. Use a whitelist mapping approach.

typescript
1-- SQL Query with server-side sorting:
2-- IMPORTANT: Column names cannot be parameterized in SQL,
3-- so validate using a whitelist in a transformer first.
4
5-- In the query, use a CASE expression for safe dynamic sorting:
6SELECT id, first_name, last_name, email, created_at, revenue, status
7FROM users
8WHERE
9 ({{ searchInput.value }} = '' OR
10 first_name ILIKE {{ '%' + searchInput.value + '%' }})
11ORDER BY
12 CASE
13 WHEN {{ table1.sortColumn || 'created_at' }} = 'first_name' THEN first_name
14 WHEN {{ table1.sortColumn || 'created_at' }} = 'last_name' THEN last_name
15 WHEN {{ table1.sortColumn || 'created_at' }} = 'email' THEN email
16 WHEN {{ table1.sortColumn || 'created_at' }} = 'status' THEN status
17 ELSE NULL
18 END
19 {{ table1.sortDirection === 'descend' ? 'DESC' : 'ASC' }},
20 CASE
21 WHEN {{ table1.sortColumn || 'created_at' }} IN ('revenue', 'created_at')
22 THEN NULL ELSE NULL
23 END,
24 -- Numeric/date sorts need separate CASE:
25 CASE
26 WHEN {{ table1.sortColumn || 'created_at' }} = 'revenue'
27 THEN revenue END
28 {{ table1.sortDirection === 'descend' ? 'DESC' : 'ASC' }},
29 CASE
30 WHEN {{ table1.sortColumn || 'created_at' }} = 'created_at'
31 THEN created_at END
32 {{ table1.sortDirection === 'descend' ? 'DESC' : 'ASC' }}
33LIMIT {{ table1.pageSize || 50 }}
34OFFSET {{ (table1.page - 1) * (table1.pageSize || 50) || 0 }};

Expected result: Clicking a column header re-queries the database with the correct ORDER BY, returning a properly sorted page of results.

6

Add Sorting Controls as Buttons

For simple apps, predefined sort buttons provide a better UX than expecting users to click column headers. Create Button components labeled 'Sort Newest First', 'Sort A-Z by Name', etc. Each button's event handler calls a JS Query that runs table1.setSort() with the appropriate configuration. For a select-based sort control, use a Select component with sort options and a JS Query that reads selectBox1.value to determine the sort.

typescript
1// Select component options (static):
2// [
3// { label: 'Newest First', value: 'created_at_desc' },
4// { label: 'Oldest First', value: 'created_at_asc' },
5// { label: 'Name A-Z', value: 'last_name_asc' },
6// { label: 'Revenue High-Low', value: 'revenue_desc' }
7// ]
8
9// JS Query: applySortFromSelect
10const sortMap = {
11 created_at_desc: { columnId: 'created_at', desc: true },
12 created_at_asc: { columnId: 'created_at', desc: false },
13 last_name_asc: { columnId: 'last_name', desc: false },
14 revenue_desc: { columnId: 'revenue', desc: true }
15};
16
17const selectedSort = sortSelect.value;
18if (sortMap[selectedSort]) {
19 await table1.setSort([sortMap[selectedSort]]);
20}

Expected result: A dropdown sort control lets users select predefined sort options that immediately reorder the table.

7

Read the Current Sort State

Access the current table sort state via table1.sort (returns the sort array), table1.sortColumn (returns the active sort column key as a string), and table1.sortDirection ('ascend' or 'descend'). These properties are useful for: displaying sort indicators outside the table, driving server-side ORDER BY in SQL queries, and persisting user sort preferences to a temporary state variable.

typescript
1// In a text component (shows active sort state):
2{{ 'Sorted by: ' + (table1.sortColumn || 'default') + ' ' + (table1.sortDirection || '') }}
3
4// In a JS Query reading current sort:
5const currentSort = table1.sort; // array of {columnId, desc}
6const columnId = table1.sortColumn; // 'created_at'
7const direction = table1.sortDirection; // 'ascend' or 'descend'
8
9// Save sort preference to temporary state:
10await sortPreference.setValue({
11 column: table1.sortColumn,
12 direction: table1.sortDirection
13});
14// Note: setValue() is async — do not read sortPreference immediately after

Expected result: You can read and respond to the current table sort state from other parts of your app.

Complete working example

SQL Query: getUsersSorted (server-side sort + pagination)
1-- SQL Query: getUsersSorted
2-- Enable: 'Run when inputs change'
3-- Server-side sorting + pagination
4
5SELECT
6 id,
7 first_name,
8 last_name,
9 email,
10 department,
11 status,
12 revenue,
13 created_at
14FROM users
15WHERE
16 (
17 {{ searchInput.value || '' }} = ''
18 OR first_name ILIKE {{ '%' + (searchInput.value || '') + '%' }}
19 OR last_name ILIKE {{ '%' + (searchInput.value || '') + '%' }}
20 )
21ORDER BY
22 -- String columns
23 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'first_name'
24 THEN first_name END
25 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
26 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'last_name'
27 THEN last_name END
28 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
29 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'department'
30 THEN department END
31 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
32 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'status'
33 THEN status END
34 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
35 -- Numeric columns
36 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'revenue'
37 THEN revenue END
38 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
39 -- Date columns (default)
40 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'created_at'
41 OR {{ table1.sortColumn || 'created_at' }} = ''
42 THEN created_at END
43 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }}
44LIMIT 50
45OFFSET {{ ((table1.page || 1) - 1) * 50 }};

Common mistakes

Why it's a problem: Numeric columns sorting alphabetically (e.g. 10 before 9)

How to avoid: Set the Column type to 'Number' or 'Currency' in the column's Inspector settings. String-type columns sort lexicographically, causing incorrect ordering for numeric values.

Why it's a problem: Server-side sort using string concatenation for the column name, enabling SQL injection

How to avoid: Never use {{ table1.sortColumn }} directly in an ORDER BY clause as a raw string. Use a CASE expression whitelist that maps allowed column names to actual SQL expressions.

Why it's a problem: Not enabling 'Run when inputs change' on the SQL query, so sorting does not trigger a re-query

How to avoid: Open the query's Advanced settings and enable 'Run when inputs change'. Retool tracks table1.sortColumn and table1.sortDirection as dependencies when referenced in the query.

Why it's a problem: Reading table1.sort immediately after table1.setSort() without awaiting

How to avoid: table1.setSort() is async. Use await table1.setSort([...]) before reading table1.sort or table1.sortColumn. Without await, you'll read the pre-update values.

Best practices

  • Set the correct Column type (Number, Date, Currency) so client-side sorting uses the right comparison algorithm — string sorting on numeric data produces wrong order.
  • For large datasets (10,000+ rows), use server-side SQL ORDER BY instead of client-side sorting to avoid loading all data into the browser.
  • Always sanitize column names in dynamic ORDER BY clauses — use a CASE expression whitelist since column names cannot be parameterized.
  • Enable 'Run when inputs change' on SQL queries that reference table1.sortColumn to make server-side sorting reactive.
  • Provide predefined sort buttons or a sort dropdown for non-technical users who may not discover column header clicking.
  • Use table1.setSort([]) to programmatically clear sorting when resetting filters or refreshing data to a known state.
  • Remember that table1.setSort() is async — await it before reading table1.sort if you need the updated value immediately.

Still stuck?

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

ChatGPT Prompt

I have a Retool table (table1) connected to a PostgreSQL query returning users (id, first_name, last_name, email, department, status, revenue, created_at). I want to implement server-side sorting so that clicking a column header re-queries the database with the correct ORDER BY clause. Write the SQL query using table1.sortColumn and table1.sortDirection Retool properties, using a CASE expression whitelist to prevent SQL injection. Also show how to add a sort dropdown Select component with options for 'Newest First', 'Name A-Z', and 'Revenue High-Low'.

Retool Prompt

Add sorting to my Retool table1. Implement: (1) default sort by created_at descending using the Inspector's Default sort settings, (2) a JS Query that uses table1.setSort() to sort by last_name ascending when a 'Sort A-Z' button is clicked, (3) a SQL query that uses table1.sortColumn and table1.sortDirection in an ORDER BY CASE expression for server-side sorting. Enable 'Run when inputs change' on the SQL query.

Frequently asked questions

How do I sort a Retool table by multiple columns at the same time?

Hold Shift while clicking column headers to add secondary sort columns. Each Shift+click adds to the sort order, indicated by a number on each sorted column header. Programmatically, pass multiple objects to table1.setSort(): await table1.setSort([{ columnId: 'department', desc: false }, { columnId: 'last_name', desc: false }]).

Why is my numeric column sorting in the wrong order (1, 10, 2 instead of 1, 2, 10)?

This happens when the Column type is set to 'String' for a numeric field. String sorting compares character by character, so '10' sorts before '2'. Set the Column type to 'Number' or 'Currency' in the column's Inspector settings to enable correct numeric sorting.

How do I make a Retool table default to showing newest items first?

Two options: (1) In the Table Inspector, find the Sort section and set the default sort column to your date field and direction to 'descending' — this applies immediately on app load. (2) Add a SQL ORDER BY created_at DESC in your query so the data arrives pre-sorted regardless of user sort interactions.

Does table1.sortColumn work for server-side SQL sorting?

Yes, but with a critical caveat: column names cannot be parameterized in SQL (only values can), so you cannot use {{ table1.sortColumn }} directly in ORDER BY without risk of SQL injection. Use a CASE expression whitelist that maps the sortColumn value to actual column expressions, rejecting any column name not in your approved list.

RapidDev

Talk to an Expert

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

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

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.