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

En use case for sp_prepare / sp_prepexec

Der er funktioner mange af os viger tilbage for, såsom markører, triggere og dynamisk SQL. Der er ingen tvivl om, at de hver især har deres use cases, men når vi ser en trigger med en markør inde i dynamisk SQL, kan det få os til at krybe (tredobbelt whammy).

Plan guides og sp_prepare er i en lignende båd:hvis du så mig bruge en af ​​dem, ville du løfte et øjenbryn; hvis du så mig bruge dem sammen, ville du nok tjekke min temperatur. Men som med markører, triggere og dynamisk SQL, har de deres anvendelsesmuligheder. Og jeg stødte for nylig på et scenario, hvor det var gavnligt at bruge dem sammen.

Baggrund

Vi har mange data. Og mange applikationer kører mod disse data. Nogle af disse applikationer er svære eller umulige at ændre, især applikationer fra en tredjepart. Så når deres kompilerede applikation sender ad hoc-forespørgsler til SQL Server, især som en forberedt erklæring, og når vi ikke har friheden til at tilføje eller ændre indekser, er adskillige tuning-muligheder straks ude af bordet.

I dette tilfælde havde vi en tabel med et par millioner rækker. En forenklet og renset version:

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;

Den udarbejdede erklæring fra ansøgningen så således ud (som det ses i plancachen):

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

Problemet er, at for nogle værdier af TypeID , ville der være mange tusinde rækker. For andre værdier ville der være færre end 10. Hvis den forkerte plan vælges (og genbruges) baseret på én parametertype, kan dette være et problem for de andre. For forespørgslen, der henter en håndfuld rækker, ønsker vi en indekssøgning med opslag for at hente de yderligere ikke-dækkede kolonner, men for forespørgslen, der returnerer 700.000 rækker, ønsker vi blot en klynget indeksscanning. (Ideelt set ville indekset dække, men denne mulighed var ikke i kortene denne gang.)

I praksis fik applikationen altid scanningsvariationen, selvom det var den, der var brug for omkring 1 % af tiden. 99 % af forespørgslerne brugte en scanning på 2 millioner rækker, når de kunne have brugt en søgning + 4 eller 5 opslag.

Vi kunne nemt gengive dette i Management Studio ved at køre denne forespørgsel:

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

Planerne kom tilbage sådan her:

Estimatet i begge tilfælde var 1.000 rækker; advarslerne til højre skyldes resterende I/O.

Hvordan kunne vi sikre os, at forespørgslen tog det rigtige valg afhængigt af parameteren? Vi bliver nødt til at få det til at rekompilere uden at tilføje hints til forespørgslen, aktivere sporingsflag eller ændre databaseindstillinger.

Hvis jeg kørte forespørgslerne uafhængigt ved hjælp af OPTION (RECOMPILE) , jeg ville få søgningen, når det er relevant:

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

Med RECOMPILE får vi mere præcise estimater, og en søgning, når vi har brug for en.

Men igen, vi kunne ikke tilføje tippet til forespørgslen direkte.

Lad os prøve en planvejledning

Masser af mennesker advarer mod planguider, men vi var lidt i et hjørne her. Vi ville bestemt foretrække at ændre forespørgslen eller indekserne, hvis vi kunne. Men dette er måske det næstbedste.

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

Virker ligetil; at teste det er problemet. Hvordan simulerer vi en udarbejdet redegørelse i Management Studio? Hvordan kan vi være sikre på, at applikationen får den guidede plan, og at det er eksplicit på grund af planvejledningen?

Hvis vi forsøger at simulere denne forespørgsel i SSMS, bliver dette behandlet som en ad hoc-erklæring, ikke en forberedt erklæring, og jeg kunne ikke få dette til at hente planvejledningen:

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

Dynamisk SQL virkede heller ikke (dette blev også behandlet som en ad hoc-sætning):

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;

Og det kunne jeg ikke, for det ville heller ikke opfange planvejledningen (parametriseringen tager over her, og jeg havde ikke frihed til at ændre databaseindstillinger, selvom dette skulle behandles som en forberedt erklæring) :

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

Jeg kan ikke tjekke plancachen for de forespørgsler, der kører fra appen, da den cachelagrede plan ikke angiver noget om planguidebrug (SSMS injicerer denne information i XML for dig, når du genererer en faktisk plan). Og hvis forespørgslen virkelig observerer RECOMPILE-tipet, som jeg sender til planguiden, hvordan kunne jeg så nogensinde se nogen beviser i plancachen?

Lad os prøve sp_prepare

Jeg har brugt sp_prepare mindre i min karriere end planvejledninger, og jeg vil ikke anbefale at bruge det til applikationskode. (Som Erik Darling påpeger, kan estimatet trækkes fra tæthedsvektoren, ikke fra at sniffe parameteren.)

I mit tilfælde ønsker jeg ikke at bruge det af præstationsmæssige årsager, jeg vil bruge det (sammen med sp_execute) til at simulere den forberedte erklæring, der kommer fra appen.

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 viser os, at planvejledningen blev brugt i begge tilfælde.

Du vil ikke være i stand til at tjekke planens cache for disse resultater på grund af omkompileringen. Men i et scenarie som mit burde du være i stand til at se effekterne i overvågning, eksplicit kontrol via udvidede hændelser eller observere lindring af symptomet, der fik dig til at undersøge denne forespørgsel i første omgang (vær blot opmærksom på, at den gennemsnitlige runtime, forespørgsel statistik osv. kan blive påvirket af yderligere kompilering).

Konklusion

Dette var et tilfælde, hvor en planvejledning var gavnlig, og sp_prepare var nyttig til at validere, at den ville fungere for applikationen. Disse er ikke ofte nyttige, og sjældnere sammen, men for mig var det en interessant kombination. Selv uden planguiden, hvis du vil bruge SSMS til at simulere en app, der sender forberedte erklæringer, er sp_prepare din ven. (Se også sp_prepexec, som kan være en genvej, hvis du ikke forsøger at validere to forskellige planer for den samme forespørgsel.)

Bemærk, at denne øvelse ikke nødvendigvis var for at få bedre præstationer hele tiden - det var for at udjævne præstationsvarians. Genkompilering er naturligvis ikke gratis, men jeg vil betale en lille bøde for at få 99 % af mine forespørgsler til at blive eksekveret på 250 ms og 1 % til at blive eksekveret på 5 sekunder, i stedet for at sidde fast med en plan, der er helt forfærdelig for enten 99 % af forespørgslerne eller 1 % af forespørgslerne.


  1. Sådan udtrækkes ugenummer i sql

  2. Tillad kun brugere at få adgang til bestemte tabeller i min indholdsudbyder

  3. Django:Forespørgselsgruppe efter måned

  4. Hvordan pg_sleep_for() virker i PostgreSQL