SQL-запрос медленный в приложении .NET, но мгновенный в SQL Server Management Studio - PullRequest
55 голосов
/ 29 апреля 2010

Вот SQL

SELECT tal.TrustAccountValue
FROM TrustAccountLog AS tal
INNER JOIN TrustAccount ta ON ta.TrustAccountID = tal.TrustAccountID
INNER JOIN Users usr ON usr.UserID = ta.UserID
WHERE usr.UserID = 70402 AND
ta.TrustAccountID = 117249 AND
tal.trustaccountlogid =  
(
 SELECT MAX (tal.trustaccountlogid)
 FROM  TrustAccountLog AS tal
 INNER JOIN TrustAccount ta ON ta.TrustAccountID = tal.TrustAccountID
 INNER JOIN Users usr ON usr.UserID = ta.UserID
 WHERE usr.UserID = 70402 AND
 ta.TrustAccountID = 117249 AND
 tal.TrustAccountLogDate < '3/1/2010 12:00:00 AM'
)

По сути, есть таблица Users, таблица TrustAccount и таблица TrustAccountLog.
Пользователи: содержит пользователей и их данные
TrustAccount: пользователь может иметь несколько TrustAccounts.
TrustAccountLog: содержит аудит всех «движений» TrustAccount.
TrustAccount связан с несколькими записями TrustAccountLog. Теперь этот запрос выполняется в миллисекундах внутри SQL Server Management Studio, но по какой-то странной причине он выполняется в моем приложении на C # вечно, а иногда даже в тайм-ауте (120 с).

Вот код в двух словах. Он вызывается несколько раз в цикле, и оператор готовится.

cmd.CommandTimeout = Configuration.DBTimeout;
cmd.CommandText = "SELECT tal.TrustAccountValue FROM TrustAccountLog AS tal INNER JOIN TrustAccount ta ON ta.TrustAccountID = tal.TrustAccountID INNER JOIN Users usr ON usr.UserID = ta.UserID WHERE usr.UserID = @UserID1 AND ta.TrustAccountID = @TrustAccountID1 AND tal.trustaccountlogid =  (SELECT MAX (tal.trustaccountlogid) FROM  TrustAccountLog AS tal INNER JOIN TrustAccount ta ON ta.TrustAccountID = tal.TrustAccountID INNER JOIN Users usr ON usr.UserID = ta.UserID WHERE usr.UserID = @UserID2 AND ta.TrustAccountID = @TrustAccountID2 AND tal.TrustAccountLogDate < @TrustAccountLogDate2 ))";
cmd.Parameters.Add("@TrustAccountID1", SqlDbType.Int).Value = trustAccountId;
cmd.Parameters.Add("@UserID1", SqlDbType.Int).Value = userId;
cmd.Parameters.Add("@TrustAccountID2", SqlDbType.Int).Value = trustAccountId;
cmd.Parameters.Add("@UserID2", SqlDbType.Int).Value = userId;
cmd.Parameters.Add("@TrustAccountLogDate2", SqlDbType.DateTime).Value =TrustAccountLogDate;

// And then...

reader = cmd.ExecuteReader();
if (reader.Read())
{
   double value = (double)reader.GetValue(0);
   if (System.Double.IsNaN(value))
      return 0;
   else
      return value;
}
else
   return 0;

Ответы [ 14 ]

62 голосов
/ 29 апреля 2010

По моему опыту, обычная причина того, что запрос выполняется в SSMS быстро, но медленно из .NET, связана с различиями в SET -соединениях соединения. Когда соединение открывается с помощью SSMS или SqlConnection, для настройки среды выполнения автоматически выдается набор SET команд. К сожалению, SSMS и SqlConnection имеют разные SET значения по умолчанию.

Одно общее отличие - SET ARITHABORT. Попробуйте ввести SET ARITHABORT ON в качестве первой команды из вашего кода .NET.

SQL Profiler можно использовать для отслеживания того, какие команды SET выдают как SSMS, так и .NET, чтобы вы могли найти другие различия.

Следующий код демонстрирует, как выполнить команду SET, но обратите внимание, что этот код не был протестирован.

using (SqlConnection conn = new SqlConnection("<CONNECTION_STRING>")) {
    conn.Open();

    using (SqlCommand comm = new SqlCommand("SET ARITHABORT ON", conn)) {
        comm.ExecuteNonQuery();
    }

    // Do your own stuff here but you must use the same connection object
    // The SET command applies to the connection. Any other connections will not
    // be affected, nor will any new connections opened. If you want this applied
    // to every connection, you must do it every time one is opened.
}
28 голосов
/ 30 апреля 2010

Если это сниффинг параметров, попробуйте добавить option(recompile) в конец вашего запроса. Я бы порекомендовал создать хранимую процедуру для инкапсуляции логики в более управляемой форме. Также договорились - зачем вы передаете 5 параметров, если вам нужно только три, судя по примеру? Можете ли вы использовать этот запрос вместо этого?

select TrustAccountValue from
(
 SELECT MAX (tal.trustaccountlogid), tal.TrustAccountValue
 FROM  TrustAccountLog AS tal
 INNER JOIN TrustAccount ta ON ta.TrustAccountID = tal.TrustAccountID
 INNER JOIN Users usr ON usr.UserID = ta.UserID
 WHERE usr.UserID = 70402 AND
 ta.TrustAccountID = 117249 AND
 tal.TrustAccountLogDate < '3/1/2010 12:00:00 AM'
 group by tal.TrustAccountValue
) q

И, для чего это стоит, вы используете неоднозначный формат даты, в зависимости от языковых настроек пользователя, выполняющего запрос. Для меня, например, это 3 января, а не 1 марта. Проверьте это:

set language us_english
go
select @@language --us_english
select convert(datetime, '3/1/2010 12:00:00 AM')
go
set language british
go
select @@language --british
select convert(datetime, '3/1/2010 12:00:00 AM')

Рекомендуется использовать формат «ISO» ггггммдд чч: мм: сс

select convert(datetime, '20100301 00:00:00') --midnight 00, noon 12
10 голосов
/ 24 сентября 2014

Была такая же проблема в тестовой среде, хотя работающая система (на том же сервере SQL) работала нормально. Добавление OPTION (RECOMPILE), а также OPTION (OPTIMIZE FOR (@ p1 UNKNOWN)) не помогло.

Я использовал SQL Profiler, чтобы перехватить точный запрос, который отправлял клиент .net, и обнаружил, что он был заключен в exec sp_executesql N'select ... и что параметры объявлены как nvarchars - сравниваемые столбцы - простые varchars.

Помещение захваченного текста запроса в SSMS подтвердило, что он выполняется так же медленно, как и для клиента .net.

Я обнаружил, что изменение типа параметров на AnsiText решило проблему:

p = cm.CreateParameter() p.ParameterName = "@company" p.Value = company p.DbType = DbType.AnsiString cm.Parameters.Add(p)

Я никогда не мог объяснить, почему тестовая и живая среда имели такое заметное различие в производительности.

7 голосов
/ 21 января 2016

Надеюсь, что ваша конкретная проблема уже решена, так как это старый пост.

После SET опции могут повлиять на повторное использование плана (полный список в конце)

SET QUOTED_IDENTIFIER ON
GO
SET ANSI_NULLS ON
GO
SET ARITHABORT ON
GO

Следующие два оператора взяты из msdn - SET ARITHABORT

Установка ARITHABORT на OFF может отрицательно повлиять на оптимизацию запросов, что приведет к проблемам с производительностью.

Настройка ARITHABORT по умолчанию для SQL Server Management Studio включена. Клиентские приложения, для которых ARITHABORT имеет значение OFF, могут получать различные планы запросов, что затрудняет поиск и устранение неполадок, связанных с неэффективными запросами. То есть один и тот же запрос может выполняться быстро в студии управления, но медленно в приложении.

Другая интересная тема для понимания - Parameter Sniffing, как указано в Медленно в приложении, быстро в SSMS? Понимание загадок производительности - Эрланд Соммарског

Еще одна возможность - это преобразование (внутренне) столбцов VARCHAR в NVARCHAR при использовании входного параметра Unicode, как описано в Устранение неполадок производительности индекса SQL для столбцов varchar - Джимми Богардом

ОПТИМИЗИРОВАТЬ ДЛЯ НЕИЗВЕСТНЫХ

В SQL Server 2008 и более поздних версиях рассмотрите вариант ОПТИМИЗАЦИЯ ДЛЯ НЕИЗВЕСТНО. НЕИЗВЕСТНО. Указывает, что оптимизатор запросов использует статистические данные вместо начального значения, чтобы определить значение локальной переменной во время оптимизации запроса.

ОПЦИЯ (РЕКОМЕНДУЕТСЯ)

Используйте «OPTION (RECOMPILE)» вместо «WITH RECOMPILE», если перекомпиляция является единственным решением. Это помогает в оптимизации встраивания параметров. Чтение Параметры сниффинга, встраивания и параметров RECOMPILE - Пол Уайт

Параметры SET

Следующие опции SET могут повлиять на повторное использование плана, исходя из msdn - Кэширование плана в SQL Server 2008

  1. ANSI_NULL_DFLT_OFF 2. ANSI_NULL_DFLT_ON 3. ANSI_NULLS 4
6 голосов
/ 30 апреля 2010

Скорее всего проблема кроется в критерии

tal.TrustAccountLogDate < @TrustAccountLogDate2

Оптимальный план выполнения будет сильно зависеть от значения параметра; передача 1910-01-01 (которая не возвращает строк) наверняка вызовет план, отличный от 2100-12-31 (который возвращает все строки).

Если в запросе значение указано как литерал, SQL-сервер знает, какое значение использовать во время генерации плана. Когда параметр используется, SQL-сервер генерирует план только один раз, а затем использует его повторно, и если значение в последующем выполнении слишком сильно отличается от исходного, план не будет оптимальным.

Чтобы исправить ситуацию, вы можете указать OPTION(RECOMPILE) в запросе. Добавление запроса к хранимой процедуре не поможет вам с этой конкретной проблемой, если только вы создаете процедуру с RECOMPILE.

Другие уже упоминали об этом («анализ параметров»), но я подумал, что простое объяснение концепции не повредит.

3 голосов
/ 29 апреля 2010

Это могут быть проблемы с преобразованием типов. Все ли идентификаторы действительно SqlDbType.Int на уровне данных?

Кроме того, зачем иметь 4 параметра, из которых 2 будут делать?

cmd.Parameters.Add("@TrustAccountID1", SqlDbType.Int).Value = trustAccountId;
cmd.Parameters.Add("@UserID1", SqlDbType.Int).Value = userId;
cmd.Parameters.Add("@TrustAccountID2", SqlDbType.Int).Value = trustAccountId;
cmd.Parameters.Add("@UserID2", SqlDbType.Int).Value = userId;

Может быть

cmd.Parameters.Add("@TrustAccountID", SqlDbType.Int).Value = trustAccountId;
cmd.Parameters.Add("@UserID", SqlDbType.Int).Value = userId;

Поскольку им обоим назначена одна и та же переменная.

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

2 голосов
/ 17 декабря 2015

В моем случае проблема заключалась в том, что моя Entity Framework генерировала запросы, использующие exec sp_executesql.

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

в моем случае столбец был определен как CHR (3), и Entity Framework передавал N'str 'в запросе, что вызывает преобразование из nchar в char. Так что для запроса, который выглядит так:

ctx.Events.Where(e => e.Status == "Snt")

Он генерировал SQL-запрос, который выглядит примерно так:

FROM [ExtEvents] AS [Extent1] ... WHERE (N''Snt'' = [Extent1].[Status]) ...

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

2 голосов
/ 29 апреля 2010

Поскольку вы, кажется, когда-либо возвращаете значение только из одной строки из одного столбца, вы можете вместо этого использовать ExecuteScalar () в объекте команды, что должно быть более эффективным:

    object value = cmd.ExecuteScalar();

    if (value == null)
        return 0;
    else
        return (double)value;
1 голос
/ 08 августа 2018

У меня была эта проблема сегодня, и это решило мою проблему: https://www.mssqltips.com/sqlservertip/4318/sql-server-stored-procedure-runs-fast-in-ssms-and-slow-in-application/

Я положил начало своего SP так: Установите ARITHABORT ON

Помоги тебе в этом!

1 голос
/ 29 апреля 2010

Возможно, звуки связаны с анализом параметров? Вы пытались точно захватить то, что клиентский код отправляет на SQL Server (использовать профилировщик, чтобы перехватить точное утверждение), а затем запустить его в Management Studio?

Отслеживание параметров: Плохая производительность плана выполнения хранимых процедур в SQL - отслеживание параметров

Я не видел этого в коде раньше, только в процедурах, но это стоит посмотреть.

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