sql >> Databáze >  >> RDS >> Sqlserver

Seskupené zřetězení v SQL Server

Seskupené zřetězení je běžný problém na SQL Serveru bez přímých a záměrných funkcí, které by jej podporovaly (jako XMLAGG v Oracle, STRING_AGG nebo ARRAY_TO_STRING(ARRAY_AGG()) v PostgreSQL a GROUP_CONCAT v MySQL). Bylo to vyžádáno, ale zatím bez úspěchu, jak dokládají tyto položky Connect:

  • Connect #247118 :SQL potřebuje verzi funkce MySQL group_Concat (Odloženo)
  • Připojení #728969 :Objednané funkce sady – klauzule WITHIN GROUP (uzavřená, protože se neopraví)

** AKTUALIZACE leden 2017 ** :STRING_AGG() bude v SQL Server 2017; přečtěte si o tom zde, zde a zde.

Co je seskupené zřetězení?

Pro nezasvěcené je seskupené zřetězení, když chcete vzít více řádků dat a zkomprimovat je do jednoho řetězce (obvykle s oddělovači, jako jsou čárky, tabulátory nebo mezery). Někdo by tomu mohl říkat „horizontální spojení“. Rychlý vizuální příklad demonstrující, jak bychom zkomprimovali seznam domácích mazlíčků patřících každému členovi rodiny, od normalizovaného zdroje po „zploštělý“ výstup:

V průběhu let bylo mnoho způsobů, jak tento problém vyřešit; zde je jen několik na základě následujících ukázkových dat:

    CREATE TABLE dbo.FamilyMemberPets
    (
      Name SYSNAME,
      Pet SYSNAME,
      PRIMARY KEY(Name,Pet)
    );
     
    INSERT dbo.FamilyMemberPets(Name,Pet) VALUES
    (N'Madeline',N'Kirby'),
    (N'Madeline',N'Quigley'),
    (N'Henry',   N'Piglet'),
    (N'Lisa',    N'Snowball'),
    (N'Lisa',    N'Snowball II');

    Nebudu demonstrovat vyčerpávající seznam všech přístupů seskupeného zřetězení, které byly kdy vytvořeny, protože se chci zaměřit na několik aspektů mého doporučeného přístupu, ale chci poukázat na několik z těch běžnějších:

    Skalární UDF
    CREATE FUNCTION dbo.ConcatFunction
    (
      @Name SYSNAME
    )
    RETURNS NVARCHAR(MAX)
    WITH SCHEMABINDING 
    AS 
    BEGIN
      DECLARE @s NVARCHAR(MAX);
     
      SELECT @s = COALESCE(@s + N', ', N'') + Pet
        FROM dbo.FamilyMemberPets
    	WHERE Name = @Name
    	ORDER BY Pet;
     
      RETURN (@s);
    END
    GO
     
    SELECT Name, Pets = dbo.ConcatFunction(Name)
      FROM dbo.FamilyMemberPets
      GROUP BY Name
      ORDER BY Name;

    Poznámka:Existuje důvod, proč to neděláme:

    SELECT DISTINCT Name, Pets = dbo.ConcatFunction(Name)
      FROM dbo.FamilyMemberPets
      ORDER BY Name;

    Pomocí DISTINCT , funkce se spustí pro každý jednotlivý řádek, poté se odstraní duplikáty; pomocí GROUP BY , jsou nejprve odstraněny duplikáty.

    Common Language Runtime (CLR)

    Toto používá GROUP_CONCAT_S funkci naleznete na http://groupconcat.codeplex.com/:

    SELECT Name, Pets = dbo.GROUP_CONCAT_S(Pet, 1)
      FROM dbo.FamilyMemberPets
      GROUP BY Name
      ORDER BY Name;
    Rekurzivní CTE

    Existuje několik variant této rekurze; tento vytáhne sadu odlišných jmen jako kotvu:

    ;WITH x as 
    (
      SELECT Name, Pet = CONVERT(NVARCHAR(MAX), Pet),
        r1 = ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Pet)
      FROM dbo.FamilyMemberPets
    ),
    a AS 
    (
      SELECT Name, Pet, r1 FROM x WHERE r1 = 1
    ),
    r AS
    (
      SELECT Name, Pet, r1 FROM a WHERE r1 = 1
      UNION ALL
      SELECT x.Name, r.Pet + N', ' + x.Pet, x.r1
        FROM x INNER JOIN r
    	ON r.Name = x.Name
    	AND x.r1 = r.r1 + 1
    )
    SELECT Name, Pets = MAX(Pet)
      FROM r
      GROUP BY Name 
      ORDER BY Name
      OPTION (MAXRECURSION 0);
    Kurzor

    Tady není moc co říct; kurzory obvykle nejsou optimální přístup, ale toto může být vaše jediná volba, pokud jste uvízli na serveru SQL Server 2000:

    DECLARE @t TABLE(Name SYSNAME, Pets NVARCHAR(MAX),
      PRIMARY KEY (Name));
     
    INSERT @t(Name, Pets)
      SELECT Name, N'' 
      FROM dbo.FamilyMemberPets GROUP BY Name;
     
    DECLARE @name SYSNAME, @pet SYSNAME, @pets NVARCHAR(MAX);
     
    DECLARE c CURSOR LOCAL FAST_FORWARD
      FOR SELECT Name, Pet 
      FROM dbo.FamilyMemberPets
      ORDER BY Name, Pet;
     
    OPEN c;
     
    FETCH c INTO @name, @pet;
     
    WHILE @@FETCH_STATUS = 0
    BEGIN
      UPDATE @t SET Pets += N', ' + @pet
        WHERE Name = @name;
     
      FETCH c INTO @name, @pet;
    END
     
    CLOSE c; DEALLOCATE c;
     
    SELECT Name, Pets = STUFF(Pets, 1, 1, N'') 
      FROM @t
      ORDER BY Name;
    GO
    Svérázná aktualizace

    Někteří lidé *milují* tento přístup; Vůbec nechápu tu přitažlivost.

    DECLARE @Name SYSNAME, @Pets NVARCHAR(MAX);
     
    DECLARE @t TABLE(Name SYSNAME, Pet SYSNAME, Pets NVARCHAR(MAX),
      PRIMARY KEY (Name, Pet));
     
    INSERT @t(Name, Pet)
      SELECT Name, Pet FROM dbo.FamilyMemberPets
      ORDER BY Name, Pet;
     
    UPDATE @t SET @Pets = Pets = COALESCE(
        CASE COALESCE(@Name, N'') 
          WHEN Name THEN @Pets + N', ' + Pet
          ELSE Pet END, N''), 
    	@Name = Name;
     
    SELECT Name, Pets = MAX(Pets)
      FROM @t
      GROUP BY Name
      ORDER BY Name;
    PRO CESTA XML

    Docela snadno můj preferovaný způsob, alespoň částečně, protože je to jediný způsob, jak *zaručit* objednávku bez použití kurzoru nebo CLR. To znamená, že se jedná o velmi surovou verzi, která neřeší několik dalších inherentních problémů, o kterých budu dále diskutovat:

    SELECT Name, Pets = STUFF((SELECT N', ' + Pet 
      FROM dbo.FamilyMemberPets AS p2
       WHERE p2.name = p.name 
       ORDER BY Pet
       FOR XML PATH(N'')), 1, 2, N'')
    FROM dbo.FamilyMemberPets AS p
    GROUP BY Name
    ORDER BY Name;

Viděl jsem, že mnoho lidí mylně předpokládá, že nový CONCAT() funkce představená v SQL Server 2012 byla odpovědí na tyto požadavky na funkce. Tato funkce je určena pouze pro práci se sloupci nebo proměnnými v jednom řádku; nelze jej použít ke spojení hodnot mezi řádky.

Další informace o FOR XML PATH

FOR XML PATH('') sám o sobě není dost dobrý – má známé problémy s entitizací XML. Pokud například aktualizujete jedno ze jmen domácích zvířat tak, aby obsahovalo závorku HTML nebo ampersand:

UPDATE dbo.FamilyMemberPets
  SET Pet = N'Qui>gle&y'
  WHERE Pet = N'Quigley';

Ty se někde během cesty přeloží do entit bezpečných pro XML:

Qui>gle&y

Vždy tedy používám PATH, TYPE).value() , takto:

SELECT Name, Pets = STUFF((SELECT N', ' + Pet 
  FROM dbo.FamilyMemberPets AS p2
   WHERE p2.name = p.name 
   ORDER BY Pet
   FOR XML PATH(N''), TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.FamilyMemberPets AS p
GROUP BY Name
ORDER BY Name;

Vždy také používám NVARCHAR , protože nikdy nevíte, kdy některý podkladový sloupec bude obsahovat Unicode (nebo bude později změněn).

V .value() můžete vidět následující varianty , nebo dokonce další:

... TYPE).value(N'.', ...
... TYPE).value(N'(./text())[1]', ...

Ty jsou zaměnitelné, všechny nakonec představují stejný řetězec; výkonnostní rozdíly mezi nimi (více níže) byly zanedbatelné a možná zcela nedeterministické.

Dalším problémem, se kterým se můžete setkat, jsou určité znaky ASCII, které nelze v XML reprezentovat; například pokud řetězec obsahuje znak 0x001A (CHAR(26) ), zobrazí se tato chybová zpráva:

Msg 6841, Level 16, State 1, Line 51
FOR XML nemohl serializovat data pro uzel 'NoName', protože obsahuje znak (0x001A), který není v XML povolen. Chcete-li tato data načíst pomocí FOR XML, převeďte je na binární, varbinární nebo obrazový datový typ a použijte direktivu BINARY BASE64.

Zdá se mi to docela složité, ale doufejme, že se toho nemusíte obávat, protože data takto neukládáte nebo se je alespoň nesnažíte používat ve seskupeném zřetězení. Pokud ano, možná se budete muset vrátit k jednomu z dalších přístupů.

Výkon

Výše uvedená ukázková data umožňují snadno dokázat, že všechny tyto metody dělají to, co očekáváme, ale je těžké je smysluplně porovnávat. Vyplnil jsem tedy tabulku mnohem větší sadou:

TRUNCATE TABLE dbo.FamilyMemberPets;
 
INSERT dbo.FamilyMemberPets(Name,Pet)
  SELECT o.name, c.name
  FROM sys.all_objects AS o
  INNER JOIN sys.all_columns AS c
  ON o.[object_id] = c.[object_id]
  ORDER BY o.name, c.name;

Pro mě to bylo 575 objektů s celkovým počtem 7 080 řádků; nejširší objekt měl 142 sloupů. Nyní znovu, pravda, neměl jsem v úmyslu porovnávat každý jednotlivý přístup v historii SQL Serveru; jen těch pár zajímavostí, které jsem zveřejnil výše. Zde byly výsledky:

Můžete si všimnout, že chybí několik uchazečů; UDF pomocí DISTINCT a rekurzivní CTE byly tak mimo grafy, že by zkreslily měřítko. Zde jsou výsledky všech sedmi přístupů v tabulkové formě:

Přístup Trvání
(milisekundy)
PRO CESTA XML 108,58
CLR 80,67
Zvláštní aktualizace 278,83
UDF (GROUP BY) 452,67
UDF (DISTINCT) 5 893,67
Kurzor 2 210,83
Rekurzivní CTE 70 240,58

Průměrná doba trvání v milisekundách pro všechny přístupy

Všimněte si také, že varianty FOR XML PATH byly testovány nezávisle, ale vykazovaly velmi malé rozdíly, takže jsem je jen zkombinoval pro průměr. Pokud to opravdu chcete vědět, .[1] zápis v mých testech vyšel nejrychleji; YMMV.

Závěr

Pokud se nenacházíte v obchodě, kde je CLR jakýmkoliv způsobem překážkou, a zejména pokud neřešíte jen jednoduché názvy nebo jiné řetězce, měli byste určitě zvážit projekt CodePlex. Nepokoušejte se znovu vynalézt kolo, nezkoušejte neintuitivní triky a hacky, abyste vytvořili CROSS APPLY nebo jiné konstrukce fungují jen o něco rychleji než přístupy bez CLR výše. Vezměte to, co funguje, a zapojte to. A sakra, protože získáte i zdrojový kód, můžete jej vylepšit nebo rozšířit, pokud chcete.

Pokud je problém CLR, pak FOR XML PATH je pravděpodobně vaše nejlepší volba, ale stále si budete muset dávat pozor na záludné postavy. Pokud jste uvízli na serveru SQL Server 2000, jedinou možnou možností je UDF (nebo podobný kód nezabalený do UDF).

Příště

Pár věcí, které chci prozkoumat v následujícím příspěvku:odstranění duplikátů ze seznamu, seřazení seznamu podle něčeho jiného, ​​než je samotná hodnota, případy, kdy může být vložení kteréhokoli z těchto přístupů do UDF bolestivé, a praktické případy použití pro tuto funkci.


  1. Jak funguje SQLite Length()

  2. Proč mohu vytvořit tabulku s PRIMARY KEY ve sloupci s možnou hodnotou Null?

  3. Přejmenování připojitelné databáze

  4. Jak získám plán provádění dotazů na serveru SQL Server?