--- title: Aggregates description: Accelerate SQL and JSON aggregates with the ParadeDB index canonical: https://www.paradedb.com/docs/reference/aggregates/overview --- ParadeDB can execute aggregate work using the [columnar](/concepts/architecture#columnar-index) portion of the ParadeDB index, which can significantly accelerate performance compared to vanilla Postgres. There are two aggregate surfaces: - **Native SQL aggregates** for normal Postgres queries: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `STDDEV`, `VARIANCE`, `BOOL_AND`, `BOOL_OR`, `ARRAY_AGG`, `STRING_AGG`, `GROUP BY`, `HAVING`, and per-aggregate `FILTER`. - **`pdb.agg` JSON aggregates** for Elasticsearch-compatible bucket and metric aggregations, including facets, histograms, percentiles, cardinality, and top hits. ## Native SQL Aggregates Use standard SQL aggregate functions when you want ordinary Postgres-shaped results. ParadeDB can push these aggregates into the index when the query contains a ParadeDB predicate and the aggregate fields are in the ParadeDB index. ```sql SELECT COUNT(*) AS count, AVG(rating) AS avg_rating, MIN(created_at) AS oldest_item, MAX(created_at) AS newest_item FROM mock_items WHERE description ||| 'running shoes'; ``` Grouped aggregates use normal `GROUP BY` syntax: ```sql SELECT rating, COUNT(*) AS count FROM mock_items WHERE description ||| 'running shoes' GROUP BY rating ORDER BY rating LIMIT 5; ``` If a query does not otherwise need a full-text predicate, add [`pdb.all()`](/reference/full-text/all) to make the query eligible for aggregate pushdown: ```sql SELECT COUNT(*) FROM mock_items WHERE id @@@ pdb.all(); ``` See [Limitations](/reference/aggregates/limitations) for the full list of supported aggregate shapes and fallback cases. ## JSON Aggregates The `pdb.agg` function accepts an Elasticsearch-compatible JSON aggregate query string. This is useful when you want bucketed or faceted responses in Elasticsearch-compatible JSON. For example, the following query counts the total number of results for a search query: ```sql SQL SELECT pdb.agg('{"value_count": {"field": "id"}}') FROM mock_items WHERE category === 'electronics'; ``` ```ts Drizzle import { search } from "@paradedb/drizzle-paradedb"; await db .select({ agg: search.agg({ value_count: { field: "id" } }), }) .from(mockItems) .where(search.term(mockItems.category, "electronics")); ``` ```python Django from paradedb import Agg, ParadeDB, Term MockItem.objects.filter( category=ParadeDB(Term('electronics')) ).aggregate(agg=Agg('{"value_count": {"field": "id"}}')) ``` ```python SQLAlchemy from sqlalchemy import select from sqlalchemy.orm import Session from paradedb.sqlalchemy import facets, pdb, search stmt = ( select(pdb.agg(facets.value_count(field="id"))) .select_from(MockItem) .where(search.term(MockItem.category, "electronics")) ) with Session(engine) as session: session.execute(stmt).all() ``` ```ruby Rails MockItem.search(:category) .term("electronics") .facets_agg(agg: ParadeDB::Aggregations.value_count(:id)) ``` ```cs EF Core await dbContext .MockItems.Where(item => EF.Functions.Term(item.Category, "electronics")) .Select(item => EF.Functions.Agg(new { value_count = new { field = "id" } })) .ToListAsync(); ``` ```ini Expected Response agg ---------------- {"value": 5.0} (1 row) ``` This query counts the number of results for every distinct group: ```sql SQL SELECT rating, pdb.agg('{"value_count": {"field": "id"}}') FROM mock_items WHERE category === 'electronics' GROUP BY rating ORDER BY rating LIMIT 5; ``` ```ts Drizzle import { search } from "@paradedb/drizzle-paradedb"; await db .select({ rating: mockItems.rating, agg: search.agg({ value_count: { field: "id" } }), }) .from(mockItems) .where(search.term(mockItems.category, "electronics")) .groupBy(mockItems.rating) .orderBy(mockItems.rating) .limit(5); ``` ```python Django from paradedb import Agg, ParadeDB, Term MockItem.objects.filter( category=ParadeDB(Term('electronics')) ).values('rating').annotate( agg=Agg('{"value_count": {"field": "id"}}') ).order_by('rating')[:5] ``` ```python SQLAlchemy from sqlalchemy import select from sqlalchemy.orm import Session from paradedb.sqlalchemy import facets, pdb, search stmt = ( select(MockItem.rating, pdb.agg(facets.value_count(field="id")).label("agg")) .where(search.term(MockItem.category, "electronics")) .group_by(MockItem.rating) .order_by(MockItem.rating) .limit(5) ) with Session(engine) as session: session.execute(stmt).all() ``` ```ruby Rails MockItem.search(:category) .term("electronics") .aggregate_by( :rating, agg: ParadeDB::Aggregations.value_count(:id) ) .order(:rating) .limit(5) ``` ```cs EF Core await dbContext .MockItems.Where(item => EF.Functions.Term(item.Category, "electronics")) .GroupBy(item => item.Rating) .Select(group => new { Rating = group.Key, Agg = EF.Functions.Agg(new { value_count = new { field = "id" } }) }) .OrderBy(result => result.Rating) .Take(5) .ToListAsync(); ``` ```ini Expected Response rating | agg --------+---------------- 3 | {"value": 1.0} 4 | {"value": 3.0} 5 | {"value": 1.0} (3 rows) ``` ## Multiple Aggregations To compute multiple aggregations at once, simply include multiple `pdb.agg` functions in the target list: ```sql SQL SELECT pdb.agg('{"avg": {"field": "rating"}}') AS avg_rating, pdb.agg('{"value_count": {"field": "id"}}') AS count FROM mock_items WHERE category === 'electronics'; ``` ```ts Drizzle import { search } from "@paradedb/drizzle-paradedb"; await db .select({ avgRating: search.agg({ avg: { field: "rating" } }), count: search.agg({ value_count: { field: "id" } }), }) .from(mockItems) .where(search.term(mockItems.category, "electronics")); ``` ```python Django from paradedb import Agg, ParadeDB, Term MockItem.objects.filter( category=ParadeDB(Term('electronics')) ).aggregate( avg_rating=Agg('{"avg": {"field": "rating"}}'), count=Agg('{"value_count": {"field": "id"}}'), ) ``` ```python SQLAlchemy from sqlalchemy import select from sqlalchemy.orm import Session from paradedb.sqlalchemy import facets, pdb, search stmt = ( select( pdb.agg(facets.avg(field="rating")).label("avg_rating"), pdb.agg(facets.value_count(field="id")).label("count"), ) .select_from(MockItem) .where(search.term(MockItem.category, "electronics")) ) with Session(engine) as session: session.execute(stmt).all() ``` ```ruby Rails MockItem.search(:category) .term("electronics") .facets_agg( avg_rating: ParadeDB::Aggregations.avg(:rating), count: ParadeDB::Aggregations.value_count(:id) ) ``` ```cs EF Core await dbContext .MockItems.Where(item => EF.Functions.Term(item.Category, "electronics")) .Select(item => new { AvgRating = EF.Functions.Agg(new { avg = new { field = "rating" } }), Count = EF.Functions.Agg(new { value_count = new { field = "id" } }) }) .ToListAsync(); ``` ```ini Expected Response avg_rating | count ----------------+---------------- {"value": 4.0} | {"value": 5.0} (1 row) ``` ## Performance Optimization By default, `pdb.agg` runs transaction visibility checks so that deleted or updated-away rows are not factored into the result set. This behavior is controlled by the `visibility` argument, which takes one of three modes. | Mode | Behavior | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `'transaction'` | Check transaction visibility. Aggregate results match vanilla Postgres. This is the default. | | `'raw'` | Skip the checks and aggregate raw index data. Faster, and approximate whenever the index still holds entries for rows your transaction cannot see. | | `'threshold'` | Check transaction visibility only when the query's estimated matching row count is below `paradedb.visibility_threshold`. | If your table is not frequently updated or you can tolerate an approximate result, the performance of aggregate queries can be improved by skipping these visibility checks. To do so, set `visibility` to `'raw'`. ```sql SQL SELECT pdb.agg('{"value_count": {"field": "id"}}', 'raw') FROM mock_items WHERE description ||| 'running shoes'; ``` ```ts Drizzle import { search } from "@paradedb/drizzle-paradedb"; await db .select({ agg: search.agg({ value_count: { field: "id" } }, false), }) .from(mockItems) .where(search.matchAny(mockItems.description, "running shoes")); ``` ```python Django from paradedb import Agg, MatchAny, ParadeDB MockItem.objects.filter( description=ParadeDB(MatchAny('running shoes')) ).aggregate( agg=Agg('{"value_count": {"field": "id"}}', exact=False) ) ``` ```python SQLAlchemy from sqlalchemy import select from sqlalchemy.orm import Session from paradedb.sqlalchemy import facets, pdb, search stmt = ( select(pdb.agg(facets.value_count(field="id"), approximate=True).label("agg")) .where(search.match_any(MockItem.description, "running shoes")) ) with Session(engine) as session: session.execute(stmt).all() ``` ```ruby Rails MockItem.search(:description) .match_any("running shoes") .facets_agg(exact: false, agg: ParadeDB::Aggregations.value_count(:id)) ``` ```cs EF Core await dbContext .MockItems.Where(item => EF.Functions.MatchAny(item.Description, "running shoes")) .Select(item => EF.Functions.Agg(new { value_count = new { field = "id" } }, false)) .ToListAsync(); ``` Skipping this check can improve query times by 2-4x in some cases, at the expense of correctness. ### Thresholded Visibility An unvacuumed dead tuple skews a small result visibly and a large one barely at all, so the tradeoff above is really a function of how many rows the query matches. `visibility => 'threshold'` makes that decision per query instead of hardcoding it in your application: visibility checks run when the estimated matching row count is below `paradedb.visibility_threshold`, and the aggregate reads raw index data otherwise. ```sql SELECT pdb.agg('{"value_count": {"field": "id"}}', 'threshold') FROM mock_items WHERE description ||| 'running shoes'; ``` `paradedb.visibility_threshold` defaults to `10000` and can be set at the system, database, session, or transaction level. ```sql SET paradedb.visibility_threshold = 50000; ``` The estimate is made for the query as a whole, so a bucketed aggregation such as `GROUP BY` or `terms` resolves to one decision for every bucket. A query matching a million rows across fifty thousand buckets aggregates every bucket from raw index data, including the low-cardinality ones. If a single query contains multiple `pdb.agg` calls, all of them must use the same `visibility` setting. Omitting the argument selects `'transaction'`, so an omitted argument alongside an explicit `'raw'` is a conflict, not a default. The `solve_mvcc` argument is deprecated in favor of `visibility`. Passing a boolean is still accepted, where `true` means `'transaction'` and `false` means `'raw'`. ## JSON Fields If `metadata` is a JSON field with key `color`, use `metadata.color` as the field name: ```sql SQL SELECT pdb.agg('{"terms": {"field": "metadata.color"}}') FROM mock_items WHERE id @@@ pdb.all(); ``` ```ts Drizzle import { search } from "@paradedb/drizzle-paradedb"; await db .select({ agg: search.agg({ terms: { field: "metadata.color" } }), }) .from(mockItems) .where(search.all(mockItems.id)); ``` ```python Django from paradedb import Agg, All, ParadeDB MockItem.objects.filter( id=ParadeDB(All()) ).aggregate(agg=Agg('{"terms": {"field": "metadata.color"}}')) ``` ```python SQLAlchemy from sqlalchemy import select from sqlalchemy.orm import Session from paradedb.sqlalchemy import facets, pdb, search stmt = ( select(pdb.agg(facets.terms(field="metadata.color"))) .select_from(MockItem) .where(search.all(MockItem.id)) ) with Session(engine) as session: session.execute(stmt).all() ``` ```ruby Rails MockItem.search(:id) .match_all .facets_agg(agg: ParadeDB::Aggregations.terms("metadata.color")) ``` ```cs EF Core await dbContext .MockItems.Where(item => EF.Functions.All(item.Id)) .Select(item => EF.Functions.Agg(new { terms = new { field = "metadata.color" } })) .ToListAsync(); ``` If a text or JSON field is used inside `pdb.agg`, it must use the [literal](/reference/tokenizers/available-tokenizers/literal) or [literal normalized](/reference/tokenizers/available-tokenizers/literal-normalized) tokenizer. ## Segment Pruning with Partitioning (Beta) `partition_by` is currently a Beta feature under active development. For aggregates filtered by high-selectivity keys (such as `tenant_id` in multi-tenant applications) or date ranges, configuring [`partition_by`](/reference/indexing/partition-by) allows ParadeDB to prune non-matching segments before computing the aggregate, significantly speeding up queries by scanning less data. See the [Partitioning Guide](/reference/indexing/partition-by) for details on column selection and pruning behavior.