sql >> Database >  >> RDS >> Sqlserver

SQL Server v.Next:STRING_AGG() prestaties

Hoewel SQL Server op Linux bijna alle krantenkoppen over v.Next heeft gestolen, zijn er nog enkele andere interessante ontwikkelingen in de volgende versie van ons favoriete databaseplatform. Op het T-SQL-front hebben we eindelijk een ingebouwde manier om gegroepeerde tekenreeksaaneenschakeling uit te voeren:STRING_AGG() .

Laten we zeggen dat we de volgende eenvoudige tabelstructuur hebben:

CREATE TABLE dbo.Objects
(
  [object_id]   int, 
  [object_name] nvarchar(261),
  CONSTRAINT PK_Objects PRIMARY KEY([object_id])
);
 
CREATE TABLE dbo.Columns
(
  [object_id] int NOT NULL
    FOREIGN KEY REFERENCES dbo.Objects([object_id]), 
  column_name sysname,
  CONSTRAINT PK_Columns PRIMARY KEY ([object_id],column_name)
);

Voor prestatietests gaan we dit invullen met sys.all_objects en sys.all_columns . Maar laten we voor een eenvoudige demonstratie eerst de volgende rijen toevoegen:

INSERT dbo.Objects([object_id],[object_name])
  VALUES(1,N'Employees'),(2,N'Orders');
 
INSERT dbo.Columns([object_id],column_name)
  VALUES(1,N'EmployeeID'),(1,N'CurrentStatus'),
        (2,N'OrderID'),(2,N'OrderDate'),(2,N'CustomerID');

Als de forums een indicatie zijn, is het een veel voorkomende vereiste om voor elk object een rij te retourneren, samen met een door komma's gescheiden lijst met kolomnamen. (Extrapoleer dat naar elk type entiteit dat u op deze manier modelleert - productnamen die zijn gekoppeld aan een bestelling, namen van onderdelen die betrokken zijn bij de assemblage van een product, ondergeschikten die rapporteren aan een manager, enz.) Dus, bijvoorbeeld, met de bovenstaande gegevens zouden we wil output als volgt:

object       columns
---------    ----------------------------
Employees    EmployeeID,CurrentStatus
Orders       OrderID,OrderDate,CustomerID

De manier waarop we dit in de huidige versies van SQL Server zouden bereiken, is waarschijnlijk om FOR XML PATH te gebruiken , zoals ik in deze eerdere post heb laten zien de meest efficiënte buiten CLR te zijn. In dit voorbeeld ziet het er als volgt uit:

SELECT [object]  = o.[object_name],
       [columns] = STUFF(
                    (SELECT N',' + c.column_name
                       FROM dbo.Columns AS c
                       WHERE c.[object_id] = o.[object_id]
                       FOR XML PATH, TYPE
                    ).value(N'.[1]',N'nvarchar(max)'),1,1,N'')
FROM dbo.Objects AS o;

Zoals te verwachten is, krijgen we dezelfde output die hierboven is gedemonstreerd. In SQL Server v.Next kunnen we dit eenvoudiger uitdrukken:

SELECT [object]  = o.[object_name],
       [columns] = STRING_AGG(c.column_name, N',')
FROM dbo.Objects AS o
INNER JOIN dbo.Columns AS c
ON o.[object_id] = c.[object_id]
GROUP BY o.[object_name];

Nogmaals, dit levert exact dezelfde uitvoer op. En we waren in staat om dit te doen met een native functie, waarbij we zowel de dure FOR XML PATH steigers, en de STUFF() functie die wordt gebruikt om de eerste komma te verwijderen (dit gebeurt automatisch).

Hoe zit het met de bestelling?

Een van de problemen met veel van de kludge-oplossingen voor gegroepeerde aaneenschakeling is dat de volgorde van de door komma's gescheiden lijst als willekeurig en niet-deterministisch moet worden beschouwd.

Voor het XML PATH oplossing, heb ik in een ander eerder bericht aangetoond dat het toevoegen van een ORDER BY is triviaal en gegarandeerd. Dus in dit voorbeeld kunnen we de kolomlijst alfabetisch op kolomnaam ordenen in plaats van het aan SQL Server over te laten om te sorteren (of niet):

SELECT [object]  = [object_name],
       [columns] = STUFF(
                    (SELECT N',' +c.column_name
                       FROM dbo.Columns AS c
                       WHERE c.[object_id] = o.[object_id]
                       ORDER BY c.column_name -- only change
                       FOR XML PATH, TYPE
                    ).value(N'.[1]',N'nvarchar(max)'),1,1,N'')
FROM dbo.Objects AS o;

Uitgang:

object       columns
---------    ----------------------------
Employees    CurrentStatus,EmployeeID
Order        CustomerID,OrderDate,OrderID

CTP 1.1 voegt WITHIN GROUP . toe naar STRING_AGG() , dus met de nieuwe aanpak kunnen we zeggen:

SELECT [object]  = o.[object_name],
       [columns] = STRING_AGG(c.column_name, N',')
                   WITHIN GROUP (ORDER BY c.column_name) -- only change
FROM dbo.Objects AS o
INNER JOIN dbo.Columns AS c
ON o.[object_id] = c.[object_id]
GROUP BY o.[object_name];

Nu krijgen we dezelfde resultaten. Merk op dat, net als een normale ORDER BY clausule, kunt u meerdere bestelkolommen of uitdrukkingen toevoegen binnen WITHIN GROUP () .

Ok, al optreden!

Met behulp van quad-core 2,6 GHz-processors, 8 GB geheugen en SQL Server CTP1.1 (14.0.100.187), heb ik een nieuwe database gemaakt, deze tabellen opnieuw gemaakt en rijen toegevoegd uit sys.all_objects en sys.all_columns . Ik heb ervoor gezorgd dat alleen objecten zijn opgenomen die ten minste één kolom hadden:

INSERT dbo.Objects([object_id], [object_name]) -- 656 rows
  SELECT [object_id], QUOTENAME(s.name) + N'.' + QUOTENAME(o.name) 
    FROM sys.all_objects AS o
    INNER JOIN sys.schemas AS s 
    ON o.[schema_id] = s.[schema_id]
    WHERE EXISTS
    (
      SELECT 1 FROM sys.all_columns 
      WHERE [object_id] = o.[object_id]
    );
 
INSERT dbo.Columns([object_id], column_name) -- 8,085 rows 
  SELECT [object_id], name 
    FROM sys.all_columns AS c  
    WHERE EXISTS
    (
      SELECT 1 FROM dbo.Objects 
      WHERE [object_id] = c.[object_id]
    );

Op mijn systeem leverde dit 656 objecten en 8.085 kolommen op (uw systeem kan iets andere getallen opleveren).

De plannen

Laten we eerst de abonnementen en Tabel I/O-tabbladen voor onze twee ongeordende query's vergelijken met Plan Explorer. Dit zijn de algemene runtime-statistieken:

Runtime-statistieken voor XML PATH (boven) en STRING_AGG() (onder)

Het grafische plan en Tabel I/O van het FOR XML PATH vraag:


Plan en tabel I/O voor XML PATH, geen bestelling

En van de STRING_AGG versie:


Plan en tabel I/O voor STRING_AGG, geen bestelling

Voor de laatste lijkt het zoeken naar geclusterde indexen een beetje verontrustend. Dit leek een goede reden om de zelden gebruikte FORCESCAN . te testen hint (en nee, dit zou zeker niet helpen bij het FOR XML PATH vraag):

SELECT [object]  = o.[object_name],
       [columns] = STRING_AGG(c.column_name, N',')
FROM dbo.Objects AS o
INNER JOIN dbo.Columns AS c WITH (FORCESCAN) -- added hint
ON o.[object_id] = c.[object_id]
GROUP BY o.[object_name];

Nu zien het plan en het tabblad Tabel I/O er veel uit beter, althans op het eerste gezicht:


Plan en tabel I/O voor STRING_AGG(), geen bestelling, met FORCESCAN

De geordende versies van de query's genereren ongeveer dezelfde plannen. Voor het FOR XML PATH versie is een sortering toegevoegd:

Sorteren toegevoegd in FOR XML PATH-versie

Voor STRING_AGG() , wordt in dit geval gekozen voor een scan, ook zonder de FORCESCAN hint, en er is geen extra sorteerbewerking vereist - dus het plan ziet er identiek uit aan de FORCESCAN versie.

Op schaal

Als we kijken naar een plan en eenmalige runtime-statistieken, kunnen we misschien een idee krijgen of STRING_AGG() presteert beter dan het bestaande FOR XML PATH oplossing, maar een grotere test is misschien logischer. Wat gebeurt er als we de gegroepeerde aaneenschakeling 5.000 keer uitvoeren?

SELECT SYSDATETIME();
GO
 
DECLARE @x nvarchar(max);
SELECT @x = STRING_AGG(c.column_name, N',')
  FROM dbo.Objects AS o
  INNER JOIN dbo.Columns AS c
  ON o.[object_id] = c.[object_id]
  GROUP BY o.[object_name];
GO 5000
SELECT [string_agg, unordered] = SYSDATETIME();
GO
 
DECLARE @x nvarchar(max);
SELECT @x = STRING_AGG(c.column_name, N',')
  FROM dbo.Objects AS o
  INNER JOIN dbo.Columns AS c WITH (FORCESCAN)
  ON o.[object_id] = c.[object_id]
  GROUP BY o.[object_name];
GO 5000
SELECT [string_agg, unordered, forcescan] = SYSDATETIME();
 
GO
DECLARE @x nvarchar(max);
SELECT @x = STUFF((SELECT N',' +c.column_name
  FROM dbo.Columns AS c
  WHERE c.[object_id] = o.[object_id]
  FOR XML PATH, TYPE).value(N'.[1]',N'nvarchar(max)'),1,1,N'')
FROM dbo.Objects AS o;
GO 5000
SELECT [for xml path, unordered] = SYSDATETIME();
 
GO
DECLARE @x nvarchar(max);
SELECT @x = STRING_AGG(c.column_name, N',')
  WITHIN GROUP (ORDER BY c.column_name)
  FROM dbo.Objects AS o
  INNER JOIN dbo.Columns AS c
  ON o.[object_id] = c.[object_id]
  GROUP BY o.[object_name];
GO 5000
SELECT [string_agg, ordered] = SYSDATETIME();
 
GO
DECLARE @x nvarchar(max);
SELECT @x = STUFF((SELECT N',' +c.column_name
  FROM dbo.Columns AS c
  WHERE c.[object_id] = o.[object_id]
  ORDER BY c.column_name
  FOR XML PATH, TYPE).value(N'.[1]',N'nvarchar(max)'),1,1,N'')
FROM dbo.Objects AS o
ORDER BY o.[object_name];
GO 5000
SELECT [for xml path, ordered] = SYSDATETIME();

Nadat ik dit script vijf keer had uitgevoerd, nam ik het gemiddelde van de duur en hier zijn de resultaten:

Duur (milliseconden) voor verschillende benaderingen van gegroepeerde aaneenschakeling

We kunnen zien dat onze FORCESCAN hint heeft de zaken echt erger gemaakt - terwijl we de kosten hebben verschoven van het zoeken naar geclusterde indexen, was het soort eigenlijk veel erger, hoewel de geschatte kosten ze relatief gelijkwaardig achtten. Wat nog belangrijker is, we kunnen zien dat STRING_AGG() biedt wel een prestatievoordeel, ongeacht of de aaneengeschakelde snaren op een specifieke manier moeten worden besteld. Net als bij STRING_SPLIT() , waar ik in maart naar keek, ben ik behoorlijk onder de indruk dat deze functie ruim vóór "v1" schaalt.

Ik heb verdere tests gepland, misschien voor een toekomstige post:

  • Als alle gegevens uit één enkele tabel komen, met en zonder een index die ordening ondersteunt
  • Vergelijkbare prestatietests op Linux

Als je in de tussentijd specifieke gebruiksscenario's hebt voor gegroepeerde aaneenschakeling, deel ze dan hieronder (of e-mail me op [email protected]). Ik sta er altijd voor open om ervoor te zorgen dat mijn tests zo realistisch mogelijk zijn.


  1. Hoe WEEKOFYEAR() werkt in MariaDB

  2. Slaapstandprobleem met Oracle Trigger voor het genereren van id uit een reeks

  3. Hoe een IF-statement in SQL uit te voeren?

  4. Wijzig het type varchar-veld in integer:kan niet automatisch worden gecast om integer te typen