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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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.
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'.
1// Alternative: set default sort via table1.setSort() in a Page Load query2// (JS Query with 'Run this query on page load' enabled)3await table1.setSort([{ columnId: 'created_at', desc: true }]);45// 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.
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).
1// In a JS Query or event handler:2// Sort by created_at descending (newest first):3await table1.setSort([{ columnId: 'created_at', desc: true }]);45// Sort by last_name ascending (A-Z):6await table1.setSort([{ columnId: 'last_name', desc: false }]);78// Reset sort (no sort applied):9await table1.setSort([]);1011// 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 }]);1516// Note: table1.setSort() is async — await it before reading table1.sortExpected result: Clicking programmatic sort buttons immediately reorders the table rows.
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.
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]);78// 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.
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.
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.45-- In the query, use a CASE expression for safe dynamic sorting:6SELECT id, first_name, last_name, email, created_at, revenue, status7FROM users8WHERE9 ({{ searchInput.value }} = '' OR10 first_name ILIKE {{ '%' + searchInput.value + '%' }})11ORDER BY12 CASE13 WHEN {{ table1.sortColumn || 'created_at' }} = 'first_name' THEN first_name14 WHEN {{ table1.sortColumn || 'created_at' }} = 'last_name' THEN last_name15 WHEN {{ table1.sortColumn || 'created_at' }} = 'email' THEN email16 WHEN {{ table1.sortColumn || 'created_at' }} = 'status' THEN status17 ELSE NULL18 END19 {{ table1.sortDirection === 'descend' ? 'DESC' : 'ASC' }},20 CASE21 WHEN {{ table1.sortColumn || 'created_at' }} IN ('revenue', 'created_at')22 THEN NULL ELSE NULL23 END,24 -- Numeric/date sorts need separate CASE:25 CASE26 WHEN {{ table1.sortColumn || 'created_at' }} = 'revenue'27 THEN revenue END28 {{ table1.sortDirection === 'descend' ? 'DESC' : 'ASC' }},29 CASE30 WHEN {{ table1.sortColumn || 'created_at' }} = 'created_at'31 THEN created_at END32 {{ 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.
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.
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// ]89// JS Query: applySortFromSelect10const 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};1617const 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.
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.
1// In a text component (shows active sort state):2{{ 'Sorted by: ' + (table1.sortColumn || 'default') + ' ' + (table1.sortDirection || '') }}34// 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'89// Save sort preference to temporary state:10await sortPreference.setValue({11 column: table1.sortColumn,12 direction: table1.sortDirection13});14// Note: setValue() is async — do not read sortPreference immediately afterExpected result: You can read and respond to the current table sort state from other parts of your app.
Complete working example
1-- SQL Query: getUsersSorted2-- Enable: 'Run when inputs change'3-- Server-side sorting + pagination45SELECT6 id,7 first_name,8 last_name,9 email,10 department,11 status,12 revenue,13 created_at14FROM users15WHERE16 (17 {{ searchInput.value || '' }} = ''18 OR first_name ILIKE {{ '%' + (searchInput.value || '') + '%' }}19 OR last_name ILIKE {{ '%' + (searchInput.value || '') + '%' }}20 )21ORDER BY22 -- String columns23 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'first_name'24 THEN first_name END25 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},26 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'last_name'27 THEN last_name END28 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},29 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'department'30 THEN department END31 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},32 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'status'33 THEN status END34 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},35 -- Numeric columns36 CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'revenue'37 THEN revenue END38 {{ 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 END43 {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }}44LIMIT 5045OFFSET {{ ((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.
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'.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation