2009-01-16 14 views
290

Próbuję przenieść aplikację opartą na MySQL do wersji Microsoft SQL Server 2005 (nie z wyboru, ale takie jest życie).Symulacja funkcji MySQL group_concat w Microsoft SQL Server 2005?

W oryginalnej aplikacji użyliśmy prawie całkowicie zgodnych z ANSI-SQL stwierdzeń, z jednym znaczącym wyjątkiem - dość często używaliśmy funkcji MySQL o group_concat.

group_concat, nawiasem mówiąc, to robi: podany tabelę, powiedzmy, nazw i projektów pracowniczych ...

SELECT empName, projID FROM project_members; 

Powroty:

ANDY | A100 
ANDY | B391 
ANDY | X010 
TOM | A100 
TOM | A510 

... i oto co uzyskać z GROUP_CONCAT:

SELECT 
    empName, group_concat(projID SEPARATOR '/') 
FROM 
    project_members 
GROUP BY 
    empName; 

powraca:

ANDY | A100/B391/X010 
TOM | A100/A510 

To, co chciałbym wiedzieć, to: Czy można napisać, powiedzmy, funkcję zdefiniowaną przez użytkownika w SQL Server, która emuluje funkcjonalność group_concat?

mam prawie żadnego doświadczenia przy użyciu UDF, procedur przechowywanych, lub coś podobnego, tylko prosto-up SQL, więc proszę zaważyć na zbyt wielu wyjaśnień :)

+0

Kilka przydatnych linków: http://www.postgresonline.com/journal/archives/191-stringagg.html i http://consultingblogs.emc.com/jamiethomson/archive/2009/07/16/string -aggregation-in-t-sql-amp-pl-sql.aspx – bernhof

+0

możliwy duplikat [Jak utworzyć rozdzielaną przecinkami listę używając zapytania SQL?] (http://stackoverflow.com/questions/1817985/how -do-i-create-a-comma-list-list-using-a-sql-query) - ten post jest szerszy, więc wybrałbym ten jeden jako kanoniczny – TMS

+0

możliwy duplikat funkcji [SQL group \ _concat w SQL Server] (http://stackoverflow.com/questions/8868604/sql-group-concat-function-in-sql-server) – Trikaldarshi

Odpowiedz

146

Żaden prawdziwy łatwy sposób to zrobić. Mnóstwo pomysłów.

Best one I've found:

SELECT table_name, LEFT(column_names , LEN(column_names)-1) AS column_names 
FROM information_schema.columns AS extern 
CROSS APPLY 
(
    SELECT column_name + ',' 
    FROM information_schema.columns AS intern 
    WHERE extern.table_name = intern.table_name 
    FOR XML PATH('') 
) pre_trimmed (column_names) 
GROUP BY table_name, column_names; 

lub wersji, która działa poprawnie, jeśli dane mogą zawierać znaków takich jak <

WITH extern 
    AS (SELECT DISTINCT table_name 
     FROM INFORMATION_SCHEMA.COLUMNS) 
SELECT table_name, 
     LEFT(y.column_names, LEN(y.column_names) - 1) AS column_names 
FROM extern 
     CROSS APPLY (SELECT column_name + ',' 
        FROM INFORMATION_SCHEMA.COLUMNS AS intern 
        WHERE extern.table_name = intern.table_name 
        FOR XML PATH(''), TYPE) x (column_names) 
     CROSS APPLY (SELECT x.column_names.value('.', 'NVARCHAR(MAX)')) y(column_names) 
+0

Ten przykład działał dla mnie, ale próbowałem zrobić kolejną agregację i to nie działało, dało mi błąd: "nazwa korelacji" pre_trimmed "jest określona wiele razy w klauzuli FROM." – PhilChuang

+6

'pre_trimmed' to tylko alias dla podzapytania. Aliasy są wymagane w przypadku podzapytań i muszą być unikatowe, więc w przypadku innej podkwerendy zmień ją na coś unikatowego ... – Koen

+0

możesz pokazać przykład bez table_name jako nazwy kolumny, która jest myląca. –

6

W poniższym kodzie trzeba ustawić PermissionLevel = Zewnętrzne na swoje właściwości projektu przed wdrożeniem i zmień bazę danych, aby ufała zewnętrznemu kodowi (koniecznie przeczytaj gdzie indziej o zagrożeniach bezpieczeństwa i alternatywach [takich jak certyfikaty]), uruchamiając "ZMIENIAJ bazę danych database_name SET TRUSTWORTHY ON".

using System; 
using System.Collections.Generic; 
using System.Data.SqlTypes; 
using System.IO; 
using System.Runtime.Serialization; 
using System.Runtime.Serialization.Formatters.Binary; 
using Microsoft.SqlServer.Server; 

[Serializable] 
[SqlUserDefinedAggregate(Format.UserDefined, 
MaxByteSize=8000, 
IsInvariantToDuplicates=true, 
IsInvariantToNulls=true, 
IsInvariantToOrder=true, 
IsNullIfEmpty=true)] 
    public struct CommaDelimit : IBinarySerialize 
{ 


[Serializable] 
private class StringList : List<string> 
{ } 

private StringList List; 

public void Init() 
{ 
    this.List = new StringList(); 
} 

public void Accumulate(SqlString value) 
{ 
    if (!value.IsNull) 
    this.Add(value.Value); 
} 

private void Add(string value) 
{ 
    if (!this.List.Contains(value)) 
    this.List.Add(value); 
} 

public void Merge(CommaDelimit group) 
{ 
    foreach (string s in group.List) 
    { 
    this.Add(s); 
    } 
} 

void IBinarySerialize.Read(BinaryReader reader) 
{ 
    IFormatter formatter = new BinaryFormatter(); 
    this.List = (StringList)formatter.Deserialize(reader.BaseStream); 
} 

public SqlString Terminate() 
{ 
    if (this.List.Count == 0) 
    return SqlString.Null; 

    const string Separator = ", "; 

    this.List.Sort(); 

    return new SqlString(String.Join(Separator, this.List.ToArray())); 
} 

void IBinarySerialize.Write(BinaryWriter writer) 
{ 
    IFormatter formatter = new BinaryFormatter(); 
    formatter.Serialize(writer.BaseStream, this.List); 
} 
    } 

Przetestowałem to za pomocą kwerendy, która wygląda tak:

SELECT 
dbo.CommaDelimit(X.value) [delimited] 
FROM 
(
    SELECT 'D' [value] 
    UNION ALL SELECT 'B' [value] 
    UNION ALL SELECT 'B' [value] -- intentional duplicate 
    UNION ALL SELECT 'A' [value] 
    UNION ALL SELECT 'C' [value] 
) X 

a rentowności: A, B, C, D

43

Być może zbyt późno, aby być z korzyścią teraz, ale czy to nie jest najłatwiejszy sposób robienia rzeczy?

SELECT  empName, projIDs = replace 
          ((SELECT Surname AS [data()] 
           FROM project_members 
           WHERE empName = a.empName 
           ORDER BY empName FOR xml path('')), ' ', REQUIRED SEPERATOR) 
FROM   project_members a 
WHERE  empName IS NOT NULL 
GROUP BY empName 
+0

Interesujące. Skończyłem już projekt, ale spróbuję spróbować tej metody. Dzięki! – DanM

+6

Dobra sztuczka - jedyny problem dotyczy nazwisk ze spacjami, które zastąpią przestrzeń separatorem. –

+0

Sam spotkałem się z takim problemem, Mark. Niestety, dopóki MSSQL nie dotrze do czasów i nie wprowadzi GROUP_CONCAT, jest to najmniejszy z intensywnych metod, jakie udało mi się wymyślić w odniesieniu do tego, co jest potrzebne. –

4

O odpowiedź J Hardiman, w jaki sposób o:

SELECT empName, projIDs= 
    REPLACE(
    REPLACE(
     (SELECT REPLACE(projID, ' ', '-somebody-puts-microsoft-out-of-his-misery-please-') AS [data()] FROM project_members WHERE empName=a.empName FOR XML PATH('')), 
     ' ', 
     '/'), 
    '-somebody-puts-microsoft-out-of-his-misery-please-', 
    ' ') 
    FROM project_members a WHERE empName IS NOT NULL GROUP BY empName 

Nawiasem mówiąc, jest wykorzystanie „Nazwisko” literówki albo ja nie rozumiejąc koncepcję tutaj?

W każdym razie, dziękuję chłopaki bo to zaoszczędziło mi trochę czasu :)

+1

Raczej nieprzyjazna odpowiedź, jeśli pytasz mnie i wcale nie jest pomocna jako odpowiedź. –

+1

tylko widząc to teraz ... Nie miałem tego na myśli, w tym czasie byłem bardzo sfrustrowany serwerem sql (nadal jestem). odpowiedzi z tego posta naprawdę były pomocne; EDYCJA: dlaczego nie było pomocne przy okazji? zrobiłem to dla mnie. – user422190

6

próbował tych, ale do moich celów w MS SQL Server 2005 dodaje był najbardziej użyteczny, który znalazłem na xaprb

declare @result varchar(8000); 

set @result = ''; 

select @result = @result + name + ' ' 

from master.dbo.systypes; 

select rtrim(@result); 

@ Mark, jak wspomniałeś, był to kosmiczny charakter, który spowodował problemy dla mnie.

146

Może trochę spóźnię się na imprezę, ale ta metoda działa dla mnie i jest łatwiejsza niż metoda COALESCE.

SELECT STUFF(
      (SELECT ',' + Column_Name 
       FROM Table_Name 
       FOR XML PATH ('')) 
      , 1, 1, '') 
+0

To pokazuje tylko jak konkatować wartości - group_concat konkluduje je przez grupę, co jest trudniejsze (i co wydaje się wymagać OP). Zobacz akceptowaną odpowiedź na SO 15154644, aby dowiedzieć się, jak to zrobić - klauzula WHERE jest krytycznym dodatkiem. – DJDave

6

Aby złączyć wszystkie nazwy kierownik projektu z projektów, które mają wielu kierowników projektów napisać:

SELECT a.project_id,a.project_name,Stuff((SELECT N'/ ' + first_name + ', '+last_name FROM projects_v 
where a.project_id=project_id 
FOR 
XML PATH(''),TYPE).value('text()[1]','nvarchar(max)'),1,2,N'' 
) mgr_names 
from projects_v a 
group by a.project_id,a.project_name 
27

Zapraszamy do obejrzenia projektu GROUP_CONCAT na Codeplex, myślę, że robi dokładnie to, czego szukasz :

This project contains a set of SQLCLR User-defined Aggregate functions (SQLCLR UDAs) that collectively offer similar functionality to the MySQL GROUP_CONCAT function. There are multiple functions to ensure the best performance based on the functionality required...

+5

Dlaczego warto odrzucić? Proszę wyjaśnić ... – MaxiWheat

+5

Użyłem tego i działa jak dotąd dobrze. –

+1

@MaxiWheat: wielu facetów nie czyta uważnie pytań lub odpowiedzi przed kliknięciem w dół. Wpływa na wpis właściciela bezpośrednio z powodu ich błędu. –

25

SQL Server 2017 ma wprowadzić nową funkcję zbiorczą

STRING_AGG (expression, separator).

Concatenates the values of string expressions and places separator values between them. The separator is not added at the end of string.

elementach łączonych można zamówić poprzez dołączenie WITHIN GROUP (ORDER BY some_expression)

Dla wersji 2005-2016 I zazwyczaj użyć metody XML w akceptowanych odpowiedź.

Może się to jednak nie udać w pewnych okolicznościach. na przykład jeśli dane mają być łączone zawiera CHAR(29) widać

FOR XML could not serialize the data ... because it contains a character (0x001D) which is not allowed in XML.

Bardziej wytrzymałe metoda, która radzi sobie ze wszystkimi postaciami byłoby użyć kruszywa CLR. Jednak stosowanie takiego podejścia do łączonych elementów jest trudniejsze.

Metoda przypisania do zmiennej to not guaranteed i należy jej unikać w kodzie produkcyjnym.

+0

Jest to również dostępne teraz w usłudze Azure SQL: https://azure.microsoft.com/en-us/roadmap/new-t-sql-tring-functions-in-azure-sql-database/ –

Powiązane problemy