De aaneenschakeling van twee of meer datasets wordt meestal uitgedrukt in T-SQL met behulp van de UNION ALL
clausule. Aangezien de SQL Server-optimizer vaak zaken als joins en aggregaten opnieuw kan ordenen om de prestaties te verbeteren, is het redelijk om te verwachten dat SQL Server ook zou overwegen om concatenatie-invoer opnieuw te ordenen, waar dit een voordeel zou bieden. De optimizer kan bijvoorbeeld de voordelen overwegen van het herschrijven van A UNION ALL B
als B UNION ALL A
.
In feite doet de SQL Server-optimizer niet doe dit. Om precies te zijn, er was enige beperkte ondersteuning voor het opnieuw ordenen van concatenatie-invoer in SQL Server-releases tot 2008 R2, maar deze werd verwijderd in SQL Server 2012 en is sindsdien niet meer opgedoken.
SQL Server 2008 R2
Intuïtief is de volgorde van aaneenschakelingsinvoer alleen van belang als er een rijdoel is . Standaard optimaliseert SQL Server uitvoeringsplannen op basis van het feit dat alle kwalificerende rijen worden teruggestuurd naar de client. Wanneer een rijdoel van kracht is, probeert de optimizer een uitvoeringsplan te vinden dat de eerste paar rijen snel produceert.
Rijdoelen kunnen op een aantal manieren worden ingesteld, bijvoorbeeld met TOP
, een FAST n
vraaghint, of door EXISTS
. te gebruiken (die van nature maximaal één rij moet vinden). Als er geen rijdoel is (d.w.z. de client heeft alle rijen nodig), maakt het over het algemeen niet uit in welke volgorde de concatenatie-invoer wordt gelezen:elke invoer wordt uiteindelijk in ieder geval volledig verwerkt.
De beperkte ondersteuning in versies tot en met SQL Server 2008 R2 is van toepassing als er een doel is van precies één rij . In deze specifieke omstandigheid zal SQL Server de concatenatie-invoer opnieuw ordenen op basis van de verwachte kosten.
Dit wordt niet gedaan tijdens op kosten gebaseerde optimalisatie (zoals men zou verwachten), maar eerder als een last-minute herschrijving van de normale optimalisatie-uitvoer na de optimalisatie. Deze opstelling heeft het voordeel dat de op kosten gebaseerde zoekruimte voor plannen niet wordt vergroot (mogelijk één alternatief voor elke mogelijke nabestelling), terwijl er toch een plan wordt geproduceerd dat is geoptimaliseerd om snel de eerste rij terug te geven.
Voorbeelden
In de volgende voorbeelden worden twee tabellen met identieke inhoud gebruikt:Een miljoen rijen met gehele getallen van één tot een miljoen. Eén tabel is een hoop zonder niet-geclusterde indexen; de andere heeft een unieke geclusterde index:
CREATE TABLE dbo.Expensive ( Val bigint NOT NULL ); CREATE TABLE dbo.Cheap ( Val bigint NOT NULL, CONSTRAINT [PK dbo.Cheap Val] UNIQUE CLUSTERED (Val) ); GO INSERT dbo.Cheap WITH (TABLOCKX) (Val) SELECT TOP (1000000) Val = ROW_NUMBER() OVER (ORDER BY SV1.number) FROM master.dbo.spt_values AS SV1 CROSS JOIN master.dbo.spt_values AS SV2 ORDER BY Val OPTION (MAXDOP 1); GO INSERT dbo.Expensive WITH (TABLOCKX) (Val) SELECT C.Val FROM dbo.Cheap AS C OPTION (MAXDOP 1);
Geen rijdoel
De volgende query zoekt naar dezelfde rijen in elke tabel en retourneert de aaneenschakeling van de twee sets:
SELECT E.Val FROM dbo.Expensive AS E WHERE E.Val BETWEEN 751000 AND 751005 UNION ALL SELECT C.Val FROM dbo.Cheap AS C WHERE C.Val BETWEEN 751000 AND 751005;
Het uitvoeringsplan dat wordt geproduceerd door de query-optimizer is:
De waarschuwing op de root SELECT
operator waarschuwt ons voor de duidelijk ontbrekende index op de heaptabel. De waarschuwing op de Table Scan-operator is toegevoegd door Sentry One Plan Explorer. Het vestigt onze aandacht op de I/O-kosten van het resterende predikaat dat verborgen is in de scan.
De volgorde van de invoer voor de aaneenschakeling doet er hier niet toe, omdat we geen rijdoel hebben ingesteld. Beide invoer worden volledig gelezen om alle resultaatrijen te retourneren. Het is interessant (hoewel dit niet gegarandeerd is) dat de volgorde van de invoer de tekstuele volgorde van de oorspronkelijke zoekopdracht volgt. Merk ook op dat de volgorde van de rijen met het eindresultaat ook niet is gespecificeerd, omdat we geen ORDER BY
op het hoogste niveau hebben gebruikt clausule. We gaan ervan uit dat een opzettelijke en definitieve bestelling niet van belang is voor de taak die voor ons ligt.
Als we de geschreven volgorde van de tabellen in de query als volgt omkeren:
SELECT C.Val FROM dbo.Cheap AS C WHERE C.Val BETWEEN 751000 AND 751005 UNION ALL SELECT E.Val FROM dbo.Expensive AS E WHERE E.Val BETWEEN 751000 AND 751005;
Het uitvoeringsplan volgt de wijziging en opent eerst de geclusterde tabel (nogmaals, dit is niet gegarandeerd):
Van beide zoekopdrachten mag worden verwacht dat ze dezelfde prestatiekenmerken hebben, omdat ze dezelfde bewerkingen uitvoeren, alleen in een andere volgorde.
Met een rijdoel
Het is duidelijk dat het ontbreken van indexering op de heaptabel het vinden van specifieke rijen normaal gesproken duurder maakt, vergeleken met dezelfde bewerking op de geclusterde tabel. Als we de optimizer vragen om een plan dat de eerste rij snel retourneert, verwachten we dat SQL Server de concatenatie-invoer opnieuw ordent, zodat eerst de goedkope geclusterde tabel wordt geraadpleegd.
De query gebruiken die de heaptabel als eerste vermeldt en een FAST 1-queryhint gebruiken om het rijdoel te specificeren:
SELECT E.Val FROM dbo.Expensive AS E WHERE E.Val BETWEEN 751000 AND 751005 UNION ALL SELECT C.Val FROM dbo.Cheap AS C WHERE C.Val BETWEEN 751000 AND 751005 OPTION (FAST 1);
Het geschatte uitvoeringsplan geproduceerd op een exemplaar van SQL Server 2008 R2 is:
Merk op dat de concatenatie-ingangen opnieuw zijn gerangschikt om de geschatte kosten voor het retourneren van de eerste rij te verlagen. Merk ook op dat de ontbrekende index en resterende I/O-waarschuwingen zijn verdwenen. Geen van beide problemen is van belang bij deze planvorm wanneer het doel is om zo snel mogelijk een enkele rij terug te geven.
Dezelfde query uitgevoerd op SQL Server 2016 (met behulp van een van beide kardinaliteitsschattingsmodellen) is:
SQL Server 2016 heeft de aaneenschakelingsingangen niet opnieuw gerangschikt. De Plan Explorer I/O-waarschuwing is teruggekeerd, maar helaas heeft de optimizer deze keer geen ontbrekende indexwaarschuwing geproduceerd (hoewel dit relevant is).
Algemene herschikking
Zoals vermeld, is het herschrijven na de optimalisatie waarbij de concatenatie-invoer opnieuw wordt geordend, alleen effectief voor:
- SQL Server 2008 R2 en eerder
- Een rijdoel van precies één
Als we echt maar één rij willen retourneren, in plaats van een plan dat is geoptimaliseerd om de eerste rij snel te retourneren (maar uiteindelijk toch alle rijen retourneert), kunnen we een TOP
gebruiken clausule met een afgeleide tabel of gemeenschappelijke tabeluitdrukking (CTE):
SELECT TOP (1) UA.Val FROM ( SELECT E.Val FROM dbo.Expensive AS E WHERE E.Val BETWEEN 751000 AND 751005 UNION ALL SELECT C.Val FROM dbo.Cheap AS C WHERE C.Val BETWEEN 751000 AND 751005 ) AS UA;
Op SQL Server 2008 R2 of eerder levert dit het optimale plan voor opnieuw geordende invoer op:
Op SQL Server 2012, 2014 en 2016 vindt geen herschikking na optimalisatie plaats:
Als we meer dan één rij willen retourneren, bijvoorbeeld met TOP (2)
, wordt de gewenste herschrijving niet toegepast op SQL Server 2008 R2 zelfs als een FAST 1
hint wordt ook gebruikt. In die situatie moeten we onze toevlucht nemen tot trucs zoals het gebruik van TOP
met een variabele en een OPTIMIZE FOR
hint:
DECLARE @TopRows bigint = 2; -- Number of rows actually needed SELECT TOP (@TopRows) UA.Val FROM ( SELECT E.Val FROM dbo.Expensive AS E WHERE E.Val BETWEEN 751000 AND 751005 UNION ALL SELECT C.Val FROM dbo.Cheap AS C WHERE C.Val BETWEEN 751000 AND 751005 ) AS UA OPTION (OPTIMIZE FOR (@TopRows = 1)); -- Just a hint
De queryhint is voldoende om een rijdoel van één in te stellen, terwijl de runtime-waarde van de variabele ervoor zorgt dat het gewenste aantal rijen (2) wordt geretourneerd.
Het daadwerkelijke uitvoeringsplan op SQL Server 2008 R2 is:
Beide geretourneerde rijen zijn afkomstig van de opnieuw geordende zoekinvoer en de tabelscan wordt helemaal niet uitgevoerd. Plan Explorer toont het aantal rijen in rood omdat de schatting voor één rij was (vanwege de hint) terwijl er twee rijen werden aangetroffen tijdens runtime.
Zonder UNION ALL
Dit probleem is ook niet beperkt tot zoekopdrachten die expliciet zijn geschreven met UNION ALL
. Andere constructies zoals EXISTS
en OR
kan er ook toe leiden dat de optimizer een concatenatie-operator introduceert, die kan lijden onder het ontbreken van herschikking van de invoer. Er was een recente vraag over Database Administrators Stack Exchange met precies dit probleem. De query van die vraag transformeren om onze voorbeeldtabellen te gebruiken:
SELECT CASE WHEN EXISTS ( SELECT 1 FROM dbo.Expensive AS E WHERE E.Val BETWEEN 751000 AND 751005 ) OR EXISTS ( SELECT 1 FROM dbo.Cheap AS C WHERE C.Val BETWEEN 751000 AND 751005 ) THEN 1 ELSE 0 END;
Het uitvoeringsplan op SQL Server 2016 heeft de heaptabel op de eerste invoer:
Op SQL Server 2008 R2 is de volgorde van de invoer geoptimaliseerd om het doel van de enkele rij van de semi-join weer te geven:
In het meer optimale plan wordt de heapscan nooit uitgevoerd.
Oplossingen
In sommige gevallen zal het voor de schrijver van de query duidelijk zijn dat een van de concatenatie-invoer altijd goedkoper zal zijn om uit te voeren dan de andere. Als dat waar is, is het redelijk om de query te herschrijven, zodat de goedkopere concatenatie-invoer als eerste in schriftelijke volgorde verschijnt. Dit betekent natuurlijk dat de schrijver van de query zich bewust moet zijn van deze beperking van de optimalisatie en bereid moet zijn om te vertrouwen op ongedocumenteerd gedrag.
Een moeilijker probleem doet zich voor wanneer de kosten van de concatenatie-ingangen variëren met de omstandigheden, misschien afhankelijk van parameterwaarden. Met behulp van OPTION (RECOMPILE)
zal niet helpen op SQL Server 2012 of later. Die optie kan helpen op SQL Server 2008 R2 of eerder, maar alleen als ook aan de eis van één rij wordt voldaan.
Als er zorgen zijn over het vertrouwen op waargenomen gedrag (queryplan-concatenatie-invoer die overeenkomt met de tekstuele volgorde van de query), kan een plangids worden gebruikt om de planvorm te forceren. Waar verschillende invoerorders optimaal zijn voor verschillende omstandigheden, kunnen meerdere plangidsen worden gebruikt, waarbij de voorwaarden vooraf nauwkeurig kunnen worden gecodeerd. Dit is echter niet ideaal.
Laatste gedachten
De query-optimizer van SQL Server bevat inderdaad een op kosten gebaseerde verkenningsregel, UNIAReorderInputs
, die in staat is om variaties in de invoervolgorde van aaneenschakelingen te genereren en alternatieven te onderzoeken tijdens op kosten gebaseerde optimalisatie (niet als een eenmalige herschrijving na optimalisatie).
Deze regel is momenteel niet ingeschakeld voor algemeen gebruik. Voor zover ik weet, wordt het alleen geactiveerd wanneer een plangids of USE PLAN
hint is aanwezig. Hierdoor kan de engine met succes een plan forceren dat is gegenereerd voor een query die in aanmerking kwam voor herschrijven van de invoer-herschikking, zelfs als de huidige query niet in aanmerking komt.
Ik heb het gevoel dat deze verkenningsregel opzettelijk beperkt is tot dit gebruik, omdat zoekopdrachten die baat zouden hebben bij het opnieuw ordenen van concatenatie-invoer als onderdeel van op kosten gebaseerde optimalisatie, niet voldoende algemeen worden geacht, of misschien omdat er bezorgdheid bestaat dat de extra inspanning niet zou lonen uit. Mijn eigen mening is dat het opnieuw ordenen van de invoer van de aaneenschakelingsoperator altijd moet worden onderzocht wanneer een rijdoel van kracht is.
Het is ook jammer dat de (beperktere) herschrijving na optimalisatie niet effectief is in SQL Server 2012 of later. Dit kan te wijten zijn aan een subtiele bug, maar ik kon hier niets over vinden in de documentatie, kennisbank of op Connect. Ik heb hier een nieuw Connect-item toegevoegd.
Update 9 augustus 2017 :Dit is nu opgelost onder traceringsvlag 4199 voor SQL Server 2014 en 2016, zie KB 4023419:
FIX:Query met UNION ALL en een rijdoel kan langzamer worden uitgevoerd in SQL Server 2014 of latere versies in vergelijking met SQL Server 2008 R2