Obtenir une analyse bien que j'attende une recherche

9

J'ai besoin d'optimiser une SELECTinstruction, mais SQL Server effectue toujours une analyse d'index au lieu d'une recherche. C'est la requête qui, bien sûr, se trouve dans une procédure stockée:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

Et voici l'indice:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

Le plan:

Plan image

Pourquoi SQL Server a-t-il choisi une analyse? Comment puis-je le réparer?

Définitions des colonnes:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

Les paramètres d'état peuvent être:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser peut être:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)
Bestter
la source
Pouvez-vous publier le plan d'exécution réel quelque part (pas une image de celui-ci, mais le fichier .sqlplan sous forme XML)? Je suppose que vous avez modifié la procédure mais n'avez pas réellement obtenu de nouvelle compilation au niveau de l'instruction. Pouvez-vous modifier du texte de la requête (comme ajouter le préfixe de schéma au nom de la table ), puis transmettre une valeur valide pour @Status?
Aaron Bertrand
1
La définition de l'index pose également la question - pourquoi la clé est-elle activée Status DESC? Pour combien de valeurs existe-t-il Status, quelles sont-elles (si le nombre est petit) et chaque valeur est-elle représentée à peu près également? Montrez-nous la sortie deSELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

Réponses:

11

Je ne pense pas que l'analyse soit causée par une recherche d'une chaîne vide (et bien que vous puissiez ajouter un index filtré pour ce cas, cela n'aidera que des variations très spécifiques de la requête). Vous êtes probablement victime d'un reniflage de paramètres et d'un plan unique non optimisé pour toutes les diverses combinaisons de paramètres (et valeurs de paramètres) que vous fournirez à cette requête.

J'appelle cela la procédure "évier de cuisine" , car vous vous attendez à ce qu'une requête fournisse tout, y compris l'évier de cuisine.

J'ai une vidéo sur ma solution ici , mais essentiellement, la meilleure expérience que j'ai pour de telles requêtes est de:

  • Construisez l'instruction dynamiquement - cela vous permettra de laisser de côté les clauses mentionnant les colonnes pour lesquelles aucun paramètre n'a été fourni, et garantit que vous disposerez d'un plan optimisé précisément pour les paramètres réels qui ont été transmis avec des valeurs.
  • UtiliserOPTION (RECOMPILE) - cela empêche des valeurs de paramètres spécifiques de forcer le mauvais type de plan, particulièrement utile lorsque vous avez un biais de données, de mauvaises statistiques ou lorsque la première exécution d'une instruction utilise une valeur atypique qui conduira à un plan différent de celui plus tard et plus fréquent exécutions.
  • Utilisez l'option serveuroptimize for ad hoc workloads - cela empêche les variations de requête qui ne sont utilisées qu'une seule fois de polluer le cache de votre plan.

Activez l'optimisation pour les charges de travail ad hoc:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

Modifiez votre procédure:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

Une fois que vous avez une charge de travail basée sur cet ensemble de requêtes que vous pouvez surveiller, vous pouvez analyser les exécutions et voir celles qui bénéficieraient le plus d'index supplémentaires ou différents - vous pouvez le faire sous divers angles, du simple "quelle combinaison de les paramètres sont fournis le plus souvent? " à "quelles requêtes individuelles ont les durées d'exécution les plus longues?" Nous ne pouvons pas répondre à ces questions uniquement en fonction de votre code, nous pouvons seulement suggérer que tout index ne sera utile que pour un sous-ensemble de toutes les combinaisons de paramètres possibles que vous essayez de prendre en charge. Par exemple, si@Statusest NULL, alors aucune recherche contre cet index non cluster n'est possible. Donc, pour les cas où les utilisateurs ne se soucient pas du statut, vous allez obtenir une analyse, sauf si vous avez un index qui répond aux autres clauses (mais un tel index ne sera pas utile non plus, compte tenu de votre logique de requête actuelle - soit une chaîne vide soit une chaîne non vide n'est pas exactement sélective).

Dans ce cas, en fonction de l'ensemble des Statusvaleurs possibles et de la répartition de ces valeurs, la OPTION (RECOMPILE)peut ne pas être nécessaire. Mais si vous avez des valeurs qui produiront 100 lignes et des valeurs qui produiront des centaines de milliers, vous voudrez peut-être qu'elles soient là (même au coût du processeur, qui devrait être marginal compte tenu de la complexité de cette requête), afin que vous puissiez get cherche dans autant de cas que possible. Si la plage de valeurs est suffisamment finie, vous pouvez même faire quelque chose de délicat avec le SQL dynamique, où vous dites "J'ai cette valeur très sélective pour @Status, donc quand cette valeur spécifique est passée, apportez cette légère modification au texte de la requête afin que cela est considéré comme une requête différente et optimisé pour cette valeur de paramètre. "

Aaron Bertrand
la source
3
J'ai utilisé cette approche plusieurs fois et c'est un moyen fantastique pour que l'optimiseur fasse les choses comme vous pensez qu'il devrait le faire de toute façon. Kim Tripp parle d'une solution similaire ici: sqlskills.com/blogs/kimberly/high-performance-procedures Et a une vidéo d'une session qu'elle a faite au PASS il y a quelques années, qui explique vraiment en détail pourquoi cela fonctionne. Cela dit, cela n'ajoute vraiment rien à ce que M. Bertrand a dit ici. C'est l'un de ces outils que tout le monde devrait garder dans sa ceinture. Cela peut vraiment économiser des douleurs massives pour ces requêtes fourre-tout.
mskinner
3

Avertissement : Certains éléments de cette réponse peuvent faire tressaillir DBA. Je l'aborde du point de vue des performances pures - comment obtenir des recherches d'index lorsque vous obtenez toujours des analyses d'index.

Avec cela à l'écart, voici.

Votre requête est ce que l'on appelle une "requête d'évier de cuisine" - une requête unique destinée à répondre à une gamme de conditions de recherche possibles. Si l'utilisateur définit @statusune valeur, vous souhaitez filtrer sur cet état. Si @statusc'est le cas NULL, renvoyez tous les statuts, etc.

Cela pose des problèmes d'indexation, mais ils ne sont pas liés à la sargabilité, car toutes vos conditions de recherche sont "égales" aux critères.

C'est discutable:

WHERE [status]=@status

Ce n'est pas discutable car SQL Server doit évaluer ISNULL([status], 0)pour chaque ligne au lieu de rechercher une seule valeur dans l'index:

WHERE ISNULL([status], 0)=@status

J'ai recréé le problème de l'évier de cuisine sous une forme plus simple:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

Si vous essayez ce qui suit, vous obtiendrez une analyse d'index, même si A est la première colonne de l'index:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

Cependant, cela produit une recherche d'index:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

Tant que vous utilisez une quantité gérable de paramètres (deux dans votre cas), vous pourriez probablement juste UNIONun tas de requêtes de recherche - essentiellement toutes les permutations des critères de recherche. Si vous avez trois critères, cela semblera compliqué, avec quatre, ce sera complètement ingérable. Tu as été prévenu.

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

Pour que le troisième de ces quatre utilise une recherche d'index, vous aurez cependant besoin d'un deuxième index (B, A). Voici à quoi pourrait ressembler votre requête avec ces modifications (y compris ma refactorisation de la requête pour la rendre plus lisible).

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

... en plus, vous aurez besoin d'un index supplémentaire Employeeavec les deux colonnes d'index inversées.

Pour être complet, je dois mentionner que cela x=@xsignifie implicitement que xcela ne peut pas être NULLparce que NULLn'est jamais égal à NULL. Cela simplifie un peu la requête.

Et, oui, la réponse SQL dynamique d'Aaron Bertrand est un meilleur choix dans la plupart des cas (c'est-à-dire chaque fois que vous pouvez vivre avec les recompilations).

Daniel Hutmacher
la source
3

Votre question de base semble être "Pourquoi" et je pense que vous pourriez trouver la réponse à propos de la minute 55 ou plus de cette Grande présentation d'Adam Machanic à TechEd il y a quelques années.

Je mentionne les 5 minutes à la minute 55 mais toute la présentation en vaut la peine. Si vous regardez le plan de requête pour votre requête, je suis sûr que vous trouverez qu'il a des prédicats résiduels pour la recherche. Fondamentalement, SQL ne peut pas "voir" toutes les parties de l'index car certaines d'entre elles sont masquées par les inégalités et d'autres conditions. Le résultat est un balayage d'index pour un super ensemble basé sur le prédicat. Ce résultat est mis en file d'attente, puis analysé à nouveau à l'aide du prédicat résiduel.

Vérifiez les propriétés de l'opérateur de numérisation (F4) et voyez si vous avez à la fois "Seek Predicate" et "Predicate" dans la liste des propriétés.

Comme d'autres l'ont indiqué, la requête est difficile à indexer telle quelle. J'ai travaillé sur de nombreux projets similaires récemment et chacun a nécessité une solution différente. :(

Rayon
la source
0

Avant de nous demander si la recherche d'index est préférée à l'analyse d'index, une règle d'or consiste à vérifier le nombre de lignes renvoyées par rapport au nombre total de lignes de la table sous-jacente. Par exemple, si vous vous attendez à ce que votre requête renvoie 10 lignes sur 1 million de lignes, la recherche d'index est probablement hautement préférée à l'analyse d'index. Cependant, si quelques milliers de lignes (ou plus) doivent être renvoyées à partir de la requête, la recherche d'index n'est PAS nécessairement préférable.

Votre requête n'est pas complexe, donc si vous pouvez publier un plan d'exécution, nous pouvons avoir de meilleures idées pour vous aider.

jyao
la source
En filtrant quelques milliers de lignes d'une table de 1 million, j'aimerais toujours une recherche - c'est toujours une amélioration considérable des performances par rapport à l'analyse de la table entière.
Daniel Hutmacher
-6

c'est juste l'original formaté

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

ceci est la révision - pas sûr à 100% mais (peut-être) essayer
même un OU cela va probablement être un problème
qui se briserait sur ActiveDirectoryUser null

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )
paparazzo
la source
3
Je ne sais pas comment cette réponse résout la question du PO.
Erik
@Erik Pouvons-nous peut-être laisser le PO faire un essai? Deux OU sont partis. Savez-vous que cela ne peut pas aider à améliorer les performances des requêtes?
paparazzo du
@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser IS NOT NULL est supprimé. Ces deux inutiles suppriment un OR et suppriment IsUserGotAnActiveDirectoryUser IS NULL. Êtes-vous sûr que cette requête ne fonctionnera pas rapidement, puis l'OP?
paparazzo
@ ypercubeᵀᴹ Aurait pu faire beaucoup de choses. Je ne cherche pas plus simple. Deux ou sont partis. Ou est généralement mauvais pour les plans de requête. J'y arrive est une sorte de club ici et je ne fais pas partie du club. Mais je fais cela pour gagner ma vie et publier ce que je sais avoir fonctionné. Mes réponses ne sont pas affectées par les votes négatifs.
paparazzo