Низкая производительность SqlCommand с более длинным CommandText - PullRequest
5 голосов
/ 28 августа 2009

Влияет ли длина CommandText команды SqlCommand? Я не говорю о тысячах персонажей. Вот что у меня есть:

SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandText = sql;

for (int i=0; i<1000; ++i)
{
    string name = i.ToString() + "Bob" + i.ToString();
    string email = i.ToString() + "Jim" + i.ToString();
    // etc...

    cmd.Parameters.Clear();
    cmd.Parameters.Add(new SqlParameter("@name", name));
    cmd.Parameters.Add(new SqlParameter("@country", country));

    DateTime cmdStart = DateTime.Now;
    cmd.ExecuteNonQuery();
    DateTime cmdEnd = DateTime.Now;
    TimeSpan len = cmdEnd - cmdStart;
}

Если я использую следующий sql, первая итерация займет 0,5 секунды. Второй занимает 1,1 секунды. Третий занимает 3,3 секунды. И так до тех пор, пока он просто не сработает в течение тайм-аута.

string sql =
    "INSERT INTO Test " +
    "           ([name] " +
    "           ,[email] " +
    "           ,[country] " +
    "           ,[comment] " +
    "           ,[date] " +
    "           ,[key_v0] " +
    "           ,[key_v1] " +
    "           ,[expires_v1] " +
    "           ,[expires_v2] " +
    "           ) " +
    "     VALUES " +
    "           (@name " +
    "           ,@email " +
    "           ,@country " +
    "           ,' ' " +
    "           ,@date " +
    "           ,@key_v0 " +
    "           ,@key_v1 " +
    "           ,@expires_v1 " +
    "           ,@expires_v2 " +
    "           )";

Однако, если я использую следующий sql, весь цикл выполняется менее чем за секунду.

string sql =
    "INSERT INTO Test " +
    "([name] " +
    ",[email] " +
    ",[country] " +
    ",[comment] " +
    ",[date] " +
    ",[key_v0] " +
    ",[key_v1] " +
    ",[expires_v1] " +
    ",[expires_v2] " +
    ") " +
    "VALUES " +
    "(@name " +
    ",@email " +
    ",@country " +
    ",' ' " +
    ",@date " +
    ",@key_v0 " +
    ",@key_v1 " +
    ",@expires_v1 " +
    ",@expires_v2 " +
    ")";

Единственная разница - это пробел. Удаление пробела привело к тому, что общее количество символов увеличилось с 428 до 203. Мне не удалось найти ничего, ссылающегося на длину CommandText, кроме ссылок на ограничения 4k и 8k. Я не близко к этому.

Я запустил обе версии с запущенным профилировщиком, и его продолжительность не превышает 10 мс для всех вызовов. Кажется, что задержка происходит с момента завершения команды внутри механизма SQL до возврата ExecuteNonQuery.

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

UPDATE: В качестве теста я добавил пробелы в конец запроса. Как только я набрал более 400 символов, он замедлился. Интересно, что при 414 символах первые 99 вставок выполняются быстро. В 415 символов, первые 9 вставок являются быстрыми. Так как я изменяю некоторые строки на основе номера итерации, это имеет смысл. например 10-я вставка немного длиннее 9-й, а 100-я вставка немного длиннее 99-й.

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

ОБНОВЛЕНИЕ 2: (Дополнительная информация в ответ на ответ Питера Элерта): Вся база данных чистая. Других таблиц нет, и тестовая таблица удаляется и воссоздается для каждого запуска. Здесь нет индексов, триггеров или внешних ключей. Существует столбец id, который является первичным ключом.

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

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

exec sp_executesql N'INSERT INTO Test ([name] ,[email] ,[country] ,[comment] ,[date] ,[key_v0] ,[key_v1] ,[expires_v1] ,[expires_v2] ) VALUES (@name ,@email ,@country ,'' '' ,@date ,@key_v0 ,@key_v1 ,@expires_v1 ,@expires_v2 )                                                                                                                                                                                                                          ',N'@name nvarchar(4),@country nvarchar(2),@email nvarchar(3),@date datetime,@key_v0 nvarchar(4000),@key_v1 nvarchar(4000),@expires_v1 datetime,@expires_v2 datetime',@name=N'9Bob',@country=N'us',@email=N'Jim',@date='2009-08-28 15:35:36.5770000',@key_v0=N'',@key_v1=N'',@expires_v1='2009-08-28 15:35:36.5770000',@expires_v2='2009-08-28 15:35:36.5770000'

Эта строка имеет длину 796 символов и работает быстро. Изменение имени с «9Bob» на «10Bob» приводит к медленной вставке. Ни 796, ни 797 не кажутся значительными цифрами. Удаление части exec sp_executesql означает длину 777 и 778. Они также не кажутся значительными.

Я в тупике.

Обновление: Выложенный здесь след: http://www.jere.us/WierdInserts.trc

Ответы [ 7 ]

3 голосов
/ 29 августа 2009

Если 10Bob намного медленнее, чем оба 9Bob и 99Bob , это может указывать на ИНДЕКС в имени, где FILLFACTOR установлен слишком высоко, или SQL Server вынужден переиндексировать страницу, когда она достигает «1» в «10Bob».

Итак, это объясняет Боба, за исключением того, что вы говорите, что индекса нет, и пробел также имеет значение ...

768 байт является важной границей в MySQL для принятия решения о том, хранится ли BLOB в строке или в отдельной таблице. Возможно, оптимизатор SQL-запросов имеет аналогичную границу?

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

SQL Server по умолчанию использует 8 тыс. Страниц, поэтому можно ожидать незначительного снижения производительности при появлении первой INSERT, которая требует новой страницы, но опять же, не имеет ничего общего с пробелами и не объясняет здесь величину задержки.

2 голосов
/ 28 августа 2009

Ну, у меня нет прямого ответа, но вот как я мог бы подойти к проблеме.

  1. Определите, кто является причиной проблемы. Первым шагом будет запуск SQL-профилировщика и проверка наличия проблем с базой данных или ее нехваткой.

SQL

Если это база данных, то я хотел бы взглянуть на несколько вещей: все сталкиваются с проблемами конкатенации строк, которые, хотя и имеют значение true, вероятно, будут занимать менее 5 мс вашего времени. Я также хотел бы сбрасывать со счетов места как источник проблемы. Снова это будет иметь небольшое значение, но не будет учитывать деградацию, которую вы описываете. Вы ищете что-то, что будет иметь эту прогрессию (0.5, 1.1, 3.3).

Я бы, в частности, посмотрел, какие индексы вы определили для этой таблицы, какие ограничения / триггеры для этой таблицы и сколько связей внешних ключей присутствуют. Кроме того, я бы вытащил запросы, которые выполняются медленно, и запустил их в диспетчере запросов (sql enterprise manager).

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

Клиент

Если это клиент, вам нужно определить, что в вашем коде вызывает проблему. Если у вас есть инструмент отслеживания производительности (например, анализатор производительности Visual Studio), который будет обрабатывать ваш код, я бы использовал его, поскольку он ОЧЕНЬ быстро подскажет вам, что занимает столько времени.

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

2 голосов
/ 28 августа 2009

Строки в .Net являются неизменяемыми. Это имеет много преимуществ, но один недостаток состоит в том, что для объединения двух из них вы должны выделить буфер для всей новой третьей строки, а не просто расширить буфер для первой строки. Код, который вы там показали, имеет 21 отдельную операцию конкатенации, которая потенциально может быть медленной. Обычно я ожидал бы, что jit-optimizer позаботится об этой проблеме для вас, но возможно, что это как-то упускает это. Вы можете попробовать объявить переменную как static readonly и посмотреть, поможет ли это.

Несмотря на это, я ожидал бы, что разница составит не более нескольких миллисекунд. Это вряд ли сделает или сломает ваш запрос. лучшее предложение, которое я могу вам дать, - это взять обе версии вашего запроса и вставить их в разные окна Management Studio вместе с инструкциями DECLARE и SET для каждого из ваших параметров и сравнить планы выполнения.

Наконец, совет Дэвида Страттона о повторном использовании тех же параметров является обоснованным. Нет смысла очищать и заново создавать одни и те же параметры каждый раунд, если вы можете просто обновить значения и повторно выполнить один и тот же запрос.

2 голосов
/ 28 августа 2009

Я думаю, что часть снижения производительности заключается в очистке и добавлении параметров (наряду с неэффективными манипуляциями со строками). Что произойдет, если вы немного измените структуру?

SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandText = sql;
cmd.Parameters.Add(new SqlParameter("@name", ""));
cmd.Parameters.Add(new SqlParameter("@country", ""));
// etc..


for (int i=0; i<1000; ++i)
{
          // etc...
    cmd.Parameters["@name"].Value =  i.ToString() + "Bob" + i.ToString();
    cmd.Parameters["@country"].Value =  i.ToString() + "Uganda" + i.ToString();
    DateTime cmdStart = DateTime.Now;
    cmd.ExecuteNonQuery();
    DateTime cmdEnd = DateTime.Now;
    TimeSpan len = cmdEnd - cmdStart;
}

Добавлена ​​

Я также неправильно смотрел на код и только сейчас понял, что это CommandType.Text, не так ли?

Является ли для вас вариант истинной хранимой процедуры на сервере, а затем вызывать ее, указав CommandType.StoredProcedure и передав имя хранимой процедуры вместо оператора QL? Я понимаю, что не отвечаю на ваш базовый вопрос, но я действительно не думаю, что длина CommandText имеет значение для SQL Server, поэтому я смотрю на другие возможные барьеры производительности.

Это выстрел в темноте, и я надеюсь, что кто-то с практическим знанием того, как CommandObject анализирует текстовые операторы SQL с параметрами, может это проверить, но я догадываюсь , что происходит то, что объект Command должен анализировать CommandText при вызове ExecuteNonQuery () (или ExecuteScalar () и т. д.).

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

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

1 голос
/ 29 августа 2009

Ваша трассировка имеет все вставки с длительностью 0-3 мс. Между выполнением есть большие большие времена : вставка заканчивается в 12:53:10, следующая начинается в 12:53:13, поэтому в клиенте * 1004 задержка составляет 3 секунды * между двумя вставками. Технически задержка может быть где угодно между клиентом и сервером, но из описанных вами симптомов я бы исключил случайный ненадежный маршрутизатор между клиентом и сервером (поведение было бы более случайным).

В каком-нибудь месте я бы посмотрел:

  • события роста базы данных / события роста журнала. Это очень часто встречается в тестах, потому что тестовая установка развертывает новую тестовую базу данных, а затем тест достигает события роста примерно в тот же момент (скажем, 10-я вставка). Может быть легко проверено с помощью счетчиков производительности и профилировщиков событий: Класс событий автоматического увеличения файла данных . Решение состоит в том, чтобы предварительно увеличить тестовую базу данных (на самом деле вы всегда должны предварительно увеличивать как mdf, так и ldf для тестирования производительности). Хотя не будет учитываться геометрическая скорость увеличения времени.
  • Сборщик мусора в клиенте. Опять же, можно отслеживать в счетчиках производительности.
  • Исчерпание пула соединений (т. Е. Тест утечки соединений, пул должен открывать новые и настроен на сохранение минимального количества, поэтому он открывается в пакетах). sys.dm_exec_connecitons будет расти. Также есть счетчик perfmon для пользовательских сессий (как на сервере, так и на счетчиках ADO.Net).
  • дефект кода. Некоторое ожидание / обработка в клиентском коде, возможно, обработка списка. Это наиболее вероятная причина, единственная, которая объясняет квадратную скорость увеличения задержки (NxN для списка длины N, N увеличивается с каждым прогоном теста, возможно, сортировкой, сохранением результата теста или чем-то подобным).
0 голосов
/ 01 февраля 2016

Вопрос задавался давно, но, возможно, мой ответ кому-нибудь поможет.Я просто столкнулся с точно такой же проблемой и обнаружил, что это происходит только внутри виртуальной машины (я использую VirtualBox для разработки).Я написал несколько тестов и запустил их внутри ВМ и на рабочем сервере.На рабочем сервере таких проблем с производительностью нет.

Возможно, есть странная ошибка в Virtualbox, может быть, что-то не так с сетевыми настройками ВМ (я использую настройки по умолчанию).

0 голосов
/ 28 августа 2009

SQL Server выполняет синтаксический анализ вашего запроса перед его выполнением, проще анализировать и меньше нежелательного кода = более быстрый анализ. Перед синтаксическим анализом он должен будет убрать все лишние пробелы, однако каждый меньший цикл процессора учитывается при выполнении нескольких операций.

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

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