Фильтрация данных в диапазоне дат на основе данных записи всегда истекает по времени с использованием EFCore. Я делаю что-то неправильно, что приводит к такой низкой производительности?

1
5

У меня есть тест, в котором много записей с течением времени. Я использую postgresql для хранения этих тестов и записей и хочу иметь возможность искать тесты, записи которых попадают в определенный временной диапазон.

У теста много записей, и у каждой записи есть значение collectionTime, которое хранится как DateTime. Когда я ищу все тесты, я использую следующий запрос с использованием EFCore и Linq:

На этом этапе я начинаю выполнять различную фильтрацию и сортировку. Это изменяет результирующий запрос в зависимости от того, какие фильтры применяет пользователь. Если пользователь просто извлекает все записи, в этот момент они разбиваются на страницы и возвращаются без фильтрации, то с запросом нет проблем. Однако, когда я добавляю свою логику для проверки того, был ли предоставлен определенный период времени, мой запрос постоянно истекает по времени.

У меня есть почти идентичное условие для проверки того, находится ли время сбора FirstReported после providedEndDate, так как фильтрация обеих сторон таким образом логически приводит к желаемым мной тестам.

После разбиения результатов на страницы я отправляю их в список: return await tests.ToListAsync(token);

Я также хочу подсчитать количество записей для целей разбиения на страницы, и CountAsync также останавливается при фильтрации тестов в диапазоне дат.

Есть ли какие-либо предложения или непосредственные опасения по поводу запроса и логики относительно того, почему этот запрос так плохо работает/истекает по времени? Я думаю, что это довольно распространенное явление, но я не могу найти никаких конкретных сообщений, которые бы касались оптимизации поиска таким образом. Спасибо

    var tests = Table.Include(t => t.RecordData).AsSplitQuery().AsNoTracking()
        .Select(r => new Test 
        { 
            Id = r.Id,
            ...additional test info here...
            FirstReported = r.RecordData.OrderBy(rd => rd.CollectionDate).FirstOrDefault().CollectionDate,
            LastReported = r.RecordData.OrderByDescending(rd => rd.CollectionDate).FirstOrDefault().CollectionDate,
            NumberOfRecords = r.RecordData.Count,
        });
    if (providedStartDate is not null)
    {
        var startDateUTC = providedStartDate.ToUniversalTime().Date;
        tests = tests.Where(x => x.NumberOfRecords > 0 && x.LastReported >= startDateUTC);
    }
Фотий
Вопрос задан1 января 2024 г.

1 Ответ

2

Фильтрация данных в диапазоне дат: оптимизация производительности в EFCore

Проблема с низкой производительностью при фильтрации данных по диапазону дат, особенно при использовании условия “всегда истекает по времени”, может быть вызвана несколькими факторами. Давайте разберем типичные ошибки и способы их исправления.

1. Отсутствие индекса на поле даты

Проблема: Если у вас нет индекса на поле даты, EFCore будет сканировать всю таблицу, чтобы найти подходящие записи. Это может быть очень медленно, особенно для больших таблиц.

Решение: Добавьте индекс на поле даты.

public class MyDbContext : DbContext
{
  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    modelBuilder.Entity<MyEntity>()
      .HasIndex(e => e.ExpirationDate);
  }
}

2. Использование DateTime.Now в LINQ-запросе

Проблема: Использование  DateTime.Now в LINQ-запросе приведет к тому, что EFCore будет выполнять запрос каждый раз, когда вы его выполняете, и не сможет использовать индекс.

Решение: Передайте текущую дату в качестве параметра запроса.

public async Task<List<MyEntity>> GetExpiredEntities(DateTime now)
{
  return await _dbContext.MyEntities
    .Where(e => e.ExpirationDate < now)
    .ToListAsync();
}

3. Использование DateTime.UtcNow вместо DateTime.Now

Проблема: Если у вас хранятся даты в UTC, но вы используете  DateTime.Now (который возвращает локальное время), то EFCore не сможет использовать индекс корректно.

Решение: Используйте  DateTime.UtcNowдля получения UTC-времени.

public async Task<List<MyEntity>> GetExpiredEntities(DateTime now)
{
  return await _dbContext.MyEntities
    .Where(e => e.ExpirationDate < DateTime.UtcNow)
    .ToListAsync();
}

4. Неправильное форматирование даты в запросе

Проблема: Если вы используете строковое представление даты в LINQ-запросе, EFCore не сможет использовать индекс.

Решение: Используйте  DateTime объекты в LINQ-запросе.

public async Task<List<MyEntity>> GetExpiredEntities(DateTime startDate, DateTime endDate)
{
  return await _dbContext.MyEntities
    .Where(e => e.ExpirationDate >= startDate && e.ExpirationDate <= endDate)
    .ToListAsync();
}

5. Использование DateTime.Now в условии “всегда истекает по времени”

Проблема: Если у вас есть условие “всегда истекает по времени”, то вы должны использовать  DateTime.MaxValue вместо DateTime.Now. Это позволит EFCore использовать индекс и вернуть все записи, которые уже истекли.

Решение:

public async Task<List<MyEntity>> GetExpiredEntities()
{
  return await _dbContext.MyEntities
    .Where(e => e.ExpirationDate < DateTime.MaxValue)
    .ToListAsync();
}

6. Неоптимальный запрос EFCore

Проблема: EFCore может генерировать неэффективный SQL-запрос, который не использует индекс.

Решение: Проверьте сгенерированный SQL-запрос с помощью  DbContext.Log и убедитесь, что он использует индекс. Если нет, попробуйте переписать LINQ-запрос или использовать  FromSqlRaw для ручного задания SQL-запроса.

Дополнительные советы:

  • Профилируйте запросы с помощью инструментов профилирования SQL (например, SQL Server Profiler).
  • Используйте инструменты анализа и оптимизации запросов (например, SQL Server Management Studio).
  • Рассмотрите возможность использования материализованных представлений в базе данных для ускорения доступа к данным.

Важно: Всегда проверяйте сгенерированный SQL-запрос и убедитесь, что он использует индекс и оптимизирован для быстрой работы.

 

Тамара
Ответ получен11 сентября 2024 г.

Ваш ответ

Загрузить файл.