Skip to main content
Drizzle ORM provides type-safe aggregate functions to perform calculations across multiple rows of data.

Aggregate Functions

Drizzle supports all standard SQL aggregate functions:
import { count } from 'drizzle-orm';
import { users } from './schema';

// Count all users
const result = await db.select({ 
  totalUsers: count() 
}).from(users);

// Count specific column (excludes nulls)
const result = await db.select({ 
  usersWithEmail: count(users.email) 
}).from(users);

Count Distinct

Count unique values:
import { countDistinct } from 'drizzle-orm';
import { orders } from './schema';

// Count unique customers
const result = await db.select({ 
  uniqueCustomers: countDistinct(orders.customerId) 
}).from(orders);

Group By

Group results by one or more columns:
import { count } from 'drizzle-orm';
import { users } from './schema';

// Count users by role
const result = await db.select({
  role: users.role,
  count: count(),
})
  .from(users)
  .groupBy(users.role);

// Result:
// [
//   { role: 'admin', count: 5 },
//   { role: 'user', count: 150 },
// ]

Having Clause

Filter grouped results:
import { count, gt } from 'drizzle-orm';

// Roles with more than 10 users
const result = await db.select({
  role: users.role,
  count: count(),
})
  .from(users)
  .groupBy(users.role)
  .having(({ count }) => gt(count, 10));

Aggregations with Joins

Combine aggregates with joined tables:
import { count, eq } from 'drizzle-orm';
import { users, posts } from './schema';

// Count posts per user
const result = await db.select({
  userId: users.id,
  userName: users.name,
  postCount: count(posts.id),
})
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .groupBy(users.id, users.name);

Multiple Aggregates

Calculate several aggregates in one query:
import { count, sum, avg, min, max } from 'drizzle-orm';
import { orders } from './schema';

const result = await db.select({
  totalOrders: count(),
  totalRevenue: sum(orders.amount),
  avgOrderValue: avg(orders.amount),
  minOrder: min(orders.amount),
  maxOrder: max(orders.amount),
}).from(orders);

// Result:
// [{
//   totalOrders: 1523,
//   totalRevenue: '45600.50',
//   avgOrderValue: '29.95',
//   minOrder: '5.00',
//   maxOrder: '499.99',
// }]
Aggregate functions return strings for numeric types to preserve precision. Convert to numbers if needed: Number(result.totalRevenue)

Conditional Aggregation

Aggregate with conditions:
import { sql, sum } from 'drizzle-orm';
import { orders } from './schema';

// Count and sum by status
const result = await db.select({
  totalOrders: count(),
  completedOrders: sum(
    sql`CASE WHEN ${orders.status} = 'completed' THEN 1 ELSE 0 END`
  ),
  completedRevenue: sum(
    sql`CASE WHEN ${orders.status} = 'completed' THEN ${orders.amount} ELSE 0 END`
  ),
}).from(orders);

Date-based Aggregations

Group by time periods:
import { sql, sum } from 'drizzle-orm';

// Sales by month
const result = await db.select({
  month: sql<string>`to_char(${orders.createdAt}, 'YYYY-MM')`,
  totalSales: sum(orders.amount),
  orderCount: count(),
})
  .from(orders)
  .groupBy(sql`to_char(${orders.createdAt}, 'YYYY-MM')`);

Window Functions

Use window functions for advanced analytics:
import { sql } from 'drizzle-orm';
import { sales } from './schema';

// Running total and rank
const result = await db.select({
  date: sales.date,
  amount: sales.amount,
  runningTotal: sql<number>`SUM(${sales.amount}) OVER (ORDER BY ${sales.date})`,
  rank: sql<number>`RANK() OVER (ORDER BY ${sales.amount} DESC)`,
}).from(sales);

Statistical Aggregations

Calculate statistical measures:
import { sql } from 'drizzle-orm';

const result = await db.select({
  avgPrice: avg(products.price),
  variance: sql<number>`VARIANCE(${products.price})`,
  stdDev: sql<number>`STDDEV(${products.price})`,
}).from(products);

Array Aggregation (PostgreSQL)

Aggregate values into arrays:
import { sql } from 'drizzle-orm';
import { users, posts } from './schema';

// Get users with array of post titles
const result = await db.select({
  userId: users.id,
  userName: users.name,
  postTitles: sql<string[]>`array_agg(${posts.title})`,
})
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .groupBy(users.id, users.name);

String Aggregation

Concatenate strings:
import { sql } from 'drizzle-orm';

const result = await db.select({
  userId: users.id,
  tags: sql<string>`string_agg(${posts.tag}, ', ')`,
})
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .groupBy(users.id);

Filtering Before Aggregation

Use WHERE to filter before grouping:
import { count, eq, gte } from 'drizzle-orm';

// Count only active users by role
const result = await db.select({
  role: users.role,
  count: count(),
})
  .from(users)
  .where(eq(users.active, true))
  .groupBy(users.role);

Distinct Aggregations

Aggregate distinct values:
import { sql, countDistinct, sumDistinct } from 'drizzle-orm';

const result = await db.select({
  uniqueCustomers: countDistinct(orders.customerId),
  uniqueProducts: countDistinct(orders.productId),
  // Sum distinct amounts (unusual but possible)
  distinctTotal: sumDistinct(orders.amount),
}).from(orders);

Nested Aggregations with Subqueries

Aggregate over aggregated results:
import { sql } from 'drizzle-orm';

// Average of per-user post counts
const userPostCounts = db.select({
  userId: posts.userId,
  postCount: count(),
})
  .from(posts)
  .groupBy(posts.userId)
  .as('user_post_counts');

const result = await db.select({
  avgPostsPerUser: avg(userPostCounts.postCount),
})
  .from(userPostCounts);

Type Safety

Aggregate functions are fully typed:
const result = await db.select({
  total: count(),          // SQL<number>
  avgPrice: avg(products.price),  // SQL<string | null>
  maxPrice: max(products.price),  // SQL<number | null>
}).from(products);

// TypeScript knows the types
result[0].total;    // number
result[0].avgPrice; // string | null
result[0].maxPrice; // number | null

Common Aggregation Patterns

Count by Category

await db.select({
  category: products.category,
  count: count(),
})
  .from(products)
  .groupBy(products.category);

Top Performers

await db.select({
  userId: sales.userId,
  total: sum(sales.amount),
})
  .from(sales)
  .groupBy(sales.userId)
  .orderBy(desc(sum(sales.amount)))
  .limit(10);

Period Comparison

await db.select({
  month: sql`DATE_TRUNC('month', created_at)`,
  revenue: sum(orders.amount),
})
  .from(orders)
  .groupBy(sql`DATE_TRUNC('month', created_at)`);

Summary Statistics

await db.select({
  count: count(),
  avg: avg(scores.value),
  min: min(scores.value),
  max: max(scores.value),
})
  .from(scores);

Performance Tips

1

Index Group By Columns

Ensure columns used in GROUP BY are indexed for faster aggregation
2

Filter Before Grouping

Use WHERE clauses to reduce rows before grouping
3

Limit Result Sets

Use LIMIT when you only need top N results
4

Use Covering Indexes

Create indexes that include all columns needed for the query

Next Steps

Joins

Combine aggregations with joins

Subqueries

Use aggregations in subqueries

Select Queries

Master the select query builder

Build docs developers (and LLMs) love