This section covers sorting by a field. To sort by BM25 relevance score, see
BM25 Scoring.
ORDER BY...LIMIT:
- All
ORDER BYfields must be indexed. If they are text fields, they must use the literal tokenizer. - At least one ParadeDB text search operator must be present at the same level as the
ORDER BY...LIMIT. A search inside a subquery such asIN (...)does not count. See searching in subqueries. - The query must have a
LIMIT. - With the exception of
lower, ordering by expressions is not supported — only the raw fields themselves. See sorting by computed values for workarounds. - Text
ORDER BYfields must use a byte-ordered collation, such asC.
Custom Scan with a TopKScanExecState in the EXPLAIN output:
Expected Response
Expected Response
TopKScanExecState in the EXPLAIN output.
Tiebreaker Sorting
To guarantee stable sorting in the event of a tie, additional columns can be provided toORDER BY.
This is particularly important when sorting by pdb.score(...), as equal scores can result in non-deterministic orderings. As noted above, all tiebreaker columns must be indexed to receive the Top K optimization.
A text tiebreaker, such as a string ID, must also use a byte-ordered collation. Prefer an integer tiebreaker where one is available.
ParadeDB is currently able to handle 3
ORDER BY columns. If there are more
than 3 columns, the ORDER BY will not be efficiently executed by ParadeDB.Sorting by Text
If a text field is present in theORDER BY clause, it must be indexed with the literal or
literal normalized tokenizer.
Sorting by lowercase text using lower(<text_field>) is also supported. To enable this, the expression lower(<text_field>) must be indexed
with either the literal or literal normalized tokenizer. See indexing expressions for more information.
Text sorts also depend on the column’s collation.
Sorting by Range
Postgres range columns (int4range, int8range, numrange, daterange, tsrange, tstzrange) can receive the Top K optimization when they appear as the leading ORDER BY key. ParadeDB compares range values using the same rules as Postgres range_cmp (empty ranges, unbounded bounds, and inclusive/exclusive endpoints).
Range columns are always available to ParadeDB’s columnar execution path, so no extra configuration is needed. If a range column appears in a later ORDER BY position, the whole ORDER BY falls back to a regular Postgres sort. Tiebreaker columns after a leading range key are fine.
Sorting by JSON
Ordering by a JSON subfield directly is not yet supported. For example, this query will not receive an optimized Top K scan:pdb.alias.
For example, to order by a numeric weight field within a JSON column:
Collation
ParadeDB sorts text by its raw bytes, so a textORDER BY field only receives the Top K optimization if its collation also sorts by bytes.
The C and POSIX collations, their .UTF-8 variants, and the builtin provider on Postgres 17+ qualify.
ICU collations and libc locales such as en_US.UTF-8 do not.
If your database’s default collation is not byte-ordered, add COLLATE "C" to text ORDER BY fields:
Expected Response
Expected Response
Expected Response
Expected Response
COLLATE "C" can change the order of results compared to a linguistic
collation. For example, uppercase letters sort before lowercase letters.Sorting by Computed Values
Because ordering by expressions is not supported,ORDER BY clauses built on CASE do not receive the Top K optimization. For example, this query lists in-stock items first:
- Store the computed value in its own column, add that column to the index, and sort by it.
- If the expression has only a few possible values, run one Top K query per value and merge the results.
Expected Response
Expected Response
CASE version as long as every row falls into exactly one branch.
If in_stock can be NULL, add a third branch for in_stock IS NULL.
Searching in Subqueries
Top K requires the search,ORDER BY, and LIMIT to be at the same level of the query.
A common way to miss this is to run one search per field in a subquery and sort the combined results outside of it:
Expected Response
Expected Response
LIMIT of their own, so each one returns every matching row, shown as ColumnarExecState instead of TopKScanExecState.
Postgres then joins all of those rows and sorts them to keep five, so the cost grows with the number of matches rather than with the LIMIT.
Instead, combine the fields with OR in a single query:
Expected Response
Expected Response
Joining After the Top K
When the query also joins other tables, ParadeDB can often execute the whole join with join pushdown. If join pushdown does not apply, for example because one of the tables has no ParadeDB index, run the Top K first in a CTE or subquery and join only the rows it returns:Expected Response
Expected Response
LIMIT after the join only if the join neither removes nor duplicates search results.
Here, every order matches exactly one product, because product_id is NOT NULL and has a foreign key to mock_items.
If a join can drop rows or return several rows per search result, the limited CTE can return fewer or different rows.
For pagination with OFFSET n, the inner LIMIT must be n plus the page size.