Database
 sql >> Base de données >  >> RDS >> Database

Un cas d'utilisation pour sp_prepare / sp_prepexec

Il y a des fonctionnalités que beaucoup d'entre nous évitent, comme les curseurs, les déclencheurs et le SQL dynamique. Il ne fait aucun doute qu'ils ont chacun leurs cas d'utilisation, mais lorsque nous voyons un déclencheur avec un curseur dans SQL dynamique, cela peut nous faire grincer des dents (triple coup dur).

Plan guides et sp_prepare sont dans un bateau similaire :si vous me voyiez utiliser l'un d'eux, vous lèveriez un sourcil; si vous me voyiez les utiliser ensemble, vous vérifieriez probablement ma température. Mais, comme pour les curseurs, les déclencheurs et le SQL dynamique, ils ont leurs cas d'utilisation. Et je suis récemment tombé sur un scénario où les utiliser ensemble était bénéfique.

Contexte

Nous avons beaucoup de données. Et beaucoup d'applications s'exécutant sur ces données. Certaines de ces applications sont difficiles ou impossibles à modifier, en particulier les applications prêtes à l'emploi d'un tiers. Ainsi, lorsque leur application compilée envoie des requêtes ad hoc à SQL Server, en particulier sous forme d'instruction préparée, et lorsque nous n'avons pas la liberté d'ajouter ou de modifier des index, plusieurs opportunités de réglage sont immédiatement exclues.

Dans ce cas, nous avions une table avec quelques millions de lignes. Une version simplifiée et aseptisée :

CREATE TABLE dbo.TheThings
(
  ThingID    bigint NOT NULL,
  TypeID     uniqueidentifier NOT NULL,
  dt1        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt2        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt3        datetime NOT NULL DEFAULT sysutcdatetime(),
  CONSTRAINT PK_TheThings PRIMARY KEY (ThingID)
);
 
CREATE INDEX ix_type ON dbo.TheThings(TypeID);
 
SET NOCOUNT ON;
GO
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1) 2500, @guid2
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;

La déclaration préparée à partir de l'application ressemblait à ceci (comme on le voit dans le cache du plan) :

(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0

Le problème est que, pour certaines valeurs de TypeID , il y aurait plusieurs milliers de lignes. Pour d'autres valeurs, il y en aurait moins de 10. Si le mauvais plan est choisi (et réutilisé) en fonction d'un type de paramètre, cela peut être gênant pour les autres. Pour la requête qui récupère une poignée de lignes, nous voulons une recherche d'index avec des recherches pour récupérer les colonnes non couvertes supplémentaires, mais pour la requête qui renvoie 700 000 lignes, nous voulons juste une analyse d'index en cluster. (Idéalement, l'index couvrirait, mais cette option n'était pas dans les cartes cette fois.)

En pratique, l'application recevait toujours la variation de numérisation, même si c'était celle qui était nécessaire environ 1 % du temps. 99 % des requêtes utilisaient une analyse de 2 millions de lignes alors qu'elles auraient pu utiliser une recherche + 4 ou 5 recherches.

Nous pourrions facilement reproduire cela dans Management Studio en exécutant cette requête :

DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO
 
DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO

Les plans sont revenus comme ceci :

L'estimation dans les deux cas était de 1 000 lignes ; les avertissements à droite sont dus à des E/S résiduelles.

Comment s'assurer que la requête a fait le bon choix en fonction du paramètre ? Nous aurions besoin de le recompiler, sans ajouter d'indices à la requête, activer les indicateurs de trace ou modifier les paramètres de la base de données.

Si j'exécutais les requêtes indépendamment en utilisant OPTION (RECOMPILE) , j'obtiendrais la recherche le cas échéant :

DBCC FREEPROCCACHE;
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE);
SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);

Avec RECOMPILE, nous obtenons des estimations plus précises et une recherche lorsque nous en avons besoin.

Mais, encore une fois, nous ne pouvions pas ajouter directement l'indice à la requête.

Essayons un guide de plan

Beaucoup de gens mettent en garde contre les guides de plan, mais nous étions un peu dans un coin ici. Nous préférerions certainement changer la requête, ou les index, si nous le pouvions. Mais cela pourrait être la meilleure chose à faire.

EXEC sys.sp_create_plan_guide   
  @name   = N'TheThingGuide',
  @stmt   = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0',
  @type   = N'SQL',
  @params = N'@P0 varchar(8000)',
  @hints  = N'OPTION (RECOMPILE)';

Semble simple; le tester est le problème. Comment simuler une instruction préparée dans Management Studio ? Comment pouvons-nous être sûrs que l'application obtient le plan guidé, et que c'est explicitement grâce au guide de plan ?

Si nous essayons de simuler cette requête dans SSMS, cela est traité comme une déclaration ad hoc, pas comme une déclaration préparée, et je n'ai pas pu obtenir ceci pour récupérer le guide du plan :

DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier
SELECT * FROM dbo.TheThings WHERE TypeID = @P0

SQL dynamique n'a pas non plus fonctionné (cela a également été traité comme une instruction ad hoc) :

DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', 
        @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier
        @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
EXEC sys.sp_executesql @sql, @params, @P0;

Et je ne pouvais pas le faire, car il ne prendrait pas non plus le guide du plan (le paramétrage prend le dessus ici, et je n'avais pas la liberté de modifier les paramètres de la base de données, même si cela devait être traité comme une déclaration préparée) :

SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';

Je ne peux pas vérifier le cache du plan pour les requêtes exécutées à partir de l'application, car le plan mis en cache n'indique rien sur l'utilisation du guide de plan (SSMS injecte ces informations dans le XML pour vous lorsque vous générez un plan réel). Et si la requête observe vraiment l'indice RECOMPILE que je transmets au guide du plan, comment pourrais-je de toute façon voir des preuves dans le cache du plan ?

Essayons sp_prepare

J'ai moins utilisé sp_prepare dans ma carrière que les guides de plan, et je ne recommanderais pas de l'utiliser pour le code d'application. (Comme le souligne Erik Darling, l'estimation peut être extraite du vecteur de densité, et non du reniflement du paramètre.)

Dans mon cas, je ne veux pas l'utiliser pour des raisons de performances, je veux l'utiliser (avec sp_execute) pour simuler l'instruction préparée provenant de l'application.

DECLARE @o int;
EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)',
     N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0';
 
EXEC sys.sp_execute @o,  'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan
EXEC sys.sp_execute @o,  'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup

SSMS nous montre que le guide de plan a été utilisé dans les deux cas.

Vous ne pourrez pas vérifier le cache du plan pour ces résultats, à cause de la recompilation. Mais dans un scénario comme le mien, vous devriez être en mesure de voir les effets de la surveillance, de la vérification explicite via des événements étendus ou de l'observation du soulagement du symptôme qui vous a poussé à enquêter sur cette requête en premier lieu (sachez simplement que le temps d'exécution moyen, la requête les statistiques, etc. peuvent être affectées par une compilation supplémentaire).

Conclusion

C'était un cas où un guide de plan était bénéfique, et sp_prepare était utile pour valider qu'il fonctionnerait pour l'application. Ce ne sont pas souvent utiles, et moins souvent ensemble, mais pour moi, c'était une combinaison intéressante. Même sans le guide de plan, si vous souhaitez utiliser SSMS pour simuler une application envoyant des instructions préparées, sp_prepare est votre ami. (Voir également sp_prepexec, qui peut être un raccourci si vous n'essayez pas de valider deux plans différents pour la même requête.)

Notez que cet exercice ne visait pas nécessairement à obtenir de meilleures performances tout le temps - il s'agissait d'aplatir la variance des performances. Les recompilations ne sont évidemment pas gratuites, mais je paierai une petite pénalité pour que 99 % de mes requêtes s'exécutent en 250 ms et 1 % s'exécutent en 5 secondes, plutôt que d'être coincé avec un plan absolument terrible pour 99 % des requêtes. ou 1 % des requêtes.