Ответ
Оптимизация LINQ направлена на уменьшение вычислительной сложности и объёма обрабатываемых данных.
1. Принцип ранней фильтрации
Всегда применяйте Where как можно раньше, особенно перед операциями, изменяющими размер набора (Select, GroupBy, OrderBy).
// Медленно: сначала проецируются все объекты, потом фильтруются
var slow = products.Select(p => p.Price * 1.2).Where(price => price > 100);
// Быстро: фильтрация происходит до проекции
var fast = products.Where(p => (p.Price * 1.2) > 100).Select(p => p.Price * 1.2);
2. Избегание множественных перечислений (N+1 проблема)
Каждое перечисление (foreach, ToList(), Count()) выполняет запрос заново. Кэшируйте результат, если используете его несколько раз.
var data = context.Products.Where(p => p.IsActive).ToList(); // Запрос выполняется 1 раз
if (data.Any()) { ... } // Работает с коллекцией в памяти
var count = data.Count(); // Работает с коллекцией в памяти
var first = data.First(); // Работает с коллекцией в памяти
3. Выбор правильного метода для проверки существования
// Плохо: считает ВСЕ элементы
if (products.Count(p => p.CategoryId == 5) > 0) { ... }
// Отлично: останавливается на первом найденном
if (products.Any(p => p.CategoryId == 5)) { ... }
4. Оптимизация операций с коллекциями в памяти
При частых проверках Contains на больших списках преобразуйте коллекцию в HashSet<T> для поиска за O(1).
List<int> largeList = ...; // 1_000_000 элементов
var lookupSet = new HashSet<int>(largeList); // Построение O(n)
// Медленно: O(n) для каждого вызова
bool bad = largeList.Contains(12345);
// Быстро: O(1) для каждого вызова после построения HashSet
bool good = lookupSet.Contains(12345);
5. Использование Join вместо вложенных циклов (SelectMany/Where)
Для связей между коллекциями Join часто эффективнее.
// Неоптимально (N*M операций)
var query1 = from order in orders
from customer in customers
where order.CustomerId == customer.Id
select new { order, customer };
// Оптимально (использует хэш-таблицы)
var query2 = from order in orders
join customer in customers on order.CustomerId equals customer.Id
select new { order, customer };
6. Отложенное (Deferred) vs. Немедленное (Immediate) выполнение Понимайте разницу, чтобы не выполнять запрос многократно.
- Отложенное:
Where,Select,OrderBy— запрос строится, но не выполняется до перечисления. - Немедленное:
ToList(),ToArray(),Count(),First()— выполняется немедленно и возвращает результат.
7. Для Entity Framework Core
- Используйте
AsNoTracking()для запросов только на чтение. - Проецируйте только нужные поля (
Select(p => new { p.Id, p.Name })), а не целые сущности. - Избегайте
ToList()перед дальнейшей фильтрацией — фильтруйте на стороне БД.
Главное правило: Всегда анализируйте сгенерированный SQL (через логгирование EF Core или Profiler) для запросов к базе данных. Оптимизации в памяти бесполезны, если сам запрос к БД неэффективен.