| name | database-optimization |
| description | "[DevOps & Infra] Use when optimizing database queries, indexes, N+1 problems, slow queries, or analyzing query performance. Triggers on keywords like "slow query", "N+1", "index", "query optimization", "database performance", "eager loading"." |
Database Optimization
Expert database performance agent for EasyPlatform. Optimizes queries, indexes, and data access patterns for MongoDB, SQL Server, and PostgreSQL.
Summary
Goal: Optimize database queries, indexes, and data access patterns for MongoDB, SQL Server, and PostgreSQL in EasyPlatform.
- N+1 queries — Use eager loading (
loadRelatedEntities) or batch load with GetByIdsAsync
- Select projections — Fetch only needed columns, not entire entities
- Parallel queries — Run independent queries with
Util.ParallelAsync() instead of sequential awaits
- Indexing — Add indexes for frequently filtered/sorted columns; use compound indexes for multi-field queries
- Pagination — Always use
PlatformCqrsPagedQuery for list endpoints
Key Principles:
- Profile before optimizing — identify actual bottlenecks with evidence
- Use platform repository patterns (expressions, projections) over raw queries
- Batch related entity loads to eliminate N+1; never query inside loops
Common Performance Issues
N+1 Query Problem
var employees = await repo.GetAllAsync(e => e.CompanyId == companyId, ct);
foreach (var emp in employees)
{
var dept = await deptRepo.GetByIdAsync(emp.DepartmentId, ct);
}
var employees = await repo.GetAllAsync(
e => e.CompanyId == companyId,
ct,
loadRelatedEntities: e => e.Department);
var employees = await repo.GetAllAsync(e => e.CompanyId == companyId, ct);
var deptIds = employees.Select(e => e.DepartmentId).Distinct().ToList();
var departments = await deptRepo.GetByIdsAsync(deptIds, ct);
var deptMap = departments.ToDictionary(d => d.Id);
employees.ForEach(e => e.Department = deptMap.GetValueOrDefault(e.DepartmentId));
Select Only Needed Columns
var employee = await repo.GetByIdAsync(id, ct);
return employee.Id;
var employeeId = await repo.FirstOrDefaultAsync(
query => query
.Where(Employee.UniqueExpr(userId, companyId))
.Select(e => e.Id),
ct);
Parallel Independent Queries
var count = await repo.CountAsync(filter, ct);
var items = await repo.GetAllAsync(filter, ct);
var stats = await statsRepo.GetAsync(companyId, ct);
var (count, items, stats) = await (
repo.CountAsync((uow, q) => queryBuilder(uow, q), ct),
repo.GetAllAsync((uow, q) => queryBuilder(uow, q).PageBy(skip, take), ct),
statsRepo.GetAsync(companyId, ct)
);
Query Optimization Patterns
GetQueryBuilder for Reusable Queries
protected override async Task<Result> HandleAsync(Query req, CancellationToken ct)
{
var queryBuilder = repo.GetQueryBuilder((uow, q) => q
.Where(Employee.OfCompanyExpr(RequestContext.CurrentCompanyId()))
.WhereIf(req.Statuses.Any(), e => req.Statuses.Contains(e.Status))
.WhereIf(req.DepartmentId.IsNotNullOrEmpty(), e => e.DepartmentId == req.DepartmentId)
.PipeIf(req.SearchText.IsNotNullOrEmpty(), q =>
fullTextSearch.Search(q, req.SearchText, Employee.SearchColumns())));
var (total, items) = await (
repo.CountAsync((uow, q) => queryBuilder(uow, q), ct),
repo.GetAllAsync((uow, q) => queryBuilder(uow, q)
.OrderByDescending(e => e.CreatedDate)
.PageBy(req.SkipCount, req.MaxResultCount), ct)
);
return new Result(items, total);
}
Conditional Filtering with WhereIf
var query = repo.GetQueryBuilder((uow, q) => q
.Where(e => e.CompanyId == companyId)
.WhereIf(status.HasValue, e => e.Status == status)
.WhereIf(deptIds.Any(), e => deptIds.Contains(e.DepartmentId))
.WhereIf(dateFrom.HasValue, e => e.CreatedDate >= dateFrom)
.WhereIf(dateTo.HasValue, e => e.CreatedDate <= dateTo));
Full-Text Search Optimization
public static Expression<Func<Employee, object?>>[] DefaultFullTextSearchColumns()
=> [e => e.FullName, e => e.Email, e => e.EmployeeCode, e => e.FullTextSearch];
.PipeIf(searchText.IsNotNullOrEmpty(), q => fullTextSearch.Search(
q,
searchText,
Employee.DefaultFullTextSearchColumns(),
fullTextAccurateMatch: true,
includeStartWithProps: [e => e.FullName, e => e.EmployeeCode]
));
Index Recommendations
MongoDB Indexes
{ "CompanyId": 1 }
{ "CompanyId": 1, "Status": 1, "CreatedDate": -1 }
{ "FullName": "text", "Email": "text", "EmployeeCode": "text" }
{ "ExternalId": 1, sparse: true }
SQL Server / PostgreSQL Indexes
CREATE INDEX IX_Employee_Company_Status
ON Employees (CompanyId, Status)
INCLUDE (FullName, Email, CreatedDate);
CREATE INDEX IX_Employee_Active
ON Employees (CompanyId, CreatedDate)
WHERE Status = 'Active' AND IsDeleted = 0;
CREATE FULLTEXT INDEX ON Employees (FullName, Email)
KEY INDEX PK_Employees;
Pagination Best Practices
var items = await repo.GetAllAsync(q => q
.Where(e => e.CompanyId == companyId)
.Where(e => e.Id > lastId)
.OrderBy(e => e.Id)
.Take(pageSize), ct);
var items = await repo.GetAllAsync(q => q
.Where(filter)
.OrderByDescending(e => e.CreatedDate)
.PageBy(skip, take), ct);
var items = await repo.GetAllAsync(q => q.Skip(1000), ct);
Bulk Operations
await repo.CreateManyAsync(entities, ct);
await repo.UpdateManyAsync(
entities,
dismissSendEvent: true,
checkDiff: false,
ct);
await repo.DeleteManyAsync(e => e.Status == Status.Deleted && e.DeletedDate < cutoffDate, ct);
Performance Analysis Workflow
Phase 1: Identify Slow Queries
- Check application logs for slow query warnings
- Review query patterns in handlers
- Look for N+1 patterns (loops with DB calls)
Phase 2: Analyze Query Plan
db.employees.find({ companyId: "x", status: "Active" }).explain("executionStats")
SET STATISTICS IO ON
SELECT * FROM Employees WHERE CompanyId = 'x' AND Status = 'Active'
Phase 3: Optimize
- Add missing indexes
- Use eager loading for related entities
- Add projections for partial data needs
- Parallelize independent queries
- Implement caching for frequently accessed data
Optimization Checklist
Anti-Patterns
- Loading entire collections: Always filter and paginate
- Fetching unused data: Use projections
- Sequential independent queries: Use parallel tuple queries
- Index on every column: Only index frequently queried fields
- Skip without ordering: Always order before pagination
IMPORTANT Task Planning Notes
- Always plan and break many small todo tasks
- Always add a final review todo task to review the works done at the end to find any fix or enhancement needed