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

Attention aux estimations

La semaine dernière, j'ai publié un article intitulé #BackToBasics :DATEFROMPARTS() , où j'ai montré comment utiliser cette fonction 2012+ pour des requêtes de plage de dates plus propres et sargables. Je l'ai utilisé pour démontrer que si vous utilisez un prédicat de date ouvert et que vous avez un index sur la colonne date/heure pertinente, vous pouvez vous retrouver avec une bien meilleure utilisation de l'index et des E/S inférieures (ou, dans le pire des cas , de même, si une recherche ne peut pas être utilisée pour une raison quelconque, ou s'il n'existe aucun index approprié :

Mais ce n'est qu'une partie de l'histoire (et pour être clair, DATEFROMPARTS() n'est pas techniquement nécessaire pour obtenir une recherche, c'est juste plus propre dans ce cas). Si nous dézoomons un peu, nous remarquons que nos estimations sont loin d'être exactes, une complexité que je ne voulais pas introduire dans le post précédent :

Ce n'est pas rare à la fois pour les prédicats d'inégalité et pour les analyses forcées. Et bien sûr, la méthode que j'ai suggérée ne donnerait-elle pas les statistiques les plus inexactes ? Voici l'approche de base (vous pouvez obtenir le schéma de la table, les index et les exemples de données de mon article précédent) :

CREATE PROCEDURE dbo.MonthlyReport_Original
  @Year  int,
  @Month int
AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
  DECLARE @End   date = DATEADD(MONTH, 1, @Start);
 
  SELECT DateColumn 
    FROM dbo.DateEntries
    WHERE DateColumn >= @Start
      AND DateColumn <  @End;
END
GO

Maintenant, des estimations inexactes ne seront pas toujours un problème, mais cela peut causer des problèmes avec des choix de plan inefficaces aux deux extrêmes. Un plan unique peut ne pas être optimal lorsque la plage choisie génère un très petit ou un très grand pourcentage de la table ou de l'index, et cela peut devenir très difficile pour SQL Server de prédire quand la distribution des données est inégale. Joseph Sack a décrit les éléments les plus typiques que les mauvaises estimations peuvent affecter dans son article :"Ten Common Threats to Execution Plan Quality :"

"[…] les mauvaises estimations de ligne peuvent avoir un impact sur diverses décisions, notamment la sélection d'index, les opérations de recherche ou d'analyse, l'exécution parallèle ou série, la sélection d'algorithme de jointure, la sélection de jointure physique interne ou externe (par exemple, la construction ou la sonde), la génération de spool, les recherches de signets par rapport à l'accès complet en cluster ou à la table de tas, la sélection d'agrégats de flux ou de hachage, et si une modification de données utilise ou non un plan large ou étroit.

Il y en a d'autres aussi, comme les allocations de mémoire qui sont trop grandes ou trop petites. Il poursuit en décrivant certaines des causes les plus courantes de mauvaises estimations, mais la principale cause dans ce cas est absente de sa liste :les estimations approximatives. Parce que nous utilisons une variable locale pour changer le int entrant paramètres à une seule date locale variable, SQL Server ne sait pas quelle sera la valeur, il fait donc des suppositions standardisées de cardinalité basées sur l'ensemble de la table.

Nous avons vu ci-dessus que l'estimation de mon approche suggérée était de 5 170 lignes. Maintenant, nous savons qu'avec un prédicat d'inégalité, et avec SQL Server ne connaissant pas les valeurs des paramètres, il devinera 30 % de la table. 31,645 * 0.3 n'est pas 5 170. Ni 31,465 * 0.3 * 0.3 , quand on se souvient qu'il y a en fait deux prédicats travaillant sur la même colonne. Alors d'où vient cette valeur de 5 170 ?

Comme le décrit Paul White dans son article, "Estimation de la cardinalité pour plusieurs prédicats", le nouvel estimateur de cardinalité dans SQL Server 2014 utilise une temporisation exponentielle, il multiplie donc le nombre de lignes de la table (31 465) par la sélectivité du premier prédicat (0,3) , puis multiplie cela par la racine carrée de la sélectivité du second prédicat (~0.547723).

31 645 * (0,3) * SQRT(0,3) ~=5 170,227

Donc, nous pouvons maintenant voir où SQL Server a proposé son estimation ; Quelles sont les méthodes que nous pouvons utiliser pour y remédier ?

  1. Transmettre les paramètres de date. Lorsque cela est possible, vous pouvez modifier l'application afin qu'elle transmette des paramètres de date appropriés au lieu de paramètres entiers séparés.
  2. Utilisez une procédure wrapper. Une variante de la méthode #1 - par exemple si vous ne pouvez pas changer l'application - consisterait à créer une deuxième procédure stockée qui accepte les paramètres de date construits à partir de la première.
  3. Utilisez OPTION (RECOMPILE) . Au moindre coût de compilation à chaque exécution de la requête, cela oblige SQL Server à optimiser en fonction des valeurs présentées à chaque fois, au lieu d'optimiser un plan unique pour des valeurs de paramètre inconnues, premières ou moyennes. (Pour un traitement approfondi de ce sujet, voir "Parameter Sniffing, Embedding, and the RECOMPILE Options" de Paul White.
  4. Utilisez SQL dynamique. Faire en sorte que SQL dynamique accepte la date construite la variable force le bon paramétrage (comme si vous aviez appelé une procédure stockée avec une date paramètre), mais c'est un peu moche, et plus difficile à entretenir.
  5. Désordre avec les indices et les indicateurs de suivi. Paul White en parle dans l'article susmentionné.

Je ne vais pas suggérer qu'il s'agit d'une liste exhaustive, et je ne vais pas réitérer les conseils de Paul sur les indices ou les indicateurs de trace, donc je vais juste me concentrer sur montrer comment les quatre premières approches peuvent atténuer le problème avec de mauvaises estimations .

    1. Paramètres de date

    CREATE PROCEDURE dbo.MonthlyReport_TwoDates
      @Start date,
      @End   date
    AS
    BEGIN
      SET NOCOUNT ON;
     
      SELECT /* Two Dates */ DateColumn
        FROM dbo.DateEntries
        WHERE DateColumn >= @Start
          AND DateColumn <  @End;
    END
    GO

    2. Procédure d'emballage

    CREATE PROCEDURE dbo.MonthlyReport_WrapperTarget
      @Start date,
      @End   date
    AS
    BEGIN
      SET NOCOUNT ON;
     
      SELECT /* Wrapper */ DateColumn
        FROM dbo.DateEntries
        WHERE DateColumn >= @Start
          AND DateColumn <  @End;
    END
    GO
     
    CREATE PROCEDURE dbo.MonthlyReport_WrapperSource
      @Year  int,
      @Month int
    AS
    BEGIN
      SET NOCOUNT ON;
      DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
      DECLARE @End   date = DATEADD(MONTH, 1, @Start);
     
      EXEC dbo.MonthlyReport_WrapperTarget @Start = @Start, @End = @End;
    END
    GO

    3. OPTION (RECOMPILER)

    CREATE PROCEDURE dbo.MonthlyReport_Recompile
      @Year  int,
      @Month int
    AS
    BEGIN
      SET NOCOUNT ON;
      DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
      DECLARE @End   date = DATEADD(MONTH, 1, @Start);
     
      SELECT /* Recompile */ DateColumn
        FROM dbo.DateEntries
          WHERE DateColumn >= @Start
          AND DateColumn < @End OPTION (RECOMPILE);
    END
    GO

    4. SQL dynamique

    CREATE PROCEDURE dbo.MonthlyReport_DynamicSQL
      @Year  int,
      @Month int
    AS
    BEGIN
      SET NOCOUNT ON;
      DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
      DECLARE @End   date = DATEADD(MONTH, 1, @Start);
     
      DECLARE @sql nvarchar(max) = N'SELECT /* Dynamic SQL */ DateColumn
        FROM dbo.DateEntries
        WHERE DateColumn >= @Start
        AND DateColumn < @End;';
     
      EXEC sys.sp_executesql @sql, N'@Start date, @End date', @Start, @End;
    END
    GO

Les épreuves

Avec les quatre ensembles de procédures en place, il était facile de construire des tests qui me montreraient les plans et les estimations dérivées de SQL Server. Étant donné que certains mois sont plus occupés que d'autres, j'ai choisi trois mois différents et je les ai tous exécutés plusieurs fois.

DECLARE @Year  int = 2012, @Month int = 7; -- 385 rows
DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
DECLARE @End   date = DATEADD(MONTH, 1, @Start);
 
EXEC dbo.MonthlyReport_Original      @Year  = @Year, @Month = @Month;
EXEC dbo.MonthlyReport_TwoDates      @Start = @Start,  @End = @End;
EXEC dbo.MonthlyReport_WrapperSource @Year  = @Year, @Month = @Month;
EXEC dbo.MonthlyReport_Recompile     @Year  = @Year, @Month = @Month;
EXEC dbo.MonthlyReport_DynamicSQL    @Year  = @Year, @Month = @Month;
 
/* repeat for @Year = 2011, @Month = 9  --    157 rows */
 
/* repeat for @Year = 2014, @Month = 4  --  2,115 rows */

Le résultat? Chaque plan donne le même Index Seek, mais les estimations ne sont correctes que sur les trois plages de dates dans l'OPTION (RECOMPILE) version. Les autres continuent d'utiliser les estimations dérivées du premier ensemble de paramètres (juillet 2012), et ainsi de suite pendant qu'ils obtiennent de meilleures estimations pour le premier l'exécution, cette estimation ne sera pas nécessairement meilleure pour la suite exécutions utilisant différents paramètres (un cas d'école classique de reniflage de paramètres) :

Notez que ce qui précède n'est pas une sortie *exacte* de SQL Sentry Plan Explorer - par exemple, j'ai supprimé les lignes de l'arborescence d'instructions qui affichaient les appels de procédure stockée externe et les déclarations de paramètres.

Ce sera à vous de déterminer si la tactique consistant à compiler à chaque fois est la meilleure pour vous, ou si vous devez "réparer" quelque chose en premier lieu. Ici, nous nous sommes retrouvés avec les mêmes plans, et aucune différence notable dans les mesures de performances d'exécution. Mais sur des tables plus grandes, avec une distribution de données plus asymétrique et des écarts plus importants dans les valeurs de prédicat (par exemple, envisagez un rapport qui peut couvrir une semaine, une année et n'importe quoi entre les deux), cela peut valoir la peine d'être étudié. Et notez que vous pouvez combiner des méthodes ici - par exemple, vous pouvez passer aux paramètres de date appropriés * et * ajouter OPTION (RECOMPILE) , si vous le souhaitez.

Conclusion

Dans ce cas précis, qui est une simplification intentionnelle, l'effort pour obtenir les estimations correctes n'a pas vraiment payé - nous n'avons pas obtenu de plan différent et les performances d'exécution étaient équivalentes. Il existe certainement d'autres cas, cependant, où cela fera une différence, et il est important de reconnaître la disparité des estimations et de déterminer si cela pourrait devenir un problème à mesure que vos données augmentent et/ou que votre distribution est biaisée. Malheureusement, il n'y a pas de réponse noire ou blanche, car de nombreuses variables affecteront la justification de la surcharge de compilation - comme dans de nombreux scénarios, IT DEPENDS™