web-dev-qa-db-fra.com

Requête SQL lente dans l'application .NET mais instantanée dans SQL Server Management Studio

Voici le 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'
)

Fondamentalement, il existe une table Utilisateurs, une table TrustAccount et une table TrustAccountLog.
Utilisateurs: contient les utilisateurs et leurs coordonnées
TrustAccount: un utilisateur peut avoir plusieurs TrustAccounts.
TrustAccountLog: contient un audit de tous les "mouvements" TrustAccount. UNE
TrustAccount est associé à plusieurs entrées TrustAccountLog. Maintenant, cette requête s'exécute en millisecondes dans SQL Server Management Studio, mais pour une raison étrange, cela prend une éternité dans mon application C # et même parfois un délai d'attente (120 s).

Voici le code en bref. Il est appelé plusieurs fois dans une boucle et l'instruction est préparée.

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;
56
n4rzul

S'il s'agit d'un reniflement de paramètre, essayez d'ajouter option(recompile) à la fin de votre requête. Je recommanderais de créer une procédure stockée pour encapsuler la logique d'une manière plus gérable. Aussi d'accord - pourquoi passez-vous 5 paramètres si vous n'en avez besoin que de trois, à en juger par l'exemple? Pouvez-vous utiliser cette requête à la place?

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

Et, pour ce que ça vaut, vous utilisez un format de date ambigu, en fonction des paramètres de langue de l'utilisateur exécutant la requête. Pour moi par exemple, c'est le 3 janvier, pas le 1er mars. Regarde ça:

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')

L'approche recommandée consiste à utiliser le format "ISO" aaaammjj hh: mm: ss

select convert(datetime, '20100301 00:00:00') --midnight 00, noon 12
28
Piotr Rodak

D'après mon expérience, la raison habituelle pour laquelle une requête s'exécute rapidement dans SSMS mais lente à partir de .NET est due à des différences dans les SET- connexions de la connexion. Lorsqu'une connexion est ouverte par SSMS ou SqlConnection, un tas de commandes SET sont automatiquement émises pour configurer l'environnement d'exécution. Malheureusement, SSMS et SqlConnection ont des valeurs par défaut SET différentes.

Une différence courante est SET ARITHABORT. Essayez d'émettre SET ARITHABORT ON comme première commande de votre code .NET.

SQL Profiler peut être utilisé pour surveiller quelles commandes SET sont émises par SSMS et .NET afin que vous puissiez trouver d'autres différences.

Le code suivant montre comment exécuter une commande SET mais notez que ce code n'a pas été testé.

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.
}
64
Daniel Renshaw

A eu le même problème dans un environnement de test, bien que le système en direct (sur le même serveur SQL) fonctionnait bien. L'ajout d'OPTION (RECOMPILE) et d'OPTION (OPTIMIZE FOR (@ p1 UNKNOWN)) n'a pas aidé.

J'ai utilisé SQL Profiler pour capturer la requête exacte que le client .net envoyait et j'ai trouvé que cela était encapsulé avec exec sp_executesql N'select ... Et que les paramètres avaient été déclarés comme nvarchars - les colonnes comparées étant de simples varchars.

Le fait de placer le texte de la requête capturée dans SSMS a confirmé qu'il s'exécute aussi lentement que sur le client .net.

J'ai trouvé que changer le type des paramètres en AnsiText a résolu le problème:

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

Je n'ai jamais pu expliquer pourquoi le test et les environnements en direct avaient une différence de performance aussi marquée.

11
Daz

J'espère que votre problème spécifique est résolu maintenant car il s'agit d'un ancien message.

Les options SET suivantes peuvent affecter la réutilisation du plan (liste complète à la fin)

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

Les deux instructions suivantes proviennent de msdn - SET ARITHABORT

La définition de ARITHABORT sur OFF peut avoir un impact négatif sur l'optimisation des requêtes, entraînant des problèmes de performances.

Le paramètre ARITHABORT par défaut pour SQL Server Management Studio est ON. Les applications client qui définissent ARITHABORT sur OFF peuvent recevoir différents plans de requête, ce qui rend difficile le dépannage des requêtes peu performantes. Autrement dit, la même requête peut s'exécuter rapidement dans le studio de gestion mais lente dans l'application.

Un autre sujet intéressant à comprendre est Parameter Sniffing comme indiqué dans Lent dans l'application, rapide dans SSMS? Comprendre les mystères de performances - par Erland Sommarskog

Une autre possibilité est la conversion (en interne) des colonnes VARCHAR en NVARCHAR tout en utilisant le paramètre d'entrée Unicode comme indiqué dans Dépannage des performances d'index SQL sur les colonnes varchar - par Jimmy Bogard

OPTIMISER POUR INCONNU

Dans SQL Server 2008 et versions ultérieures, envisagez OPTIMISER POUR INCONNU. INCONNU: spécifie que l'optimiseur de requête utilise des données statistiques au lieu de la valeur initiale pour déterminer la valeur d'une variable locale lors de l'optimisation de requête.

OPTION (RECOMPILE)

Utilisez "OPTION (RECOMPILE)" au lieu de "AVEC RECOMPILE" si la recompilation est la seule solution. Il aide à l'optimisation de l'intégration des paramètres. Lire Reniflage de paramètres, incorporation et options RECOMPILE - par Paul White

Options SET

Les options SET suivantes peuvent affecter la réutilisation du plan, en fonction de msdn - Planification de la mise en cache dans SQL Server 2008

  1. ANSI_NULL_DFLT_OFF 2. ANSI_NULL_DFLT_ON 3. ANSI_NULLS 4. ANSI_PADDING 5. ANSI_WARNINGS 6. ARITHABORT 7. CONCAT_NULL_YIELDS_NUL 8. DATEFIRST 9. DATEFORMAT 10. FORCEPLAN 11. LANGUAGE 12. NO_BROWSETTED 13. NUMERO
7
Lijo

Le problème réside probablement dans le critère

tal.TrustAccountLogDate < @TrustAccountLogDate2

Le plan d'exécution optimal dépendra fortement de la valeur du paramètre, le passage de 1910-01-01 (qui ne renvoie aucune ligne) provoquera très certainement un plan différent de 2100-12-31 (qui renvoie toutes les lignes).

Lorsque la valeur est spécifiée en tant que littéral dans la requête, le serveur SQL sait quelle valeur utiliser lors de la génération du plan. Lorsqu'un paramètre est utilisé, SQL Server ne génère le plan qu'une seule fois, puis le réutilise, et si la valeur lors d'une exécution ultérieure diffère trop de celle d'origine, le plan ne sera pas optimal.

Pour remédier à la situation, vous pouvez spécifier OPTION(RECOMPILE) dans la requête. L'ajout de la requête à une procédure stockée ne vous aidera pas avec ce particulier problème, sauf si vous créez la procédure WITH RECOMPILE.

D'autres l'ont déjà mentionné ("reniflage de paramètres"), mais je pensais qu'une explication simple du concept ne ferait pas de mal.

6
erikkallen

Il peut s'agir de problèmes de conversion de type. Tous les identifiants sont-ils vraiment SqlDbType.Int au niveau des données?

Aussi, pourquoi avoir 4 paramètres où 2 feront l'affaire?

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;

Pourrait être

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

Puisqu'ils sont tous deux affectés de la même variable.

(Cela pourrait amener le serveur à faire un plan différent car il attend quatre variables différentes en tant qu'op. À 4 constantes - ce qui en fait 2 variables pourrait faire une différence pour l'optimisation du serveur.)

3
Hogan

Puisque vous semblez ne renvoyer que la valeur d'une ligne d'une colonne, vous pouvez utiliser ExecuteScalar () sur l'objet de commande à la place, ce qui devrait être plus efficace:

    object value = cmd.ExecuteScalar();

    if (value == null)
        return 0;
    else
        return (double)value;
2
Dan Diplo

Dans mon cas, le problème était que mon Entity Framework générait des requêtes utilisant exec sp_executesql.

Lorsque les paramètres ne correspondent pas exactement au type, le plan d'exécution n'utilise pas d'index car il décide de placer la conversion dans la requête elle-même. Comme vous pouvez l'imaginer, cela se traduit par une performance beaucoup plus lente.

dans mon cas, la colonne était définie comme CHR (3) et Entity Framework passait N'str 'dans la requête, ce qui provoquait une conversion de nchar en char. Donc, pour une requête qui ressemble à ceci:

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

Il générait une requête SQL qui ressemble à ceci:

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

La solution la plus simple dans mon cas était de changer le type de colonne, sinon vous pouvez lutter avec votre code pour lui faire passer le bon type en premier lieu.

2
Eyal

Sons probablement liés au reniflage des paramètres? Avez-vous essayé de capturer exactement ce que le code client envoie à SQL Server (utilisez le profileur pour intercepter l'instruction exacte), puis exécutez-le dans Management Studio?

Reniflage des paramètres: performances du plan d'exécution des procédures stockées SQL médiocres - reniflement des paramètres

Je n'ai jamais vu cela dans le code auparavant, seulement dans les procédures, mais ça vaut le coup d'oeil.

1
Meff

J'ai eu ce problème aujourd'hui et cela résout mon problème: https://www.mssqltips.com/sqlservertip/4318/sql-server-stored-procedure-runs-fast-in-ssms-and-slow-in -application /

Je mets le début de mon SP this: Set ARITHABORT ON

Holp cela vous aide!

1
Italo Reis

Vous ne semblez pas fermer votre lecteur de données - cela pourrait commencer à s'additionner sur un certain nombre d'itérations ...

0
Paddy

Je me rends compte que l'OP ne mentionne pas l'utilisation de procédures stockées, mais il existe une solution alternative aux problèmes de reniflage de paramètres lors de l'utilisation de procédures stockées qui est moins élégante mais qui a fonctionné pour moi lorsque OPTION(RECOMPILE) ne semble pas faire n'importe quoi.

Copiez simplement vos paramètres dans les variables déclarées dans la procédure et utilisez-les à la place.

Exemple:

ALTER PROCEDURE [ExampleProcedure]
@StartDate DATETIME,
@EndDate DATETIME
AS
BEGIN

--reassign to local variables to avoid parameter sniffing issues
DECLARE @MyStartDate datetime,
        @MyEndDate datetime

SELECT 
    @MyStartDate = @StartDate,
    @MyEndDate = @EndDate

--Rest of procedure goes here but refer to @MyStartDate and @MyEndDate
END
0
GlacialSpoon

J'ai eu un problème avec une cause racine différente qui correspondait exactement au titre des symptômes de cette question.

Dans mon cas, le problème était que le jeu de résultats était maintenu ouvert par le code .NET de l'application alors qu'il parcourait chaque enregistrement renvoyé et exécutait trois autres requêtes sur la base de données! Sur plusieurs milliers de lignes, cela donnait à croire que la requête d'origine avait été lente à terminer en fonction des informations de synchronisation de SQL Server.

Le correctif consistait donc à refactoriser le code .NET effectuant les appels afin qu'il ne garde pas le jeu de résultats ouvert lors du traitement de chaque ligne.

0
Tim Abell