Skip to main content

ParadeDB Predicate

In order for ParadeDB to push down an aggregate, a ParadeDB predicate must be present in the query.
If your query does not contain a ParadeDB operator, a way to “force” aggregate pushdown is to append the all query to the query’s WHERE clause.

Date Grouping

GROUP BY DATE(created_at) and GROUP BY created_at::date support aggregate pushdown when created_at is a bare timestamp without time zone column that is columnar indexed. The usual aggregate pushdown requirements still apply, including a ParadeDB operator in the query. The result matches Postgres for the full timestamp range, including infinity, -infinity and NULL. Date grouping can be combined with other grouping columns and per-aggregate FILTER clauses. The following forms still fall back to native Postgres execution:
  • DATE(timestamptz_column), because the result depends on the session TimeZone.
  • DATE(text_column::timestamp) and other non-bare timestamp expressions.
  • Other scalar grouping functions, such as date_trunc(...) and lower(...).

Join Support

Aggregate pushdown works across joins as well as single tables. When every participating table has a ParadeDB index, ParadeDB computes the result directly from the index’s columnar storage, without scanning the underlying table rows. The custom aggregate scan is enabled by default; to turn it off, run SET paradedb.enable_aggregate_custom_scan TO off;.

Supported shapes

pdb.agg() over joins

A pdb.agg() field name must belong to exactly one indexed table in the join. When the same field name exists in several tables, qualify it with the table alias, as in "field": "p.category". The qualifier is also accepted when the planner reduces the join to that one table. Postgres cannot see the fields inside a spec, so an outer join whose table is read only there is removed as unused and the spec fails with an unknown field; reference such a table outside the spec, or use an inner join. pdb.agg() over a NUMERIC field runs on this path as well, on a single table or over a join, while a missing value is not accepted for such a field. terms on an array field is also supported over joins; metric aggregations over array fields are not. A few pdb.agg() shapes run on a single table but not over a join: metric aggregations over JSON sub-fields, such as {"sum": {"field": "metadata.qty"}}, terms on a JSON sub-field that holds numbers, min_doc_count: 0, which lists every term of the column, and a field indexed from a computed expression, such as (upper(name)::pdb.literal('alias=name_upper')). A field indexed through a plain tokenizer cast, such as (name::pdb.literal), works over a join like the column itself. pdb.agg() has no native Postgres implementation, so it cannot fall back. A pdb.agg() over a join that uses an aggregation outside the list above, such as range, histogram, date_histogram, filter, composite, stats, percentiles, or top_hits, raises an error.

pdb.agg() as a window function over joins

The faceted query form pdb.agg(...) OVER () runs over a join as well. The document covers every row the join produces, while ORDER BY ... LIMIT picks the rows returned; each returned row carries the same document. The document can be used inside other target list expressions, such as pdb.agg(...) OVER () ->> 'value'. The spec follows the rules of pdb.agg() over joins above. This path turns down the following shapes. Because pdb.agg() cannot fall back to Postgres, a query with one of them raises an error rather than running natively:
  • No LIMIT, or a LIMIT or OFFSET whose value is not known when the query is planned, such as a parameter under a generic plan
  • A visibility other than the default
  • A FILTER (WHERE ...) clause on the window function
  • PARTITION BY, ORDER BY, or a frame inside the OVER clause
  • SELECT DISTINCT with the bare document in the target list, or with the document inside an expression that also reads a column, such as (pdb.agg(...) OVER () ->> 'value') || p.category. Under DISTINCT the document is only accepted inside an expression that reads no other column, such as pdb.agg(...) OVER () ->> 'value'

Fallback to Postgres

For aggregates over joins, ParadeDB falls back to native Postgres execution when any of the following are true:
  • One or more tables in the join lacks a ParadeDB index
  • The join has no equality join condition (e.g. CROSS JOIN)
  • Join keys, GROUP BY columns, or aggregate arguments are not indexed columns
  • The query uses window functions (OVER ...), ROLLUP, CUBE, GROUPING SETS, LATERAL, or DISTINCT ON. The faceted query form pdb.agg(...) OVER () is pushed down, on a single table and over a join, as described above
  • GROUP BY uses a scalar function like date_trunc(...) or lower(...) (JSON sub-field access via ->> is supported)
  • The aggregate argument or result is wrapped in an expression such as COALESCE(SUM(...), 0) or a cast
When a fallback happens, the query still runs correctly through Postgres’ native planner. ParadeDB simply does not accelerate it.