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:
| Pitfall | Symptom | Impact |
|---|---|---|
| N+1 queries | Hundreds of SQL queries for a single page | Massive latency |
| Over-fetching | Loading entire entities when only a few columns are needed | Wasted memory and bandwidth |
| Client evaluation | Filtering/sorting happens in C# instead of SQL | Full table scans |
| Missing indexes | Slow WHERE/ORDER BY on unindexed columns | Query timeouts |
| Unnecessary tracking | Change tracker monitors read-only entities | Memory overhead |
| Repeated compilation | LINQ expression trees recompiled on every request | CPU 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:
csharp1// Efficient: filtering happens in SQL2var products = await context.Products3 .Where(p => p.Price > 100)4 .ToListAsync();5// SQL: SELECT ... FROM Products WHERE Price > 10067// Inefficient: loads ALL rows, then filters in C#8var products = context.Products9 .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:
csharp1var query = context.Products2 .Where(p => p.Price > 100)3 .OrderBy(p => p.Name);45Console.WriteLine(query.ToQueryString());6// Prints the SQL that would be executed
Logging
Configure EF Core to log all SQL queries:
csharp1protected override void OnConfiguring(DbContextOptionsBuilder options)2 => options3 .UseSqlite("Data Source=app.db")4 .LogTo(Console.WriteLine, LogLevel.Information);
Database Profiling Tools
For production analysis, use database-specific tools:
- SQLite:
.explaincommand 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
SqlBulkCopyor database-specific bulk insert tools - Complex reporting queries - consider raw SQL with
FromSqlRawor 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.