Counts and aggregate reports
Count records, group data, filter totals, and build rollups or cubes.
Generate Opportunity before trying these examples. Kysoql uses the org's
Describe metadata to decide which fields can be grouped or aggregated.
Count records
Use bare count() when you only need a number:
import { db } from "./db";
const count = await db
.selectFrom("Opportunity")
.where("IsClosed", "=", false)
.select(({ fn }) => fn.count())
.execute();Use count(field) or another aggregate when you want rows that can also be
grouped, filtered, or sorted:
import { db } from "./db";
const rows = await db
.selectFrom("Opportunity")
.select(({ fn }) => fn.count("Id").as("opportunityCount"))
.execute();Give row-producing aggregates an alias rather than relying on Salesforce names
such as expr0.
Aggregate functions
import { db } from "./db";
const totals = db
.selectFrom("Opportunity")
.select(({ fn }) => [
fn.count("Id").as("opportunityCount"),
fn.countDistinct("AccountId").as("accountCount"),
fn.sum("Amount").as("totalAmount"),
fn.avg("Amount").as("averageAmount"),
fn.min("CloseDate").as("firstCloseDate"),
fn.max("CloseDate").as("lastCloseDate"),
])
.where("IsClosed", "=", false);| Function | Result |
|---|---|
count(field), countDistinct(field) | number |
sum(field), avg(field) | number | null |
min(field), max(field) | The field value, including null when applicable |
format(aggregate) | A localized string with the aggregate's nullability |
Group results
Group before selecting ordinary fields:
import { db } from "./db";
const byStage = db
.selectFrom("Opportunity")
.select(({ fn }) => fn.sum("Amount").as("totalAmount"))
.groupBy("StageName")
.select("StageName")
.orderBy("StageName");You can also use grouping to return distinct grouped values without adding a dummy aggregate:
import { db } from "./db";
const stages = db
.selectFrom("Opportunity")
.groupBy("StageName")
.select("StageName")
.orderBy("StageName");This is SOQL grouping, not a general SQL distinct() method.
Filter groups with HAVING
WHERE filters source records. HAVING filters the grouped result.
import { db } from "./db";
const significantStages = db
.selectFrom("Opportunity")
.select(({ fn }) => [
fn.count("Id").as("opportunityCount"),
fn.sum("Amount").as("totalAmount"),
])
.where("IsClosed", "=", false)
.groupBy("StageName")
.select("StageName")
.having((eb) =>
eb.and([
eb(eb.fn.count("Id"), ">", 5),
eb(eb.fn.sum("Amount"), ">=", 100_000),
]),
);Order by an aggregate
Use the aggregate expression itself rather than its alias:
import { db } from "./db";
const ranked = db
.selectFrom("Opportunity")
.select(({ fn }) => fn.sum("Amount").as("totalAmount"))
.groupBy("StageName")
.select("StageName")
.orderBy(({ fn }) => fn.sum("Amount"), "desc", "last")
.orderBy("StageName");Rollups and cubes
import { db } from "./db";
const pipeline = db
.selectFrom("Opportunity")
.select(({ fn }) => fn.sum("Amount").as("totalAmount"))
.groupByRollup(["StageName", "Type"])
.select(["StageName", "Type"])
.select(({ fn }) => [
fn.grouping("StageName").as("stageSubtotal"),
fn.grouping("Type").as("typeSubtotal"),
]);Use groupByCube() for cube grouping. Rollup and cube accept up to three fields.
Their subtotal rows can make grouping fields nullable. fn.grouping(field)
returns 0 | 1, where 1 marks a subtotal for that field.
- Ordinary selected fields must be part of the grouping set.
- Ordinary grouping, rollup, and cube cannot be mixed in one query.
HAVINGdoes not support semi-joins.- Root aggregate
OFFSETis limited to0..2000. - Bare
count()has a deliberately smaller builder surface than grouped aggregates. - Salesforce still decides whether a particular aggregate result can be paged.
clearGroupBy() refuses to leave behind selections or clauses that still depend
on grouping. When you need to turn a grouped report back into a record query,
rebuilding the query is usually clearer.
For calendar-style reports, continue with date functions. For Apex output, see Compile for Apex.