8 minlesson

EF Core Performance Overview

EF Core Performance Overview

EF Core is a powerful ORM, but the convenience it provides can mask performance issues. Understanding how EF Core translates your C# code into SQL is the first step toward writing efficient data access code.

Common Performance Pitfalls

Most EF Core performance problems fall into a few well-known categories:

PitfallSymptomImpact
N+1 queriesHundreds of SQL queries for a single pageMassive latency
Over-fetchingLoading entire entities when only a few columns are neededWasted memory and bandwidth
Client evaluationFiltering/sorting happens in C# instead of SQLFull table scans
Missing indexesSlow WHERE/ORDER BY on unindexed columnsQuery timeouts
Unnecessary trackingChange tracker monitors read-only entitiesMemory overhead
Repeated compilationLINQ expression trees recompiled on every requestCPU overhead

Many of these issues are invisible until your dataset grows or your application scales. A query that returns instantly with 100 rows may take seconds with 100,000 rows.

Why Generated SQL Matters

EF Core translates your LINQ expressions into SQL. Understanding that translation is critical because small changes in your C# code can produce dramatically different SQL:

csharp
1// Efficient: filtering happens in SQL
2var products = await context.Products
3 .Where(p => p.Price > 100)
4 .ToListAsync();
5// SQL: SELECT ... FROM Products WHERE Price > 100
6
7// Inefficient: loads ALL rows, then filters in C#
8var products = context.Products
9 .AsEnumerable()
10 .Where(p => p.Price > 100)
11 .ToList();
12// SQL: SELECT ... FROM Products (no WHERE clause!)

The second query fetches every row from the database and filters in memory. With a large table, this is orders of magnitude slower.

Inspecting Generated SQL

EF Core provides several ways to see the SQL it generates:

ToQueryString()

Call ToQueryString() on any IQueryable to see the SQL without executing it:

csharp
1var query = context.Products
2 .Where(p => p.Price > 100)
3 .OrderBy(p => p.Name);
4
5Console.WriteLine(query.ToQueryString());
6// Prints the SQL that would be executed

Logging

Configure EF Core to log all SQL queries:

csharp
1protected override void OnConfiguring(DbContextOptionsBuilder options)
2 => options
3 .UseSqlite("Data Source=app.db")
4 .LogTo(Console.WriteLine, LogLevel.Information);

Database Profiling Tools

For production analysis, use database-specific tools:

  • SQLite: .explain command or DB Browser for SQLite
  • SQL Server: SQL Server Profiler, Extended Events, or Query Store
  • PostgreSQL: EXPLAIN ANALYZE

Performance Strategy

Approaching EF Core performance systematically prevents both under-optimization and over-optimization:

1. Measure First

Never optimize based on assumptions. Profile your queries to identify actual bottlenecks:

  • Enable SQL logging during development
  • Use ToQueryString() to inspect generated SQL for complex queries
  • Monitor query counts per request (watch for N+1 patterns)

2. Optimize Queries

Once you've identified a slow query, apply targeted fixes:

  • Add indexes for frequently filtered or sorted columns
  • Use projections (.Select()) to fetch only needed columns
  • Fix N+1 problems with eager loading (.Include()) or projections
  • Use AsNoTracking for read-only queries

3. Optimize Writes

Bulk operations and batching can dramatically reduce write overhead:

  • ExecuteUpdate/ExecuteDelete for bulk changes without loading entities
  • AddRange instead of individual Add calls
  • Batch SaveChanges calls instead of saving after every entity

4. Cache and Pool

Reduce repeated overhead:

  • Compiled queries to eliminate LINQ expression tree compilation
  • DbContext pooling to reuse context instances

When EF Core Is Not Enough

For some scenarios, EF Core's abstraction layer adds overhead that matters:

  • Bulk imports of millions of rows - consider SqlBulkCopy or database-specific bulk insert tools
  • Complex reporting queries - consider raw SQL with FromSqlRaw or a dedicated reporting tool
  • Real-time analytics - consider materialized views or pre-computed aggregates

EF Core's FromSqlRaw and SqlQueryRaw methods let you drop to raw SQL when needed while still benefiting from EF Core's materialization.

Summary

  • EF Core performance issues often stem from N+1 queries, over-fetching, or client evaluation
  • Always inspect the generated SQL to understand what EF Core is doing
  • Use ToQueryString() and logging to see the SQL during development
  • Measure before optimizing, and apply targeted fixes to actual bottlenecks
  • For bulk operations, use EF Core 7+ features like ExecuteUpdate and ExecuteDelete

In the following lessons, we'll dive into each optimization technique in detail.