To filter data in a Retool table, enable the built-in filter bar in the Table Inspector, use SQL WHERE clauses with {{ }} parameters for server-side filtering, or filter the query result in a JavaScript transformer. For dynamic user-driven filters, bind a Text Input component to your SQL query's WHERE clause and re-run the query on each change.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Three Ways to Filter Table Data in Retool
Retool gives you three distinct approaches to filtering table data, each suited to a different situation. The built-in filter bar is quickest for power users who want to filter ad hoc without any configuration. SQL WHERE clauses are best for server-side filtering on large datasets where loading all rows client-side is impractical. JavaScript transformer filtering is best when you already have data loaded and want lightweight client-side filtering without re-querying the database.
This tutorial walks through all three methods and shows how to combine them — for example, a SQL query with an ILIKE clause driven by a Text Input component, plus a transformer for secondary client-side filtering. Understanding the difference between client-side and server-side filtering is critical when working with large datasets.
Prerequisites
- A Retool app with a Table component connected to a SQL or REST query
- The query returns a meaningful dataset (ideally 10+ rows to see filtering in action)
- Familiarity with Retool's {{ }} expression syntax
- For server-side filtering: access to a SQL resource (PostgreSQL, MySQL, etc.)
Step-by-step guide
Enable the Built-in Filter Bar
Select the Table component on your canvas. In the Inspector panel, scroll to the Interaction section and toggle 'Allow filtering' to on. A filter icon appears in the table header. Users can click it to add filter rules per column, selecting operators like equals, contains, greater than, etc. This is entirely client-side — it filters the already-loaded data in the browser without re-querying.
Expected result: A filter icon appears in the table header, and clicking it reveals per-column filter controls.
Add a Text Input for Dynamic SQL Filtering
Drag a Text Input component onto your canvas from the component panel. Name it 'searchInput' (you can rename it in the Inspector's General section under 'Component name'). This component's value will drive your SQL WHERE clause. Set the placeholder text to something like 'Search by name...' in the Inspector's General section.
Expected result: A searchInput text field appears on the canvas ready to receive user input.
Write a Dynamic SQL Query with ILIKE
Open the query editor (bottom panel) and write a SQL query that uses {{ searchInput.value }} in the WHERE clause. Use ILIKE for case-insensitive partial matching on PostgreSQL, or LIKE with LOWER() on MySQL. Retool safely parameterizes {{ }} values to prevent SQL injection. Enable 'Run when inputs change' in the query's Advanced settings so the query re-runs automatically when the user types.
1-- PostgreSQL example with ILIKE:2SELECT id, first_name, last_name, email, status, created_at3FROM users4WHERE5 ({{ searchInput.value }} = '' OR6 first_name ILIKE {{ '%' + searchInput.value + '%' }} OR7 last_name ILIKE {{ '%' + searchInput.value + '%' }} OR8 email ILIKE {{ '%' + searchInput.value + '%' }})9ORDER BY created_at DESC10LIMIT 500;1112-- MySQL equivalent:13SELECT id, first_name, last_name, email, status, created_at14FROM users15WHERE16 ({{ searchInput.value }} = '' OR17 LOWER(first_name) LIKE LOWER({{ '%' + searchInput.value + '%' }}) OR18 LOWER(email) LIKE LOWER({{ '%' + searchInput.value + '%' }}))19ORDER BY created_at DESC20LIMIT 500;Expected result: The table re-queries and filters whenever the user types in searchInput.
Add a Dropdown Filter for Category/Status
For enumerated fields like status, a Select component is more user-friendly than a text input. Drag a Select component onto the canvas, name it 'statusFilter', and configure its options statically: [{ label: 'All', value: '' }, { label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }]. Then add a condition to your SQL query that uses {{ statusFilter.value }}.
1-- Combined text search + status dropdown filter:2SELECT id, first_name, last_name, email, status, created_at3FROM users4WHERE5 ({{ searchInput.value }} = '' OR6 first_name ILIKE {{ '%' + searchInput.value + '%' }} OR7 email ILIKE {{ '%' + searchInput.value + '%' }})8 AND9 ({{ statusFilter.value }} = '' OR status = {{ statusFilter.value }})10ORDER BY created_at DESC11LIMIT 500;Expected result: Users can now filter by both a text search and a status dropdown simultaneously.
Add a Date Range Filter
For date-based filtering, use a Date Range Picker component (name it 'dateRangeFilter'). Add BETWEEN conditions to your SQL query using dateRangeFilter.value.start and dateRangeFilter.value.end. Always cast date strings to proper timestamps in SQL to avoid comparison issues.
1-- Adding date range to the combined filter:2SELECT id, first_name, last_name, email, status, created_at3FROM users4WHERE5 ({{ searchInput.value }} = '' OR6 first_name ILIKE {{ '%' + searchInput.value + '%' }} OR7 email ILIKE {{ '%' + searchInput.value + '%' }})8 AND9 ({{ statusFilter.value }} = '' OR status = {{ statusFilter.value }})10 AND11 ({{ !dateRangeFilter.value.start }} OR12 created_at >= {{ dateRangeFilter.value.start }}::date)13 AND14 ({{ !dateRangeFilter.value.end }} OR15 created_at <= {{ dateRangeFilter.value.end }}::date + INTERVAL '1 day')16ORDER BY created_at DESC;Expected result: Users can filter by date range in addition to text search and status.
Filter Client-Side Using a JavaScript Transformer
When you already have data loaded and do not want to re-query the database, use a standalone transformer (Code tab → + Add → Transformer) to filter the data. The transformer auto-executes whenever its dependencies change. Important: transformers are read-only — they can only return values, not trigger queries or set state. Create a transformer named 'filteredUsers' and bind the Table's Data source to {{ filteredUsers.value }}.
1// Transformer: filteredUsers2// Dependencies: query1.data, searchInput.value, statusFilter.value34const rows = query1.data;5const search = searchInput.value?.toLowerCase() ?? '';6const status = statusFilter.value ?? '';78return rows.filter(row => {9 const matchesSearch = !search ||10 row.first_name?.toLowerCase().includes(search) ||11 row.last_name?.toLowerCase().includes(search) ||12 row.email?.toLowerCase().includes(search);1314 const matchesStatus = !status || row.status === status;1516 return matchesSearch && matchesStatus;17});Expected result: The table immediately updates as the user types or selects a filter, with no database round-trip.
Add a 'Clear Filters' Button
Add a Button component labeled 'Clear Filters'. In its event handler (Inspector → Interaction → Event handlers), add multiple 'Set component value' actions: set searchInput to empty string, set statusFilter to empty string, and set dateRangeFilter to null. You can chain these actions by clicking '+ Add action' under the same event handler.
1// If using a JS Query for the clear action instead of multiple event handler actions:2await searchInput.setValue('');3await statusFilter.setValue('');4await dateRangeFilter.setValue(null);5// Note: setValue() is async — do not read the updated value immediately after.Expected result: Clicking the button resets all filter controls and the table returns to showing all rows.
Show the Active Filter Count as a Badge
Add a Text component near your filters to display how many filters are active. Bind it to a transformer that counts non-empty filter values. This gives users clarity on whether filters are applied. Text component value: {{ activeFilterCount.value + ' filter(s) active' }}
1// Transformer: activeFilterCount2let count = 0;3if (searchInput.value) count++;4if (statusFilter.value) count++;5if (dateRangeFilter.value?.start) count++;6return count;Expected result: A text label near the filters shows '2 filter(s) active' when the user has applied filters.
Complete working example
1-- SQL Query: getUsersFiltered2-- Enable: Run when inputs change3SELECT4 id,5 first_name,6 last_name,7 email,8 status,9 created_at10FROM users11WHERE12 (13 {{ searchInput.value }} = ''14 OR first_name ILIKE {{ '%' + searchInput.value + '%' }}15 OR last_name ILIKE {{ '%' + searchInput.value + '%' }}16 OR email ILIKE {{ '%' + searchInput.value + '%' }}17 )18 AND ({{ statusFilter.value }} = '' OR status = {{ statusFilter.value }})19ORDER BY created_at DESC20LIMIT 1000;2122// -----------------------------------------------23// JS Transformer: filteredUsers (client-side fallback)24// Table Data source: {{ filteredUsers.value }}25const rows = getUsersFiltered.data;26const search = searchInput.value?.toLowerCase() ?? '';27const status = statusFilter.value ?? '';2829return rows.filter(row => {30 const matchesSearch = !search ||31 row.first_name?.toLowerCase().includes(search) ||32 row.last_name?.toLowerCase().includes(search) ||33 row.email?.toLowerCase().includes(search);34 const matchesStatus = !status || row.status === status;35 return matchesSearch && matchesStatus;36});Common mistakes when filtering Data in a Retool Table Component
Why it's a problem: Filtering causes zero results when the search input is empty
How to avoid: Add an OR condition: ({{ searchInput.value }} = '' OR field ILIKE ...). Without this guard, an empty string passed to ILIKE matches nothing.
Why it's a problem: Using a transformer to filter but forgetting it is read-only
How to avoid: Transformers cannot call query.trigger() or use setState(). If your filter logic needs to fire a database query, put the logic in a JS Query and trigger it from an event handler.
Why it's a problem: Not enabling 'Run when inputs change' on the SQL query
How to avoid: Go to the query's Advanced settings and enable 'Run when inputs change'. Without this, the query only runs once on page load and does not respond to user input changes.
Why it's a problem: Using client-side transformer filtering for a table with 50,000+ rows
How to avoid: Client-side filtering loads all rows into the browser first, which is slow and expensive. Use SQL WHERE clauses for server-side filtering on large datasets.
Best practices
- Always add a fallback condition like {{ searchInput.value }} = '' so that an empty filter returns all rows rather than zero rows.
- Use ILIKE (PostgreSQL) or LOWER/LIKE (MySQL) for case-insensitive text search — never filter on exact case unless intentional.
- Enable 'Run when inputs change' on your query rather than requiring users to manually click a Search button for a smoother UX.
- Add LIMIT to filtered SQL queries — without a limit, a broad search on a large table can time out or cause performance issues.
- For large datasets (10,000+ rows), always use server-side SQL filtering instead of client-side transformer filtering to avoid loading all data into the browser.
- Transformers are read-only — if your filtering logic needs to trigger a query or update state, use a JS Query instead of a transformer.
- Debounce text input filters with the 'Debounce (ms)' setting in the Text Input's Interaction section to reduce the number of SQL queries fired while the user is typing.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a Retool app with a Table component showing users from a PostgreSQL database. I want to add filtering so users can: (1) search by first name, last name, or email using a text input (case-insensitive), (2) filter by status using a dropdown with options All/Active/Inactive, and (3) filter by date range using a Date Range Picker. Write the SQL query using {{ }} Retool syntax that handles all three filters together, ensuring empty filters return all rows. Also show how to add a 'Clear Filters' button.
Add three filter controls to my Retool app: a Text Input named searchInput, a Select named statusFilter with options All/''/Active/Inactive, and a Date Range Picker named dateRangeFilter. Update my SQL query (getUsersFiltered) to filter users by all three fields simultaneously using ILIKE for text search. Enable 'Run when inputs change' and add a Clear Filters button that resets all three controls.
Frequently asked questions
Does the Retool table built-in filter bar work with server-side data?
The built-in filter bar only filters data that is already loaded in the browser — it does not re-query the server. If your table has server-side pagination and only shows page 1, the filter bar only filters those rows. For full-dataset filtering, use SQL WHERE clauses driven by input components with 'Run when inputs change' enabled.
How do I prevent SQL injection when using {{ }} in WHERE clauses?
Retool automatically parameterizes values inside {{ }} expressions in SQL queries, treating them as bound parameters rather than raw SQL. This prevents SQL injection. However, avoid wrapping {{ }} values in string concatenation on the SQL side — keep them as separate parameters.
Can I filter a Retool table by multiple columns at once?
Yes. In SQL queries, add multiple AND conditions for each filter control. In transformer-based filtering, chain .filter() conditions or use logical AND inside a single .filter() callback. The built-in filter bar also supports multi-column filter rules via its Add Filter UI.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation