Низкая производительность ToList и низкая производительность foreach

Я строю программу, которая использует базу данных с 3 таблицами (Worker, Task, TaskStep), и у меня есть метод, который получает дату и отчет о сборке для конкретного работника задачи и там шаги для конкретного дня.

Структура базы данных выглядит следующим образом:

MySQL 5.2

Worker столбцы таблицы:

workerID(VARCHAR(45)),
name(VARCHAR(45)),
age(int),
...

Tasks столбцы таблицы:

TaskID(VARCHAR(45)),
description(VARCHAR(45)),
date(DATE),
...

TaskSteps столбцы таблицы:

TaskStepID(VARCHAR(45)),
description(VARCHAR(45)),
date(DATE),
...

Нет индексации ни для одной таблицы

Проблема в том, что это очень, очень медленно! (~ 20 секунд)

Вот код:

using WorkerDailyReport = Dictionary<task, IEnumerable<taskStep>>;

private void Buildreport(DateTime date)
{
    var report = new WorkerDailyReport();    

    // Load from DB
    var sw = new Stopwatch();
    sw.Start();

    var startOfDay  = date.Date;
    var endOfDay    = startOfDay.AddDays(1);
    var db          = new WorkEntities();

    const string    workerID   = "80900855";

    IEnumerable<task> _tasks = db.task
                    .Where(ta =>    ta.date     >= startOfDay   &&
                                    ta.date     <  endOfDay     &&
                                    ta.workerID == workerID)
                    .ToList();

    sw.Stop();
    Console.WriteLine("Load From DB time - " + sw.Elapsed + 
                      ", Count - "           + _tasks.Count());   

    // Build the report
    sw.Restart();

    foreach (var t in _tasks)
    {
        var ts = db.taskStep.Where(s => s.taskID == task.taskID);

        report.Add(t, ts);
    }

    sw.Stop();
    Console.WriteLine("Build report time - " + sw.Elapsed);

    // Do somthing with the report
    foreach (var t in report)
    {
        sw.Restart();

        foreach (var subNode in t.Value)
        {
            // Do somthing..
        }

        Console.WriteLine("Do somthing time - " + sw.Elapsed + 
                          ", Count - " + t.Value.Count());
    }
}

Как вы можете видеть, я поместил StopWatch в каждую часть, чтобы проверить, что займет так много времени, и вот результаты:

1)

Если я запускаю код, как указано выше:

Приставка:

Load From DB time - 00:00:00.0013774, Count - 577

Build report time - 00:00:03.6305722

Do somthing time - 00:00:07.7573754, Count - 21

Do somthing time - 00:00:08.2811928, Count - 11

Do somthing time - 00:00:07.8715531, Count - 14

Do somthing time - 00:00:08.0430597, Count - 0

Do somthing time - 00:00:07.7867790, Count - 9

Do somthing time - 00:00:07.3485209, Count - 39

.........

внутренний прогон foreach занимает около 7-9! Секунд пробежит не более 40 записей.

2)

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

Приставка:

Load From DB time - 00:00:04.3568445, Count - 577

Build report time - 00:00:00.0018535

Do somthing time - 00:00:00.0191099, Count - 21

Do somthing time - 00:00:00.0144895, Count - 11

Do somthing time - 00:00:00.0150208, Count - 14

Do somthing time - 00:00:00.0179021, Count - 0

Do somthing time - 00:00:00.0151372, Count - 9

Do somthing time - 00:00:00.0155703, Count - 39

.........

Теперь загрузка из базы данных занимает намного больше времени, 4+ сек. Но время создания отчета составляет ~1 мс. И каждый внутренний foreach занимает ~10 мс.

Первый способ невозможен (577 * ~8 секунд), а второй вариант также очень медленный, и я не вижу y.

Есть идеи, что здесь происходит?

1) Почему ToList() так медленно?

2) почему без ToList(), Внутренний foreach а отчет о сборке тормозит?

Как я могу сделать это быстрее?

Thnx.

3 ответа

Когда вы не используете.ToList(), C# не загружает ваши данные из базы данных до тех пор, пока вам не понадобится первая выборка данных из базы данных, это происходит из-за отложенной загрузки в платформе сущностей.

и на каждом шаге внутреннего цикла for-each ваша программа запрашивает запрос из базы данных, и это очень медленно.

но когда вы используете.ToList (), вы сразу запускаете запрос и сначала получаете все записи, а это медленно. затем во внутреннем цикле for-each ваша программа имеет все записи в памяти.

извините за слабость в английском языке:D

Чтобы повысить производительность, вы должны использовать один запрос для получения данных из всех таблиц:

var _joinedTasks = db.task.Where(ta =>    ta.date     >= startOfDay   &&
                                    ta.date     <  endOfDay     &&
                                    ta.workerID == workerID)
                    .Join(db.taskStep, t => t.taskID, ts=>ts.taskID, (t, ts) => new {t, ts})
                    .GroupBy(g => g.t, v=>v.ts).AsEnumerable();

Затем вы можете добавить его в словарь:

var report = _joinedTasks.ToDictionary(g=>g.Key);

И используйте этот отчет, как вы хотите.

LINQ ToList() всегда оценивает последовательность сразу - в вашем случае SQL-запрос к базе данных.

В первом случае вы получили Load From DB time - 00:00:00.0013774, Count - 577 - это было быстро, так как вы не запустили SQL-запрос. Однако запрос был выполнен чуть позже - вот почему вы получили Build report time - 00:00:03.6305722 (медленно)

Во втором случае добавление ToList() Принудительная оценка запроса немедленно (выполнение SQL), и поэтому вы получили следующие времена:

  • Load From DB time - 00:00:04.3568445, Count - 577 - SQL-запрос к базе данных (медленный)
  • Build report time - 00:00:00.0018535 - работает с данными, уже находящимися в памяти (быстро)

Интересным является тот факт, что ваш запрос, который возвращает 577 элементов, занял более 3 секунд. Это может произойти из-за отсутствия индексов в одной из таблиц.

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

.Where(ta => ta.date >= startOfDay &&
             ta.date < endOfDay &&
             ta.workerID == workerID)

С ростом количества предметов в Tasks таблица вашего запроса займет больше и больше времени.

Поэтому я очень рекомендую создать индексы на Tasks.date а также Tasks.workerId колонны. Это должно улучшить начальное время запроса (при условии, что ваше соединение с базой данных быстрое, т.е. вы не соединяетесь с базой данных, развернутой через океан).

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

К сожалению, я не могу посоветовать больше Do somthing time ... как вы не предоставили код. Но если вы примените тот же совет, я уверен, что вы также получите некоторые улучшения.

Другие вопросы по тегам