# How to Implement Data Sorting in Retool Tables

- Tool: Retool
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

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

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

## Before you start

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

```
// Alternative: set default sort via table1.setSort() in a Page Load query
// (JS Query with 'Run this query on page load' enabled)
await table1.setSort([{ columnId: 'created_at', desc: true }]);

// Multiple default sorts (multi-column):
await table1.setSort([
  { columnId: 'department', desc: false },
  { columnId: 'last_name', desc: false }
]);
```

**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).

```
// In a JS Query or event handler:
// Sort by created_at descending (newest first):
await table1.setSort([{ columnId: 'created_at', desc: true }]);

// Sort by last_name ascending (A-Z):
await table1.setSort([{ columnId: 'last_name', desc: false }]);

// Reset sort (no sort applied):
await table1.setSort([]);

// Toggle sort direction based on current state:
const currentSort = table1.sort;
const isDesc = currentSort?.[0]?.desc ?? false;
await table1.setSort([{ columnId: 'created_at', desc: !isDesc }]);

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

```
// Multi-column sort: department ascending, then last_name ascending:
await table1.setSort([
  { columnId: 'department', desc: false },
  { columnId: 'last_name', desc: false },
  { columnId: 'first_name', desc: false }
]);

// Business-logic sort: priority descending, then due_date ascending:
await table1.setSort([
  { columnId: 'priority', desc: true },
  { columnId: 'due_date', desc: false }
]);
```

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

```
-- SQL Query with server-side sorting:
-- IMPORTANT: Column names cannot be parameterized in SQL,
-- so validate using a whitelist in a transformer first.

-- In the query, use a CASE expression for safe dynamic sorting:
SELECT id, first_name, last_name, email, created_at, revenue, status
FROM users
WHERE
  ({{ searchInput.value }} = '' OR
   first_name ILIKE {{ '%' + searchInput.value + '%' }})
ORDER BY
  CASE
    WHEN {{ table1.sortColumn || 'created_at' }} = 'first_name' THEN first_name
    WHEN {{ table1.sortColumn || 'created_at' }} = 'last_name' THEN last_name
    WHEN {{ table1.sortColumn || 'created_at' }} = 'email' THEN email
    WHEN {{ table1.sortColumn || 'created_at' }} = 'status' THEN status
    ELSE NULL
  END
  {{ table1.sortDirection === 'descend' ? 'DESC' : 'ASC' }},
  CASE
    WHEN {{ table1.sortColumn || 'created_at' }} IN ('revenue', 'created_at')
    THEN NULL ELSE NULL
  END,
  -- Numeric/date sorts need separate CASE:
  CASE
    WHEN {{ table1.sortColumn || 'created_at' }} = 'revenue'
    THEN revenue END
  {{ table1.sortDirection === 'descend' ? 'DESC' : 'ASC' }},
  CASE
    WHEN {{ table1.sortColumn || 'created_at' }} = 'created_at'
    THEN created_at END
  {{ table1.sortDirection === 'descend' ? 'DESC' : 'ASC' }}
LIMIT {{ table1.pageSize || 50 }}
OFFSET {{ (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.

```
// Select component options (static):
// [
//   { label: 'Newest First', value: 'created_at_desc' },
//   { label: 'Oldest First', value: 'created_at_asc' },
//   { label: 'Name A-Z', value: 'last_name_asc' },
//   { label: 'Revenue High-Low', value: 'revenue_desc' }
// ]

// JS Query: applySortFromSelect
const sortMap = {
  created_at_desc: { columnId: 'created_at', desc: true },
  created_at_asc: { columnId: 'created_at', desc: false },
  last_name_asc: { columnId: 'last_name', desc: false },
  revenue_desc: { columnId: 'revenue', desc: true }
};

const selectedSort = sortSelect.value;
if (sortMap[selectedSort]) {
  await table1.setSort([sortMap[selectedSort]]);
}
```

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

```
// In a text component (shows active sort state):
{{ 'Sorted by: ' + (table1.sortColumn || 'default') + ' ' + (table1.sortDirection || '') }}

// In a JS Query reading current sort:
const currentSort = table1.sort; // array of {columnId, desc}
const columnId = table1.sortColumn; // 'created_at'
const direction = table1.sortDirection; // 'ascend' or 'descend'

// Save sort preference to temporary state:
await sortPreference.setValue({
  column: table1.sortColumn,
  direction: table1.sortDirection
});
// 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 code example

File: `SQL Query: getUsersSorted (server-side sort + pagination)`

```javascript
-- SQL Query: getUsersSorted
-- Enable: 'Run when inputs change'
-- Server-side sorting + pagination

SELECT
  id,
  first_name,
  last_name,
  email,
  department,
  status,
  revenue,
  created_at
FROM users
WHERE
  (
    {{ searchInput.value || '' }} = ''
    OR first_name ILIKE {{ '%' + (searchInput.value || '') + '%' }}
    OR last_name  ILIKE {{ '%' + (searchInput.value || '') + '%' }}
  )
ORDER BY
  -- String columns
  CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'first_name'
    THEN first_name END
    {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
  CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'last_name'
    THEN last_name END
    {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
  CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'department'
    THEN department END
    {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
  CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'status'
    THEN status END
    {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
  -- Numeric columns
  CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'revenue'
    THEN revenue END
    {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }},
  -- Date columns (default)
  CASE WHEN {{ table1.sortColumn || 'created_at' }} = 'created_at'
       OR  {{ table1.sortColumn || 'created_at' }} = ''
    THEN created_at END
    {{ table1.sortDirection === 'descend' ? 'DESC NULLS LAST' : 'ASC NULLS LAST' }}
LIMIT 50
OFFSET {{ ((table1.page || 1) - 1) * 50 }};
```

## Common mistakes

- **Numeric columns sorting alphabetically (e.g. 10 before 9)** — undefined Fix: 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.
- **Server-side sort using string concatenation for the column name, enabling SQL injection** — undefined Fix: 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.
- **Not enabling 'Run when inputs change' on the SQL query, so sorting does not trigger a re-query** — undefined Fix: 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.
- **Reading table1.sort immediately after table1.setSort() without awaiting** — undefined Fix: 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.

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

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-implement-data-sorting-in-retool-tables
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-implement-data-sorting-in-retool-tables
