# Groups and Aggregates Grouping and aggregation for analytics and reporting. ## Overview Grouping and aggregation enable SQL-like `GROUP BY` operations with aggregate functions (COUNT, SUM, AVG, MIN, MAX) for analytics and reporting queries. **Common Use Cases:** - Sales totals by category - Order counts by customer - Average rating by product - Revenue by month - Inventory by warehouse ## Aggregate Functions ### Count ```json { "aggregates": [ { "path": "*", "type": "Count" } ] } ``` **SQL Equivalent:** `SELECT COUNT(*)` **Response:** ```json { "aggregates": [ { "path": "*", "type": "Count", "value": 150 } ] } ``` ### Sum ```json { "aggregates": [ { "path": "totalAmount", "type": "Sum" } ] } ``` **SQL Equivalent:** `SELECT SUM(totalAmount)` **Response:** ```json { "aggregates": [ { "path": "totalAmount", "type": "Sum", "value": 125430.50 } ] } ``` ### Average ```json { "aggregates": [ { "path": "price", "type": "Average" } ] } ``` **SQL Equivalent:** `SELECT AVG(price)` ### Min ```json { "aggregates": [ { "path": "price", "type": "Min" } ] } ``` **SQL Equivalent:** `SELECT MIN(price)` ### Max ```json { "aggregates": [ { "path": "price", "type": "Max" } ] } ``` **SQL Equivalent:** `SELECT MAX(price)` ## Multiple Aggregates ```json { "aggregates": [ { "path": "*", "type": "Count" }, { "path": "totalAmount", "type": "Sum" }, { "path": "totalAmount", "type": "Average" }, { "path": "totalAmount", "type": "Min" }, { "path": "totalAmount", "type": "Max" } ] } ``` **SQL Equivalent:** ```sql SELECT COUNT(*), SUM(totalAmount), AVG(totalAmount), MIN(totalAmount), MAX(totalAmount) ``` **Response:** ```json { "aggregates": [ { "path": "*", "type": "Count", "value": 450 }, { "path": "totalAmount", "type": "Sum", "value": 547820.75 }, { "path": "totalAmount", "type": "Average", "value": 1217.38 }, { "path": "totalAmount", "type": "Min", "value": 12.50 }, { "path": "totalAmount", "type": "Max", "value": 9999.99 } ] } ``` ## Grouping ### Single Group ```json { "groups": [ { "path": "category" } ], "aggregates": [ { "path": "*", "type": "Count" } ] } ``` **SQL Equivalent:** ```sql SELECT category, COUNT(*) FROM products GROUP BY category ``` **Response:** ```json { "groupedData": [ { "key": { "category": "Electronics" }, "count": 125, "aggregates": [ { "path": "*", "type": "Count", "value": 125 } ] }, { "key": { "category": "Books" }, "count": 200, "aggregates": [ { "path": "*", "type": "Count", "value": 200 } ] }, { "key": { "category": "Toys" }, "count": 75, "aggregates": [ { "path": "*", "type": "Count", "value": 75 } ] } ] } ``` ### Multiple Groups ```json { "groups": [ { "path": "category" }, { "path": "status" } ], "aggregates": [ { "path": "*", "type": "Count" } ] } ``` **SQL Equivalent:** ```sql SELECT category, status, COUNT(*) FROM orders GROUP BY category, status ``` **Response:** ```json { "groupedData": [ { "key": { "category": "Electronics", "status": "Pending" }, "count": 25, "aggregates": [{ "path": "*", "type": "Count", "value": 25 }] }, { "key": { "category": "Electronics", "status": "Completed" }, "count": 100, "aggregates": [{ "path": "*", "type": "Count", "value": 100 }] }, { "key": { "category": "Books", "status": "Pending" }, "count": 40, "aggregates": [{ "path": "*", "type": "Count", "value": 40 }] } ] } ``` ## Filtering with Grouping ### Filter Before Grouping (WHERE) ```json { "filters": [ { "path": "orderDate", "operator": "GreaterThanOrEqual", "value": "2024-01-01T00:00:00Z" } ], "groups": [ { "path": "category" } ], "aggregates": [ { "path": "totalAmount", "type": "Sum" } ] } ``` **SQL Equivalent:** ```sql SELECT category, SUM(totalAmount) FROM orders WHERE orderDate >= '2024-01-01' GROUP BY category ``` ## Common Scenarios ### Scenario 1: Sales by Category ```json { "groups": [ { "path": "category" } ], "aggregates": [ { "path": "*", "type": "Count" }, { "path": "totalAmount", "type": "Sum" }, { "path": "totalAmount", "type": "Average" } ] } ``` **Response:** ```json { "groupedData": [ { "key": { "category": "Electronics" }, "count": 150, "aggregates": [ { "path": "*", "type": "Count", "value": 150 }, { "path": "totalAmount", "type": "Sum", "value": 247350.00 }, { "path": "totalAmount", "type": "Average", "value": 1649.00 } ] } ] } ``` ### Scenario 2: Orders by Customer ```json { "groups": [ { "path": "customerId" } ], "aggregates": [ { "path": "*", "type": "Count" }, { "path": "totalAmount", "type": "Sum" } ], "sorts": [ { "path": "totalAmount", "descending": true } ] } ``` ### Scenario 3: Monthly Revenue ```json { "filters": [ { "path": "orderDate", "operator": "GreaterThanOrEqual", "value": "2024-01-01T00:00:00Z" } ], "groups": [ { "path": "month" } ], "aggregates": [ { "path": "totalAmount", "type": "Sum" }, { "path": "*", "type": "Count" } ] } ``` ### Scenario 4: Product Ratings ```json { "filters": [ { "path": "rating", "operator": "GreaterThan", "value": 0 } ], "groups": [ { "path": "productId" } ], "aggregates": [ { "path": "rating", "type": "Average" }, { "path": "*", "type": "Count" } ] } ``` ### Scenario 5: Inventory by Warehouse ```json { "groups": [ { "path": "warehouseId" }, { "path": "category" } ], "aggregates": [ { "path": "quantity", "type": "Sum" } ] } ``` ## Advanced Examples ### Top Customers by Revenue ```json { "groups": [ { "path": "customerId" } ], "aggregates": [ { "path": "totalAmount", "type": "Sum" }, { "path": "*", "type": "Count" } ], "sorts": [ { "path": "totalAmount", "descending": true } ], "page": 1, "pageSize": 10 } ``` ### Sales Summary by Region and Category ```json { "filters": [ { "path": "orderDate", "operator": "GreaterThanOrEqual", "value": "2024-01-01T00:00:00Z" } ], "groups": [ { "path": "region" }, { "path": "category" } ], "aggregates": [ { "path": "*", "type": "Count" }, { "path": "totalAmount", "type": "Sum" }, { "path": "totalAmount", "type": "Average" } ], "sorts": [ { "path": "region", "descending": false }, { "path": "totalAmount", "descending": true } ] } ``` ## Preparing Data for Grouping ### Add Computed Properties For grouping by month, year, etc., add computed properties to your DTO: ```csharp public class Order { public int Id { get; set; } public DateTime OrderDate { get; set; } public decimal TotalAmount { get; set; } } public record OrderDto { public int Id { get; init; } public DateTime OrderDate { get; init; } public decimal TotalAmount { get; init; } // Computed properties for grouping public int Year => OrderDate.Year; public int Month => OrderDate.Month; public string YearMonth => $"{OrderDate.Year}-{OrderDate.Month:D2}"; } ``` **Group by Month:** ```json { "groups": [ { "path": "yearMonth" } ], "aggregates": [ { "path": "totalAmount", "type": "Sum" } ] } ``` ## Client-Side Processing ### TypeScript ```typescript interface GroupedResult { groupedData: Array<{ key: Record; count: number; aggregates: Array<{ path: string; type: string; value: number; }>; }>; } async function getSalesByCategory(): Promise> { const request = { groups: [{ path: "category" }], aggregates: [ { path: "*", type: "Count" }, { path: "totalAmount", type: "Sum" } ] }; const response = await fetch('/api/query/orderDynamicQuery', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request) }); return await response.json(); } // Process results const result = await getSalesByCategory(); result.groupedData.forEach(group => { const category = group.key.category; const count = group.aggregates.find(a => a.type === 'Count')?.value; const total = group.aggregates.find(a => a.type === 'Sum')?.value; console.log(`${category}: ${count} orders, $${total} total`); }); ``` ### C# HttpClient ```csharp public class OrderAnalyticsClient { private readonly HttpClient _httpClient; public async Task> GetSalesByCategoryAsync() { var request = new { groups = new[] { new { path = "category" } }, aggregates = new[] { new { path = "*", type = "Count" }, new { path = "totalAmount", type = "Sum" } } }; var response = await _httpClient.PostAsJsonAsync( "/api/query/orderDynamicQuery", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync>(); } } public class GroupedResult { public List GroupedData { get; set; } = new(); } public class GroupedItem { public Dictionary Key { get; set; } = new(); public int Count { get; set; } public List Aggregates { get; set; } = new(); } public class AggregateResult { public string Path { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; public decimal Value { get; set; } } ``` ## Performance Considerations ### Add Indexes for Grouped Columns ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { // Index columns used in GROUP BY entity.HasIndex(e => e.Category); entity.HasIndex(e => e.CustomerId); entity.HasIndex(e => e.Status); // Composite index for common groupings entity.HasIndex(e => new { e.Category, e.Status }); }); } ``` ### Filter Before Grouping Always apply filters before grouping to reduce the dataset: ```json { "filters": [ { "path": "orderDate", "operator": "GreaterThanOrEqual", "value": "2024-01-01T00:00:00Z" } ], "groups": [{ "path": "category" }], "aggregates": [{ "path": "totalAmount", "type": "Sum" }] } ``` ## Best Practices ### ✅ DO - Filter data before grouping to reduce dataset - Add indexes on grouped columns - Use pagination with grouped results - Group by indexed columns when possible - Use multiple aggregates to get comprehensive statistics - Sort grouped results for better presentation ### ❌ DON'T - Don't group by high-cardinality columns (like IDs) without pagination - Don't group without aggregates (just use distinct filtering) - Don't skip filtering when working with large datasets - Don't group by computed columns that can't use indexes ## See Also - [Dynamic Queries Overview](README.md) - [Getting Started](getting-started.md) - [Filters and Sorts](filters-and-sorts.md) - [Queryable Providers](queryable-providers.md)