Database
 sql >> Baza danych >  >> RDS >> Database

Zgrupowane łączenie:zamawianie i usuwanie duplikatów

W moim ostatnim poście pokazałem kilka skutecznych podejść do grupowej konkatenacji. Tym razem chciałem omówić kilka dodatkowych aspektów tego problemu, które możemy łatwo rozwiązać za pomocą FOR XML PATH podejście:uporządkowanie listy i usunięcie duplikatów.

Istnieje kilka sposobów, w jakie ludzie chcą, aby lista rozdzielana przecinkami była uporządkowana. Czasami chcą, aby pozycja na liście była uporządkowana alfabetycznie; Pokazałem to już w poprzednim poście. Ale czasami chcą go posortować według innego atrybutu, który w rzeczywistości nie jest wprowadzany w wyniku; na przykład, może chcę najpierw uporządkować listę według najnowszych pozycji. Weźmy prosty przykład, gdzie mamy tabele Employees i CoffeeOrders. Po prostu wypełnijmy zamówienia jednej osoby na kilka dni:

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

Jeśli użyjemy istniejącego podejścia bez określenia ORDER BY , otrzymujemy dowolną kolejność (w tym przypadku najprawdopodobniej zobaczysz wiersze w kolejności, w jakiej zostały wstawione, ale nie polegaj na tym z większymi zestawami danych, większą liczbą indeksów itp.):

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;

Wyniki (pamiętaj, że możesz uzyskać *różne* wyniki, chyba że określisz ORDER BY ):

Nazwa | Zamówienia
Jack | Duży podwójny podwójny, Średni podwójny podwójny, Duży Vanilla Latte, Średni podwójny podwójny

Jeśli chcemy uporządkować listę alfabetycznie, jest to proste; po prostu dodajemy 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;

Wyniki:

Nazwa | Zamówienia
Jack | Duży podwójny podwójny, Duży Vanilla Latte, Średni podwójny podwójny, Średni podwójny podwójny

Możemy również uporządkować według kolumny, która nie pojawia się w zestawie wyników; na przykład możemy najpierw zamówić według najnowszego zamówienia kawy:

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;

Wyniki:

Nazwa | Zamówienia
Jack | Średni podwójny podwójny, Duży Vanilla Latte, Średni podwójny podwójny, Duży podwójny podwójny

Inną rzeczą, którą często chcemy robić, jest usuwanie duplikatów; w końcu nie ma powodu, aby dwukrotnie zobaczyć „Medium double double”. Możemy to wyeliminować, używając 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;

Teraz *zdarza się* uporządkowanie wyników alfabetycznie, ale znowu nie możesz na tym polegać:

Nazwa | Zamówienia
Jack | Duża podwójna podwójna, Duża waniliowa Latte, Średnia podwójna podwójna

Jeśli chcesz zagwarantować, że zamawiasz w ten sposób, możesz po prostu ponownie dodać ORDER BY:

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;

Wyniki są takie same (ale powtórzę, w tym przypadku to tylko zbieg okoliczności; jeśli chcesz taką kolejność, zawsze tak mów):

Nazwa | Zamówienia
Jack | Duża podwójna podwójna, Duża waniliowa Latte, Średnia podwójna podwójna

Ale co, jeśli chcemy wyeliminować duplikaty *i* najpierw posortować listę według najnowszego zamówienia kawy? Twoim pierwszym odruchem może być zachowanie GROUP BY i po prostu zmień ORDER BY , tak:

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 nie zadziała, ponieważ OrderDate nie jest pogrupowane ani zagregowane jako część zapytania:

Komunikat 8127, poziom 16, stan 1, wiersz 64
Kolumna „dbo.CoffeeOrders.OrderDate” jest nieprawidłowa w klauzuli ORDER BY, ponieważ nie jest zawarta ani w funkcji agregującej, ani w klauzuli GROUP BY.

Rozwiązaniem, które wprawdzie sprawia, że ​​zapytanie jest trochę brzydsze, jest najpierw pogrupowanie zamówień oddzielnie, a następnie pobranie tylko wierszy z maksymalną datą zamówienia kawy na pracownika:

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

Wyniki:

Nazwa | Zamówienia
Jack | Średni podwójny podwójny, Duży Vanilla Latte, Duży podwójny podwójny

Osiąga to oba nasze cele:wyeliminowaliśmy duplikaty i uporządkowaliśmy listę według czegoś, czego w rzeczywistości nie ma na liście.

Wydajność

Być może zastanawiasz się, jak źle te metody radzą sobie z bardziej niezawodnym zestawem danych. Zamierzam wypełnić naszą tabelę 100 000 wierszy, zobaczę, jak radzą sobie bez żadnych dodatkowych indeksów, a następnie ponownie uruchomię te same zapytania z odrobiną dostrojenia indeksu w celu obsługi naszych zapytań. Tak więc najpierw uzyskaj 100 000 wierszy rozłożonych na 1000 pracowników:

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

Teraz po prostu uruchommy każde z naszych zapytań dwa razy i zobaczmy, jaki jest czas przy drugiej próbie (zrobimy skok wiary i założymy, że – w idealnym świecie – będziemy pracować z zagruntowaną pamięcią podręczną ). Uruchomiłem je w SQL Sentry Plan Explorer, ponieważ jest to najłatwiejszy znany mi sposób na porównanie kilku indywidualnych zapytań:

Czas trwania i inne metryki środowiska wykonawczego dla różnych metod FOR XML PATH

Te czasy (czas trwania w milisekundach) naprawdę nie są wcale takie złe IMHO, kiedy myślisz o tym, co się tutaj dzieje. Najbardziej skomplikowanym planem, przynajmniej wizualnie, wydawał się ten, w którym usunęliśmy duplikaty i posortowaliśmy według najnowszej kolejności:

Plan wykonania zapytań pogrupowanych i posortowanych

Ale nawet najdroższy operator tutaj – funkcja XML z wartościami tabelarycznymi – wydaje się być w całości CPU (chociaż dobrowolnie przyznam, że nie jestem pewien, jaka część rzeczywistej pracy jest ujawniona w szczegółach planu zapytania):

Właściwości operatora dla funkcji zwracającej tabelę XML

„Cały procesor” jest zazwyczaj w porządku, ponieważ większość systemów jest związana z we/wy i/lub pamięcią, a nie z procesorem. Jak często mówię, w większości systemów zamienię część mojego zapasu procesora na pamięć lub dysk każdego dnia tygodnia (jeden z powodów, dla których lubię OPTION (RECOMPILE) jako rozwiązanie wszechobecnych problemów związanych z podsłuchiwaniem parametrów).

To powiedziawszy, gorąco zachęcam do przetestowania tych podejść z podobnymi wynikami, które można uzyskać z podejścia GROUP_CONCAT CLR w CodePlex, a także do wykonywania agregacji i sortowania w warstwie prezentacji (szczególnie, jeśli zachowujesz w jakiś sposób znormalizowane dane buforowania warstwy).


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQL, jak usunąć dane i tabele

  2. Ulepszenia Showplan dla UDF

  3. Maskowanie danych w czasie rzeczywistym za pomocą wyzwalaczy

  4. Adaptacyjny próg łączenia

  5. Mity dotyczące wydajności:indeksy klastrowe a nieklastrowe