sql >> Database >  >> RDS >> Sqlserver

SQL Server v.Next:STRING_AGG Prestaties, deel 2

Vorige week heb ik een paar snelle prestatievergelijkingen gemaakt, waarbij ik gebruik maakte van de nieuwe STRING_AGG() functie tegen het traditionele FOR XML PATH aanpak die ik al tijden gebruik. Ik heb zowel ongedefinieerde/willekeurige volgorde als expliciete volgorde getest, en STRING_AGG() kwam in beide gevallen als beste uit de bus:

    SQL Server v.Next:STRING_AGG() Prestaties, Deel 1

Voor die tests heb ik verschillende dingen weggelaten (niet allemaal opzettelijk):

  1. Mikael Eriksson en Grzegorz Łyp wezen er allebei op dat ik niet de absoluut meest efficiënte FOR XML PATH gebruikte. bouwen (en voor de duidelijkheid, ik heb dat nooit gedaan).
  2. Ik heb geen tests uitgevoerd op Linux; alleen op Windows. Ik verwacht niet dat die heel verschillend zullen zijn, maar aangezien Grzegorz heel verschillende tijdsduren zag, is dit nader onderzoek waard.
  3. Ik heb ook alleen getest wanneer uitvoer een eindige, niet-LOB-tekenreeks zou zijn - wat volgens mij de meest voorkomende gebruikssituatie is (ik denk niet dat mensen gewoonlijk elke rij in een tabel samenvoegen tot één door komma's gescheiden string, maar dit is de reden waarom ik in mijn vorige post om uw use-case (s) heb gevraagd).
  4. Voor de besteltests heb ik geen index gemaakt die nuttig zou kunnen zijn (of iets geprobeerd waarbij alle gegevens uit één enkele tabel kwamen).

In dit bericht ga ik een paar van deze items behandelen, maar niet allemaal.

VOOR XML-PAD

Ik had het volgende gebruikt:

... FOR XML PATH, TYPE).value(N'.[1]', ...

Na deze opmerking van Mikael heb ik mijn code bijgewerkt om in plaats daarvan deze iets andere constructie te gebruiken:

... FOR XML PATH(''), TYPE).value(N'text()[1]', ...

Linux versus Windows

Aanvankelijk had ik alleen de moeite genomen om tests op Windows uit te voeren:

Microsoft SQL Server vNext (CTP1.1) - 14.0.100.187 (X64) 
	Dec 10 2016 02:51:11 
	Copyright (C) 2016 Microsoft Corporation. All rights reserved.
	Developer Edition (64-bit) on Windows Server 2016 Datacenter 6.3  (Build 14393: ) (Hypervisor)

Maar Grzegorz maakte duidelijk dat hij (en vermoedelijk vele anderen) alleen toegang had tot de Linux-smaak van CTP 1.1. Dus heb ik Linux aan mijn testmatrix toegevoegd:

Microsoft SQL Server vNext (CTP1.1) - 14.0.100.187 (X64) 
	Dec 10 2016 02:51:11 
	Copyright (C) 2016 Microsoft Corporation. All rights reserved.
	on Linux (Ubuntu 16.04.1 LTS)

Enkele interessante maar volledig tangentiële observaties:

  • @@VERSION toont geen editie in deze build, maar SERVERPROPERTY('Edition') retourneert de verwachte Developer Edition (64-bit) .
  • Op basis van de bouwtijden die in de binaire bestanden zijn gecodeerd, lijken de Windows- en Linux-versies nu op hetzelfde moment en uit dezelfde bron te zijn gecompileerd. Of dit was een gek toeval.

Ongeordende tests

Ik begon met het testen van de willekeurig geordende uitvoer (waar er geen expliciet gedefinieerde volgorde is voor de aaneengeschakelde waarden). In navolging van Grzegorz gebruikte ik WideWorldImporters (standaard), maar voerde een join uit tussen Sales.Orders en Sales.OrderLines . De fictieve vereiste hier is om een ​​lijst van alle bestellingen uit te voeren, en samen met elke bestelling, een door komma's gescheiden lijst van elke StockItemID .

Sinds StockItemID is een geheel getal, we kunnen een gedefinieerde varchar . gebruiken , wat betekent dat de string 8000 tekens lang kan zijn voordat we ons zorgen hoeven te maken dat we MAX nodig hebben. Aangezien een int een maximale lengte van 11 kan hebben (echt 10, indien niet ondertekend), plus een komma, betekent dit dat een bestelling in het ergste geval ongeveer 8.000/12 (666) voorraadartikelen moet ondersteunen (bijv. alle StockItemID-waarden hebben 11 cijfers). In ons geval is de langste ID 3 cijfers, dus totdat er gegevens worden toegevoegd, zouden we in feite 8.000/4 (2.000) unieke voorraadartikelen in een enkele bestelling nodig hebben om MAX te rechtvaardigen. In ons geval zijn er in totaal maar 227 artikelen op voorraad, dus MAX is niet nodig, maar dat moet je goed in de gaten houden. Als zo'n grote reeks in uw scenario mogelijk is, moet u varchar(max) gebruiken in plaats van de standaard (STRING_AGG() retourneert nvarchar(max) , maar wordt afgekapt tot 8.000 bytes tenzij de invoer is een MAX-type).

De initiële zoekopdrachten (om voorbeelduitvoer te tonen en om de duur van enkele uitvoeringen te observeren):

SET STATISTICS TIME ON;
GO
 
SELECT o.OrderID, StockItemIDs = STRING_AGG(ol.StockItemID, ',')
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO
 
SELECT o.OrderID, 
  StockItemIDs = STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,'')
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO
 
SET STATISTICS TIME OFF;
 
/*
   Sample output:
 
       OrderID    StockItemIDs
       =======    ============
       1          67
       2          50,10
       3          114
       4          206,130,50
       5          128,121,155
 
   Important SET STATISTICS TIME metrics (SQL Server Execution Times):
 
      Windows:
        STRING_AGG:    CPU time =  217 ms,  elapsed time =  405 ms.
        FOR XML PATH:  CPU time = 1954 ms,  elapsed time = 2097 ms.
 
      Linux:
        STRING_AGG:    CPU time =  627 ms,  elapsed time =  472 ms.
        FOR XML PATH:  CPU time = 2188 ms,  elapsed time = 2223 ms.
*/

Ik negeerde de ontledings- en compileertijdgegevens volledig, omdat ze altijd precies nul waren of dichtbij genoeg om niet relevant te zijn. Er waren kleine afwijkingen in de uitvoeringstijden voor elke run, maar niet veel - de opmerkingen hierboven weerspiegelen de typische delta in runtime (STRING_AGG leek daar een klein voordeel te halen uit parallellisme, maar alleen op Linux, terwijl FOR XML PATH niet op beide platforms). Beide machines hadden een enkele socket, toegewezen quad-core CPU, 8 GB geheugen, kant-en-klare configuratie en geen andere activiteit.

Toen wilde ik op schaal testen (eenvoudig een enkele sessie die 500 keer dezelfde query uitvoert). Ik wilde niet alle uitvoer, zoals in de bovenstaande query, 500 keer retourneren, omdat dat SSMS zou hebben overweldigd - en hopelijk vertegenwoordigt het hoe dan ook geen real-world queryscenario's. Dus ik heb de uitvoer toegewezen aan variabelen en heb zojuist de totale tijd voor elke batch gemeten:

SELECT sysdatetime();
GO
 
DECLARE @i int, @x varchar(8000);
SELECT @i = o.OrderID, @x = STRING_AGG(ol.StockItemID, ',')
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO 500
 
SELECT sysdatetime();
GO
 
DECLARE @i int, @x varchar(8000);
SELECT @i = o.OrderID, 
    @x = STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,'')
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO 500
 
SELECT sysdatetime();

Ik heb die tests drie keer uitgevoerd en het verschil was enorm - bijna een orde van grootte. Hier is de gemiddelde duur van de drie tests:

Gemiddelde duur, in milliseconden, voor 500 uitvoeringen van variabele toewijzing

Ik heb op deze manier ook een aantal andere dingen getest, vooral om er zeker van te zijn dat ik de soorten tests dekte die Grzegorz uitvoerde (zonder het LOB-gedeelte).

  1. Alleen de lengte van de uitvoer selecteren
  2. De maximale lengte van de uitvoer ophalen (van een willekeurige rij)
  3. Alle uitvoer selecteren in een nieuwe tabel

Alleen de lengte van de uitvoer selecteren

Deze code loopt alleen door elke bestelling, voegt alle StockItemID-waarden samen en retourneert vervolgens alleen de lengte.

SET STATISTICS TIME ON;
GO
 
SELECT LEN(STRING_AGG(ol.StockItemID, ','))
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO
 
SELECT LEN(STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,''))
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO
 
SET STATISTICS TIME OFF;
 
/*
  Windows:
    STRING_AGG:   CPU time =  142 ms,  elapsed time =  351 ms.
    FOR XML PATH: CPU time = 1984 ms,  elapsed time = 2120 ms.
 
  Linux:
    STRING_AGG:   CPU time =  310 ms,  elapsed time =  191 ms.
    FOR XML PATH: CPU time = 2149 ms,  elapsed time = 2167 ms.    
*/

Voor de batchversie heb ik opnieuw variabele toewijzing gebruikt in plaats van te proberen veel resultatensets terug te sturen naar SSMS. De variabele toewijzing zou op een willekeurige rij terechtkomen, maar dit vereist nog steeds volledige scans, omdat de willekeurige rij niet als eerste wordt geselecteerd.

SELECT sysdatetime();
GO
 
DECLARE @i int;
SELECT @i = LEN(STRING_AGG(ol.StockItemID, ','))
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO 500
 
SELECT sysdatetime();
GO
 
DECLARE @i int;
SELECT @i = LEN(STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,''))
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO 500
 
SELECT sysdatetime();

Prestatiestatistieken van 500 uitvoeringen:

500 uitvoeringen van het toewijzen van LEN() aan een variabele

Nogmaals, we zien FOR XML PATH is veel langzamer, zowel op Windows als Linux.

De maximale lengte van de uitgang selecteren

Een kleine variatie op de vorige test, deze haalt gewoon het maximum lengte van de aaneengeschakelde uitvoer:

SET STATISTICS TIME ON;
GO
 
SELECT MAX(s) FROM (SELECT s = LEN(STRING_AGG(ol.StockItemID, ','))
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID) AS x;
GO
 
SELECT MAX(s) FROM (SELECT s = LEN(STUFF(
    (SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),
	1,1,''))
  FROM Sales.Orders AS o
  GROUP BY o.OrderID) AS x;
GO
 
SET STATISTICS TIME OFF;
 
/*
  Windows:
    STRING_AGG:   CPU time =  188 ms,  elapsed time =  48 ms.
    FOR XML PATH: CPU time = 1891 ms,  elapsed time = 907 ms.
 
  Linux:
    STRING_AGG:   CPU time =  270 ms,  elapsed time =   83 ms.
    FOR XML PATH: CPU time = 2725 ms,  elapsed time = 1205 ms.
*/

En op schaal wijzen we die uitvoer gewoon weer toe aan een variabele:

SELECT sysdatetime();
GO
 
DECLARE @i int;
SELECT @i = MAX(s) FROM (SELECT s = LEN(STRING_AGG(ol.StockItemID, ','))
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID) AS x;
GO 500
 
SELECT sysdatetime();
GO
 
DECLARE @i int;
SELECT @i = MAX(s) FROM (SELECT s = LEN(STUFF
  (
    (SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),
	1,1,''))
  FROM Sales.Orders AS o
  GROUP BY o.OrderID) AS x;
GO 500
 
SELECT sysdatetime();

Prestatieresultaten, voor 500 uitvoeringen, gemiddeld over drie runs:

500 uitvoeringen van het toewijzen van MAX(LEN()) aan een variabele

Je zou een patroon kunnen opmerken in deze tests - FOR XML PATH is altijd een hond, zelfs met de prestatieverbeteringen die in mijn vorige bericht zijn voorgesteld.

SELECTEER IN

Ik wilde zien of de methode van aaneenschakeling enige invloed had op schrijven de gegevens terug naar schijf, zoals het geval is in sommige andere scenario's:

SET NOCOUNT ON;
GO
SET STATISTICS TIME ON;
GO
 
DROP TABLE IF EXISTS dbo.HoldingTank_AGG;
 
SELECT o.OrderID, x = STRING_AGG(ol.StockItemID, ',')
  INTO dbo.HoldingTank_AGG
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO
 
DROP TABLE IF EXISTS dbo.HoldingTank_XML;
 
SELECT o.OrderID, x = STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,'')
  INTO dbo.HoldingTank_XML
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO
 
SET STATISTICS TIME OFF;
 
/*
  Windows:
    STRING_AGG:   CPU time =  218 ms,  elapsed time =   90 ms.
    FOR XML PATH: CPU time = 4202 ms,  elapsed time = 1520 ms.
 
  Linux:
    STRING_AGG:   CPU time =  277 ms,  elapsed time =  108 ms.
    FOR XML PATH: CPU time = 4308 ms,  elapsed time = 1583 ms.
*/

In dit geval zien we dat misschien SELECT INTO kon profiteren van een beetje parallellisme, maar toch zien we FOR XML PATH worstelen, met looptijden die een orde van grootte langer zijn dan STRING_AGG .

De batchversie heeft zojuist de SET STATISTICS-commando's verwisseld voor SELECT sysdatetime(); en dezelfde GO 500 . toegevoegd na de twee hoofdbatches zoals bij de vorige tests. Hier is hoe dat uitpakte (nogmaals, vertel me of je deze eerder hebt gehoord):

500 uitvoeringen van SELECT INTO

Bestelde tests

Ik heb dezelfde tests uitgevoerd met de geordende syntaxis, bijvoorbeeld:

... STRING_AGG(ol.StockItemID, ',') 
    WITHIN GROUP (ORDER BY ol.StockItemID) ...
 
... WHERE ol.OrderID = o.OrderID
    ORDER BY ol.StockItemID
    FOR XML PATH('') ...

Dit had heel weinig invloed op iets - dezelfde set van vier testopstellingen vertoonde over de hele linie bijna identieke statistieken en patronen.

Ik ben benieuwd of dit anders is wanneer de aaneengeschakelde uitvoer in niet-LOB staat of waar de aaneenschakeling strings moet bestellen (met of zonder een ondersteunende index).

Conclusie

Voor niet-LOB-tekenreeksen , is het mij duidelijk dat STRING_AGG heeft een definitief prestatievoordeel ten opzichte van FOR XML PATH , zowel op Windows als Linux. Merk op dat, om de eis van varchar(max) te vermijden, of nvarchar(max) , heb ik niets gebruikt dat lijkt op de tests die Grzegorz uitvoerde, wat zou betekenen dat alle waarden uit een kolom, over een hele tabel, in een enkele reeks zouden worden samengevoegd. In mijn volgende bericht zal ik de gebruikssituatie bekijken waarbij de uitvoer van de aaneengeschakelde string mogelijk groter is dan 8.000 bytes, en dus LOB-typen en -conversies zouden moeten worden gebruikt.


  1. Wanneer sorteert SQL Server terugspoelen?

  2. Oracle DROP TABLE INDIEN BESTAAT Alternatieven

  3. Een gids voor de MariaDB Columnstore voor MySQL-beheerders

  4. Laden van klasse com.mysql.jdbc.Driver ... is verouderd bericht