Запрашивает базу данных для каждой записи быстрее, чем с помощью LINQ

#c# #mysql #.net #performance #linq

#c# #mysql #.net #Производительность #linq

Вопрос:

У меня есть около 700 тысяч строк, которые повторяются. Для каждой строки в базе данных выполняется оператор SELECT sql, чтобы проверить, существует ли поле ‘name’ из этой текущей записи в соответствующей таблице.

База данных, прочитанная 700 тысяч раз, кажется мне очень неэффективной, поэтому я решил прочитать все данные перед циклом, сохранить их в DataTable и проверять, содержится ли соответствующая запись в DataTable через LINQ, на каждой итерации.

При этом производительность значительно снизилась. Сейчас процесс занимает примерно вдвое больше времени (многократно проверено с помощью сравнительного анализа).

Это исходный (более быстрый) код:

 for (int index = 0; index < dtSightings.Rows.Count; index  )
{
   DataTable dtResults = Utilities.ExecuteQueryMysqlString(connectionString, "SELECT name FROM my_table WHERE name = @name AND month_year = @monthYear", dictionary);

   if (dtResults == null || dtResults.Rows.Count == 0)
   {
   //Continue
   }
}

public static DataTable ExecuteQueryMysqlString(string connectionString, string sql, Dictionary<string, object> listParameters)
        {
            DataTable dtResults = new DataTable();

            if (string.IsNullOrWhiteSpace(connectionString) == false)
            {
                connectionString  = ";Allow User Variables=True;";

                try
                {
                    using (MySqlConnection connection = new MySqlConnection(connectionString))
                    {
                        connection.Open();

                        using (MySqlCommand cmd = connection.CreateCommand())
                        {
                            cmd.CommandTimeout = 0;
                            cmd.CommandText = sql;

                            if (listParameters != null amp;amp; listParameters.Count > 0)
                            {
                                foreach (string currentKey in listParameters.Keys)
                                {
                                    cmd.Parameters.Add(new MySqlParameter(currentKey, GetDictionaryValue(listParameters, currentKey)));
                                }
                            }

                            using (MySqlDataAdapter da = new MySqlDataAdapter(cmd))
                            {
                                da.Fill(dtResults);
                            }
                        }
                    }

                    return dtResults;
                }
                catch (Exception ex)
                {
                    MessageBox.Show("ERROR: "   ex.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return dtResults;
                }
            }
            else
            {
                return dtResults;
            }
        }
  

Это «оптимизированный» (однако более медленный) код:

 DataTable dt= Utilities.ExecuteQueryMysqlString(connectionString, "SELECT name, month_year FROM my_table", null);

for (int index = 0; index < dtSightings.Rows.Count; index  )
{
  DataRow row = dt.AsEnumerable().Where(r => r.Field<string>("name").Equals(name, StringComparison.InvariantCultureIgnoreCase) amp;amp; r.Field<DateTime>("month_year") == new DateTime(billYear, billMonth, 1)).FirstOrDefault();

  if (hasResidentBeenDiscoveredPreviously == null)
  {
    //Continue
  }
}
  

Я не понимаю, почему первый подход намного быстрее. Возможно, существует более оптимизированный подход вместо второго подхода?

Комментарии:

1. Похоже, что вы ссылаетесь на две таблицы в одной базе данных, почему бы не позволить SQL выполнить эту работу и вернуть список записей, которых нет ни в одной из таблиц. Left join Или a where not exists могли бы творить чудеса.

2. На самом деле существует только одна таблица — «my_table»

3. Вы можете присоединить таблицу к самой себе, если это необходимо.

4. Некоторое улучшение: используйте AsQueryable вместо AsEnumerable , создайте переменную вне Linq, где вы объявляете только один раз new DateTime(billYear, billMonth, 1)) , а затем используйте обработчик внутри Linq

5. Пожалуйста, дайте нам знать больше о содержании dtSightings.

Ответ №1:

Подход LINQ является медленным, потому что Where это в основном линейный поиск, и при выполнении внутри цикла он может действительно замедлить процесс.

Что вам действительно нужно, так это быстрая структура данных поиска на основе хэша. Я бы посоветовал вам использовать HashSet с пользовательскими данными, подобными этому (в основном для поддержки поиска имен без учета регистра):

 public struct NameDatePair : IEquatable<NameDatePair>
{
    public readonly string Name;
    public readonly DateTime Date;
    public NameDatePair(string name, DateTime date) { Name = name; Date = date; }
    static IEqualityComparer<string> NameComparer {  get { return StringComparer.InvariantCultureIgnoreCase; } }
    public override int GetHashCode() { return NameComparer.GetHashCode(Name) ^ Date.GetHashCode(); }
    public override bool Equals(object obj) { return obj is NameDatePair amp;amp; Equals((NameDatePair)obj); }
    public bool Equals(NameDatePair other) { return NameComparer.Equals(Name, other.Name) amp;amp; Date == other.Date; }
}
  

И вот как вы можете использовать это в вашем случае (это должно быть намного быстрее, чем оба ваших подхода):

 var dt = Utilities.ExecuteQueryMysqlString(connectionString, "SELECT name, month_year FROM my_table", null);
var nameDatePairSet = new HashSet<NameDatePair>(dt.AsEnumerable().Select(
    r => new NameDatePair(r.Field<string>("name"), r.Field<DateTime>("month_year"))));

for (int index = 0; index < dtSightings.Rows.Count; index  )
{
    var dr = dtSightings.Rows[index];
    var name = dr.Field<string>("name");
    var billYear = dr.Field<int>("billYear");
    var billMonth = dr.Field<int>("billMonth");
    bool exists = nameDatePairSet.Contains(new NameDatePair(name, new DateTime(billYear, billMonth, 1)));
}
  

(Поскольку вы не показали, откуда берутся переменные name , billYear и billMonth , в приведенном выше коде есть некоторые предположения, вы можете настроить его под свои нужды)

Комментарии:

1. Самосоединение SQL, если это практично, будет намного быстрее.

Ответ №2:

Оба ваших примера кода имеют эту базовую структуру.

 1. For each row in one bunch of rows ...
  2. read the rows in another bunch of rows ...
    3. to identify a particular situation.
  

Это само определение O (n-squared) алгоритма. Его производительность плохая и резко ухудшается по мере получения большего количества данных.

Ваш первый пример быстрее, потому что вы используете SELECT для чтения строк на шаге 2, и ваш DMBS, вероятно, оптимизирует его. Во втором примере на шаге 2 вы выполняете итерацию по всем строкам для каждой строки на шаге 1.

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

Трудно точно сказать, с чем вы это делаете dtSightings : вы запускаете переменную index, но, похоже, вы нигде ее не используете. В любом случае, этот алгоритм должен привести вас от O (n в квадрате) к O (n log n).

 1. make a HashSet ready to hold results for matching in dtSightings.
2. for each row in dtSightings ...
   a. populate that row into the HashSet
3. for each row in your query..
   b. Look it up in your HashSet
   c. If you get a hit (a non-match, I believe) report it.
  

Шаги 2 и 3 занимают O (n) времени: они пропорциональны количеству обрабатываемых строк. Подэтап b занимает O (log n) при каждом запуске. Вот откуда берется O (n log n).

Любой программист, который имеет дело с огромными потоками данных, должен понимать вычислительную сложность — O (n) — чтобы добиться успеха.

Комментарии:

1. Спасибо за объяснение! 1