Aggregations
Complete guide to using aggregation functions in CosmosQL - count, sum, average, min, max, and group by operations with full type safety.
Complete guide to using aggregation functions in CosmosQL - count, sum, average, min, max, and group by operations with full type safety.
Table of Contents
- Overview
- Count Operations
- Aggregate Operations
- findMany with Aggregations
- Group By Operations
- Convenience Methods
- Type Safety
- Real-World Examples
- Performance & Best Practices
Overview
CosmosQL provides a complete set of aggregation functions that are:
- ✅ Fully type-safe - TypeScript infers exact result types
- ✅ SQL-compatible - Translates to efficient Cosmos DB SQL
- ✅ Partition-aware - Optimized for Cosmos DB's partitioning
- ✅ Composable - Combine multiple aggregations in one query
Available Aggregations
| Operation | Description | Works With |
|---|---|---|
_count | Count documents | All fields |
_sum | Sum values | Number fields |
_avg | Average values | Number fields |
_min | Find minimum | All fields |
_max | Find maximum | All fields |
Count Operations
Basic Count
Count documents in a partition:
import { createClient, container, field } from 'cosmosql';
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
amount: field.number(),
status: field.string(),
}).partitionKey('customerId');
const client = createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'mydb',
});
const db = await client.withContainers({ orders: orderSchema });
// Count orders for a customer
const count = await db.orders.count({
partitionKey: 'customer-123',
});
// Result: 42Count with Filters
Add WHERE conditions to count specific documents:
// Count only completed orders
const completedCount = await db.orders.count({
partitionKey: 'customer-123',
where: {
status: 'completed',
},
});
// Count with complex filters
const recentHighValueCount = await db.orders.count({
partitionKey: 'customer-123',
where: {
status: 'completed',
amount: { gte: 100 },
createdAt: { gte: new Date('2024-01-01') },
},
});Cross-Partition Count
⚠️ Use sparingly - Cross-partition queries are expensive in Cosmos DB.
// Count ALL orders across all customers
const totalOrders = await db.orders.count({
enableCrossPartitionQuery: true,
});
// Cross-partition count with filter
const allCompletedOrders = await db.orders.count({
enableCrossPartitionQuery: true,
where: { status: 'completed' },
});Count Specific Fields
Count non-null values in specific fields:
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: {
select: {
amount: true, // Count orders with amount
discount: true, // Count orders with discount (might be optional)
trackingNumber: true,
},
},
});
// Result type is inferred:
// {
// _count: {
// amount: number;
// discount: number;
// trackingNumber: number;
// }
// }Aggregate Operations
Single Aggregation
// Sum all order amounts for a customer
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: {
amount: true,
},
});
// Result: { _sum: { amount: 1250.50 | null } }Multiple Aggregations
Combine multiple operations in one query (more efficient than separate queries):
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true, // Total count
_sum: { amount: true }, // Total revenue
_avg: { amount: true }, // Average order value
_min: { amount: true }, // Smallest order
_max: { amount: true }, // Largest order
});
// Result type (fully inferred):
// {
// _count: number;
// _sum: { amount: number | null };
// _avg: { amount: number | null };
// _min: { amount: number | null };
// _max: { amount: number | null };
// }
console.log(`
Orders: ${stats._count}
Total Revenue: $${stats._sum.amount}
Average Order: $${stats._avg.amount}
Range: $${stats._min.amount} - $${stats._max.amount}
`);Aggregations with Filters
// Stats for completed orders only
const completedStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: { status: 'completed' },
_count: true,
_sum: { amount: true },
_avg: { amount: true },
});
// Year-to-date statistics
const ytdStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: {
createdAt: { gte: new Date('2024-01-01') },
},
_sum: { amount: true, tax: true, shipping: true },
_count: true,
});Multiple Field Aggregations
const productSchema = container('products', {
id: field.string(),
category: field.string(),
price: field.number(),
cost: field.number(),
stock: field.number(),
weight: field.number(),
}).partitionKey('category');
const stats = await db.products.aggregate({
partitionKey: 'electronics',
_sum: {
stock: true, // Total inventory
price: true, // Total value at retail
cost: true, // Total cost basis
},
_avg: {
price: true, // Average price
weight: true, // Average weight
},
_min: {
price: true, // Cheapest product
},
_max: {
price: true, // Most expensive product
},
});
// Calculate profit margin
const totalProfit = stats._sum.price! - stats._sum.cost!;
const margin = (totalProfit / stats._sum.price!) * 100;
console.log(`Profit margin: ${margin.toFixed(2)}%`);findMany with Aggregations
The findMany method supports aggregations, allowing you to fetch data and calculate statistics in a single, efficient query. This combines the power of data retrieval with aggregation operations.
Return Types
Without Aggregations (Backward Compatible):
// No aggregate parameter
const users = await db.users.findMany({ partitionKey: "john@example.com" });
// Type: User[]
const selectedUsers = await db.users.findMany({
partitionKey: "john@example.com",
select: { id: true, name: true }
});
// Type: Array<{ id: string, name: string }>With Aggregations:
// With aggregate parameter
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: { _count: true }
});
// Type: { data: User[], _count: number }Basic Usage
const result = await db.users.findMany({
partitionKey: "john@example.com",
where: { isActive: true },
aggregate: {
_count: true,
_avg: { age: true }
}
});
console.log(`Found ${result._count} active users`);
console.log(`Average age: ${result._avg.age}`);
console.log(`Users:`, result.data);Multiple Aggregations
Combine data retrieval with comprehensive statistics:
const stats = await db.orders.findMany({
partitionKey: "customer-123",
aggregate: {
_count: true,
_sum: { totalAmount: true },
_avg: { totalAmount: true },
_min: { createdAt: true },
_max: { createdAt: true }
}
});
console.log(`Order Statistics:`);
console.log(` Total orders: ${stats._count}`);
console.log(` Total revenue: ${stats._sum.totalAmount}`);
console.log(` Average order: ${stats._avg.totalAmount}`);
console.log(` First order: ${stats._min.createdAt}`);
console.log(` Latest order: ${stats._max.createdAt}`);
console.log(` Orders: ${stats.data.length} items`);With Filtering and Selection
const result = await db.products.findMany({
partitionKey: "category-electronics",
where: {
price: { gte: 100 },
inStock: true
},
select: { id: true, name: true, price: true },
aggregate: {
_count: true,
_avg: { price: true },
_min: { price: true },
_max: { price: true }
}
});
// Only selected fields in data
console.log(result.data); // Array<{ id: string, name: string, price: number }>
// All aggregations available
console.log(`Expensive products: ${result._count}`);
console.log(`Price range: ${result._min.price} - ${result._max.price}`);Paginated Results with Total Count
Perfect for building paginated UIs with accurate page counts:
const result = await db.users.findMany({
partitionKey: "john@example.com",
take: 10, // Get first 10 users
skip: 0,
aggregate: {
_count: true, // Count ALL users matching query
}
});
console.log(`Showing ${result.data.length} of ${result._count} total users`);
// Calculate total pages
const totalPages = Math.ceil(result._count / 10);
console.log(`Page 1 of ${totalPages}`);Cross-Partition Queries
const result = await db.products.findMany({
where: { category: "electronics" },
enableCrossPartitionQuery: true,
aggregate: {
_count: true,
_avg: { price: true }
}
});
console.log(`Electronics across all partitions:`);
console.log(` Total products: ${result._count}`);
console.log(` Average price: ${result._avg.price}`);Behavior & Performance
Parallel Execution:
- Data query and aggregation query execute in parallel
- Both queries use the same
where,partitionKey, andenableCrossPartitionQueryfilters - Results are combined into a single response object
Filtering Rules:
whereclause: Applied to both data and aggregation queriesorderBy,take,skip: Applied only to data query (aggregations count all matching records)select: Applied only to data query (aggregations work on full records)
Comparison with Separate Methods
// Before: Two separate queries
const users = await db.users.findMany({ partitionKey: "john@example.com" });
const stats = await db.users.aggregate({
partitionKey: "john@example.com",
_count: true,
_avg: { age: true }
});
// After: Single efficient query
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: true,
_avg: { age: true }
}
});
// result.data contains users
// result._count and result._avg contain aggregationsWhen to Use Each Approach:
Use findMany with aggregate:
- When you need both data and aggregations
- For paginated results with total counts
- When filtering is shared between data and aggregations
Use separate aggregate() method:
- When you only need aggregations (no data)
- For complex group-by operations
- When data and aggregations have different filters
All Aggregation Operations
Count Records:
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: { _count: true }
});
// result._count: number
// Count specific fields (non-null values)
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: { select: { name: true, email: true } }
}
});
// result._count: { name: number, email: number }Sum, Average, Min, Max:
const result = await db.orders.findMany({
partitionKey: "customer-123",
aggregate: {
_sum: { amount: true, tax: true },
_avg: { amount: true },
_min: { amount: true, createdAt: true },
_max: { amount: true, createdAt: true }
}
});
// All typed correctly based on field types
// number fields return number | null
// date fields return Date | nullFull TypeScript Support
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: true,
_avg: { age: true },
_sum: { score: true }
}
});
// Type: {
// data: User[],
// _count: number,
// _avg: { age: number | null },
// _sum: { score: number | null }
// }Group By Operations
Group By is the most powerful aggregation feature - it splits data into groups and performs aggregations on each group.
Group by Single Field
const salesSchema = container('sales', {
id: field.string(),
storeId: field.string(),
category: field.string(),
amount: field.number(),
quantity: field.number(),
}).partitionKey('storeId');
// Sales by category
const categoryStats = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true, quantity: true },
_avg: { amount: true },
});
// Result type (fully inferred):
// Array<{
// category: string;
// _count: number;
// _sum: { amount: number | null; quantity: number | null };
// _avg: { amount: number | null };
// }>
// Use the results
for (const group of categoryStats) {
console.log(`${group.category}:
Sales: $${group._sum.amount}
Units: ${group._sum.quantity}
Avg: $${group._avg.amount}
`);
}Group by Multiple Fields
Use an array to group by multiple fields:
// Sales by store AND category
const detailedStats = await db.sales.groupBy({
by: ['storeId', 'category'] as const, // 'as const' for better type inference
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
});
// Result type:
// Array<{
// storeId: string;
// category: string;
// _count: number;
// _sum: { amount: number | null };
// }>
// Find best performing store-category combination
const topPerformer = detailedStats.reduce((max, group) =>
(group._sum.amount || 0) > (max._sum.amount || 0) ? group : max
);
console.log(`Top: ${topPerformer.storeId} - ${topPerformer.category}`);Group By with Ordering
Sort results by aggregation values:
// Top categories by revenue
const topCategories = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
orderBy: {
_sum_amount: 'desc', // Order by aggregated field
},
take: 10, // Top 10
});
// Categories with most transactions
const popularCategories = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
orderBy: {
_count: 'desc',
},
take: 5,
});Group By with Pagination
Grouped results are sorted and paginated in memory after the database query. take and skip limit the returned groups, but do not reduce the query cost or memory needed to fetch them.
// Paginated category report
const page1 = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
orderBy: { category: 'asc' },
take: 20,
skip: 0,
});
const page2 = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
orderBy: { category: 'asc' },
take: 20,
skip: 20,
});Group By with Filters
Combine WHERE clauses with GROUP BY:
const userSchema = container('analytics', {
id: field.string(),
userId: field.string(),
eventType: field.string(),
country: field.string(),
timestamp: field.date(),
duration: field.number(),
}).partitionKey('userId');
// Events by type for a specific user in the last 30 days
const recentEvents = await db.analytics.groupBy({
by: 'eventType',
partitionKey: 'user-123',
where: {
timestamp: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
},
_count: true,
_avg: { duration: true },
});
// Cross-partition: Events by country for premium users
const countryStats = await db.analytics.groupBy({
by: 'country',
enableCrossPartitionQuery: true,
where: {
eventType: 'purchase',
},
_count: true,
_sum: { duration: true },
});Convenience Methods
For simple single-field aggregations, use convenience methods:
sum()
// Quick sum - cleaner than aggregate()
const totalRevenue = await db.orders.sum('amount', {
partitionKey: 'customer-123',
});
// Returns: number | null
// With filter
const completedRevenue = await db.orders.sum('amount', {
partitionKey: 'customer-123',
where: { status: 'completed' },
});avg()
// Average order value
const avgOrderValue = await db.orders.avg('amount', {
partitionKey: 'customer-123',
});
// Returns: number | null
// Average rating for a product
const avgRating = await db.reviews.avg('rating', {
partitionKey: 'product-456',
where: { verified: true },
});min()
// Find cheapest product
const cheapestPrice = await db.products.min('price', {
partitionKey: 'electronics',
});
// Returns: number | null
// Earliest order date
const firstOrder = await db.orders.min('createdAt', {
partitionKey: 'customer-123',
});
// Returns: Date | null (type inferred from field!)max()
// Most expensive product
const highestPrice = await db.products.max('price', {
partitionKey: 'electronics',
});
// Latest activity
const lastSeen = await db.users.max('lastLoginAt', {
partitionKey: 'tenant-1',
});
// Returns: Date | nullType Safety
CosmosQL provides complete type inference for all aggregation operations.
Automatic Type Inference
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
amount: field.number(),
tax: field.number().optional(),
status: field.string(),
createdAt: field.date(),
}).partitionKey('customerId');
// TypeScript knows exact types
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true, tax: true },
_min: { createdAt: true },
_max: { createdAt: true },
});
// ✅ All these are correctly typed:
const count: number = stats._count;
const total: number | null = stats._sum.amount;
const taxTotal: number | null = stats._sum.tax;
const firstOrder: Date | null = stats._min.createdAt;
const lastOrder: Date | null = stats._max.createdAt;
// ❌ TypeScript errors:
// stats._sum.status; // Error: 'status' is not a number field
// stats._avg.createdAt; // Error: 'createdAt' is not a number fieldGroup By Type Inference
// Single field grouping
const result1 = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
});
// Type: Array<{ status: string; _count: number; _sum: { amount: number | null } }>
// Multiple field grouping
const result2 = await db.orders.groupBy({
by: ['customerId', 'status'] as const,
enableCrossPartitionQuery: true,
_count: true,
});
// Type: Array<{ customerId: string; status: string; _count: number }>
// Access fields with full type safety
for (const group of result1) {
console.log(group.status); // ✅ string
console.log(group._count); // ✅ number
console.log(group._sum.amount); // ✅ number | null
}Optional Fields
const productSchema = container('products', {
id: field.string(),
category: field.string(),
price: field.number(),
salePrice: field.number().optional(), // Optional field
}).partitionKey('category');
// TypeScript knows salePrice might be undefined
const result = await db.products.aggregate({
partitionKey: 'electronics',
_sum: { price: true, salePrice: true }, // ✅ Works with optional fields
_avg: { salePrice: true },
});
// All aggregation results are nullable
const total: number | null = result._sum.price;
const saleTotal: number | null = result._sum.salePrice;Type-Safe Field Selection
// Only number fields can be summed/averaged
await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: {
amount: true, // ✅ number field
// status: true, // ❌ Error: 'status' is not a number
},
_avg: {
amount: true, // ✅ number field
// createdAt: true, // ❌ Error: dates can't be averaged
},
});
// Any field can be used with min/max/count
await db.orders.aggregate({
partitionKey: 'customer-123',
_min: {
amount: true, // ✅ number
status: true, // ✅ string
createdAt: true, // ✅ date
},
});Real-World Examples
E-Commerce Analytics Dashboard
// Schema
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
status: field.string(),
totalAmount: field.number(),
itemCount: field.number(),
discount: field.number().optional(),
createdAt: field.date(),
}).partitionKey('customerId');
// Customer lifetime value
async function getCustomerLTV(customerId: string) {
const stats = await db.orders.aggregate({
partitionKey: customerId,
where: { status: 'completed' },
_count: true,
_sum: { totalAmount: true, itemCount: true, discount: true },
_avg: { totalAmount: true },
_min: { createdAt: true },
_max: { createdAt: true },
});
return {
totalOrders: stats._count,
lifetimeValue: stats._sum.totalAmount || 0,
totalItems: stats._sum.itemCount || 0,
totalSavings: stats._sum.discount || 0,
avgOrderValue: stats._avg.totalAmount || 0,
firstOrderDate: stats._min.createdAt,
lastOrderDate: stats._max.createdAt,
customerSince: stats._min.createdAt
? Math.floor((Date.now() - stats._min.createdAt.getTime()) / (1000 * 60 * 60 * 24))
: 0,
};
}
// Sales by status report
async function getSalesStatusReport() {
const statusReport = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
_sum: { totalAmount: true, itemCount: true },
_avg: { totalAmount: true },
});
return statusReport.map(group => ({
status: group.status,
count: group._count,
revenue: group._sum.totalAmount || 0,
items: group._sum.itemCount || 0,
avgValue: group._avg.totalAmount || 0,
}));
}
// Top customers
async function getTopCustomers(limit: number = 10) {
const customers = await db.orders.groupBy({
by: 'customerId',
enableCrossPartitionQuery: true,
_count: true,
_sum: { totalAmount: true },
orderBy: { _sum_totalAmount: 'desc' },
take: limit,
});
return customers.map(c => ({
customerId: c.customerId,
orders: c._count,
spent: c._sum.totalAmount || 0,
}));
}Analytics Platform
const eventSchema = container('events', {
id: field.string(),
userId: field.string(),
eventType: field.string(),
page: field.string(),
duration: field.number(),
timestamp: field.date(),
}).partitionKey('userId');
// User engagement metrics
async function getUserEngagement(userId: string, days: number = 30) {
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const metrics = await db.events.aggregate({
partitionKey: userId,
where: { timestamp: { gte: since } },
_count: true,
_sum: { duration: true },
_avg: { duration: true },
});
const eventBreakdown = await db.events.groupBy({
by: 'eventType',
partitionKey: userId,
where: { timestamp: { gte: since } },
_count: true,
_avg: { duration: true },
});
return {
totalEvents: metrics._count,
totalTime: metrics._sum.duration || 0,
avgSessionTime: metrics._avg.duration || 0,
eventTypes: eventBreakdown,
};
}
// Platform-wide statistics
async function getPlatformStats() {
// Events by type
const eventTypes = await db.events.groupBy({
by: 'eventType',
enableCrossPartitionQuery: true,
where: {
timestamp: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
_count: true,
_avg: { duration: true },
orderBy: { _count: 'desc' },
});
// Page engagement
const pages = await db.events.groupBy({
by: 'page',
enableCrossPartitionQuery: true,
where: {
timestamp: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
_count: true,
_sum: { duration: true },
orderBy: { _count: 'desc' },
take: 10,
});
return { eventTypes, topPages: pages };
}Inventory Management
const productSchema = container('products', {
id: field.string(),
warehouseId: field.string(),
category: field.string(),
stock: field.number(),
reorderPoint: field.number(),
cost: field.number(),
price: field.number(),
}).partitionKey('warehouseId');
// Warehouse inventory summary
async function getWarehouseSummary(warehouseId: string) {
const summary = await db.products.aggregate({
partitionKey: warehouseId,
_count: true,
_sum: { stock: true, cost: true, price: true },
});
const categoryBreakdown = await db.products.groupBy({
by: 'category',
partitionKey: warehouseId,
_count: true,
_sum: { stock: true },
_avg: { price: true },
});
const inventoryValue = (summary._sum.cost || 0) * (summary._sum.stock || 0);
const retailValue = (summary._sum.price || 0) * (summary._sum.stock || 0);
return {
totalProducts: summary._count,
totalStock: summary._sum.stock || 0,
inventoryValue,
retailValue,
potentialProfit: retailValue - inventoryValue,
byCategory: categoryBreakdown,
};
}
// Low stock alerts
async function getLowStockItems(warehouseId: string) {
const items = await db.products.findMany({
partitionKey: warehouseId,
where: {
stock: { lte: field.number() }, // Stock <= reorderPoint
},
});
// Get summary stats
const stats = await db.products.aggregate({
partitionKey: warehouseId,
where: {
stock: { lte: field.number() },
},
_count: true,
_sum: { stock: true },
});
return {
items,
totalLowStockItems: stats._count,
totalLowStockUnits: stats._sum.stock || 0,
};
}Performance & Best Practices
✅ DO: Use Partition Keys
Always provide a partition key when possible:
// ✅ GOOD - Single partition query (fast & cheap)
const count = await db.orders.count({
partitionKey: 'customer-123',
});
// ⚠️ AVOID - Cross-partition query (slow & expensive)
const count = await db.orders.count({
enableCrossPartitionQuery: true,
});✅ DO: Combine Multiple Aggregations
More efficient to get multiple aggregations in one query:
// ✅ GOOD - Single query
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true },
_avg: { amount: true },
});
// ❌ BAD - Three separate queries
const count = await db.orders.count({ partitionKey: 'customer-123' });
const sum = await db.orders.sum('amount', { partitionKey: 'customer-123' });
const avg = await db.orders.avg('amount', { partitionKey: 'customer-123' });✅ DO: Use WHERE Clauses
Filter data to reduce computation:
// ✅ GOOD - Only aggregate what you need
const completedStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: { status: 'completed' },
_sum: { amount: true },
});
// ⚠️ AVOID - Aggregating everything then filtering
const allStats = await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: { amount: true },
});
// Then filtering in application code✅ DO: Use Pagination for Large Results
// ✅ GOOD - Paginate large group by results
async function* getAllGroups() {
let skip = 0;
const pageSize = 100;
while (true) {
const page = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
take: pageSize,
skip,
});
if (page.length === 0) break;
yield* page;
skip += pageSize;
}
}⚠️ AVOID: Unnecessary Cross-Partition Queries
// ❌ BAD - Expensive cross-partition query
const total = await db.orders.sum('amount', {
enableCrossPartitionQuery: true,
});
// ✅ BETTER - Store aggregates separately if needed frequently
// Use a separate container or materialized views⚠️ AVOID: Aggregating Large Date Ranges Without Filters
// ❌ BAD - Scanning entire history
const allTimeStats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
_sum: { amount: true },
});
// ✅ BETTER - Limit to relevant time period
const recentStats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
where: {
createdAt: { gte: new Date('2024-01-01') },
},
_sum: { amount: true },
});Understanding NULL Results
Aggregations return null when there are no rows:
const stats = await db.orders.aggregate({
partitionKey: 'new-customer',
_sum: { amount: true },
_avg: { amount: true },
});
// stats._sum.amount might be null (no orders)
// Always handle null case:
const total = stats._sum.amount ?? 0;
const average = stats._avg.amount ?? 0;
// Or use nullish coalescing in display:
console.log(`Total: $${stats._sum.amount || 0}`);Cosmos DB RU Costs
Aggregation operations consume Request Units (RUs):
- Single Partition: ~2-5 RUs (very efficient)
- Cross-Partition: 5-100+ RUs depending on data size
- Group By: Higher RU cost, especially cross-partition
Monitor your RU consumption and optimize queries accordingly.
Advanced Patterns
Combining Aggregations with Find
// Get both detail and summary
async function getOrderReport(customerId: string) {
const [orders, stats] = await Promise.all([
db.orders.findMany({
partitionKey: customerId,
orderBy: { createdAt: 'desc' },
take: 10,
}),
db.orders.aggregate({
partitionKey: customerId,
_count: true,
_sum: { amount: true },
_avg: { amount: true },
}),
]);
return { recentOrders: orders, summary: stats };
}Calculating Percentages
const statusBreakdown = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
});
const total = statusBreakdown.reduce((sum, g) => sum + g._count, 0);
const withPercentages = statusBreakdown.map(group => ({
status: group.status,
count: group._count,
percentage: ((group._count / total) * 100).toFixed(2) + '%',
}));Time-Series Aggregations
// Daily sales for last 30 days
async function getDailySales(days: number = 30) {
const results = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() - i);
const startOfDay = new Date(date.setHours(0, 0, 0, 0));
const endOfDay = new Date(date.setHours(23, 59, 59, 999));
const stats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
where: {
createdAt: { gte: startOfDay, lte: endOfDay },
status: 'completed',
},
_count: true,
_sum: { amount: true },
});
results.push({
date: startOfDay,
orders: stats._count,
revenue: stats._sum.amount || 0,
});
}
return results;
}Error Handling
import { CosmosError } from 'cosmosql';
try {
const stats = await db.orders.aggregate({
// partitionKey missing!
_sum: { amount: true },
});
} catch (error) {
if (error instanceof CosmosError) {
if (error.statusCode === 429) {
// Rate limited - retry with backoff
console.log('Rate limited, retry after:', error.retryAfter);
} else if (error.code === 'PARTITION_KEY_REQUIRED') {
// Missing partition key
console.error('Must provide partition key or enable cross-partition query');
}
}
throw error;
}Migration from Raw Cosmos DB
If you're migrating from raw Cosmos DB SDK:
Before (Raw SDK)
const querySpec = {
query: 'SELECT COUNT(1) as count, SUM(c.amount) as total FROM c WHERE c.customerId = @customerId',
parameters: [{ name: '@customerId', value: 'customer-123' }],
};
const { resources } = await container.items.query(querySpec).fetchAll();
const result = resources[0];After (CosmosQL)
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true },
});
// Result is fully typed!Summary
CosmosQL's aggregation functions provide:
- Type Safety - Full TypeScript inference for all operations
- Clean API - Intuitive, composable operations
- Performance - Optimized queries with partition awareness
- Flexibility - From simple counts to complex group by operations
- Developer Experience - IntelliSense, autocompletion, and compile-time checks
Start with simple count() operations, then explore aggregate() for multiple operations, and finally groupBy() for advanced analytics. Always consider partition keys and cross-partition costs when designing your queries.
For more examples, see the API reference or join our community for support.