sql >> Databáze >  >> RDS >> Database

Seskupené zřetězení:Objednávání a odstraňování duplikátů

Ve svém posledním příspěvku jsem ukázal některé účinné přístupy ke skupinovému zřetězení. Tentokrát jsem chtěl mluvit o několika dalších aspektech tohoto problému, které můžeme snadno provést pomocí FOR XML PATH přístup:seřazení seznamu a odstranění duplikátů.

Existuje několik způsobů, jak jsem viděl, že lidé chtějí seřadit seznam oddělený čárkami. Někdy chtějí, aby byla položka v seznamu řazena abecedně; Už jsem to ukázal ve svém předchozím příspěvku. Ale někdy to chtějí seřadit podle nějakého jiného atributu, který ve skutečnosti není uveden ve výstupu; například chci nejprve seřadit seznam podle nejnovější položky. Vezměme si jednoduchý příklad, kde máme tabulku Zaměstnanci a tabulku CoffeeOrders. Pojďme na několik dní vyplnit objednávky jedné osoby:

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');

Pokud použijeme stávající přístup bez zadání ORDER BY , dostaneme libovolné pořadí (v tomto případě je to pravděpodobně tak, že řádky uvidíte v pořadí, v jakém byly vloženy, ale nespoléhejte se na to u větších datových sad, více indexů atd.):

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;

Výsledky (nezapomeňte, že můžete získat *odlišné* výsledky, pokud nezadáte ORDER BY ):

Jméno | Objednávky
Jack | Large double double, Medium double double, Large Vanilla Latte, Medium double double

Pokud chceme seznam seřadit abecedně, je to jednoduché; jen přidáme ORDER BY c.OrderDetails :

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;

Výsledky:

Jméno | Objednávky
Jack | Large double double, Large Vanilla Latte, Medium double double, Medium double double

Můžeme také seřadit podle sloupce, který se ve výsledkové sadě nevyskytuje; například můžeme objednávat podle poslední objednávky kávy jako první:

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;

Výsledky:

Jméno | Objednávky
Jack | Medium double double, Large Vanilla Latte, Medium double double, Large double double

Další věc, kterou často chceme udělat, je odstranit duplikáty; koneckonců není důvod vidět „Medium double double“ dvakrát. Můžeme to eliminovat pomocí GROUP BY :

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;

Nyní se *stane* seřadit výstup abecedně, ale opět se na to nemůžete spolehnout:

Jméno | Objednávky
Jack | Large double double, Large Vanilla Latte, Medium double double

Pokud chcete zaručit, že objednání tímto způsobem, můžete jednoduše přidat znovu OBJEDNÁVKU:

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;

Výsledky jsou stejné (ale budu se opakovat, v tomto případě je to jen náhoda; pokud chcete toto pořadí, vždy to řekněte):

Jméno | Objednávky
Jack | Large double double, Large Vanilla Latte, Medium double double

Ale co když chceme odstranit duplikáty *a* nejprve seřadit seznam podle poslední objednávky kávy? Vaším prvním sklonem může být ponechat GROUP BY a stačí změnit ORDER BY , takto:

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;

To nebude fungovat, protože OrderDate není seskupen ani agregován jako součást dotazu:

Msg 8127, Level 16, State 1, Line 64
Sloupec "dbo.CoffeeOrders.OrderDate" je neplatný v klauzuli ORDER BY, protože není obsažen ani v agregační funkci, ani v klauzuli GROUP BY.

Řešením, které nepochybně dělá dotaz trochu ošklivějším, je nejprve seskupit objednávky odděleně a poté vzít pouze řádky s maximálním datem pro danou objednávku kávy na zaměstnance:

;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;

Výsledky:

Jméno | Objednávky
Jack | Medium double double, Large Vanilla Latte, Large double double

Tím jsme splnili oba naše cíle:odstranili jsme duplikáty a seřadili jsme seznam podle něčeho, co v seznamu ve skutečnosti není.

Výkon

Možná se divíte, jak špatně si tyto metody vedou proti robustnějšímu souboru dat. Naplním naši tabulku 100 000 řádky, podívám se, jak si vedou bez jakýchkoli dalších indexů, a pak znovu spustím stejné dotazy s trochou ladění indexu, abychom podpořili naše dotazy. Nejprve tedy získání 100 000 řádků rozložených mezi 1 000 zaměstnanců:

-- 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;

Nyní spusťte každý z našich dotazů dvakrát a na druhý pokus se podívejme, jaké je načasování (zde uděláme skok důvěry a předpokládejme, že – v ideálním světě – budeme pracovat s primovanou mezipamětí ). Spustil jsem je v SQL Sentry Plan Explorer, protože je to nejjednodušší způsob, který znám, a porovnat spoustu jednotlivých dotazů:

Délka a další metriky doby běhu pro různé přístupy FOR XML PATH

Tyto časování (trvání je v milisekundách) opravdu nejsou IMHO vůbec tak špatné, když se zamyslíte nad tím, co se tu vlastně dělá. Nejsložitější plán, alespoň vizuálně, se zdál být ten, kde jsme odstranili duplikáty a seřadili je podle poslední objednávky:

Plán provádění pro seskupený a seřazený dotaz

Ale i ten nejdražší operátor zde – funkce s hodnotou tabulky XML – se zdá být celý CPU (i když přiznám, že si nejsem jistý, kolik skutečné práce je odhaleno v detailech plánu dotazů):

Vlastnosti operátora pro funkci s hodnotou tabulky XML

"Všechny CPU" je obvykle v pořádku, protože většina systémů je vázána I/O a/nebo pamětí, nikoli CPU. Jak říkám docela často, ve většině systémů vyměním část své kapacity CPU za paměť nebo disk kterýkoli den v týdnu (jeden z důvodů, proč se mi líbí OPTION (RECOMPILE) jako řešení všudypřítomných problémů s čicháním parametrů).

To znamená, že vám důrazně doporučuji otestovat tyto přístupy proti podobným výsledkům, které můžete získat z přístupu GROUP_CONCAT CLR na CodePlex, stejně jako provádění agregace a třídění na úrovni prezentace (zejména pokud uchováváte normalizovaná data v nějakém vrstvě mezipaměti).


  1. Přechod z MySQL na PostgreSQL - tipy, triky a problémy?

  2. Jak opravit „Neplatný název objektu ‚OPENJSON‘.“ v SQL Server

  3. Získejte oprávnění sloupců pro tabulku v SQL Server pomocí T-SQL:sp_column_privileges

  4. Připojte aplikaci pro iPhone k PostgreSQL pomocí Libpq