В чем смысл и польза от использования SqlCommand.Prepare ()?

14

Я наткнулся на код разработчика, где метод SqlCommand.Prepare () (см. MSDN) широко используется перед выполнением SQL-запросов. И мне интересно, в чем выгода?

Образец:

command.Prepare();
command.ExecuteNonQuery();
//...
command.Parameters[0].Value = 20;
command.ExecuteNonQuery();

Я немного поиграл и проследил. Выполнение команды после вызова Prepare()метода заставляет Sql Server выполнить следующий оператор:

declare @p1 int
set @p1=1
exec sp_prepexec @p1 output,N'@id int,@desc text',N'INSERT INTO dbo.testtable (id) VALUES (@id)',@id=20'
select @p1

После этого, когда параметр получает свое значение и SqlCommand.ExecuteNonQuery()вызывается, на Sql-сервере выполняется следующее:

exec sp_execute 1,@id=20

Для меня это выглядит так, будто это заявление компилируется, как только Prepare()выполняется. Интересно, в чем выгода? Означает ли это, что он помещается в кэш плана и может использоваться повторно, как только будет выполнен последний запрос с требуемыми значениями параметров?

Я выяснил (и задокументировал это в другом вопросе ), что SqlCommands, которые выполняются с SqlParameters, всегда заключены в sp_executesqlвызовы процедур. Это позволяет Sql Server сохранять и повторно использовать планы независимо от значений параметров.

В связи с этим мне интересно, является ли этот prepare()метод бесполезным или устаревшим или я что-то здесь упускаю?

Magier
источник
Я всегда думал, что это как-то связано с предотвращением SQL-инъекций, поэтому вы должны подготовить запрос для принятия определенных переменных определенных типов. Таким образом, если вы не передадите переменные этих типов, запрос завершится неудачей
Марк Синкинсон
Хотя, говоря это, это объяснение PHP, вероятно, так же верно и для .NET php.net/manual/en/pdo.prepared-statements.php
Марк Синкинсон
«Да, сэр. Водитель, приготовьтесь к выезду». "Что ты готовишь ?! Ты всегда готовишься! Просто иди!"
Джон на все руки

Ответы:

19

Подготовка пакета SQL отдельно от выполнения подготовленного пакета SQL - это эффективная конструкция **бесполезно для SQL Server, учитывая, как планы выполнения кэшируются. Разделение этапов подготовки (разбора, привязки любых параметров и компиляции) и выполнения имеет смысл только тогда, когда нет кэширования. Цель состоит в том, чтобы сэкономить время, затрачиваемое на синтаксический анализ и компиляцию, путем повторного использования существующего плана, и именно это делает внутренний кэш планов. Но не все РСУБД выполняют этот уровень кэширования (очевидно, из-за наличия этих функций), и поэтому клиентский код должен запросить, чтобы план был «кэширован», и таким образом сохранить этот идентификатор кэша, а затем повторно использовать Это. Это дополнительная нагрузка на клиентский код (и программист должен помнить, чтобы сделать это и сделать это правильно), который не нужен. На самом деле, страница MSDN для метода IDbCommand.Prepare () гласит:

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

Следует отметить , что, насколько мои тестирования показывает (что соответствует тому , что вы показываете в вопросе), называя SqlCommand.Prepare () , это не делать «подготовить» -только операцию: она вызывает sp_prepexec , который готовит и выполняет SQL ; он не вызывает sp_prepare, который только анализирует и компилирует (и не принимает никаких значений параметров, только их имена и типы данных). Следовательно, не может быть никакого преимущества «прекомпиляции» вызова, SqlCommand.Prepareтак как он выполняет немедленное выполнение (предполагая, что цель - отложить выполнение). ОДНАКО , есть интересная потенциальная незначительная выгода от звонка SqlCommand.Prepare(), даже если он звонит sp_prepexecвместо sp_prepare. Пожалуйста, смотрите примечание (** ) внизу для деталей.

Мои тесты показывают , что даже тогда , когда SqlCommand.Prepare()вызывается (и обратите внимание , что этот призыв никак не выдавать любые команды на SQL Server), то ExecuteNonQueryили ExecuteReaderчто следует полностью выполнена, и если это ExecuteReader, он возвращает строки. Тем не менее, SQL Server Profiler действительно показывает, что первый SqlCommand.Execute______()вызов после SqlCommand.Prepare()вызова регистрируется как событие «Prepare SQL», а последующие .Execute___()вызовы регистрируются как события «Exec Prepared SQL».


Тестовый код

Он был загружен в PasteBin по адресу: http://pastebin.com/Yc2Tfvup . Код создает консольное приложение .NET / C #, которое предназначено для запуска во время трассировки SQL Server Profiler (или сеанса расширенных событий). Он делает паузу после каждого шага, чтобы было ясно, какие операторы имеют определенный эффект.


ОБНОВИТЬ

Нашли больше информации и небольшую потенциальную причину, чтобы не звонить SqlCommand.Prepare(). Одна вещь, которую я заметил в своем тестировании, и что следует отметить для тех, кто работает с этим тестовым консольным приложением: явный вызов никогда не выполняется sp_unprepare. Я немного покопался и нашел следующий пост на форумах MSDN:

SqlCommand - Готовить или не готовить?

Принятый ответ содержит кучу информации, но существенными моментами являются:

  • ** То, что готовит на самом деле, экономит вас - это, прежде всего, время, необходимое для передачи строки запроса по сети.
  • Подготовленный запрос не гарантирует сохранность кэшированного плана и не быстрее, чем специальный запрос, когда он достигает сервера.
  • RPC (CommandType.StoredProcedure) ничего не получают от подготовки.
  • На сервере подготовленный дескриптор - это в основном индекс в карте памяти, содержащей TSQL для выполнения.
  • Карта хранится отдельно для каждого соединения, кросс-соединение не возможно.
  • Сброс, отправленный, когда клиент повторно использует соединение из пула, очищает карту.

Я также нашел следующий пост от команды SQLCAT, который, похоже, связан, но может быть просто проблемой, специфичной для ODBC, тогда как SqlClient корректно очищается после удаления SqlConnection. Трудно сказать, поскольку в посте SQLCAT не упоминаются какие-либо дополнительные тесты, которые могут помочь доказать это как причину, например, очистка пула соединений и т. Д.

Следите за подготовленными операторами SQL


ВЫВОД

При условии:

  • единственное реальное преимущество звонка SqlCommand.Prepare()заключается в том, что вам не нужно повторно отправлять текст запроса по сети,
  • вызов sp_prepareи sp_prepexecсохранение текста запроса как части памяти соединения (т. е. памяти SQL Server)

Я рекомендовал бы против вызова , SqlCommand.Prepare()поскольку единственным потенциальным преимуществом является сохранение сетевых пакетов в то время как нижняя сторона принимает больше памяти сервера. Хотя объем потребляемой памяти, вероятно, очень мал в большинстве случаев, пропускная способность сети редко бывает проблемой, так как большинство серверов БД напрямую подключаются к серверам приложений с минимальной скоростью 10 мегабит (в наши дни, скорее всего, 100 мегабит или гигабит) ( а некоторые даже на одной коробке ;-). Это и память почти всегда более дефицитный ресурс, чем пропускная способность сети.

Я полагаю, что для любого, кто пишет программное обеспечение, которое может подключаться к нескольким базам данных, удобство стандартного интерфейса может изменить этот анализ затрат / выгод. Но даже в этом случае я должен верить, что все еще достаточно просто абстрагировать «стандартный» интерфейс БД, который учитывает разных провайдеров (и, следовательно, различия между ними, такие как отсутствие необходимости вызова Prepare()), учитывая, что это базовый объект. Ориентированное программирование, которое вы, вероятно, уже делаете в коде своего приложения ;-).

Соломон Руцкий
источник
Спасибо за этот обширный ответ. Я проверил ваши шаги и получил два отклонения от ваших ожиданий / результатов: на шаге 4 я получил дополнительный результат. На шаге 10 я получил другой дескриптор plan_handle, чем на шаге 2.
Magier
1
@Magier Не уверен насчет шага 4 ... может быть, у вас были разные параметры SET или текст запроса был разным? Даже однозначная (видимая или нет) разница будет другим планом. Копирование и вставка с этой веб-страницы может не помочь, поэтому для большей уверенности скопируйте запрос (все, что находится в одинарных кавычках) из sp_prepareв вызов sp_executesql. Для шага 10, да, я вчера провёл больше тестов и видел несколько случаев с разными ручками для sp_prepare, но всё равно никогда не было одинаковым для sp_executesql. Я проверю еще одну вещь и обновлю свой ответ.
Соломон Руцкий,
1
@Magier На самом деле, тест, который я проводил до этого, охватывал его, и я не думаю, что есть реальное повторное использование плана, особенно в свете того сообщения на форуме MSDN, в котором говорится, что он кэширует только SQL. Мне все еще интересно, как он может повторно использовать plan_handle, тогда как sp_executesqlне может или не может. Но независимо от того, время разбора все еще там, sp_prepareесли план был удален из кеша, поэтому я удалю эту часть моего ответа (интересно, но не имеет значения). В любом случае, эта часть была более интересной, поскольку она не затрагивала ваш прямой вопрос. Все остальное все еще применяется.
Соломон Руцкий
1
Это было то объяснение, которое я искал.
CSharpie
5

В связи с этим мне интересно, является ли метод prepare () бесполезным или устаревшим или я что-то здесь упускаю?

Я бы сказал, что метод Prepare имеет ограниченную ценность SqlCommand world, а не то, что он совершенно бесполезен. Одним из преимуществ является то, что Prepare является частью интерфейса IDbCommand, который реализует SqlCommand. Это позволяет тому же коду запускаться с другими поставщиками СУБД (которые могут требовать Prepare) без условной логики, чтобы определить, следует ли вызывать Prepare или нет.

Prepare не имеет значения для провайдера .NET для SQL Server, кроме случаев использования AFAIK.

Дэн Гусман
источник