sql >> Database >  >> RDS >> Database

Een use-case voor sp_prepare / sp_prepexec

Er zijn functies waar velen van ons voor terugdeinzen, zoals cursors, triggers en dynamische SQL. Het lijdt geen twijfel dat ze elk hun gebruiksscenario's hebben, maar als we een trigger zien met een cursor in dynamische SQL, kan dat ons doen huiveren (driedubbele klap).

Planguides en sp_prepare zitten in hetzelfde schuitje:als je me er een zou zien gebruiken, zou je een wenkbrauw optrekken; als je me ze samen zou zien gebruiken, zou je waarschijnlijk mijn temperatuur controleren. Maar net als bij cursors, triggers en dynamische SQL hebben ze hun gebruiksscenario's. En ik kwam onlangs een scenario tegen waarin het nuttig was om ze samen te gebruiken.

Achtergrond

We hebben veel gegevens. En veel applicaties die tegen die gegevens draaien. Sommige van die applicaties zijn moeilijk of onmogelijk te wijzigen, met name kant-en-klare applicaties van een derde partij. Dus wanneer hun gecompileerde toepassing ad-hocquery's naar SQL Server verzendt, met name als een voorbereide instructie, en wanneer we niet de vrijheid hebben om indexen toe te voegen of te wijzigen, zijn verschillende afstemmingsmogelijkheden onmiddellijk van tafel.

In dit geval hadden we een tabel met een paar miljoen rijen. Een vereenvoudigde en opgeschoonde versie:

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;

De voorbereide verklaring van de applicatie zag er als volgt uit (zoals te zien in de plancache):

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

Het probleem is dat voor sommige waarden van TypeID , zouden er vele duizenden rijen zijn. Voor andere waarden zouden er minder dan 10 zijn. Als het verkeerde plan wordt gekozen (en hergebruikt) op basis van één parametertype, kan dit problemen opleveren voor de andere. Voor de query die een handvol rijen ophaalt, willen we een indexzoekopdracht met lookups om de extra niet-bedekte kolommen op te halen, maar voor de query die 700K rijen retourneert, willen we gewoon een geclusterde indexscan. (Idealiter zou de index dekken, maar deze optie zat er deze keer niet in.)

In de praktijk kreeg de applicatie altijd de scanvariatie, ook al was die ongeveer 1% van de tijd nodig. 99% van de zoekopdrachten gebruikten een scan van 2 miljoen rijen terwijl ze een zoekfunctie + 4 of 5 zoekopdrachten hadden kunnen gebruiken.

We kunnen dit gemakkelijk reproduceren in Management Studio door deze query uit te voeren:

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

De plannen kwamen zo terug:

De schatting was in beide gevallen 1.000 rijen; de waarschuwingen aan de rechterkant zijn te wijten aan resterende I/O.

Hoe konden we ervoor zorgen dat de query de juiste keuze maakte, afhankelijk van de parameter? We zouden het opnieuw moeten compileren, zonder hints aan de query toe te voegen, traceervlaggen aan te zetten of database-instellingen te wijzigen.

Als ik de query's onafhankelijk heb uitgevoerd met OPTION (RECOMPILE) , ik zou de zoekactie krijgen als dat van toepassing is:

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

Met RECOMPILE krijgen we nauwkeurigere schattingen en zoeken we wanneer we die nodig hebben.

Maar nogmaals, we konden de hint niet rechtstreeks aan de zoekopdracht toevoegen.

Laten we een plangids proberen

Veel mensen waarschuwen voor plangidsen, maar we zaten hier een beetje in een hoekje. We zouden zeker de voorkeur geven aan het wijzigen van de query, of de indexen, als we konden. Maar dit zou wel eens het beste kunnen zijn.

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

Lijkt eenvoudig; testen is het probleem. Hoe simuleren we een voorbereide verklaring in Management Studio? Hoe kunnen we er zeker van zijn dat de toepassing het begeleide plan krijgt en dat dit expliciet komt door de plangids?

Als we deze query in SSMS proberen te simuleren, wordt dit behandeld als een ad-hocverklaring, niet als een voorbereide verklaring, en ik kon dit niet krijgen om de plangids op te halen:

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

Dynamische SQL werkte ook niet (dit werd ook behandeld als een ad hoc statement):

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;

En ik kon dit niet doen, omdat het ook de plangids niet zou ophalen (parametrering neemt het hier over, en ik had niet de vrijheid om database-instellingen te wijzigen, zelfs als dit zou worden behandeld als een voorbereide verklaring) :

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

Ik kan de plancache niet controleren voor de query's die vanuit de app worden uitgevoerd, omdat het in de cache opgeslagen plan niets aangeeft over het gebruik van de plangids (SSMS injecteert die informatie voor u in de XML wanneer u een daadwerkelijk plan genereert). En als de query echt de RECOMPILE-hint die ik doorgeef aan de plangids waarneemt, hoe zou ik dan ooit enig bewijs in de plancache kunnen zien?

Laten we sp_prepare proberen

Ik heb sp_prepare in mijn carrière minder gebruikt dan plangidsen, en ik zou het niet aanraden om het voor applicatiecode te gebruiken. (Zoals Erik Darling opmerkt, kan de schatting worden afgeleid uit de dichtheidsvector, niet uit het snuiven van de parameter.)

In mijn geval wil ik het niet gebruiken om prestatieredenen, ik wil het (samen met sp_execute) gebruiken om de voorbereide verklaring van de app te simuleren.

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 laat ons zien dat de plangids in beide gevallen is gebruikt.

U kunt de plancache niet controleren op deze resultaten vanwege het opnieuw compileren. Maar in een scenario als het mijne zou je de effecten moeten kunnen zien in monitoring, expliciet controleren via Extended Events, of de verlichting van het symptoom observeren waardoor je deze query in de eerste plaats hebt onderzocht (houd er rekening mee dat de gemiddelde runtime, query statistieken enz. kunnen worden beïnvloed door aanvullende compilatie).

Conclusie

Dit was een geval waarin een plangids nuttig was, en sp_prepare was nuttig om te valideren dat het voor de toepassing zou werken. Deze zijn niet vaak handig, en minder vaak samen, maar voor mij was het een interessante combinatie. Zelfs zonder de plangids, als u SSMS wilt gebruiken om een ​​app te simuleren die voorbereide verklaringen verzendt, is sp_prepare uw vriend. (Zie ook sp_prepexec, wat een snelkoppeling kan zijn als u niet twee verschillende abonnementen voor dezelfde zoekopdracht probeert te valideren.)

Merk op dat deze oefening niet noodzakelijk was om steeds betere prestaties te krijgen - het was om de prestatievariantie af te vlakken. Hercompilaties zijn uiteraard niet gratis, maar ik betaal een kleine boete om 99% van mijn zoekopdrachten in 250 ms en 1% in 5 seconden te laten uitvoeren, in plaats van vast te zitten aan een plan dat absoluut verschrikkelijk is voor 99% van de vragen of 1% van de zoekopdrachten.


  1. Django + Psycopg2:InterfaceError:alleen protocol 3 ondersteund

  2. Geavanceerde MySql-query:tabel bijwerken met informatie uit een andere tabel

  3. Prestaties en prijzen van PostgreSQL DigitalOcean vergelijken - ScaleGrid versus door DigitalOcean beheerde databases

  4. Oracle Sequence genereert geen doorlopend nummer