sql >> Database teknologi >  >> RDS >> Database

Vær opmærksom på skøn

I sidste uge publicerede jeg et indlæg kaldet #BackToBasics :DATEFROMPARTS() , hvor jeg viste, hvordan man bruger denne 2012+ funktion til renere, sargable datointervalforespørgsler. Jeg brugte det til at demonstrere, at hvis du bruger et åbent datoprædikat, og du har et indeks på den relevante dato/klokkeslæt kolonne, kan du ende med meget bedre indeksforbrug og lavere I/O (eller i værste fald , det samme, hvis en søgning ikke kan bruges af en eller anden grund, eller hvis der ikke findes et passende indeks):

Men det er kun en del af historien (og for at være klar, DATEFROMPARTS() er ikke teknisk påkrævet for at få en søgning, det er bare renere i så fald). Hvis vi zoomer lidt ud, bemærker vi, at vores estimater er langt fra nøjagtige, en kompleksitet, jeg ikke ønskede at introducere i det forrige indlæg:

Dette er ikke ualmindeligt for både ulighedsprædikater og med tvungne scanninger. Og selvfølgelig, ville den metode, jeg foreslog, ikke give den mest unøjagtige statistik? Her er den grundlæggende tilgang (du kan få tabelskemaet, indekser og eksempeldata fra mit tidligere indlæg):

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

Nu vil unøjagtige estimater ikke altid være et problem, men det kan forårsage problemer med ineffektive planvalg i de to yderpunkter. En enkelt plan er muligvis ikke optimal, når det valgte interval vil give en meget lille eller meget stor procentdel af tabellen eller indekset, og det kan blive meget svært for SQL Server at forudsige, hvornår datafordelingen er ujævn. Joseph Sack skitserede de mere typiske ting, dårlige estimater kan påvirke i sit indlæg, "Ti almindelige trusler mod kvaliteten af ​​eksekveringsplanen:"

"[...] dårlige rækkeestimater kan påvirke en række beslutninger, herunder valg af indeks, søgning vs. scanningsoperationer, parallel versus seriel udførelse, valg af joinalgoritmer, valg af indre vs. ydre fysisk sammenføjning (f.eks. build vs. sonde), spoolgenerering, bogmærkeopslag vs. fuld klynge- eller heap-tabeladgang, strøm- eller hash-samlet valg, og om en dataændring bruger en bred eller smal plan."

Der er også andre, såsom hukommelsesstipendier, der er for store eller for små. Han fortsætter med at beskrive nogle af de mere almindelige årsager til dårlige skøn, men den primære årsag i dette tilfælde mangler på hans liste:gæt. Fordi vi bruger en lokal variabel til at ændre den indgående int parametre til en enkelt lokal date variabel, ved SQL Server ikke, hvad værdien vil være, så den foretager standardiserede gæt på kardinalitet baseret på hele tabellen.

Vi så ovenfor, at estimatet for min foreslåede tilgang var 5.170 rækker. Nu ved vi, at med et ulighedsprædikat, og med SQL Server, der ikke kender parameterværdierne, vil den gætte 30% af tabellen. 31,645 * 0.3 er ikke 5.170. Det er heller ikke 31,465 * 0.3 * 0.3 , når vi husker, at der faktisk er to prædikater, der arbejder mod den samme kolonne. Så hvor kommer denne værdi på 5.170 fra?

Som Paul White beskriver i sit indlæg, "Cardinality Estimation for Multiple Predicates", bruger den nye kardinalitetsestimator i SQL Server 2014 eksponentiel backoff, så den multiplicerer rækkeantallet i tabellen (31.465) med selektiviteten af ​​det første prædikat (0,3) , og gange derefter det med kvadratroden af selektiviteten af ​​det andet prædikat (~0,547723).

31.645 * (0.3) * SQRT(0.3) ~=5.170.227

Så nu kan vi se, hvor SQL Server kom med sit estimat; hvad er nogle af de metoder, vi kan bruge til at gøre noget ved det?

  1. Indtast datoparametre. Når det er muligt, kan du ændre applikationen, så den passerer i korrekte datoparametre i stedet for separate heltalsparametre.
  2. Brug en indpakningsprocedure. En variation af metode #1 – for eksempel hvis du ikke kan ændre applikationen – ville være at oprette en anden lagret procedure, der accepterer konstruerede datoparametre fra den første.
  3. Brug OPTION (RECOMPILE) . Med små omkostninger ved kompilering, hver gang forespørgslen køres, tvinger dette SQL Server til at optimere baseret på de værdier, der præsenteres hver gang, i stedet for at optimere en enkelt plan for ukendte, første eller gennemsnitlige parameterværdier. (For en grundig behandling af dette emne, se Paul Whites "Parameter Sniffing, Embedding, and the RECOMPILE Options."
  4. Brug dynamisk SQL. At have dynamisk SQL accepterer den konstruerede date variabel fremtvinger korrekt parametrering (ligesom hvis du havde kaldt en lagret procedure med en date parameter), men den er lidt grim og sværere at vedligeholde.
  5. Rodder med hints og sporingsflag. Paul White fortæller om nogle af disse i det førnævnte indlæg.

Jeg vil ikke foreslå, at dette er en udtømmende liste, og jeg vil ikke gentage Pauls råd om hints eller sporingsflag, så jeg vil kun fokusere på at vise, hvordan de første fire tilgange kan afbøde problemet med dårlige skøn .

    1. Datoparametre

    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. Indpakningsprocedure

    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. MULIGHED (GENKOMPILER)

    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. Dynamisk SQL

    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

Testene

Med de fire sæt procedurer på plads var det nemt at konstruere tests, der kunne vise mig planerne og estimaterne, som SQL Server udledte. Da nogle måneder er mere travle end andre, valgte jeg tre forskellige måneder og udførte dem alle flere gange.

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 */

Resultatet? Hver enkelt plan giver det samme indekssøgning, men estimaterne er kun korrekte på tværs af alle tre datointervaller i OPTION (RECOMPILE) version. Resten fortsætter med at bruge estimaterne afledt af det første sæt parametre (juli 2012), og så mens de får bedre estimater for den første udførelse, vil det estimat ikke nødvendigvis være bedre for efterfølgende eksekveringer ved hjælp af forskellige parametre (et klassisk, lærebogstilfælde af parametersniffing):

Bemærk, at ovenstående ikke er *præcis* output fra SQL Sentry Plan Explorer – for eksempel fjernede jeg sætningstrærækkerne, der viste de ydre lagrede procedurekald og parametererklæringer.

Det vil være op til dig at afgøre, om taktikken med at kompilere hver gang er bedst for dig, eller om du skal "rette" noget i første omgang. Her endte vi med de samme planer og ingen mærkbare forskelle i runtime-ydeevnemålinger. Men på større tabeller, med mere skæv datafordeling og større varianser i prædikatværdier (overvej f.eks. en rapport, der kan dække en uge, et år og alt derimellem), kan det være værd at undersøge. Og bemærk, at du kan kombinere metoder her - for eksempel kan du skifte til korrekte datoparametre *og* tilføje OPTION (RECOMPILE) , hvis du ville.

Konklusion

I dette specifikke tilfælde, som er en bevidst forenkling, gav indsatsen for at få de korrekte estimater ikke rigtig pote – vi fik ikke en anden plan, og runtime-ydelsen var tilsvarende. Der er dog helt sikkert andre tilfælde, hvor dette vil gøre en forskel, og det er vigtigt at erkende estimatforskelle og afgøre, om det kan blive et problem, efterhånden som dine data vokser og/eller din distribution skævvokser. Desværre er der ikke noget sort-hvidt svar, da mange variabler vil påvirke, om kompileringsomkostninger er berettiget – som med mange scenarier, IT DEPENDS™


  1. Failover for PostgreSQL-replikering 101

  2. Sådan opretter du et sikkert login-script i PHP og MySQL

  3. Hvordan konverterer jeg fra BLOB til TEXT i MySQL?

  4. Hvordan får du PyPy, Django og PostgreSQL til at arbejde sammen?