Skip to main content
This section covers sorting by a field. To sort by BM25 relevance score, see BM25 Scoring.
ParadeDB is highly optimized for quickly returning the Top K results out of the index. In SQL, this means queries that contain an ORDER BY...LIMIT:
In order for a Top K query to be executed by ParadeDB vs. vanilla Postgres, all of the following conditions must be met:
  1. All ORDER BY fields must be indexed. If they are text fields, they must use the literal tokenizer.
  2. 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 as IN (...) does not count. See searching in subqueries.
  3. The query must have a LIMIT.
  4. With the exception of lower, ordering by expressions is not supported — only the raw fields themselves. See sorting by computed values for workarounds.
  5. Text ORDER BY fields must use a byte-ordered collation, such as C.
To verify that ParadeDB is executing the Top K, look for a Custom Scan with a TopKScanExecState in the EXPLAIN output:
If any of the above conditions are not met, the query cannot be fully optimized and you will not see a TopKScanExecState in the EXPLAIN output.

Tiebreaker Sorting

To guarantee stable sorting in the event of a tie, additional columns can be provided to ORDER 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 the ORDER 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.
This allows sorting by lowercase to be optimized.

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:
However, you can achieve an optimized Top K scan by projecting the JSON field, casting it to the desired type, and indexing it with pdb.alias. For example, to order by a numeric weight field within a JSON column:
You can then query and order by the exact underlying expression, which will now use the Top K optimization:

Collation

ParadeDB sorts text by its raw bytes, so a text ORDER 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:
With a collation that is not byte-ordered, Postgres sorts the results itself instead, and ParadeDB emits a warning:
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:
There are two ways to keep the Top K optimization:
  1. Store the computed value in its own column, add that column to the index, and sort by it.
  2. If the expression has only a few possible values, run one Top K query per value and merge the results.
The second approach looks like this:
Each branch is a Top K query, so the outer sort only sees at most 10 rows. This returns the same rows as the 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:
The searches have no 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:

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:
This returns the same rows as applying the 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.