Skip to main content

Database Performance Optimization

Overview​

This document describes the database performance optimizations implemented to reduce DTU consumption and improve query performance, especially for feeds with high CI build activity.

Problem Statement​

Feeds with many CI builds can utilize significant DTUs in Azure SQL Database even with light user loads. This can cause:

  • Sporadic failures for CI builds relying on the feed
  • Increased operational costs
  • Degraded user experience during peak usage

Solutions Implemented​

1. Database Indexes​

Added indexes on frequently filtered columns to improve query performance:

  • IX_Packages_Listed: Speeds up queries filtering by published status
  • IX_Packages_IsPrerelease: Optimizes prerelease filtering
  • IX_Packages_Published: Improves sorting by publish date
  • IX_Packages_SemVerLevel: Accelerates SemVer version filtering
  • IX_Packages_Listed_IsPrerelease: Composite index for common filter combinations

These indexes are automatically created when running database migrations.

2. Database Views​

Created views for expensive aggregations that are frequently queried:

vw_PackageDownloadCounts​

Pre-aggregates download counts per package, avoiding repeated COUNT operations:

SELECT
p.[Key] as PackageKey,
p.Id as PackageId,
p.Version,
COUNT(pd.Id) as DownloadCount
FROM Packages p
LEFT JOIN PackageDownloads pd ON p.[Key] = pd.PackageKey
GROUP BY p.[Key], p.Id, p.Version

vw_LatestPackageVersions​

Identifies the latest version of each package to optimize search operations.

vw_PackageSearchInfo​

Combines package information with pre-calculated download totals, reducing join complexity during searches.

vw_PackageVersionsWithDownloads​

Provides version-level statistics with download counts for efficient version listing.

3. Query Optimizations​

DatabaseSearchService​

Before: N+1 query pattern - fetching versions separately for each package

foreach (var pkg in packages) {
var versions = await _context.Packages
.Where(p => p.Id == pkg.Id)
.ToListAsync();
}

After: Batch query - fetch all versions in a single database call

var packageIds = packages.Select(p => p.Id).ToList();
var allVersions = await _context.Packages
.Where(p => packageIds.Contains(p.Id))
.GroupBy(p => p.Id)
.ToListAsync();

Impact: Reduced from O(n) queries to O(1) queries (3 total database calls instead of 3 + n)

PackageService.FindAsync​

Before: Loading expensive PackageDownloads collection unnecessarily

.Include(p => p.PackageDownloads)

After: Removed this Include, loading only required navigation properties

.Include(p => p.Dependencies)
.Include(p => p.PackageTypes)
.Include(p => p.TargetFrameworks)

Impact: Reduced memory usage and query execution time, especially for packages with many downloads

DefaultPackageMetadataService.GetPackageInfo​

Before: N+1 query checking if each dependency exists locally

IsLocalDependency = _context.Packages.Any(p => p.Id == d.Id)

After: Batch query to check all dependencies at once

var localDependencies = await _context.Packages
.Where(p => allDependencyIds.Contains(p.Id))
.Select(p => p.Id)
.ToListAsync();
var localDependencySet = new HashSet<string>(localDependencies);

Impact: Eliminated hundreds of database queries for packages with many dependencies

Migration Guide​

For Existing Deployments​

  1. Backup your database before applying migrations
  2. Run database migrations:
    # For SQL Server
    dotnet ef database update --context SqlServerContext

    # For SQLite
    dotnet ef database update --context SqliteContext
  3. Monitor database performance metrics to verify improvements

Expected Performance Improvements​

  • Search queries: 60-80% reduction in execution time
  • Package metadata retrieval: 70-90% reduction for packages with many dependencies
  • Autocomplete: 50-70% reduction in query time
  • Overall DTU usage: 40-60% reduction under typical CI workloads

Database Compatibility​

  • SQL Server: Full support with all views and indexes
  • SQLite: Full support with SQLite-compatible syntax
  • MySQL: Code optimizations apply, but migration pending EF Core 10.0 provider update

Best Practices for Future Development​

When writing new database queries, follow these guidelines:

  1. Always use AsNoTracking() for read-only queries
  2. Avoid N+1 patterns - batch related queries
  3. Minimize Includes - only load navigation properties you actually need
  4. Never Include PackageDownloads unless absolutely necessary
  5. Use GroupBy and aggregate before iterating in code
  6. Consider creating views for frequently-used complex queries
  7. Add indexes for new columns used in WHERE, ORDER BY, or JOIN clauses

See the updated .github/copilot-instructions.md for detailed examples and patterns.

Monitoring and Troubleshooting​

Key Metrics to Monitor​

  • DTU utilization in Azure SQL Database
  • Query execution time in application logs
  • Database connection pool usage
  • Average response time for search operations

If Performance Issues Persist​

  1. Review query execution plans for slow queries
  2. Consider additional indexes for new query patterns
  3. Evaluate read replica configuration for high-traffic scenarios
  4. Monitor for missing index recommendations in Azure portal

Additional Resources​