sql >> Database >  >> RDS >> Database

Prestatieverrassingen en aannames:DATEDIFF

Het is heel gemakkelijk te bewijzen dat de volgende twee uitdrukkingen exact hetzelfde resultaat opleveren:de eerste dag van de huidige maand.

SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0),
       CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));

En ze nemen ongeveer evenveel tijd in beslag om te berekenen:

SELECT SYSDATETIME();
GO
DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
GO 1000000
GO
SELECT SYSDATETIME();
GO
DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE());
GO 1000000
SELECT SYSDATETIME();

Op mijn systeem duurden beide batches ongeveer 175 seconden om te voltooien.

Dus waarom zou je de ene methode verkiezen boven de andere? Als een van hen echt knoeit met kardinaliteitsschattingen .

Laten we als snelle inleiding deze twee waarden vergelijken:

SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), -- today: 2013-09-01
       DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0); -- today: 1786-05-01
--------------------------------------^^^^^^^^^^^^ notice how these are swapped

(Merk op dat de werkelijke waarden die hier worden weergegeven, zullen veranderen, afhankelijk van wanneer u dit bericht leest - "vandaag" waarnaar wordt verwezen in de opmerking is 5 september 2013, de dag dat dit bericht werd geschreven. In oktober 2013 zal de uitvoer bijvoorbeeld be 2013-10-01 en 1786-04-01 .)

Met dat uit de weg, laat me je laten zien wat ik bedoel ...

Een repro

Laten we een heel eenvoudige tabel maken, met alleen een geclusterde DATE kolom, en laad 15.000 rijen met de waarde 1786-05-01 en 50 rijen met de waarde 2013-09-01 :

CREATE TABLE dbo.DateTest
(
  CreateDate DATE
);
 
CREATE CLUSTERED INDEX x ON dbo.DateTest(CreateDate);
 
INSERT dbo.DateTest(CreateDate) 
SELECT TOP (15000) DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0)
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
UNION ALL
SELECT TOP (50) DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0)
FROM sys.all_objects;

En laten we dan eens kijken naar de daadwerkelijke plannen voor deze twee vragen:

SELECT /* Query 1 */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
 
SELECT /* Query 2 */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0);

De grafische plannen zien er goed uit:


Grafisch plan voor DATEDIFF(MONTH, 0, GETDATE()) vraag


Grafisch plan voor DATEDIFF(MONTH, GETDATE(), 0) vraag

Maar de geschatte kosten kloppen niet - let op hoeveel hoger de geschatte kosten zijn voor de eerste zoekopdracht, die slechts 50 rijen retourneert, vergeleken met de tweede zoekopdracht, die 15.000 rijen retourneert!


Statementraster met geschatte kosten

En het tabblad Topbewerkingen laat zien dat de eerste zoekopdracht (op zoek naar 2013-09-01 ) schatte dat het 15.000 rijen zou vinden, terwijl het er in werkelijkheid slechts 50 zou vinden; de tweede zoekopdracht toont het tegenovergestelde aan:het verwachtte 50 rijen te vinden die overeenkomen met 1786-05-01 , maar vond er 15.000. Gebaseerd op onjuiste kardinaliteitsschattingen zoals deze, weet ik zeker dat u zich kunt voorstellen wat voor ingrijpende gevolgen dit zou kunnen hebben voor complexere zoekopdrachten tegen veel grotere datasets.


Bovenste tabblad Bewerkingen voor eerste zoekopdracht [DATEDIFF(MONTH, 0, GETDATUM())]


Bovenste tabblad Bewerkingen voor tweede zoekopdracht [DATEDIFF(MONTH, 0, GETDATUM())]

Een iets andere variant van de zoekopdracht, waarbij een andere uitdrukking wordt gebruikt om het begin van de maand te berekenen (waarnaar wordt verwezen aan het begin van de post), vertoont dit symptoom niet:

SELECT /* Query 3 */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));

Het plan lijkt erg op vraag 1 hierboven, en als je niet beter zou kijken, zou je denken dat deze plannen gelijkwaardig zijn:


Grafisch plan voor niet-DATEDIFF-query

Als je echter naar het tabblad Topactiviteiten kijkt, zie je dat de schatting klopt:


Bovenste tabblad Bewerkingen met nauwkeurige schattingen

Voor deze specifieke gegevensomvang en -query is de impact op de nettoprestaties (met name duur en leesbewerkingen) grotendeels irrelevant. En het is belangrijk op te merken dat de query's zelf nog steeds de juiste gegevens retourneren; het is alleen dat de schattingen verkeerd zijn (en kunnen leiden tot een slechter plan dan ik hier heb aangetoond). Dat gezegd hebbende, als je constanten afleidt met DATEDIFF binnen uw vragen op deze manier, zou u deze impact echt in uw omgeving moeten testen.

Dus waarom gebeurt dit?

Simpel gezegd, SQL Server heeft een DATEDIFF bug waarbij het de tweede en derde argumenten verwisselt bij het evalueren van de expressie voor kardinaliteitsschatting. Dit lijkt constant vouwen in te houden, althans perifeer; er zijn veel meer details over constant folden in dit Books Online-artikel, maar helaas onthult het artikel geen informatie over deze specifieke bug.

Er is een oplossing - of is die er?

Er is een Knowledge Base-artikel (KB #2481274) dat beweert het probleem aan te pakken, maar het heeft een paar eigen problemen:

  1. Het KB-artikel beweert dat het probleem is opgelost in verschillende servicepacks of cumulatieve updates voor SQL Server 2005, 2008 en 2008 R2. Het symptoom is echter nog steeds aanwezig in branches die daar niet expliciet worden genoemd, hoewel ze sinds de publicatie van het artikel veel extra CU's hebben gezien. Ik kan dit probleem nog steeds reproduceren op SQL Server 2008 SP3 CU #8 (10.0.5828) en SQL Server 2012 SP1 CU #5 (11.0.3373).
  2. Er wordt nagelaten te vermelden dat, om van de fix te profiteren, u traceervlag 4199 moet inschakelen (en "profiteert" van alle andere manieren waarop specifieke traceervlaggen de optimizer kunnen beïnvloeden). Het feit dat deze traceringsvlag vereist is voor de fix wordt vermeld in een gerelateerd Connect-item, #630583, maar deze informatie is niet teruggekeerd in het KB-artikel. Noch het KB-artikel noch het Connect-item geven enig inzicht in de oorzaak (dat de argumenten voor DATEDIFF zijn verwisseld tijdens de evaluatie). Aan de positieve kant, het uitvoeren van de bovenstaande query's met de traceringsvlag aan (met behulp van OPTION (QUERYTRACEON 4199) ) levert plannen op die niet het probleem met de onjuiste schatting hebben.
  3. Het stelt voor dat u dynamische SQL gebruikt om het probleem te omzeilen. In mijn tests, met een andere uitdrukking (zoals de bovenstaande die geen gebruik maakt van DATEDIFF ) loste het probleem op in moderne versies van zowel SQL Server 2008 als SQL Server 2012. Het aanbevelen van dynamische SQL hier is onnodig ingewikkeld en waarschijnlijk overdreven, aangezien een andere uitdrukking het probleem zou kunnen oplossen. Maar als je dynamische SQL zou gebruiken, zou ik het op deze manier doen in plaats van de manier die ze aanbevelen in het KB-artikel, vooral om de risico's van SQL-injectie te minimaliseren:

    DECLARE 
      @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0),
      @sql NVARCHAR(MAX) = N'SELECT COUNT(*) FROM dbo.DateTest 
        WHERE CreateDate = @date;';
     
    EXEC sp_executesql @sql, N'@date DATE', @date;

    (En je kunt OPTION (RECOMPILE) toevoegen daar, afhankelijk van hoe u wilt dat SQL Server het snuiven van parameters afhandelt.)

    Dit leidt tot hetzelfde plan als de eerdere zoekopdracht die geen gebruik maakt van DATEDIFF , met de juiste schattingen en 99,1% van de kosten in de geclusterde index zoeken.

    Een andere benadering die u zou kunnen verleiden (en met u, ik bedoel mij, toen ik voor het eerst begon met onderzoeken) is om een ​​variabele te gebruiken om de waarde vooraf te berekenen:

    DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
     
    SELECT COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d;

    Het probleem met deze aanpak is dat je met een variabele een stabiel plan krijgt, maar de kardinaliteit zal gebaseerd zijn op een gok (en het type gok hangt af van de aan- of afwezigheid van statistieken) . In dit geval zijn dit de geschatte versus de werkelijke:


    Tabblad Topbewerkingen voor query die een variabele gebruikt

    Dit klopt duidelijk niet; het lijkt erop dat SQL Server heeft geraden dat de variabele overeen zou komen met 50% van de rijen in de tabel.

SQL Server 2014

Ik heb een iets ander probleem gevonden in SQL Server 2014. De eerste twee query's zijn opgelost (door wijzigingen in de kardinaliteitsschatter of andere oplossingen), wat betekent dat de DATEDIFF argumenten worden niet meer gewisseld. Hoera!

Er lijkt echter een regressie te zijn geïntroduceerd voor de tijdelijke oplossing van het gebruik van een andere uitdrukking - nu lijdt deze aan een onnauwkeurige schatting (gebaseerd op dezelfde 50% gok als het gebruik van een variabele). Dit zijn de zoekopdrachten die ik heb uitgevoerd:

SELECT /* 0, GETDATE() (2013) */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0);
 
SELECT /* GETDATE(), 0 (1786) */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0);
 
SELECT /* Non-DATEDIFF */ COUNT(*) FROM dbo.DateTest
  WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));
 
DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE());
 
SELECT /* Variable */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d;
 
DECLARE 
  @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0),
  @sql NVARCHAR(MAX) = N'SELECT /* Dynamic SQL */ COUNT(*) FROM dbo.DateTest 
    WHERE CreateDate = @date;';
 
EXEC sp_executesql @sql, N'@date DATE', @date;

Hier is het overzichtsschema waarin de geschatte kosten en de werkelijke runtime-statistieken worden vergeleken:


Geschatte kosten voor de 5 voorbeeldquery's op SQL Server 2014

En dit zijn hun geschatte en werkelijke rijtellingen (samengesteld met Photoshop):


Geschatte en werkelijke rijtellingen voor de 5 query's op SQL Server 2014

Uit deze uitvoer blijkt duidelijk dat de uitdrukking die het probleem eerder oploste, nu een andere heeft geïntroduceerd. Ik weet niet zeker of dit een symptoom is van het lopen in een CTP (bijvoorbeeld iets dat zal worden opgelost) of dat dit echt een regressie is.

In dit geval heeft traceringsvlag 4199 (op zichzelf) geen effect; de nieuwe kardinaliteitsschatter doet gissingen en is gewoon niet correct. Of het tot een daadwerkelijk prestatieprobleem leidt, hangt sterk af van vele andere factoren die buiten het bestek van dit bericht vallen.

Als u dit probleem tegenkomt, kunt u - in ieder geval in de huidige CTP's - het oude gedrag herstellen met OPTION (QUERYTRACEON 9481, QUERYTRACEON 4199) . Traceringsvlag 9481 schakelt de nieuwe kardinaliteitsschatter uit, zoals beschreven in deze release-opmerkingen (die zeker zal verdwijnen of op zijn minst op een bepaald moment zal bewegen). Dit herstelt op zijn beurt de juiste schattingen voor de niet-DATEDIFF versie van de query, maar lost helaas nog steeds niet het probleem op waarbij een schatting wordt gemaakt op basis van een variabele (en het gebruik van alleen TF9481, zonder TF4199, dwingt de eerste twee query's terug te gaan naar het oude gedrag van het verwisselen van argumenten).

Conclusie

Ik moet toegeven dat dit een grote verrassing voor me was. Een pluim voor Martin Smith en t-clausen.dk voor het volharden en overtuigen dat dit een reëel en geen ingebeeld probleem was. Ook een grote dank aan Paul White (@SQL_Kiwi) die me heeft geholpen om gezond te blijven en me heeft herinnerd aan de dingen die ik niet mag zeggen. :-)

Omdat ik me niet bewust was van deze bug, was ik onvermurwbaar dat het betere queryplan werd gegenereerd door simpelweg de querytekst te wijzigen, niet vanwege de specifieke wijziging. Het blijkt dat soms een wijziging in een zoekopdracht waarvan u aanneemt zal geen verschil maken, eigenlijk wel. Dus ik raad aan dat als je vergelijkbare querypatronen in je omgeving hebt, je ze test en ervoor zorgt dat kardinaliteitsschattingen goed uitkomen. En maak een notitie om ze opnieuw te testen wanneer u een upgrade uitvoert.


  1. Prestatiewaarde van COMB-gidsen

  2. Converteren van Oracle's RAW (16) naar .NET's GUID

  3. Hoe krijg ik de eerste en laatste datum van het lopende jaar?

  4. De prestatie-impact van een adhoc-workload onderzoeken