sql >> Database >  >> RDS >> Database

Gegroepeerde aaneenschakeling :Duplicaten bestellen en verwijderen

In mijn laatste bericht heb ik een aantal efficiënte benaderingen van gegroepeerde aaneenschakeling laten zien. Deze keer wilde ik het hebben over een paar extra facetten van dit probleem die we gemakkelijk kunnen bereiken met de FOR XML PATH aanpak:de lijst ordenen en duplicaten verwijderen.

Er zijn een paar manieren waarop ik heb gezien dat mensen de door komma's gescheiden lijst willen bestellen. Soms willen ze dat het item in de lijst alfabetisch wordt gerangschikt; Dat liet ik al zien in mijn vorige post. Maar soms willen ze dat het wordt gesorteerd op een ander kenmerk dat eigenlijk niet in de uitvoer wordt geïntroduceerd; bijvoorbeeld, misschien wil ik de lijst eerst op meest recente item sorteren. Laten we een eenvoudig voorbeeld nemen, waar we een Employees-tabel en een CoffeeOrders-tabel hebben. Laten we de bestellingen van één persoon voor een paar dagen invullen:

CREATE TABLE dbo.Employees
(
  EmployeeID INT PRIMARY KEY,
  Name NVARCHAR(128)
);
 
INSERT dbo.Employees(EmployeeID, Name) VALUES(1, N'Jack');
 
CREATE TABLE dbo.CoffeeOrders
(
  EmployeeID INT NOT NULL REFERENCES dbo.Employees(EmployeeID),
  OrderDate DATE NOT NULL,
  OrderDetails NVARCHAR(64)
);
 
INSERT dbo.CoffeeOrders(EmployeeID, OrderDate, OrderDetails)
  VALUES(1,'20140801',N'Large double double'),
        (1,'20140802',N'Medium double double'),
        (1,'20140803',N'Large Vanilla Latte'),
        (1,'20140804',N'Medium double double');

Als we de bestaande aanpak gebruiken zonder een ORDER BY . op te geven , krijgen we een willekeurige volgorde (in dit geval is het zeer waarschijnlijk dat u de rijen ziet in de volgorde waarin ze zijn ingevoegd, maar vertrouw daar niet op met grotere datasets, meer indexen, enz.):

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Resultaten (onthoud dat u *andere* resultaten kunt krijgen, tenzij u een ORDER BY opgeeft ):

Naam | Bestellingen
Jack | Groot dubbel dubbel, Medium dubbel dubbel, Large Vanilla Latte, Medium dubbel dubbel

Als we de lijst alfabetisch willen ordenen, is dat eenvoudig; we voegen gewoon ORDER BY c.OrderDetails . toe :

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  ORDER BY c.OrderDetails  -- only change
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Resultaten:

Naam | Bestellingen
Jack | Groot dubbel dubbel, Large Vanilla Latte, Medium dubbel dubbel, Medium dubbel dubbel

We kunnen ook sorteren op een kolom die niet in de resultatenset voorkomt; we kunnen bijvoorbeeld de meest recente koffiebestelling eerst bestellen:

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  ORDER BY c.OrderDate DESC  -- only change
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Resultaten:

Naam | Bestellingen
Jack | Medium dubbel dubbel, Large Vanilla Latte, Medium dubbel dubbel, Groot dubbel dubbel

Een ander ding dat we vaak willen doen, is het verwijderen van duplicaten; er is immers weinig reden om "Medium double double" twee keer te zien. We kunnen dat elimineren door GROUP BY te gebruiken :

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  GROUP BY c.OrderDetails  -- removed ORDER BY and added GROUP BY here
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Nu gebeurt dit *toevallig* om de uitvoer alfabetisch te ordenen, maar nogmaals, u kunt hier niet op vertrouwen:

Naam | Bestellingen
Jack | Groot dubbel dubbel, Large Vanilla Latte, Medium dubbel dubbel

Als je die bestelling op deze manier wilt garanderen, kun je gewoon opnieuw een ORDER BY toevoegen:

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  GROUP BY c.OrderDetails
  ORDER BY c.OrderDetails  -- added ORDER BY
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

De resultaten zijn hetzelfde (maar ik herhaal, dit is in dit geval gewoon toeval; als je deze volgorde wilt, zeg dat dan altijd):

Naam | Bestellingen
Jack | Groot dubbel dubbel, Large Vanilla Latte, Medium dubbel dubbel

Maar wat als we duplicaten willen elimineren * en * de lijst eerst op de meest recente koffiebestelling willen sorteren? Uw eerste neiging zou kunnen zijn om de GROUP BY . te behouden en verander gewoon de ORDER BY , zoals dit:

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  GROUP BY c.OrderDetails
  ORDER BY c.OrderDate DESC  -- changed ORDER BY
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Dat zal niet werken, aangezien de OrderDate is niet gegroepeerd of geaggregeerd als onderdeel van de zoekopdracht:

Msg 8127, niveau 16, staat 1, regel 64
Kolom "dbo.CoffeeOrders.OrderDate" is ongeldig in de ORDER BY-clausule omdat deze niet is opgenomen in een aggregatiefunctie of de GROUP BY-clausule.

Een tijdelijke oplossing, die de vraag weliswaar een beetje lelijker maakt, is om de bestellingen eerst apart te groeperen en dan alleen de rijen met de maximale datum voor die koffiebestelling per medewerker te nemen:

;WITH grouped AS
(
  SELECT EmployeeID, OrderDetails, OrderDate = MAX(OrderDate)
   FROM dbo.CoffeeOrders
   GROUP BY EmployeeID, OrderDetails
)
SELECT e.Name, Orders = STUFF((SELECT N', ' + g.OrderDetails
  FROM grouped AS g
  WHERE g.EmployeeID = e.EmployeeID
  ORDER BY g.OrderDate DESC
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Resultaten:

Naam | Bestellingen
Jack | Medium dubbel dubbel, Large Vanilla Latte, Large dubbel dubbel

Hiermee bereiken we onze beide doelen:we hebben duplicaten geëlimineerd en we hebben de lijst gerangschikt op iets dat niet echt in de lijst staat.

Prestaties

U vraagt ​​zich misschien af ​​hoe slecht deze methoden presteren ten opzichte van een robuustere dataset. Ik ga onze tabel vullen met 100.000 rijen, kijken hoe ze het doen zonder extra indexen en dan dezelfde zoekopdrachten opnieuw uitvoeren met een beetje indexafstemming om onze zoekopdrachten te ondersteunen. Dus eerst 100.000 rijen krijgen verspreid over 1.000 medewerkers:

-- clear out our tiny sample data
DELETE dbo.CoffeeOrders;
DELETE dbo.Employees;
 
-- create 1000 fake employees
INSERT dbo.Employees(EmployeeID, Name) 
SELECT TOP (1000) 
  EmployeeID = ROW_NUMBER() OVER (ORDER BY t.[object_id]),
  Name = LEFT(t.name + c.name, 128)
FROM sys.all_objects AS t
INNER JOIN sys.all_columns AS c
ON t.[object_id] = c.[object_id];
 
-- create 100 fake coffee orders for each employee
-- we may get duplicates in here for name
INSERT dbo.CoffeeOrders(EmployeeID, OrderDate, OrderDetails)
SELECT e.EmployeeID, 
  OrderDate = DATEADD(DAY, ROW_NUMBER() OVER 
    (PARTITION BY e.EmployeeID ORDER BY c.[guid]), '20140630'),
  LEFT(c.name, 64)
 FROM dbo.Employees AS e
 CROSS APPLY 
 (
   SELECT TOP (100) name, [guid] = NEWID() 
     FROM sys.all_columns 
     WHERE [object_id] < e.EmployeeID
     ORDER BY NEWID()
 ) AS c;

Laten we nu elk van onze query's twee keer uitvoeren en kijken hoe de timing is bij de tweede poging (we zullen hier een sprong in het diepe wagen en aannemen dat we - in een ideale wereld - zullen werken met een geprepareerde cache ). Ik heb deze uitgevoerd in SQL Sentry Plan Explorer, omdat dit de gemakkelijkste manier is die ik ken om een ​​aantal individuele vragen te vergelijken:

Duur en andere runtime-statistieken voor verschillende FOR XML PATH-benaderingen

Deze timings (duur is in milliseconden) zijn echt niet zo slecht IMHO, als je bedenkt wat hier eigenlijk wordt gedaan. Het meest gecompliceerde plan, in ieder geval visueel, leek het plan te zijn waarbij we duplicaten verwijderden en sorteerden op meest recente volgorde:

Uitvoeringsplan voor gegroepeerde en gesorteerde zoekopdracht

Maar zelfs de duurste operator hier - de XML-tabelwaardefunctie - lijkt allemaal CPU te zijn (hoewel ik vrijelijk zal toegeven dat ik niet zeker weet hoeveel van het daadwerkelijke werk wordt weergegeven in de details van het queryplan):

Operatoreigenschappen voor de XML-tabelwaardefunctie

"Alle CPU's" is meestal oké, aangezien de meeste systemen I/O-gebonden en/of geheugengebonden zijn, niet CPU-gebonden. Zoals ik vaak zeg, in de meeste systemen ruil ik elke dag van de week een deel van mijn CPU-ruimte in voor geheugen of schijf (een van de redenen waarom ik OPTION (RECOMPILE) leuk vind als een oplossing voor alomtegenwoordige problemen met het snuiven van parameters).

Dat gezegd hebbende, raad ik je ten zeerste aan om deze benaderingen te testen tegen vergelijkbare resultaten die je kunt krijgen van de GROUP_CONCAT CLR-benadering op CodePlex, en om de aggregatie en sortering op de presentatielaag uit te voeren (vooral als je de genormaliseerde gegevens op een of andere manier bewaart van caching-laag).


  1. Kolomnaam Oracle-tabel met spatie

  2. Alibaba Cloud

  3. Hoe sys.dm_exec_describe_first_result_set werkt in SQL Server

  4. ST_HexagonGrid geom vector om alle punten te vinden