В моем приложении есть простая вставка, которая при захвате с помощью профилировщика выглядит так
insert into ford.tblFordCompoundFlowVehicle
(FordCompoundFlowID, CompoundVehicleID, SortOrder, Status1ToSend, Status2ToSend, FordFlowTriggerID, SendTriggerSatisfied, DateSend, FordCompoundFlowDefinitionID)
select fcfd.FordCompoundFlowID,
9711,
fcfdd.SortOrder,
fcfdd.Status1ToSend,
fcfdd.Status2ToSend,
fcfdd.FordFlowTriggerID,
0,
null,
2
from ford.tblFordCompoundFlowDefinitionDetail fcfdd
inner join ford.tblFordCompoundFlowDefinition fcfd on fcfdd.FordCompoundFlowDefinitionID = fcfd.FordCompoundFlowDefinitionID
where fcfdd.FordCompoundFlowDefinitionID = 2
order by fcfdd.SortOrder
Это отправляется в базу данных с помощью Dapper следующим образом.
string sql =
$"""
insert into ford.tblFordCompoundFlowVehicle
(FordCompoundFlowID, CompoundVehicleID, SortOrder, Status1ToSend, Status2ToSend, FordFlowTriggerID, SendTriggerSatisfied, DateSend, FordCompoundFlowDefinitionID)
select fcfd.FordCompoundFlowID,
{compoundVehicleID},
fcfdd.SortOrder,
fcfdd.Status1ToSend,
fcfdd.Status2ToSend,
fcfdd.FordFlowTriggerID,
0,
null,
{fordCompoundFlowDefinitionID}
from ford.tblFordCompoundFlowDefinitionDetail fcfdd
inner join ford.tblFordCompoundFlowDefinition fcfd on fcfdd.FordCompoundFlowDefinitionID = fcfd.FordCompoundFlowDefinitionID
where fcfdd.FordCompoundFlowDefinitionID = {fordCompoundFlowDefinitionID.Value}
order by fcfdd.SortOrder
OPTION(RECOMPILE)
""";
connection.Execute(sql, null, transaction);
Я не использовал SqlCommand, чтобы сделать этот код немного короче, кроме того, все значения имеют int, поэтому SQL-инъекция в любом случае будет затруднена.
Теперь о проблеме: когда этот запрос выполняется, он занимает так много времени, что время ожидания истекает, но когда я записываю запрос в профилировщике и запускаю его в SSMS, это происходит очень быстро.
Я еще попробовал опцию OPTION(RECOMPILE) в приложении, но она не помогает.
Планы выполнения:
Итак, я получил план выполнения от SSMS
и зафиксировал план медленного применения из Монитор активности
У меня самого нет знаний, чтобы сравнить эти планы и увидеть проблему, поэтому я надеюсь, что кто-нибудь здесь сможет сравнить их и помочь мне указать на проблему.
Транзакция совершается так
using (SqlConnection connection = new SqlConnection(ConnectionString))
{
connection.Open();
using (SqlTransaction transaction = connection.BeginTransaction())
{
try
{
fordCompoundFlowDefinitionID = CreateFordCompoundFlowVehicle(connection, transaction, compoundVehicleID);
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
throw;
}
}
}
private int? CreateFordCompoundFlowVehicle(SqlConnection connection, SqlTransaction transaction, int compoundVehicleID)
{
int? result = null;
// check if there is any importdata for this compoundvehicleid, if not than it stops right here
int? fordCompoundFlowDefinitionID = GetFordCompoundFlowDefinitionID(connection, transaction, compoundVehicleID);
if (fordCompoundFlowDefinitionID.HasValue)
{
string sql =
$"""
insert into ford.tblFordCompoundFlowVehicle
(FordCompoundFlowID, CompoundVehicleID, SortOrder, Status1ToSend, Status2ToSend, FordFlowTriggerID, SendTriggerSatisfied, DateSend, FordCompoundFlowDefinitionID)
select fcfd.FordCompoundFlowID,
{compoundVehicleID},
fcfdd.SortOrder,
fcfdd.Status1ToSend,
fcfdd.Status2ToSend,
fcfdd.FordFlowTriggerID,
0,
null,
{fordCompoundFlowDefinitionID}
from ford.tblFordCompoundFlowDefinitionDetail fcfdd
inner join ford.tblFordCompoundFlowDefinition fcfd on fcfdd.FordCompoundFlowDefinitionID = fcfd.FordCompoundFlowDefinitionID
where fcfdd.FordCompoundFlowDefinitionID = {fordCompoundFlowDefinitionID.Value}
order by fcfdd.SortOrder
OPTION(RECOMPILE)
""";
-- THIS TIMES OUT
connection.Execute(sql, null, transaction);
result = fordCompoundFlowDefinitionID;
}
return result;
}
Я изменил код на параметризованный запрос, как было предложено, но получил тот же результат.
string sql =
"""
insert into ford.tblFordCompoundFlowVehicle
(FordCompoundFlowID, CompoundVehicleID, SortOrder, Status1ToSend, Status2ToSend, FordFlowTriggerID, SendTriggerSatisfied, DateSend, FordCompoundFlowDefinitionID)
select fcfd.FordCompoundFlowID,
@CompoundVehicleID,
fcfdd.SortOrder,
fcfdd.Status1ToSend,
fcfdd.Status2ToSend,
fcfdd.FordFlowTriggerID,
0,
null,
@FordCompoundFlowDefinitionID
from ford.tblFordCompoundFlowDefinitionDetail fcfdd
inner join ford.tblFordCompoundFlowDefinition fcfd on fcfdd.FordCompoundFlowDefinitionID = fcfd.FordCompoundFlowDefinitionID
where fcfdd.FordCompoundFlowDefinitionID = @FordCompoundFlowDefinitionID
order by fcfdd.SortOrder
OPTION(RECOMPILE)
""";
var parameters = new {CompoundVehicleID = compoundVehicleID, FordCompoundFlowDefinitionID = fordCompoundFlowDefinitionID };
connection.Execute(sql, parameters, transaction);
Захваченный оператор в профилировщике
exec sp_executesql N'insert into ford.tblFordCompoundFlowVehicle
(FordCompoundFlowID, CompoundVehicleID, SortOrder, Status1ToSend, Status2ToSend, FordFlowTriggerID, SendTriggerSatisfied, DateSend, FordCompoundFlowDefinitionID)
select fcfd.FordCompoundFlowID,
@CompoundVehicleID,
fcfdd.SortOrder,
fcfdd.Status1ToSend,
fcfdd.Status2ToSend,
fcfdd.FordFlowTriggerID,
0,
null,
@FordCompoundFlowDefinitionID
from ford.tblFordCompoundFlowDefinitionDetail fcfdd
inner join ford.tblFordCompoundFlowDefinition fcfd on fcfdd.FordCompoundFlowDefinitionID = fcfd.FordCompoundFlowDefinitionID
where fcfdd.FordCompoundFlowDefinitionID = @FordCompoundFlowDefinitionID
order by fcfdd.SortOrder
OPTION(RECOMPILE)',N'@CompoundVehicleID int,@FordCompoundFlowDefinitionID int',@CompoundVehicleID=9711,@FordCompoundFlowDefinitionID=2
go
System.Data.SqlClient.SqlException HResult=0x80131904
Сообщение=Тайм-аут истек. Срок ожидания истек до завершение операции или сервер не отвечает. Операция отменен пользователем. Источник = Core .Net Поставщик данных SqlClient
StackTrace: в System.Data.SqlClient.SqlConnection.OnError (исключение SqlException, Логическое значение BreakConnection, Action1 wrapCloseInAction) at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action1 оберткаCloseInAction) на System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, логическое значение callerHasConnectionLock, логическое значение asyncClose) в System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, поток данных SqlDataReader, BulkCopySimpleResultSet BulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady) в System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, строка сбросаOptionsString) в System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean асинхронный, тайм-аут Int32, задача и задача, логическое значение asyncWrite, SqlDataReader дс) в System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource1 completion, Int32 timeout, Task& task, Boolean asyncWrite, String method) at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource1 завершение, логическое значение sendToPipe, тайм-аут Int32, логическое значение asyncWrite, Строковое имя метода) в System.Data.SqlClient.SqlCommand.ExecuteNonQuery() в Dapper.SqlMapper.ExecuteCommand(IDbConnection cnn, CommandDefinition& команда, Action2 paramReader) in /_/Dapper/SqlMapper.cs:line 2965 at Dapper.SqlMapper.ExecuteImpl(IDbConnection cnn, CommandDefinition& command) in /_/Dapper/SqlMapper.cs:line 656 at Dapper.SqlMapper.Execute(IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Nullable1 командаTimeout, Nullable1 commandType) in /_/Dapper/SqlMapper.cs:line 527 at WebServiceMobile.Repositories.Compound.RepositoryFordFlow.CreateFordCompoundFlowVehicle(SqlConnection connection, SqlTransaction transaction, Int32 compoundVehicleID) in C:\Development\Git\WebServices\WebServiceMobile\Repositories\Compound\Ford\RepositoryFordFlow.cs:line 124 at WebServiceMobile.Repositories.Compound.RepositoryFordFlow.HandleIncomingVehicle(Int32 compoundVehicleID, Nullable1 dateInActual) в C:\Development\Git\WebServices\WebServiceMobile\Repositories\Compound\Ford\RepositoryFordFlow.cs:line 29Внутреннее исключение 1: Win32Exception: истекло время ожидания операции ожидания.
Я попробовал без использования транзакции, тот же результат
System.Data.SqlClient.SqlException: «Время ожидания истекло. Тайм-аут период, прошедший до завершения операции или сервер неисправен не отвечает. Операция отменена пользователем.'
Новый план, полученный из запроса, выполняемого в приложении.
план:
https://www.brentozar.com/pastetheplan/?id=B1vTWaMIR
живая статистика:
https://www.brentozar.com/pastetheplan/?id=H1-8MpGUA
Запуск sp_who2
Как и предполагалось, я запустил exec sp_who2 во время выполнения медленного запроса, вот результат
@sa-es-ir Я отредактировал вопрос и включил эту информацию
«Я не использовал SqlCommand, чтобы сделать этот код немного короче, кроме того, все значения являются целыми, поэтому внедрение sql в любом случае будет затруднено...» - это все равно означает, что вы столкнетесь с проблемами с принудительной параметризацией.
@Dai Я верну SqlCommand, как только время ожидания будет исправлено, пожалуйста, давайте сосредоточимся на реальной проблеме
обязательно: sommarskog.se/query-plan-mysteries.html
@sa-es-ir Я не успеваю выполнить коммит из-за тайм-аута. Вот почему я добавил оба плана выполнения, чтобы вы могли помочь мне выяснить, почему у меня истекает время.
@MitchWheat Да, я читал здесь, но все сводится к изучению планов выполнения, о которых у меня недостаточно знаний
@GuidoG Моя точка зрения актуальна, потому что принудительная параметризация может привести к неправильному выбору плана запроса, что, в свою очередь, приводит к замедлению выполнения запроса/оператора.
@Dai Хорошо, когда вы сравните оба плана, вы увидите, что это произошло тогда? Я не могу разобраться в планах
Можете ли вы также поделиться исключением? Это запрос или время ожидания соединения? также, если возможно, попробуйте проверить SQL Profiler, чтобы узнать, какое именно приложение пытается запустить на SQL-сервере.
@sa-es-ir Я уже показываю в вопросе то, что я захватил из профилировщика
@sa-es-ir Теперь включено точное сообщение об ошибке
@Dai Я параметризовал запрос (см. редактирование в вопросе), но результат тот же
@GuidoG Да, извини, я только что прочитал это еще раз. Я думаю, что проблема не в плане запроса, а в транзакции, и, возможно, таблица заблокирована, вы также можете проверить сообщение об исключении в профилировщике SQL: nimblegecko.com/…
@sa-es-ir Я не использую профилировщик из ssms, а использую экспресс-профилировщик, я не вижу там никаких исключений
@sa-es-ir Если стол заблокирован, должно ли это быть не видно в планах?
@sa-es-ir Я пробовал без транзакции, результат тот же
Оба вставленных плана имеют SET ARITHABORT ON, что предполагает, что они оба были созданы в результате выполнения запросов в SSMS. Как выглядит «плохой» план Дэппера? то есть: он почти наверняка будет иметь другой набор @@OPTIONS, который может влиять на генерацию, выполнение и поведение плана.
@AlwaysLearning Извините, но медленный план, который я получил из монитора активности в SSMS, недавних медленных запросов, плана отображения правой кнопкой мыши, а затем сохранил его. Так это должен быть план из приложения или я что-то упускаю?
Быстрое и грязное решение для такого рода проблем: обновить статистику (с полным сканированием) для некоторой базовой таблицы (при этом обновляется статистика И уничтожается кэшированные планы), сразу же после этого запускается запрос приложения - это часто (но не всегда) создает и сохраняет лучший план запроса. Примечание. Тогда не используйте with(recompile).
В качестве быстрой проверки вы можете попробовать добавить к запросам SET ARITHABORT OFF; перед их выполнением в SSMS, чтобы проверить, выполняются ли они медленно. Включение параметра «Показать фактический план выполнения» на панели инструментов должно выявить различия.
@AlwaysLearning Я пытаюсь снова захватить план из приложения, дайте мне минутку
@AlwaysLearning ОК, я записал парамитризованный план запроса при повторном запуске из приложения, посмотрите последние изменения в вопросе, я поместил туда план и действующую статистику, надеюсь, это поможет вам или кому-то другому, чтобы помочь мне
@AlwaysLearning Если SET ARITHABORT OFF в ssms, он по-прежнему остается там быстрым
@GuidoG, пока выполняется запрос, можешь ли ты запустить EXEC sp_who2 и проверить столбцы status и blkBy?
@Арво, я попробовал это, но результат тот же
@sa-es-ir ОК, я сделал это, я поместил результат в последнюю редакцию вопроса
@GuidoG Теперь это имеет смысл! вам нужно проверить Blk By, который показывает, что другой SPID блокирует запрос, и попытаться найти этот запрос в той же таблице.
@sa-es-ir Да, наконец-то мы добились некоторого прогресса, большое спасибо, что показали мне это
@GuidoG С нетерпением жду возможности увидеть реальную проблему :)
@sa-es-ir Я опубликовал ответ, в котором объясняется, что произошло и что я сделал не так





Я нашел проблему, кажется, при запуске из приложения произошла блокировка, которая переводила мой запрос в приостановленное состояние.
Я обнаружил это благодаря комментарию @sa-es-ir, который заставил меня бежать sp_who2 во время выполнения медленного запроса. Затем я заметил блокировку того же приложения, из которого выполнял запрос, но с другим spid.
Таким образом, запрос на самом деле не имел большого отношения к проблеме, и планы выполнения, я думаю, должны быть похожими, но поскольку запрос продолжал ждать, клиент устал ждать и вызвал тайм-аут.
Как это произошло?
Кажется, что когда ваше соединение в C# начинает транзакцию, и вы создаете новое соединение, которое начинает новую транзакцию, тогда любой запрос, которому нужна строка, которая была вставлена/обновлена в первой транзакции, должен ждать, пока эта транзакция не завершится.
В принципе у меня было что-то вроде этого
using (SqlConnection connection = new SqlConnection(ConnectionString))
{
connection.Open();
using (SqlTransaction transaction = connection.BeginTransaction())
{
try
{
SomeProcedure();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
throw;
}
}
}
public void SomeProcedure()
{
using (SqlConnection connection = new SqlConnection(ConnectionString))
{
connection.Open();
using (SqlTransaction transaction = connection.BeginTransaction())
{
try
{
// HERE WAS MY QUERY THAT TIMED OUT
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
throw;
}
}
}
}
Теперь я изменил свой метод, чтобы он проверял, находится ли он в существующем соединении/транзакции, а затем использовал его и создавал новый только в том случае, если это не так.
Как вы определили
transactionи совершаете ли вы это?