Skip to content
Stand With Ukraine

Aggregate functions

Various aggregate functions are supported (count, min, max, string_agg, etc.) and it's possible to call a custom aggregate function.

Each aggregate function is accepting such options:

ts
type AggregateOptions = {
  // Add DISTINCT inside of function call.
  distinct?: boolean;

  // The same argument as in .order() to be set inside of function call.
  order?: OrderArg | OrderArg[];

  // The same argument as in .where() to be set inside of function call.
  filter?: WhereArg;

  // The same argument as in .orWhere() to support OR logic of the filter clause.
  filterOr?: WhereArg[];

  // Adds WITHIN GROUP SQL statement.
  withinGroup?: boolean;

  // Defines OVER clause.
  // Can be the name of a window defined by calling the .window() method,
  // or object the same as the .window() method takes to define a window.
  over?: WindowName | OverOptions;
};

Calling aggregate function on a table will return a simple value:

ts
const result: number = await db.table.count();

All functions can be called inside a select callback to select an aggregated value:

ts
// avg can be null in case when no records
const result: { count: number; avg: number | null }[] = await db.table.select({
  count: (q) => q.count(),
  avg: (q) => q.avg('price'),
});

They can be used in having:

ts
db.table.having((q) => q.count().gte(10));

Functions can be chained with column operators. Strictly according to the return type of the function, count can be chained with gt but not with contains.

ts
// numeric functions can be chained with `gt`, `lt`, and other numeric operators:
const bool = await db.table.sum('numericColumn').gt(5);

await db.table.select({
  someTitleContainsXXX: (q) => q.stringAgg('title').contains('xxx'),
  notAllBooleansAreTrue: (q) => q.boolAnd('booleanColumn').not(true),
});

Multiple aggregate functions can be joined with and or or in a such way:

ts
// SELECT count(*) > 5 AND "numericColumn" < 100 FROM "table"
const bool = await db.table
  .count()
  .gt(5)
  .and(db.table.sum('numericColumn').lt(100));

const { theSameBool } = await db.table.select({
  theSameBool: (q) => q.count().gt(5).and(q.sum('numericColumn').lt(100)),
});

count

Count records with the count function:

ts
// count all records:
const result: number = await db.table.count();

// count records where a column is not NULL:
db.table.count('name');

// see options above:
db.table.count('*', aggregateOptions);

// select counts of people grouped by city
db.people
  .select('city', {
    population: (q) => q.count(),
  })
  .group('city');

min

Get the minimum value for the specified numeric column, returns number or null if there are no records.

ts
const result: number | null = await db.table.min(
  'numericColumn',
  aggregateOptions,
);

// select min product price grouped by product category
db.product
  .select('category', {
    minPrice: (q) => q.min('price'),
  })
  .group('category')
  .take();

max

Gets the maximum value for the specified numeric column, returns number or null if there are no records.

ts
const result: number | null = await db.table.max(
  'numericColumn',
  aggregateOptions,
);

// select max product price grouped by product category
db.product
  .select('category', {
    maxPrice: (q) => q.max('price'),
  })
  .group('category')
  .take();

sum

Retrieve the sum of the values of a given numeric column, returns number or null if there are no records.

ts
const result: number | null = await db.table.sum(
  'numericColumn',
  aggregateOptions,
);

// select sum of employee salaries grouped by years
db.employee
  .select('year', {
    yearlySalaries: (q) => q.sum('salary'),
  })
  .group('year');

avg

Retrieve the average value of a numeric column, it returns a number or null if there are no records.

ts
const result: number | null = db.table.avg('numericColumn', aggregateOptions);

// select average movies ratings
db.movie
  .select('title', {
    averageRating: (q) => q.avg('rating'),
  })
  .group('title');

bitAnd

Bitwise and aggregation, returns number or null if there are no records.

ts
const result: number | null = db.table.bitAnd(
  'numericColumn',
  aggregateOptions,
);

// select grouped `bitAnd`
db.table
  .select('someColumn', {
    bitAnd: (q) => q.bitAnd('numericColumn'),
  })
  .group('someColumn');

bitOr

Bitwise or aggregation, returns number or null if there are no records.

ts
const result: number | null = db.table.bitOr('numericColumn', aggregateOptions);

// select grouped `bitOr`
db.table
  .select('someColumn', {
    bitOr: (q) => q.bitOr('numericColumn'),
  })
  .group('someColumn');

boolAnd

Aggregate booleans with and logic, it returns boolean or null if there are no records.

ts
const result: boolean | null = db.table.boolAnd(
  'booleanColumn',
  aggregateOptions,
);

// select grouped `boolAnd`
db.table
  .select('someColumn', {
    boolAnd: (q) => q.boolAnd('booleanColumn'),
  })
  .group('someColumn');

boolOr

Aggregate booleans with or logic, it returns boolean or null if there are no records.

ts
const result: boolean | null = db.table.boolOr(
  'booleanColumn',
  aggregateOptions,
);

// select grouped `boolOr`
db.table
  .select('someColumn', {
    boolOr: (q) => q.boolOr('booleanColumn'),
  })
  .group('someColumn');

every

Equivalent to boolAnd.

jsonAgg and jsonbAgg

Aggregate values into an array by using json_agg. Returns array of values or null if there are no records.

jsonAgg is working a bit faster, jsonbAgg is better only when applying JSON operations in SQL.

ts
const idsOrNull: number[] | null = db.table.jsonAgg('id', aggregateOptions);

const namesOrNull: string[] | null = db.table.jsonbAgg(
  'name',
  aggregateOptions,
);

// select grouped `jsonAgg`
db.table
  .select('someColumn', {
    jsonAgg: (q) => q.jsonAgg('anyColumn'),
  })
  .group('someColumn');

jsonObjectAgg and jsonbObjectAgg

It does the construction of JSON objects, keys are provided strings and values can be table columns or raw SQL expressions, and returns object or null if no records.

jsonObjectAgg is different from jsonbObjectAgg by internal representation in the database, jsonObjectAgg is a bit faster as it constructs a simple string.

ts
import { TextColumn } from './string';

// object has type { nameAlias: string, foo: string } | null
const object = await db.table.jsonObjectAgg(
  {
    // select a column with alias
    nameAlias: 'name',
    // select raw SQL with alias
    foo: sql<string>`"bar" || "baz"`,
  },
  aggregateOptions,
);

// select aggregated object
db.table.select('id', {
  object: (q) =>
    q.jsonObjectAgg({
      nameAlias: 'name',
      foo: sql<string>`"bar" || "baz"`,
    }),
});

stringAgg

Select joined strings, it returns a string or null if no records.

ts
const result: string | null = db.table.stringAgg(
  'name',
  ', ',
  aggregateOptions,
);

// select joined strings grouped by some column
db.table
  .select('someColumn', {
    joinedNames: (q) => q.stringAgg('name', ', '),
  })
  .group('someColumn');

xmlAgg

Concatenates xml columns, returns a string or null if no records.

ts
const xml: string | null = await db.table.xmlAgg('xmlColumn', aggregateOptions);

// select joined XMLs grouped by some column
db.table
  .select('someColumn', {
    joinedXMLs: (q) => q.xmlAgg('xml'),
  })
  .group('someColumn');