Common database performance mistakes and solutions to avoid in .Net API development

DotNet Full Stack Dev
5 min readSep 11, 2024

--

When developing APIs in .NET Core, database performance is a critical factor in ensuring that your application runs efficiently and scales well under load. Here are some common database performance mistakes developers make and how to avoid them:

Embark on a journey of continuous learning and exploration with DotNet-FullStack-Dev. Uncover more by visiting our https://dotnet-fullstack-dev.blogspot.com reach out for further information.

1. Not Using Asynchronous Database Calls

Mistake:

  • Using synchronous database calls (.Result, .Wait(), or blocking methods) in an API can cause thread blocking, leading to poor performance, especially under heavy loads.

Solution:

  • Use asynchronous database access methods provided by Entity Framework (EF) Core or ADO.NET. Asynchronous methods release threads back to the thread pool while waiting for I/O operations, allowing the server to handle more requests simultaneously.
// BAD: Synchronous call
var items = dbContext.Items.ToList();

// GOOD: Asynchronous call
var items = await dbContext.Items.ToListAsync();

2. Not Using Indexes Properly

Mistake:

  • Missing or poorly designed database indexes can lead to slow queries, especially when filtering or joining large datasets. Without indexes, the database has to perform full table scans, which can drastically impact performance.

Solution:

  • Ensure you create appropriate indexes on frequently queried columns, especially those used in WHERE, JOIN, ORDER BY, and GROUP BY clauses.

Tips:

  • Use indexes on foreign keys, primary keys, and commonly queried columns.
  • Be cautious about over-indexing as it can slow down write operations (INSERT, UPDATE, DELETE).
-- Adding an index on the 'LastName' column to improve query performance
CREATE INDEX idx_last_name ON Employees(LastName);

3. Fetching Too Much Data (Over-fetching)

Mistake:

  • Fetching entire records when only a subset of columns is needed or retrieving large amounts of data unnecessarily can lead to excessive memory usage and network traffic, slowing down your API.

Solution:

  • Use projection to fetch only the required columns instead of entire entities. This minimizes the amount of data transferred from the database to the API.
// BAD: Fetching entire entity
var employees = await dbContext.Employees.ToListAsync();

// GOOD: Fetching only necessary fields
var employeeNames = await dbContext.Employees
.Select(e => new { e.FirstName, e.LastName })
.ToListAsync();

4. N+1 Query Problem

Mistake:

  • The N+1 query problem occurs when your application makes multiple database queries within a loop, usually because of improper eager or lazy loading strategies. This leads to inefficiency since a query is made for each record, resulting in a large number of unnecessary queries.

Solution:

  • Use eager loading (Include method) or explicit loading to retrieve related data in a single query. In many cases, a single JOIN or Include can fetch all the required data instead of making separate queries.
// BAD: N+1 query problem
var orders = await dbContext.Orders.ToListAsync();
foreach (var order in orders)
{
var customer = await dbContext.Customers.FindAsync(order.CustomerId);
}

// GOOD: Eager loading with Include to prevent N+1 problem
var ordersWithCustomers = await dbContext.Orders
.Include(o => o.Customer)
.ToListAsync();

5. Not Using Pagination for Large Data Sets

Mistake:

  • Returning large datasets (thousands or more rows) in a single API call without any form of pagination. This increases the memory load on the server, increases response time, and can cause API consumers to time out.

Solution:

  • Implement pagination using Skip and Take in your queries to limit the number of rows returned in a single request.
int pageSize = 10;
int pageIndex = 1;

// BAD: Fetching all records
var allItems = await dbContext.Items.ToListAsync();

// GOOD: Implementing pagination
var pagedItems = await dbContext.Items
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.ToListAsync();

6. Using Count() Inefficiently

Mistake:

  • Calling .Count() on a collection or query unnecessarily, especially when the data is already being retrieved, can result in additional database calls and degrade performance.

Solution:

  • Use CountAsync() when you only need the count, or avoid calling .Count() if the data is already available. Avoid unnecessary counting in loops or collections that are already fully loaded.
// BAD: Unnecessary use of Count()
if (dbContext.Items.Where(i => i.IsActive).ToList().Count() > 0)
{
// Do something
}

// GOOD: Directly count in the database
if (await dbContext.Items.CountAsync(i => i.IsActive) > 0)
{
// Do something
}

7. Not Using Transactions for Bulk Operations

Mistake:

  • Performing multiple inserts, updates, or deletes in a loop without a transaction can cause each operation to be committed individually. This leads to slower performance, especially with large datasets.

Solution:

  • Wrap bulk operations inside a transaction to ensure all operations are committed at once, improving performance and ensuring atomicity.
// BAD: Individual transactions for each insert
foreach (var item in items)
{
dbContext.Items.Add(item);
await dbContext.SaveChangesAsync(); // This commits for every item
}

// GOOD: Using a transaction for bulk operations
using (var transaction = await dbContext.Database.BeginTransactionAsync())
{
foreach (var item in items)
{
dbContext.Items.Add(item);
}
await dbContext.SaveChangesAsync(); // Commit all changes in one transaction
await transaction.CommitAsync();
}

8. Misuse of Include (Overloading Eager Loading)

Mistake:

  • Overusing .Include() to eagerly load related entities even when they are not required. This causes unnecessary JOIN operations, leading to performance bottlenecks.

Solution:

  • Only use .Include() when the related data is absolutely necessary for that particular query. Otherwise, rely on lazy loading or query the related data only when needed.
// BAD: Eager loading unnecessary related entities
var orders = await dbContext.Orders
.Include(o => o.Customer)
.Include(o => o.Products)
.ToListAsync();

// GOOD: Load only required entities
var orders = await dbContext.Orders.ToListAsync(); // No eager loading if not needed

9. Incorrect Use of SaveChanges()

Mistake:

  • Calling SaveChanges() multiple times in a loop (e.g., after every entity addition), which leads to multiple database round trips.

Solution:

  • Batch multiple operations and call SaveChanges() once after processing all the changes to minimize database round trips.
// BAD: SaveChanges called for each entity
foreach (var item in items)
{
dbContext.Items.Add(item);
await dbContext.SaveChangesAsync();
}

// GOOD: Call SaveChanges once for all entities
dbContext.Items.AddRange(items);
await dbContext.SaveChangesAsync();

10. Not Using Database Connection Pooling

Mistake:

  • If you don’t configure connection pooling properly, your application may suffer from slow database connections, especially under high load. Each database connection creation is expensive.

Solution:

  • Ensure that connection pooling is enabled and configured for optimal performance in your database connection string. By default, connection pooling is enabled, but you can adjust the pool size to fit your application needs.
"ConnectionStrings": {
"DefaultConnection": "Server=yourserver;Database=yourdb;User Id=user;Password=pass;Max Pool Size=100;"
}

Conclusion

Database performance is critical to the success of any API, especially in high-concurrency environments. By addressing common mistakes such as over-fetching data, ignoring pagination, inefficient query patterns, and improper use of transactions, you can drastically improve the performance and scalability of your .NET Core APIs. Understanding how to use Entity Framework (EF) Core and ADO.NET efficiently is key to avoiding these issues, and keeping best practices in mind will help you build high-performance APIs.

--

--

DotNet Full Stack Dev
DotNet Full Stack Dev

Written by DotNet Full Stack Dev

Join me to master .NET Full Stack Development & boost your skills by 1% daily with insights, examples, and techniques! https://dotnet-fullstack-dev.blogspot.com

No responses yet