Database Performance Patterns
When to Use This Skill
Use this skill when:
- Designing data access layers
- Optimizing slow database queries
- Choosing between EF Core and Dapper
- Avoiding common performance pitfalls
Core Principles
- Separate read and write models - Don't use the same types for both
- Think in batches - Avoid N+1 queries
- Only retrieve what you need - No SELECT *
- Apply row limits - Always have a configurable Take/Limit
- Do joins in SQL - Never in application code
- AsNoTracking for reads - EF Core change tracking is expensive
Read/Write Model Separation (CQRS Pattern)
Read and write models are fundamentally different - they have different shapes, columns, and purposes. Don't create a single "User" entity and reuse it everywhere.
- Read models are denormalized, optimized for query efficiency, and return multiple projection types (UserProfile, UserSummary, UserDetailForAdmin)
- Write models are normalized, validation-focused, and accept strongly-typed commands (CreateUserCommand, UpdateUserCommand)
Architecture
Read Store Interface
Write Store Interface
Key structural differences illustrated:
- Read store returns multiple different DTOs (UserProfile, UserSummary, bool flag)
- Write store returns minimal data (just UserId on create) or void
- Read queries are stateless projections - no tracking needed
- Write operations focus on command validation, not retrieving data afterwards
- Different databases/tables can back read vs write (eventual consistency pattern)
Always Apply Row Limits
Never return unbounded result sets. Every read method should have a configurable limit.
Pattern: Limit Parameter
EF Core with Pagination
AsNoTracking for Read Queries
EF Core's change tracking is expensive. Disable it for read-only queries.
Configure Default Behavior
Then explicitly enable tracking when needed:
Avoid N+1 Queries
The N+1 problem: fetching a list, then querying for each item's related data.
The Problem
Solution 1: Include (EF Core)
Solution 2: Batch Query (Dapper)
Never Do Application-Side Joins
Joins must happen in SQL, not in C#.
Avoid Cartesian Explosions
Multiple Include calls can cause Cartesian products.
Solution: Split Queries
Solution: Explicit Projection
Constrain Column Sizes
Define maximum lengths in your EF Core model to prevent oversized data.
Don't Build Generic Repositories
Generic repositories hide query complexity and make optimization difficult.
Problems with generic repositories:
- Can't optimize specific queries
- No way to enforce limits
- Hide N+1 problems
- Make it easy to fetch too much data
- Encourage lazy thinking about data access
Dapper for Read-Heavy Workloads
For complex read queries, Dapper with explicit SQL is often cleaner and faster.
When to Use EF Core vs Dapper
You can use both in the same project - EF Core for writes, Dapper for reads.
Quick Reference
Resources
- EF Core Performance: https://learn.microsoft.com/en-us/ef/core/performance/
- Dapper: https://github.com/DapperLib/Dapper
- AsSplitQuery: https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries


