Performance Optimization Guide
Database optimization, caching strategies, and monitoring for high-performance operations
Overview
This guide provides comprehensive performance optimization strategies for the Shirinzad E-Commerce Platform, covering database optimization, caching strategies, query optimization, and monitoring.
Database Optimization
1. Index Strategy (46+ Indexes)
The platform implements a comprehensive indexing strategy with over 46 indexes for optimal query performance.
Products Table Indexes
-- Single Column Indexes
CREATE INDEX IX_AppProducts_Slug ON AppProducts(Slug);
CREATE INDEX IX_AppProducts_Code ON AppProducts(Code);
CREATE INDEX IX_AppProducts_IsActive ON AppProducts(IsActive);
CREATE INDEX IX_AppProducts_IsFeatured ON AppProducts(IsFeatured);
CREATE INDEX IX_AppProducts_BrandId ON AppProducts(BrandId);
CREATE INDEX IX_AppProducts_CreationTime ON AppProducts(CreationTime);
-- Composite Indexes for Better Performance
CREATE INDEX IX_AppProducts_IsActive_IsFeatured
ON AppProducts(IsActive, IsFeatured) INCLUDE (Price, AverageRating);
CREATE INDEX IX_AppProducts_BrandId_IsActive
ON AppProducts(BrandId, IsActive) INCLUDE (Name, Price, MainImageUrl);
CREATE INDEX IX_AppProducts_Price_IsActive
ON AppProducts(Price, IsActive) INCLUDE (Name, Slug);
CREATE INDEX IX_AppProducts_AverageRating_IsActive
ON AppProducts(AverageRating DESC, IsActive) INCLUDE (Name, Price);
Categories Table Indexes
CREATE INDEX IX_AppCategories_Slug ON AppCategories(Slug);
CREATE INDEX IX_AppCategories_IsActive ON AppCategories(IsActive);
CREATE INDEX IX_AppCategories_DisplayOrder ON AppCategories(DisplayOrder);
CREATE INDEX IX_AppCategories_ParentId ON AppCategories(ParentId);
Orders Table Indexes
CREATE INDEX IX_AppOrders_OrderNumber ON AppOrders(OrderNumber);
CREATE INDEX IX_AppOrders_UserId ON AppOrders(UserId);
CREATE INDEX IX_AppOrders_Status ON AppOrders(Status);
CREATE INDEX IX_AppOrders_CustomerEmail ON AppOrders(CustomerEmail);
CREATE INDEX IX_AppOrders_CreationTime ON AppOrders(CreationTime);
-- Composite index for user order history
CREATE INDEX IX_AppOrders_UserId_Status_CreationTime
ON AppOrders(UserId, Status, CreationTime DESC);
Product Reviews Table Indexes
CREATE INDEX IX_AppProductReviews_ProductId ON AppProductReviews(ProductId);
CREATE INDEX IX_AppProductReviews_UserId ON AppProductReviews(UserId);
CREATE INDEX IX_AppProductReviews_IsApproved ON AppProductReviews(IsApproved);
CREATE INDEX IX_AppProductReviews_CreationTime ON AppProductReviews(CreationTime);
-- Composite index for approved reviews
CREATE INDEX IX_AppProductReviews_ProductId_IsApproved_CreationTime
ON AppProductReviews(ProductId, IsApproved, CreationTime DESC);
2. Database Statistics & Query Store
Enable Auto-Update Statistics
-- SQL Server
ALTER DATABASE ShirinzadShop SET AUTO_UPDATE_STATISTICS_ASYNC ON;
ALTER DATABASE ShirinzadShop SET AUTO_CREATE_STATISTICS ON;
-- Rebuild statistics weekly
UPDATE STATISTICS AppProducts WITH FULLSCAN;
UPDATE STATISTICS AppOrders WITH FULLSCAN;
UPDATE STATISTICS AppProductReviews WITH FULLSCAN;
Enable Query Store for Performance Monitoring
ALTER DATABASE ShirinzadShop SET QUERY_STORE = ON;
ALTER DATABASE ShirinzadShop SET QUERY_STORE (
OPERATION_MODE = READ_WRITE,
DATA_FLUSH_INTERVAL_SECONDS = 900,
INTERVAL_LENGTH_MINUTES = 60,
MAX_STORAGE_SIZE_MB = 1024,
QUERY_CAPTURE_MODE = AUTO
);
3. Partitioning Strategy
For high-traffic scenarios, implement table partitioning on large tables like Orders:
-- Partition Orders by creation date (monthly)
CREATE PARTITION FUNCTION PF_OrdersByMonth (DATETIME2)
AS RANGE RIGHT FOR VALUES (
'2025-01-01', '2025-02-01', '2025-03-01',
'2025-04-01', '2025-05-01', '2025-06-01',
'2025-07-01', '2025-08-01', '2025-09-01',
'2025-10-01', '2025-11-01', '2025-12-01'
);
CREATE PARTITION SCHEME PS_OrdersByMonth
AS PARTITION PF_OrdersByMonth
ALL TO ([PRIMARY]);
-- Apply to Orders table
ALTER TABLE AppOrders DROP CONSTRAINT PK_AppOrders;
ALTER TABLE AppOrders ADD CONSTRAINT PK_AppOrders
PRIMARY KEY CLUSTERED (Id, CreationTime) ON PS_OrdersByMonth(CreationTime);
Query Optimization
1. N+1 Query Prevention
Bad - N+1 Queries
var products = await _productRepository.GetListAsync();
foreach (var product in products)
{
// This triggers N additional queries!
product.Brand = await _brandRepository.GetAsync(product.BrandId);
}
Good - Eager Loading
var queryable = await _productRepository.GetQueryableAsync();
var products = await AsyncExecuter.ToListAsync(
queryable
.Include(p => p.Brand)
.Include(p => p.ProductCategories)
.ThenInclude(pc => pc.Category)
.Where(p => p.IsActive)
);
2. Projection to DTOs
Project directly to DTOs instead of loading entire entities to reduce memory usage and improve performance.
Bad - Load Entire Entity
var products = await _productRepository.GetListAsync();
return products.Select(p => ObjectMapper.Map<Product, ProductDto>(p)).ToList();
Good - Direct Projection
var queryable = await _productRepository.GetQueryableAsync();
var productDtos = await AsyncExecuter.ToListAsync(
queryable
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
MainImageUrl = p.MainImageUrl,
BrandName = p.Brand != null ? p.Brand.Name : null
})
);
3. AsNoTracking() for Read-Only Queries
Use AsNoTracking() for read-only queries to skip change tracking and improve performance.
var queryable = await _productRepository.GetQueryableAsync();
var products = await AsyncExecuter.ToListAsync(
queryable
.AsNoTracking() // Don't track changes - faster reads
.Where(p => p.IsActive)
.Include(p => p.Brand)
);
4. Pagination Best Practices
Always use pagination for list queries to prevent loading too much data at once.
public async Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
var queryable = await _productRepository.GetQueryableAsync();
// Apply filters first
queryable = queryable.Where(p => p.IsActive);
// Get count BEFORE pagination
var totalCount = await AsyncExecuter.CountAsync(queryable);
// Apply sorting and pagination
queryable = queryable
.OrderByDescending(p => p.CreationTime)
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
var products = await AsyncExecuter.ToListAsync(queryable);
return new PagedResultDto<ProductDto>(
totalCount,
ObjectMapper.Map<List<Product>, List<ProductDto>>(products)
);
}
5. Compiled Queries
Use compiled queries for frequently executed queries to avoid re-parsing and re-compiling.
private static readonly Func<ShopDbContext, Guid, Task<Product>> GetProductByIdQuery =
EF.CompileAsyncQuery((ShopDbContext context, Guid id) =>
context.Products
.Include(p => p.Brand)
.FirstOrDefault(p => p.Id == id));
public async Task<Product> GetProductByIdAsync(Guid id)
{
var dbContext = await _productRepository.GetDbContextAsync();
return await GetProductByIdQuery(dbContext, id);
}
API Performance
1. Response Compression (Brotli & Gzip)
Response compression is configured to reduce payload sizes by up to 80%.
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
2. Response Caching Middleware
// In Program.cs
builder.Services.AddResponseCaching();
app.UseResponseCaching();
Apply to Controllers
[ApiController]
[Route("api/app/product")]
public class ProductController : AbpController
{
[HttpGet]
[ResponseCache(Duration = 600)] // 10 minutes
public async Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
// ...
}
[HttpGet("{id}")]
[ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "id" })]
public async Task<ProductDto> GetAsync(Guid id)
{
// ...
}
}
3. Output Caching
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder
.With(c => c.HttpContext.Request.Path.StartsWithSegments("/api/app/product"))
.Expire(TimeSpan.FromMinutes(10)));
});
app.UseOutputCache();
4. Async/Await Best Practices
Bad - Blocking Call
public async Task<ProductDto> GetProductAsync(Guid id)
{
// .Result blocks the thread!
var product = _productRepository.GetAsync(id).Result;
return ObjectMapper.Map<Product, ProductDto>(product);
}
Good - Proper Async
public async Task<ProductDto> GetProductAsync(Guid id)
{
var product = await _productRepository.GetAsync(id);
return ObjectMapper.Map<Product, ProductDto>(product);
}
5. Background Jobs for Heavy Operations
Queue heavy operations as background jobs to avoid blocking API responses.
public class OrderAppService : ApplicationService
{
private readonly IBackgroundJobManager _backgroundJobManager;
public async Task<OrderDto> PlaceOrderAsync(CheckoutDto input)
{
var order = await CreateOrderAsync(input);
// Queue background jobs instead of blocking
await _backgroundJobManager.EnqueueAsync(new SendOrderConfirmationEmailArgs
{
OrderId = order.Id
});
await _backgroundJobManager.EnqueueAsync(new UpdateInventoryArgs
{
OrderId = order.Id
});
return ObjectMapper.Map<Order, OrderDto>(order);
}
}
Connection Pooling
Database Connection Pooling
Configure connection pooling to reuse database connections and reduce overhead.
"ConnectionStrings": {
"Default": "Server=localhost;Database=ShirinzadShop;User Id=sa;Password=***;TrustServerCertificate=True;Pooling=true;Min Pool Size=10;Max Pool Size=100;Connection Lifetime=0;"
}
| Parameter | Value | Description |
|---|---|---|
| Pooling | true | Enable connection pooling |
| Min Pool Size | 10 | Minimum number of connections in pool |
| Max Pool Size | 100 | Maximum number of connections in pool |
| Connection Lifetime | 0 | Connection lifetime in seconds (0 = no limit) |
Performance Benchmarks
Target Performance Metrics
| Endpoint | Target Response Time | Max Concurrent Users | Error Rate |
|---|---|---|---|
| GET /api/app/product (list) | < 300ms | 1000 | < 0.1% |
| GET /api/app/product/{id} | < 100ms | 2000 | < 0.1% |
| POST /api/app/cart/add-item | < 200ms | 500 | < 0.5% |
| POST /api/app/order/checkout | < 500ms | 200 | < 1% |
| GET /api/app/category | < 150ms | 1500 | < 0.1% |
Database Query Targets
| Query Type | Target Time |
|---|---|
| Simple SELECT by ID | < 5ms |
| Product list with filters | < 50ms |
| Complex joins (products + categories + brand) | < 100ms |
| Full-text search | < 200ms |
| Order history | < 150ms |
Load Testing
Using k6 for Load Testing
Install k6
# Windows
choco install k6
# Linux
sudo apt-get install k6
# Docker
docker pull grafana/k6
Create Load Test Script (load-test.js)
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 200 }, // Ramp up to 200 users
{ duration: '5m', target: 200 }, // Stay at 200 users
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // Error rate under 1%
},
};
export default function () {
// Test product list endpoint
const listRes = http.get('https://localhost:5001/api/app/product');
check(listRes, {
'product list status 200': (r) => r.status === 200,
'product list response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
// Test product detail endpoint
const detailRes = http.get('https://localhost:5001/api/app/product/some-product-id');
check(detailRes, {
'product detail status 200': (r) => r.status === 200,
'product detail response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(1);
}
Run Load Test
k6 run load-test.js
Monitoring & Application Performance Management
Application Insights Integration
Configuration
{
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=xxx",
"EnableAdaptiveSampling": true,
"EnablePerformanceCounterCollectionModule": true
}
}
Track Custom Metrics
public class ProductAppService : ApplicationService
{
private readonly TelemetryClient _telemetryClient;
public async Task<ProductDto> GetAsync(Guid id)
{
var stopwatch = Stopwatch.StartNew();
var product = await _productRepository.GetAsync(id);
stopwatch.Stop();
_telemetryClient.TrackMetric("ProductLoad.Duration", stopwatch.ElapsedMilliseconds);
return ObjectMapper.Map<Product, ProductDto>(product);
}
}
MiniProfiler for Development
builder.Services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
}).AddEntityFramework();
app.UseMiniProfiler();
Access profiler at: https://localhost:5001/profiler/results-index
Health Checks
builder.Services.AddHealthChecks()
.AddSqlServer(configuration.GetConnectionString("Default"))
.AddRedis(configuration["Redis:Configuration"])
.AddCheck<ProductCatalogHealthCheck>("product-catalog");
app.MapHealthChecks("/health");
Recommended APM Tools
- Application Insights - Microsoft's APM solution, native Azure integration
- New Relic - Comprehensive APM with distributed tracing
- Datadog - Full-stack monitoring and analytics
- Elastic APM - Open-source APM built on Elastic Stack
- Seq - Structured logging and analysis
Scalability Strategies
Horizontal Scaling
- Deploy multiple API server instances behind a load balancer
- Use Redis for distributed caching across servers
- Store session state in Redis (not in-memory)
- Use distributed background job queue
- Implement sticky sessions if needed
Database Read Replicas
Set up read replicas to distribute read load across multiple database instances.
"ConnectionStrings": {
"Default": "Server=primary-db;Database=ShirinzadShop;...",
"ReadOnly": "Server=read-replica;Database=ShirinzadShop;ApplicationIntent=ReadOnly;..."
}
CDN for Static Assets
- Store product images in Azure Blob Storage or AWS S3
- Serve static assets through CDN (Azure CDN, CloudFlare, AWS CloudFront)
- Optimize images (resize, compress, WebP format)
- Implement lazy loading for images
- Use image thumbnails for list views
Performance Checklist
Application Level
- Enable response compression (Brotli/Gzip)
- Implement distributed caching (Redis)
- Use async/await consistently
- Enable response caching for static endpoints
- Queue background jobs for heavy operations
- Implement rate limiting
- Use output caching
Database Level
- Create appropriate indexes (46+ indexes implemented)
- Enable Query Store for monitoring
- Enable auto-update statistics
- Use AsNoTracking() for read-only queries
- Implement pagination everywhere
- Avoid N+1 queries (use Include/ThenInclude)
- Use projection to DTOs
- Consider table partitioning for large tables
Infrastructure Level
- Use CDN for static assets
- Enable HTTP/2 or HTTP/3
- Optimize images (resize, compress)
- Set up connection pooling
- Configure database connection limits
- Use load balancer for horizontal scaling
- Enable database read replicas
Monitoring
- Set up Application Insights or APM tool
- Configure health checks
- Enable performance profiling
- Set up alerts for slow queries
- Monitor error rates
- Track response times
- Monitor database CPU/memory