Как оптимизировать запрос списков против списков - PullRequest
1 голос
/ 27 января 2020

Я создаю приложение, используя Xamarin.Forms, и я сталкиваюсь с очень медленным запросом к данным, которые мне нужно оптимизировать, если это возможно.

Чтобы понять вопрос, я пытаясь создать кадр, мне нужно хорошо объяснить отношения с базой данных. Программное обеспечение для бизнеса, которое я пытаюсь создать, позволяет пользователю распределять сотрудников по рабочим группам и рабочим группам. В целях этого объяснения мы можем игнорировать задания, даже если объекты базы данных включают в себя «задание».

Для ежедневного планирования работы сотрудников я создал kanban, который позволяет пользователю перетаскивать имена сотрудников в экипаж, на который они хотят назначить расписание на день, и по завершении редактирования они используют панель инструментов. кнопки для перехода к следующей дате, которую они хотят запланировать. В фоновом режиме код создает объекты базы данных, которые создают связь между сотрудником, датой и командой.

Для планирования операций изо дня в день я создал планировщик в стиле Ганта с боковой прокруткой, который позволяет пользователю перетаскивать блоки операций на команду на день. В фоновом режиме код создает объекты базы данных, которые создают связь между операцией, датой и командой.

Вот простая версия того, как выглядят объекты базы данных.

public interface IEmployee
{
    long? Id { get; set; }
    string Name { get; }
    string Password { get; set; }
}
public interface ICrewMember
{
    long? Id { get; set; }
    IEmployee Employee { get; set; }
    bool IsLeader { get; set; }
    ICrew Crew { get; set; }
    DateTime Date { get; set; }
}
public interface IJobSchedule
{
    long? Id { get; set; }
    IOperation Operation { get; set; }
    DateTime Date { get; set; }
    ICrew Crew { get; set; }
}
public interface IOperation
{
    long? Id { get; set; }
    int Priority { get; set; }
}

Таким образом, сложность сценария возникает, когда я хочу найти, какие операции сотрудник был запланирован на. Я должен сначала запросить, чтобы найти объекты расписания сотрудника, создать список команд / дат, для которых они были запланированы, затем получить список графиков работы, которые соответствуют списку дат / экипажей, а затем свести его к отдельный список операций (поскольку операция может быть запланирована на несколько дней). Вот мой текущий код для этого:

public async Task<List<IOperation>> GetOperationsByEmployee(IDataService<IJobSchedule> JobScheduleRepository)
{
    JobScheduleRepository = JobScheduleRepository ??
        throw new ArgumentNullException(nameof(JobScheduleRepository));

    var result = new List<IOperation>();

    var empSchedMatches = await GetEmployeeSchedules().ConfigureAwait(false);

    var jobSchedules = await GetJobSchedules(JobScheduleRepository, empSchedMatches).ConfigureAwait(false);

    result = jobSchedules.Select(x => x.Operation).Distinct().ToList();

    return result;
}
private async Task<IEnumerable<ICrewMember>> GetEmployeeSchedules()
{
    //Get complete list of employee schedules to sort through
    var allEmpSched = await CrewMemberRepository.GetItemsAsync().ConfigureAwait(false);

    //Get schedules with date greater than or equal to Date for this employee
    var empSchedMatches = allEmpSched.Where(x => x.Date >= Date && x.Employee == Employee);
    return empSchedMatches;
}
private async Task<IEnumerable<IJobSchedule>> GetJobSchedules(IDataService<IJobSchedule> JobScheduleRepository, IEnumerable<ICrewMember> employeeSchedules)
{
    //Get complete list of job schedules to sort through
    var allJobSched = await JobScheduleRepository.GetItemsAsync().ConfigureAwait(false);
    allJobSched = allJobSched.Where(x => x.Date >= Date && x.Crew != null && x.Operation != null);
    int count = allJobSched.Count();


    var result = new List<IJobSchedule>();

    foreach (var empSched in employeeSchedules)
    {
        //For each employee schedule, there should be 1 matching job schedule
        //if the crew was assigned a job for that day
        var matches = allJobSched.Where(x => x.Date == empSched.Date && x.Crew == empSched.Crew);
        result.AddRange(matches);

        string message = $"GetJobSchedules() comparing ({count}) Job Schedules " +
            $"to empSched.{empSched.Id} crew.{empSched.Crew.Id} date.{empSched.Date:M/d}";
        System.Diagnostics.Debug.WriteLine(message);
    }

    return result;
}

Для того, чтобы попытаться просмотреть процесс, я добавил несколько разных битов кода, которые выводили шаги в отладчик , включая секундомер. Вот вывод отладчика:

[0:] Method Called: GetOperationsByEmployee
[0:] GetOperationsByEmployee() executing query...
[0:] Method Called: GetEmployeeSchedules
[0:] Method Called: GetJobSchedules
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.17196 crew.3 date.2/6
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.18096 crew.3 date.2/4
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.18221 crew.3 date.2/3
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.18902 crew.3 date.2/7
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.21243 crew.3 date.1/27
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.21321 crew.3 date.1/28
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.21360 crew.3 date.1/29
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.21399 crew.3 date.1/30
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.21438 crew.3 date.1/31
[0:] GetJobSchedules() comparing (51) Job Schedules to empSched.21528 crew.3 date.2/5
[0:] Data loaded 6391 ms

Так что, когда я запускаю приложение, используя фиктивные данные с примерно 10 объектами в памяти, оно запускается за ~ 100 мс. Когда у меня есть 50 000 объектов в реальной базе данных с 30 сотрудниками, 10 командами, 500 заданиями и 1500 операциями для сортировки, это занимает ~ 7000 мс. Это сравнение, вероятно, было очевидным, но суть в том, что мне нужно найти способ, если возможно, оптимизировать запрос. Я хотел бы приблизить время загрузки к 1 секунде, если это возможно.

Как всегда, спасибо за любую помощь!

Редактировать

Боюсь, я не получаю ответы, на которые надеялся, потому что Я на самом деле не ищу совет по вопросу доступа к данным, я ищу совет по LINQ стороне вопроса. Я не знаю, поможет ли это понять сценарий доступа к данным, поэтому я кратко объясню.

Я кодирую в Xamarin.Forms, используя Autofa c в качестве моего инжектора зависимости. Я пытаюсь использовать интерфейсы, чтобы позволить вызовам к службе данных быть абстрагированными от службы данных.

Данные хранятся на сервере SQL на сервере здесь, в офисе. Приложение использует API для SQL для SQLite под названием Zumero . Zumero синхронизирует запрошенные таблицы с сервера SQL и помещает их в локальный файл на мобильном устройстве.

Я использую Entity Framework Core для передачи данных в программу, снова используя интерфейсы и отображение полей, чтобы попытаться абстрагировать вызовы для объектов базы данных отдельно от самих объектов базы данных. .

Редактировать 2

Я попытаюсь повторить здесь вопрос, чтобы было более понятно, что я ищу:

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

Воображаемый вопрос:

Что такое запланированные на данный момент операции Боба?

Воображаемые строки в таблицах данных:

  1. Сотрудники

    1. Боб
    2. Джим
    3. Ларри
  2. График работы сотрудников

    1. Боб 1/1/2020 Бетон Экипаж 1
    2. Боб 1/2/2020 Бетонная бригада 1
    3. Боб 1/3/2020 Мельничная бригада 2
  3. Графики работы

    1. Экипаж 1 1/1/2020 Бетонная операция 1
    2. Экипаж 2 1/1/2020 Мельница Операция 1
    3. Экипаж 1 1/2/2020 Бетонная операция 1
    4. Экипаж 1 1/3/2020 Бетонная операция 3
  4. Операции

    1. Бетонная операция 1
    2. Работа на мельнице 1
    3. Конкретная операция 3

Желаемый результат:

Боб в настоящее время выполняет следующие операции по расписанию: Конкретная операция 1

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

Спасибо за любую помощь!

Ответы [ 5 ]

1 голос
/ 27 января 2020

Ответ прост: вы создаете целые таблицы для времени выполнения и сопоставляете их с помощью C#, и это неправильно.

Для этого и создана база данных, и вы должны ее использовать.

У вас есть много вариантов, запросов, представлений, хранимых процедур, но наверняка запрашивать всю базу данных и выполнять сопоставления с помощью кода - неправильный путь.

0 голосов
/ 06 февраля 2020

На поставленный вопрос есть две части.

Вопрос 1: Как мне написать код для перехода от одного ко многим ко многим к одному?

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

public async Task<List<IOperation>> GetOperationsByEmployee(
    IDataService<IJobSchedule> JobScheduleRepository, 
    DateTime Date, 
    IEmployee Employee)
{
    var result = new List<IOperation>();

    var empSchedMatches = allEmployeeSchedules.Where(x => x.Date >= Date && x.Employee == Employee);

    var jobSchedules = new List<IJob>();

    foreach (var empSched in empSchedMatches)
    {
        var matches = allJobSched.Where(x => x.Date == empSched.Date && x.Crew == empSched.Crew);
        jobSchedules.AddRange(matches);
    }

    result = jobSchedules.Select(x => x.Operation).Distinct().ToList();

    return result;
}

Вопрос 2: Как мне ускорить этот запрос?

Я немного поспорил по поводу выбора ORM здесь, но у меня не было никаких конкретные шаги по реализации для улучшения кода. Затем я поговорил с программистом, который объяснил мне, что у него огромный опыт использования Entity Framework с SQLite. Он дал мне несколько конкретных указателей и пример кода для улучшения скорости загрузки базы данных. Он объяснил, что Entity Framework отслеживает загружаемые объекты, но если операция считывает данные, которые не нужно отслеживать, скорость загрузки можно улучшить, отключив отслеживание. Он дал мне следующий пример кода:

internal class ReadOnlyEFDatabase : EFDatabase
{
    public ReadOnlyEFDatabase(string dbPath, DbContextOptions options) : base(dbPath, options)
    {
        this.ChangeTracker.AutoDetectChangesEnabled = false;
        this.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    }
    public override int SaveChanges()
    {
        throw new Exception("Attempting to save changes from a read-only connection to the database.");
    }
}

Спасибо Джереми Шили из zumero за эту помощь !!!!

0 голосов
/ 27 января 2020

ЕСЛИ вы настаиваете, это должно быть сделано на 100% в LINQ, что я не рекомендую, поскольку вам нужно лучше выполнять предварительную фильтрацию поступающих данных, тогда ваша проблема заключается в том, что вы выполняете исчерпывающий поиск для каждой строки в таблице. Вместо этого я рекомендую создать словарь и сначала загрузить в него строки.

Вместо:

private async Task<IEnumerable<ICrewMember>> GetEmployeeSchedules()
{
    //Get complete list of employee schedules to sort through
    var allEmpSched = await CrewMemberRepository.GetItemsAsync().ConfigureAwait(false);

    //Get schedules with date greater than or equal to Date for this employee
    var empSchedMatches = allEmpSched.Where(x => x.Date >= Date && x.Employee == Employee);
    return empSchedMatches;
}

Сделайте это:

var dict = new Dictionary<Employee, List<IJobSchedule>>
foreach(var item in allEmpSched) {
  if (dict.TryGetValue(item.Employee, out var schedules)) {
    schedules.Add(item)
  } else {
    dict[item.Employee] = new List<IJobSchedule>() { item };
  }
}

Затем, используя этот словарь посмотрите на своего сотрудника. По сути, это то, что база данных будет делать для вас автоматически, если у вас есть правильно настроенный индекс вместе с соответствующим предложением SELECT, но там вы go.

Edit: Я вижу, что вы ' Вы можете переместить это в ORM, так что он напишет правильный SQL для вас. Я не знаю, как это сделать, потому что я не использую ORM.

Почему я не использую ORM? Есть три причины. Во-первых, доступ к данным - это не то, что должно быть предоставлено машине для встраивания в ваше приложение. Доступ к данным часто является наиболее часто используемым кодом в программном пакете. Обратите на это внимание и разработайте его хорошо. Компьютеры не могут обеспечить эффективную замену правильному дизайну.

Во-вторых, сам язык SQL является абстракцией физических средств для доступа к базовым данным. Когда выполняется запрос SQL, первое, что делает ядро ​​базы данных, - это разрабатывает план его выполнения. По сути, SQL интерпретируется и компилируется в сгенерированный код. Если вы добавите другой генератор кода (ORM), результаты, естественно, будут различаться, и редко ORM дает хорошие результаты, не тратя много времени на настройку. Потратьте свое время на написание хороших SQL запросов.

Наконец, ORM действительно не устраняют проблему несоответствия импеданса между сильными объектно-ориентированными моделями и реляционной базой данных. Вы должны начать с решения проблемы в самой модели данных и написать свой код для работы с реляционными объектами, а не с глубоко вложенными объектами. Сделав это, вы обнаружите, что написание собственных запросов не так уж сложно.

0 голосов
/ 27 января 2020

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

Здесь много кода, но только в качестве примера.

    var allJobSched = await JobScheduleRepository.GetItemsAsync().ConfigureAwait(false);
allJobSched = allJobSched.Where(x => x.Date >= Date && x.Crew != null && x.Operation != null);
int count = allJobSched.Count();

В приведенном выше фрагменте вы сначала извлекаете все расписания заданий из базы данных, это неизменно включает в себя большое вытягивание данных по проводам. из вашей базы данных (не очень хорошо).

Вам было бы намного лучше, если бы вы написали что-то похожее на следующее.

    var jobs= await JobScheduleRepository.GetValidJobsAfterDate(Date);

Реализация самого запроса должна быть отправлена ​​вашим репозиторием. к вашей базе данных, обработанной там, вы никогда не должны извлекать огромные коллекции данных для работы в памяти (в большинстве случаев).

0 голосов
/ 27 января 2020

Есть несколько вещей, которые вы могли бы сделать, чтобы ускорить ваш запрос. Есть одно очевидное изменение, которое я бы реализовал (если я достаточно понимаю этот поток):

//change to something where you pass your parameters as to not need to load all of your jobs every time
var allJobSched = await JobScheduleRepository.GetItemsAsync().ConfigureAwait(false);
//change to 
var matchingJobSched = await JobScheduleRepository.FindMatchingJobSchedules(DateTime date, int crewId).ConfigureAwait(false);

Похоже, вы делаете это для каждого объекта, поэтому этот рефакторинг должен выполняться над всем вашим кодом.

Еще можно попробовать написать хранимую процедуру для этого действия и опустить время ожидания ORM.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...