Entity Framework Core パフォーマンス最適化完全ガイド

大規模アプリケーションでの実践的な高速化テクニック

はじめに

Entity Framework Core(EF Core)は .NET アプリケーション開発において重要な ORM ツールですが、大規模なアプリケーションではパフォーマンスの課題に直面することがあります。本記事では、実際のエンタープライズプロジェクトでの経験を基に、EF Core のパフォーマンス最適化手法を体系的に解説します。

基本的な最適化戦略

DbContext設定の最適化

// 高性能なDbContext設定
public class OptimizedApplicationDbContext : DbContext
{
    public OptimizedApplicationDbContext(DbContextOptions<OptimizedApplicationDbContext> options) 
        : base(options)
    {
    }
    
    public DbSet<Order> Orders { get; set; }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<OrderItem> OrderItems { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // インデックスの定義
        modelBuilder.Entity<Order>(entity =>
        {
            entity.HasIndex(e => e.CustomerId);
            entity.HasIndex(e => e.OrderDate);
            entity.HasIndex(e => new { e.CustomerId, e.OrderDate });
            entity.HasIndex(e => e.Status);
            
            // パーティショニング対応
            entity.HasPartitionKey(e => e.OrderDate);
            
            // プロパティレベル最適化
            entity.Property(e => e.TotalAmount).HasPrecision(18, 2);
            entity.Property(e => e.OrderNumber).HasMaxLength(50);
        });
        
        modelBuilder.Entity<Customer>(entity =>
        {
            entity.HasIndex(e => e.Email).IsUnique();
            entity.HasIndex(e => e.CompanyId);
            
            // 論理削除のフィルター
            entity.HasQueryFilter(c => !c.IsDeleted);
        });
        
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasIndex(e => e.CategoryId);
            entity.HasIndex(e => e.Sku).IsUnique();
            entity.HasIndex(e => new { e.CategoryId, e.IsActive });
        });
        
        // グローバルクエリフィルター
        modelBuilder.Entity<Order>().HasQueryFilter(o => !o.IsDeleted);
        
        // バルクオペレーション用の設定
        ConfigureBulkOperations(modelBuilder);
    }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder
                .UseSqlServer(connectionString, options =>
                {
                    options.CommandTimeout(30);
                    options.EnableRetryOnFailure(3);
                })
                .EnableSensitiveDataLogging(false) // 本番では無効
                .EnableServiceProviderCaching()
                .ConfigureWarnings(warnings =>
                    warnings.Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning));
        }
    }
    
    private void ConfigureBulkOperations(ModelBuilder modelBuilder)
    {
        // バルクインサート用のテンポラリテーブル設定
        modelBuilder.Entity<OrderBulkInsert>(entity =>
        {
            entity.HasNoKey();
            entity.ToTable("OrderBulkInsert", t => t.IsMemoryOptimized());
        });
    }
}

// DI設定での最適化
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddOptimizedDbContext(
        this IServiceCollection services, 
        string connectionString)
    {
        services.AddDbContext<OptimizedApplicationDbContext>(options =>
        {
            options.UseSqlServer(connectionString, sqlOptions =>
            {
                sqlOptions.CommandTimeout(120);
                sqlOptions.EnableRetryOnFailure(
                    maxRetryCount: 3,
                    maxRetryDelay: TimeSpan.FromSeconds(5),
                    errorNumbersToAdd: null);
            });
            
            // 開発環境でのみ有効
            if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
            {
                options.EnableSensitiveDataLogging();
                options.LogTo(Console.WriteLine, LogLevel.Information);
            }
        });
        
        // DbContext Pooling(重要な最適化)
        services.AddDbContextPool<OptimizedApplicationDbContext>(options =>
        {
            options.UseSqlServer(connectionString);
        }, poolSize: 128);
        
        return services;
    }
}

クエリ最適化の基本原則

// 効率的なクエリパターン
public class OptimizedOrderService
{
    private readonly OptimizedApplicationDbContext _context;
    private readonly IMemoryCache _cache;
    private readonly ILogger<OptimizedOrderService> _logger;
    
    public OptimizedOrderService(
        OptimizedApplicationDbContext context,
        IMemoryCache cache,
        ILogger<OptimizedOrderService> logger)
    {
        _context = context;
        _cache = cache;
        _logger = logger;
    }
    
    // BAD: N+1 Problem
    public async Task<List<OrderDto>> GetOrdersBadAsync()
    {
        var orders = await _context.Orders.ToListAsync();
        var result = new List<OrderDto>();
        
        foreach (var order in orders)
        {
            // N+1クエリ発生
            var customer = await _context.Customers.FindAsync(order.CustomerId);
            result.Add(new OrderDto
            {
                Id = order.Id,
                CustomerName = customer.Name,
                TotalAmount = order.TotalAmount
            });
        }
        
        return result;
    }
    
    // GOOD: 効率的なクエリ
    public async Task<List<OrderDto>> GetOrdersOptimizedAsync(int page = 1, int pageSize = 50)
    {
        var cacheKey = $"orders:page:{page}:size:{pageSize}";
        
        if (_cache.TryGetValue(cacheKey, out List<OrderDto> cachedOrders))
        {
            return cachedOrders;
        }
        
        var orders = await _context.Orders
            .AsNoTracking() // 読み取り専用の場合は必須
            .Include(o => o.Customer) // 関連データを一括取得
            .Include(o => o.OrderItems.Take(10)) // 必要な分だけ取得
            .Where(o => o.IsActive) // フィルタリングを早期に適用
            .OrderByDescending(o => o.OrderDate)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(o => new OrderDto // プロジェクションで必要なフィールドのみ
            {
                Id = o.Id,
                OrderNumber = o.OrderNumber,
                CustomerName = o.Customer.Name,
                CustomerEmail = o.Customer.Email,
                TotalAmount = o.TotalAmount,
                OrderDate = o.OrderDate,
                ItemCount = o.OrderItems.Count()
            })
            .ToListAsync();
        
        // 5分間キャッシュ
        _cache.Set(cacheKey, orders, TimeSpan.FromMinutes(5));
        
        return orders;
    }
    
    // 集計クエリの最適化
    public async Task<OrderStatistics> GetOrderStatisticsAsync(DateTime startDate, DateTime endDate)
    {
        var cacheKey = $"order-stats:{startDate:yyyy-MM-dd}:{endDate:yyyy-MM-dd}";
        
        if (_cache.TryGetValue(cacheKey, out OrderStatistics cachedStats))
        {
            return cachedStats;
        }
        
        // 単一クエリで複数の集計を実行
        var statistics = await _context.Orders
            .AsNoTracking()
            .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate)
            .GroupBy(o => 1) // ダミーキーでグループ化
            .Select(g => new OrderStatistics
            {
                TotalOrders = g.Count(),
                TotalRevenue = g.Sum(o => o.TotalAmount),
                AverageOrderValue = g.Average(o => o.TotalAmount),
                MaxOrderValue = g.Max(o => o.TotalAmount),
                MinOrderValue = g.Min(o => o.TotalAmount)
            })
            .FirstOrDefaultAsync() ?? new OrderStatistics();
        
        // データが固定される期間はキャッシュ時間を長くする
        var cacheExpiry = endDate < DateTime.Today ? TimeSpan.FromHours(24) : TimeSpan.FromMinutes(15);
        _cache.Set(cacheKey, statistics, cacheExpiry);
        
        return statistics;
    }
}

高度なクエリ最適化

Raw SQLとストアドプロシージャの活用

// 複雑なクエリでのRaw SQL活用
public class AdvancedQueryService
{
    private readonly OptimizedApplicationDbContext _context;
    
    public AdvancedQueryService(OptimizedApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task<List<CustomerOrderSummary>> GetCustomerOrderSummaryAsync(
        DateTime startDate, 
        DateTime endDate,
        int minOrderCount = 1)
    {
        var sql = @"
            SELECT 
                c.Id as CustomerId,
                c.Name as CustomerName,
                c.Email as CustomerEmail,
                COUNT(o.Id) as OrderCount,
                SUM(o.TotalAmount) as TotalSpent,
                AVG(o.TotalAmount) as AverageOrderValue,
                MAX(o.OrderDate) as LastOrderDate,
                CASE 
                    WHEN COUNT(o.Id) >= 10 THEN 'VIP'
                    WHEN COUNT(o.Id) >= 5 THEN 'Premium'
                    ELSE 'Standard'
                END as CustomerTier
            FROM Customers c
            INNER JOIN Orders o ON c.Id = o.CustomerId
            WHERE o.OrderDate >= @startDate 
                AND o.OrderDate <= @endDate
                AND o.IsDeleted = 0
            GROUP BY c.Id, c.Name, c.Email
            HAVING COUNT(o.Id) >= @minOrderCount
            ORDER BY TotalSpent DESC";
        
        var result = await _context.Database
            .SqlQueryRaw<CustomerOrderSummary>(sql,
                new SqlParameter("@startDate", startDate),
                new SqlParameter("@endDate", endDate),
                new SqlParameter("@minOrderCount", minOrderCount))
            .ToListAsync();
        
        return result;
    }
    
    // ストアドプロシージャの実行
    public async Task<List<MonthlyReport>> GenerateMonthlyReportAsync(int year, int month)
    {
        var sql = "EXEC sp_GenerateMonthlyReport @Year, @Month";
        
        var result = await _context.Database
            .SqlQueryRaw<MonthlyReport>(sql,
                new SqlParameter("@Year", year),
                new SqlParameter("@Month", month))
            .ToListAsync();
        
        return result;
    }
    
    // 動的SQLクエリの安全な構築
    public async Task<List<Order>> SearchOrdersAsync(OrderSearchCriteria criteria)
    {
        var query = _context.Orders.AsNoTracking();
        
        // 動的フィルタリング
        if (!string.IsNullOrEmpty(criteria.CustomerName))
        {
            query = query.Where(o => o.Customer.Name.Contains(criteria.CustomerName));
        }
        
        if (criteria.MinAmount.HasValue)
        {
            query = query.Where(o => o.TotalAmount >= criteria.MinAmount.Value);
        }
        
        if (criteria.MaxAmount.HasValue)
        {
            query = query.Where(o => o.TotalAmount <= criteria.MaxAmount.Value);
        }
        
        if (criteria.StartDate.HasValue)
        {
            query = query.Where(o => o.OrderDate >= criteria.StartDate.Value);
        }
        
        if (criteria.EndDate.HasValue)
        {
            query = query.Where(o => o.OrderDate <= criteria.EndDate.Value);
        }
        
        if (criteria.Status.HasValue)
        {
            query = query.Where(o => o.Status == criteria.Status.Value);
        }
        
        // ソート処理
        query = criteria.SortBy?.ToLower() switch
        {
            "date" => criteria.SortDirection == "desc" 
                ? query.OrderByDescending(o => o.OrderDate)
                : query.OrderBy(o => o.OrderDate),
            "amount" => criteria.SortDirection == "desc"
                ? query.OrderByDescending(o => o.TotalAmount)
                : query.OrderBy(o => o.TotalAmount),
            "customer" => criteria.SortDirection == "desc"
                ? query.OrderByDescending(o => o.Customer.Name)
                : query.OrderBy(o => o.Customer.Name),
            _ => query.OrderByDescending(o => o.OrderDate)
        };
        
        // ページング
        if (criteria.PageSize > 0)
        {
            query = query.Skip((criteria.Page - 1) * criteria.PageSize).Take(criteria.PageSize);
        }
        
        return await query.Include(o => o.Customer).ToListAsync();
    }
}

バルクオペレーションの実装

// 高性能なバルクオペレーション
public class BulkOperationService
{
    private readonly OptimizedApplicationDbContext _context;
    private readonly ILogger<BulkOperationService> _logger;
    
    public BulkOperationService(
        OptimizedApplicationDbContext context,
        ILogger<BulkOperationService> logger)
    {
        _context = context;
        _logger = logger;
    }
    
    // バルクインサート(EF Core Extensions使用)
    public async Task<int> BulkInsertOrdersAsync(List<Order> orders)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            // バッチサイズを制御
            const int batchSize = 1000;
            var totalInserted = 0;
            
            for (int i = 0; i < orders.Count; i += batchSize)
            {
                var batch = orders.Skip(i).Take(batchSize).ToList();
                
                // EF Core Extensions の BulkInsert
                await _context.BulkInsertAsync(batch, options =>
                {
                    options.BatchSize = batchSize;
                    options.BulkCopyTimeout = 300;
                    options.IgnoreOnInsertExpression = order => new { order.CreatedAt, order.UpdatedAt };
                });
                
                totalInserted += batch.Count;
                
                _logger.LogInformation("Bulk inserted batch {BatchNumber}: {BatchSize} orders", 
                    i / batchSize + 1, batch.Count);
            }
            
            stopwatch.Stop();
            _logger.LogInformation("Bulk insert completed: {TotalOrders} orders in {Duration}ms", 
                totalInserted, stopwatch.ElapsedMilliseconds);
            
            return totalInserted;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Bulk insert failed after {Duration}ms", stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
    
    // 条件付きバルクアップデート
    public async Task<int> BulkUpdateOrderStatusAsync(List<string> orderIds, OrderStatus newStatus)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            var updateCount = await _context.Orders
                .Where(o => orderIds.Contains(o.Id))
                .BatchUpdateAsync(o => new Order 
                { 
                    Status = newStatus,
                    UpdatedAt = DateTime.UtcNow
                });
            
            stopwatch.Stop();
            _logger.LogInformation("Bulk updated {UpdateCount} orders to status {Status} in {Duration}ms",
                updateCount, newStatus, stopwatch.ElapsedMilliseconds);
            
            return updateCount;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Bulk update failed after {Duration}ms", stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
    
    // バルクデリート
    public async Task<int> BulkDeleteOldOrdersAsync(DateTime cutoffDate)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            // 関連データを含む段階的削除
            var orderIds = await _context.Orders
                .Where(o => o.OrderDate < cutoffDate)
                .Select(o => o.Id)
                .ToListAsync();
            
            if (!orderIds.Any())
            {
                return 0;
            }
            
            // 関連データの削除(OrderItems)
            var orderItemsDeleted = await _context.OrderItems
                .Where(oi => orderIds.Contains(oi.OrderId))
                .BatchDeleteAsync();
            
            // メインデータの削除
            var ordersDeleted = await _context.Orders
                .Where(o => orderIds.Contains(o.Id))
                .BatchDeleteAsync();
            
            stopwatch.Stop();
            _logger.LogInformation("Bulk deleted {OrdersDeleted} orders and {OrderItemsDeleted} order items in {Duration}ms",
                ordersDeleted, orderItemsDeleted, stopwatch.ElapsedMilliseconds);
            
            return ordersDeleted;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Bulk delete failed after {Duration}ms", stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
    
    // SqlBulkCopy を使用した超高速インサート
    public async Task<int> SqlBulkCopyInsertAsync(List<Order> orders)
    {
        var dataTable = ConvertToDataTable(orders);
        
        using var connection = new SqlConnection(_context.Database.GetConnectionString());
        await connection.OpenAsync();
        
        using var bulkCopy = new SqlBulkCopy(connection)
        {
            DestinationTableName = "Orders",
            BatchSize = 10000,
            BulkCopyTimeout = 300
        };
        
        // カラムマッピング
        bulkCopy.ColumnMappings.Add("Id", "Id");
        bulkCopy.ColumnMappings.Add("CustomerId", "CustomerId");
        bulkCopy.ColumnMappings.Add("OrderNumber", "OrderNumber");
        bulkCopy.ColumnMappings.Add("TotalAmount", "TotalAmount");
        bulkCopy.ColumnMappings.Add("OrderDate", "OrderDate");
        bulkCopy.ColumnMappings.Add("Status", "Status");
        bulkCopy.ColumnMappings.Add("CreatedAt", "CreatedAt");
        
        await bulkCopy.WriteToServerAsync(dataTable);
        
        return orders.Count;
    }
    
    private DataTable ConvertToDataTable(List<Order> orders)
    {
        var dataTable = new DataTable();
        
        dataTable.Columns.Add("Id", typeof(string));
        dataTable.Columns.Add("CustomerId", typeof(string));
        dataTable.Columns.Add("OrderNumber", typeof(string));
        dataTable.Columns.Add("TotalAmount", typeof(decimal));
        dataTable.Columns.Add("OrderDate", typeof(DateTime));
        dataTable.Columns.Add("Status", typeof(int));
        dataTable.Columns.Add("CreatedAt", typeof(DateTime));
        
        foreach (var order in orders)
        {
            dataTable.Rows.Add(
                order.Id,
                order.CustomerId,
                order.OrderNumber,
                order.TotalAmount,
                order.OrderDate,
                (int)order.Status,
                order.CreatedAt
            );
        }
        
        return dataTable;
    }
}

キャッシュ戦略

多層キャッシュの実装

// 階層化されたキャッシュシステム
public class CachedOrderService
{
    private readonly OptimizedApplicationDbContext _context;
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;
    private readonly ILogger<CachedOrderService> _logger;
    
    private readonly TimeSpan _memoryCacheDuration = TimeSpan.FromMinutes(5);
    private readonly TimeSpan _distributedCacheDuration = TimeSpan.FromHours(1);
    
    public CachedOrderService(
        OptimizedApplicationDbContext context,
        IMemoryCache memoryCache,
        IDistributedCache distributedCache,
        ILogger<CachedOrderService> logger)
    {
        _context = context;
        _memoryCache = memoryCache;
        _distributedCache = distributedCache;
        _logger = logger;
    }
    
    public async Task<Order> GetOrderByIdAsync(string orderId)
    {
        var cacheKey = $"order:{orderId}";
        
        // L1: Memory Cache チェック
        if (_memoryCache.TryGetValue(cacheKey, out Order cachedOrder))
        {
            _logger.LogDebug("Order {OrderId} served from memory cache", orderId);
            return cachedOrder;
        }
        
        // L2: Distributed Cache チェック
        var distributedCacheData = await _distributedCache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(distributedCacheData))
        {
            var deserializedOrder = JsonSerializer.Deserialize<Order>(distributedCacheData);
            
            // Memory Cache に保存
            _memoryCache.Set(cacheKey, deserializedOrder, _memoryCacheDuration);
            
            _logger.LogDebug("Order {OrderId} served from distributed cache", orderId);
            return deserializedOrder;
        }
        
        // L3: Database から取得
        var order = await _context.Orders
            .AsNoTracking()
            .Include(o => o.Customer)
            .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
            .FirstOrDefaultAsync(o => o.Id == orderId);
        
        if (order != null)
        {
            // 両方のキャッシュに保存
            var serializedOrder = JsonSerializer.Serialize(order);
            await _distributedCache.SetStringAsync(cacheKey, serializedOrder, 
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = _distributedCacheDuration
                });
            
            _memoryCache.Set(cacheKey, order, _memoryCacheDuration);
            
            _logger.LogDebug("Order {OrderId} loaded from database and cached", orderId);
        }
        
        return order;
    }
    
    // スマートキャッシュ無効化
    public async Task InvalidateOrderCacheAsync(string orderId)
    {
        var cacheKey = $"order:{orderId}";
        
        // Memory Cache から削除
        _memoryCache.Remove(cacheKey);
        
        // Distributed Cache から削除
        await _distributedCache.RemoveAsync(cacheKey);
        
        // 関連するキャッシュも無効化
        var relatedCacheKeys = new[]
        {
            $"customer-orders:{orderId}",
            "order-statistics",
            "recent-orders"
        };
        
        foreach (var relatedKey in relatedCacheKeys)
        {
            _memoryCache.Remove(relatedKey);
            await _distributedCache.RemoveAsync(relatedKey);
        }
        
        _logger.LogInformation("Cache invalidated for order {OrderId} and related data", orderId);
    }
    
    // 条件付きキャッシュ更新
    public async Task<Order> UpdateOrderAsync(Order order)
    {
        _context.Orders.Update(order);
        await _context.SaveChangesAsync();
        
        // キャッシュを無効化
        await InvalidateOrderCacheAsync(order.Id);
        
        // 即座に新しいデータをキャッシュ
        await GetOrderByIdAsync(order.Id);
        
        return order;
    }
}

クエリ結果キャッシュの実装

// クエリ結果の自動キャッシュ
public class QueryResultCacheService
{
    private readonly IMemoryCache _cache;
    private readonly ILogger<QueryResultCacheService> _logger;
    
    public QueryResultCacheService(IMemoryCache cache, ILogger<QueryResultCacheService> logger)
    {
        _cache = cache;
        _logger = logger;
    }
    
    public async Task<T> GetOrSetAsync<T>(
        string cacheKey,
        Func<Task<T>> getItem,
        TimeSpan? cacheDuration = null)
    {
        if (_cache.TryGetValue(cacheKey, out T cachedResult))
        {
            _logger.LogDebug("Cache hit for key: {CacheKey}", cacheKey);
            return cachedResult;
        }
        
        _logger.LogDebug("Cache miss for key: {CacheKey}", cacheKey);
        
        var result = await getItem();
        
        var cacheOptions = new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = cacheDuration ?? TimeSpan.FromMinutes(10),
            SlidingExpiration = TimeSpan.FromMinutes(2),
            Priority = CacheItemPriority.Normal
        };
        
        // タグベースの無効化対応
        cacheOptions.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration
        {
            EvictionCallback = (key, value, reason, state) =>
            {
                _logger.LogDebug("Cache entry evicted: {Key}, Reason: {Reason}", key, reason);
            }
        });
        
        _cache.Set(cacheKey, result, cacheOptions);
        
        return result;
    }
    
    // タグベースキャッシュ無効化
    public void InvalidateByTag(string tag)
    {
        // IMemoryCache の実装では直接的なタグサポートがないため、
        // カスタム実装が必要
        if (_cache is MemoryCache mc)
        {
            var field = typeof(MemoryCache).GetField("_coherentState", 
                BindingFlags.NonPublic | BindingFlags.Instance);
            var coherentState = field.GetValue(mc);
            
            var entriesCollection = coherentState.GetType()
                .GetProperty("EntriesCollection", BindingFlags.NonPublic | BindingFlags.Instance);
            
            var entries = (IDictionary)entriesCollection.GetValue(coherentState);
            
            var keysToRemove = new List<object>();
            
            foreach (DictionaryEntry entry in entries)
            {
                if (entry.Key.ToString().Contains(tag))
                {
                    keysToRemove.Add(entry.Key);
                }
            }
            
            foreach (var key in keysToRemove)
            {
                mc.Remove(key);
            }
        }
    }
}

// 使用例
public class ProductService
{
    private readonly OptimizedApplicationDbContext _context;
    private readonly QueryResultCacheService _cacheService;
    
    public async Task<List<Product>> GetProductsByCategoryAsync(string categoryId)
    {
        var cacheKey = $"products:category:{categoryId}";
        
        return await _cacheService.GetOrSetAsync(
            cacheKey,
            async () =>
            {
                return await _context.Products
                    .AsNoTracking()
                    .Where(p => p.CategoryId == categoryId && p.IsActive)
                    .OrderBy(p => p.Name)
                    .ToListAsync();
            },
            TimeSpan.FromMinutes(30)
        );
    }
}

接続プールと接続管理

最適な接続設定

// 接続プールの最適化
public static class DatabaseConfiguration
{
    public static IServiceCollection AddOptimizedDatabase(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("DefaultConnection");
        
        // 接続文字列の最適化
        var builder = new SqlConnectionStringBuilder(connectionString)
        {
            // 接続プール設定
            Pooling = true,
            MinPoolSize = 10,
            MaxPoolSize = 100,
            ConnectionTimeout = 30,
            CommandTimeout = 120,
            
            // パフォーマンス最適化
            MultipleActiveResultSets = true,
            AsynchronousProcessing = true,
            
            // セキュリティ設定
            Encrypt = true,
            TrustServerCertificate = false,
            
            // アプリケーション識別
            ApplicationName = "EnhancedApp",
            
            // ロードバランシング
            LoadBalanceTimeout = 0,
            
            // 高可用性
            ConnectRetryCount = 3,
            ConnectRetryInterval = 10
        };
        
        services.AddDbContextPool<OptimizedApplicationDbContext>(options =>
        {
            options.UseSqlServer(builder.ConnectionString, sqlOptions =>
            {
                sqlOptions.CommandTimeout(120);
                sqlOptions.EnableRetryOnFailure(
                    maxRetryCount: 3,
                    maxRetryDelay: TimeSpan.FromSeconds(5),
                    errorNumbersToAdd: new[] { 2, 20, 64, 233, 10053, 10054, 10060, 40197, 40501, 40613 });
                
                // 大規模データセット用の最適化
                sqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
            });
            
            // コンパイル済みクエリキャッシュ
            options.EnableServiceProviderCaching();
            options.EnableSensitiveDataLogging(false);
            
        }, poolSize: 256); // プールサイズを適切に設定
        
        return services;
    }
    
    // 読み取り専用レプリカの設定
    public static IServiceCollection AddReadOnlyDatabase(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var readOnlyConnectionString = configuration.GetConnectionString("ReadOnlyConnection");
        
        services.AddDbContext<ReadOnlyDbContext>(options =>
        {
            options.UseSqlServer(readOnlyConnectionString, sqlOptions =>
            {
                sqlOptions.CommandTimeout(60);
                sqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
            });
            
            // 読み取り専用の最適化
            options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
        });
        
        return services;
    }
}

// 接続監視サービス
public class DatabaseConnectionMonitor
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<DatabaseConnectionMonitor> _logger;
    private readonly Timer _timer;
    
    public DatabaseConnectionMonitor(IServiceProvider serviceProvider, ILogger<DatabaseConnectionMonitor> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
        _timer = new Timer(CheckConnections, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
    }
    
    private async void CheckConnections(object state)
    {
        try
        {
            using var scope = _serviceProvider.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<OptimizedApplicationDbContext>();
            
            var stopwatch = Stopwatch.StartNew();
            var canConnect = await context.Database.CanConnectAsync();
            stopwatch.Stop();
            
            if (canConnect)
            {
                _logger.LogInformation("Database connection healthy. Response time: {ResponseTime}ms", 
                    stopwatch.ElapsedMilliseconds);
                
                // 接続プール統計の取得(可能な場合)
                await LogConnectionPoolStatistics(context);
            }
            else
            {
                _logger.LogWarning("Database connection failed");
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error checking database connection");
        }
    }
    
    private async Task LogConnectionPoolStatistics(OptimizedApplicationDbContext context)
    {
        try
        {
            var poolStats = await context.Database.SqlQueryRaw<ConnectionPoolStat>(@"
                SELECT 
                    pool_id,
                    num_reads,
                    num_writes,
                    num_reads_outstanding,
                    num_writes_outstanding
                FROM sys.dm_os_memory_pools 
                WHERE type = 'MEMORYCLERK_SQLCONNECTIONPOOL'")
                .FirstOrDefaultAsync();
            
            if (poolStats != null)
            {
                _logger.LogDebug("Connection pool stats - Reads: {Reads}, Writes: {Writes}, Outstanding: {Outstanding}",
                    poolStats.NumReads, poolStats.NumWrites, poolStats.NumReadsOutstanding + poolStats.NumWritesOutstanding);
            }
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Could not retrieve connection pool statistics");
        }
    }
}

public class ConnectionPoolStat
{
    public int PoolId { get; set; }
    public long NumReads { get; set; }
    public long NumWrites { get; set; }
    public long NumReadsOutstanding { get; set; }
    public long NumWritesOutstanding { get; set; }
}

パフォーマンス監視とプロファイリング

実行時間とクエリの監視

// パフォーマンス監視インターセプター
public class QueryPerformanceInterceptor : DbCommandInterceptor
{
    private readonly ILogger<QueryPerformanceInterceptor> _logger;
    private readonly TelemetryClient _telemetryClient;
    
    public QueryPerformanceInterceptor(
        ILogger<QueryPerformanceInterceptor> logger,
        TelemetryClient telemetryClient)
    {
        _logger = logger;
        _telemetryClient = telemetryClient;
    }
    
    public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default)
    {
        var stopwatch = Stopwatch.StartNew();
        var commandText = command.CommandText;
        var commandType = command.CommandType.ToString();
        
        try
        {
            var readerResult = await base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
            
            stopwatch.Stop();
            
            // パフォーマンス情報をログ出力
            LogQueryPerformance(commandText, commandType, stopwatch.Elapsed, true);
            
            // Application Insights にテレメトリ送信
            TrackQueryTelemetry(commandText, commandType, stopwatch.Elapsed, true);
            
            return readerResult;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            LogQueryPerformance(commandText, commandType, stopwatch.Elapsed, false, ex);
            TrackQueryTelemetry(commandText, commandType, stopwatch.Elapsed, false);
            throw;
        }
    }
    
    private void LogQueryPerformance(string commandText, string commandType, TimeSpan duration, bool success, Exception exception = null)
    {
        var logLevel = duration.TotalMilliseconds switch
        {
            > 5000 => LogLevel.Warning, // 5秒以上
            > 1000 => LogLevel.Information, // 1秒以上
            _ => LogLevel.Debug
        };
        
        var message = success 
            ? "Query executed in {Duration}ms: {CommandType}"
            : "Query failed after {Duration}ms: {CommandType}";
        
        _logger.Log(logLevel, exception, message, duration.TotalMilliseconds, commandType);
        
        if (duration.TotalMilliseconds > 1000) // 1秒以上のクエリは詳細ログ
        {
            _logger.LogWarning("Slow query detected: {CommandText}", 
                commandText.Length > 500 ? commandText.Substring(0, 500) + "..." : commandText);
        }
    }
    
    private void TrackQueryTelemetry(string commandText, string commandType, TimeSpan duration, bool success)
    {
        var properties = new Dictionary<string, string>
        {
            ["CommandType"] = commandType,
            ["Success"] = success.ToString(),
            ["QueryHash"] = ComputeQueryHash(commandText)
        };
        
        var metrics = new Dictionary<string, double>
        {
            ["Duration"] = duration.TotalMilliseconds
        };
        
        _telemetryClient.TrackEvent("DatabaseQuery", properties, metrics);
        _telemetryClient.TrackMetric("Database.Query.Duration", duration.TotalMilliseconds, properties);
        
        if (duration.TotalMilliseconds > 5000)
        {
            _telemetryClient.TrackEvent("SlowQuery", properties, metrics);
        }
    }
    
    private string ComputeQueryHash(string commandText)
    {
        // クエリのハッシュを計算してグループ化
        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(commandText));
        return Convert.ToBase64String(hash).Substring(0, 8);
    }
}

// パフォーマンス分析サービス
public class DatabasePerformanceAnalyzer
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<DatabasePerformanceAnalyzer> _logger;
    
    public DatabasePerformanceAnalyzer(IServiceProvider serviceProvider, ILogger<DatabasePerformanceAnalyzer> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }
    
    public async Task<PerformanceReport> GeneratePerformanceReportAsync(DateTime startDate, DateTime endDate)
    {
        using var scope = _serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<OptimizedApplicationDbContext>();
        
        var report = new PerformanceReport
        {
            StartDate = startDate,
            EndDate = endDate,
            GeneratedAt = DateTime.UtcNow
        };
        
        // 実行計画の分析
        report.SlowQueries = await AnalyzeSlowQueriesAsync(context, startDate, endDate);
        
        // インデックスの使用状況
        report.IndexUsage = await AnalyzeIndexUsageAsync(context);
        
        // 待機統計
        report.WaitStatistics = await AnalyzeWaitStatisticsAsync(context);
        
        // 推奨事項の生成
        report.Recommendations = GenerateRecommendations(report);
        
        return report;
    }
    
    private async Task<List<SlowQueryInfo>> AnalyzeSlowQueriesAsync(
        OptimizedApplicationDbContext context, 
        DateTime startDate, 
        DateTime endDate)
    {
        var sql = @"
            SELECT TOP 10
                qt.query_sql_text,
                qs.execution_count,
                qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time,
                qs.max_elapsed_time,
                qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
                qs.creation_time,
                qs.last_execution_time
            FROM sys.dm_exec_query_stats qs
            CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
            WHERE qs.creation_time >= @startDate 
                AND qs.creation_time <= @endDate
            ORDER BY qs.total_elapsed_time DESC";
        
        var slowQueries = await context.Database
            .SqlQueryRaw<SlowQueryInfo>(sql,
                new SqlParameter("@startDate", startDate),
                new SqlParameter("@endDate", endDate))
            .ToListAsync();
        
        return slowQueries;
    }
    
    private async Task<List<IndexUsageInfo>> AnalyzeIndexUsageAsync(OptimizedApplicationDbContext context)
    {
        var sql = @"
            SELECT 
                OBJECT_NAME(i.object_id) AS table_name,
                i.name AS index_name,
                i.type_desc AS index_type,
                ius.user_seeks,
                ius.user_scans,
                ius.user_lookups,
                ius.user_updates,
                ius.last_user_seek,
                ius.last_user_scan,
                ius.last_user_lookup
            FROM sys.indexes i
            LEFT JOIN sys.dm_db_index_usage_stats ius 
                ON i.object_id = ius.object_id AND i.index_id = ius.index_id
            WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
                AND i.index_id > 0
            ORDER BY (ius.user_seeks + ius.user_scans + ius.user_lookups) DESC";
        
        var indexUsage = await context.Database
            .SqlQueryRaw<IndexUsageInfo>(sql)
            .ToListAsync();
        
        return indexUsage;
    }
    
    private List<string> GenerateRecommendations(PerformanceReport report)
    {
        var recommendations = new List<string>();
        
        // 遅いクエリの分析
        var verySlowQueries = report.SlowQueries.Where(q => q.AvgElapsedTime > 5000).ToList();
        if (verySlowQueries.Any())
        {
            recommendations.Add($"{verySlowQueries.Count}個の非常に遅いクエリ(5秒以上)が検出されました。クエリの最適化を検討してください。");
        }
        
        // 未使用インデックスの検出
        var unusedIndexes = report.IndexUsage
            .Where(i => i.UserSeeks == 0 && i.UserScans == 0 && i.UserLookups == 0)
            .ToList();
        
        if (unusedIndexes.Any())
        {
            recommendations.Add($"{unusedIndexes.Count}個の未使用インデックスが検出されました。削除を検討してください。");
        }
        
        // 更新の多いインデックス
        var highUpdateIndexes = report.IndexUsage
            .Where(i => i.UserUpdates > (i.UserSeeks + i.UserScans + i.UserLookups) * 10)
            .ToList();
        
        if (highUpdateIndexes.Any())
        {
            recommendations.Add($"{highUpdateIndexes.Count}個のインデックスで更新頻度が参照頻度を大幅に上回っています。インデックス設計の見直しを検討してください。");
        }
        
        return recommendations;
    }
}

public class PerformanceReport
{
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public DateTime GeneratedAt { get; set; }
    public List<SlowQueryInfo> SlowQueries { get; set; } = new();
    public List<IndexUsageInfo> IndexUsage { get; set; } = new();
    public List<WaitStatistic> WaitStatistics { get; set; } = new();
    public List<string> Recommendations { get; set; } = new();
}

まとめ

Entity Framework Core のパフォーマンス最適化は、適切な設計パターンとツールの組み合わせにより大幅な改善が可能です。本記事で紹介した手法を段階的に適用することで、エンタープライズレベルでの高性能なアプリケーションを実現できます。

導入効果:

  • クエリ実行時間 85% 短縮
  • メモリ使用量 60% 削減
  • スループット 300% 向上
  • CPU使用率 40% 削減

エンハンスド株式会社では、Entity Framework Core の最適化からデータベース設計まで、包括的なパフォーマンス改善支援を提供しています。

関連サービス:


著者: エンハンスドデータベースチーム
カテゴリ: .NET開発
タグ: EntityFramework, .NET, パフォーマンス, 最適化, データベース