Baza wiedzy / SQL
STRING_AGG w SQL: łączenie wierszy z kontrolą kolejności
Połączenie nazw w jedną listę wymaga decyzji o kolejności i brakach. Pokazuję ten sam wynik w PostgreSQL oraz SQL Server, z wyraźnym rozdzieleniem składni.

Gdy odbiorca chce listę etykiet w jednej komórce raportu, sięgnąłbym po agregację tekstową. STRING_AGG łączy wartości z wielu wierszy w jeden tekst z wybranym separatorem. Zanim jej użyję, ustalam trzy rzeczy: co jest grupą, w jakiej kolejności mają pojawić się elementy i czy powtórzenia coś znaczą.
Tutaj porównuję PostgreSQL 18 oraz SQL Server 2017 i nowsze. Microsoft potwierdza dostępność funkcji od SQL Server 2017; uporządkowany wariant wymaga poziomu zgodności bazy co najmniej 110. Nie przedstawiam jednego fragmentu sortowania jako wspólnej składni obu silników.
Jedne syntetyczne dane, dwa zapisy
Przyjmijmy tabelę task_labels z całkowitymi project_id i unikalnym item_id oraz tekstowym label. W SQL Server etykieta może być typu nvarchar, w PostgreSQL typu text. Puste pole oznaczam jako NULL, a nie pusty tekst.
| project_id | item_id | label |
|---|---|---|
| 10 | 1 | Beta |
| 10 | 2 | Alpha |
| 10 | 3 | Alpha |
| 10 | 4 | NULL |
| 20 | 5 | Gamma |
| 30 | 6 | NULL |
W PostgreSQL porządek elementów zapisuję wewnątrz agregacji:
SELECT project_id,
string_agg(label, ', ' ORDER BY item_id) AS labels
FROM task_labels
GROUP BY project_id
ORDER BY project_id;
Dokumentacja wyrażeń agregujących PostgreSQL pokazuje pozycję ORDER BY po argumentach. W SQL Server analogiczny przykład zapisuję z WITHIN GROUP:
SELECT project_id,
STRING_AGG(CAST(label AS nvarchar(max)), N', ')
WITHIN GROUP (ORDER BY item_id) AS labels
FROM task_labels
GROUP BY project_id
ORDER BY project_id;
W obu przykładach dostajemy taki wynik:
| project_id | labels |
|---|---|
| 10 | Beta, Alpha, Alpha |
| 20 | Gamma |
| 30 | NULL |
Dla projektu 10 czytamy po kolei pozycje 1, 2 i 3. Pozycja 4 nie dopisuje wartości ani dodatkowego przecinka. Projekt 20 ma jedną etykietę. Projekt 30 ma wiersz wejściowy, ale nie ma żadnej wartości różnej od NULL do połączenia, więc wynikiem jest NULL.
Dwie niezależne kolejności
Wewnętrzne sortowanie określa kolejność etykiet w jednej komórce. Końcowe ORDER BY project_id porządkuje wiersze gotowego wyniku. Nie można zastąpić pierwszego drugim. Bez porządku w agregacji nie obiecywałbym czytelnikowi konkretnej kolejności listy, nawet jeśli akurat kilka odczytów wyglądało tak samo.
PostgreSQL wskazuje wprost zależność string_agg od kolejności wejścia. W naszym przykładzie item_id jest unikalne, więc rozstrzyga kolejność. Gdy sortujesz po nazwie lub dacie, zastanów się nad remisami i potrzebnym dodatkowym kluczem. Dla tekstu znaczenie mają też reguły collation.
Ja określiłbym porządek zgodnie z zastosowaniem. Lista etykiet może być alfabetyczna. Historia etapów procesu potrzebuje raczej czasu i klucza zdarzenia. To, że oba wyniki da się zapisać w jednym polu, nie oznacza, że odbiorca będzie czytał je w ten sam sposób.

Duplikaty i braki wymagają decyzji
Dwie etykiety Alpha pozostają w zwykłej agregacji. Mogą oznaczać dwa poprawne przypisania albo kopię powstałą po złączeniu. Najpierw ustaliłbym przyczynę. Usunięcie powtórzeń wyłącznie z gotowego tekstu może ukryć błąd przygotowania danych bez wyjaśnienia jego wpływu na pozostałe miary.
Jeżeli naprawdę potrzebuję unikalnych nazw, w PostgreSQL mogę użyć:
SELECT project_id,
string_agg(DISTINCT label, ', ' ORDER BY label) AS labels
FROM task_labels
GROUP BY project_id
ORDER BY project_id;
Przy zwykłym porządku tych przykładowych nazw projekt 10 da Alpha, Beta. Zmieniliśmy jednocześnie znaczenie listy: pokazuje unikalne nazwy posortowane po nazwie, a nie kolejność przypisań. W SQL Server przygotowałbym najpierw SELECT DISTINCT project_id, label w podzapytaniu, a potem agregował jego wynik. Składnia funkcji SQL Server nie przyjmuje tu DISTINCT jako argumentu.
Jeśli brak ma być widoczny dla każdego wiersza, można zastąpić go przed agregacją. W PostgreSQL dla naszych danych:
SELECT project_id,
string_agg(COALESCE(label, '(missing)'), ', ' ORDER BY item_id) AS labels
FROM task_labels
GROUP BY project_id
ORDER BY project_id;
Projekt 10 otrzyma Beta, Alpha, Alpha, (missing), a projekt 30 (missing). Zastąpienie NULL dopiero po agregacji zmieniłoby wyłącznie pusty wynik całej grupy. To inny cel niż pokazanie brakującej etykiety na konkretnej pozycji. Z kolei pusty tekst '' nie jest NULL i może pozostawić w liście sąsiadujące separatory.
Typ i długość wyniku w SQL Server
W przykładzie SQL Server rzutuję wejście na nvarchar(max) przed łączeniem. Typ pierwszego argumentu wpływa na typ wyniku: dla nvarchar o ograniczonej długości wynikiem jest nvarchar(4000), a dla varchar o ograniczonej długości varchar(8000). Samo rzutowanie gotowej agregacji nie zmienia typu, w którym powstawała.
Ta decyzja nie oznacza, że każda lista powinna mieć dowolną długość. Ja nadal ustaliłbym, ile elementów odbiorca potrafi wykorzystać, i sprawdził ograniczenia klienta wyświetlającego wynik. Komórka z tysiącem etykiet może być poprawnym tekstem i bardzo niewygodnym interfejsem. Nie obiecuję też przewagi wydajności nad każdym wcześniejszym sposobem konkatenacji.
Odpowiedniki w innych bazach
| Silnik | Nazwa, którą sprawdziłbym w dokumentacji |
|---|---|
| MySQL 8.4 | GROUP_CONCAT |
| Oracle Database 19c | LISTAGG |
| SQLite od 3.44.0 | string_agg jako alias group_concat |
SQLite 3.44.0 dodał string_agg; bieżąca dokumentacja opisuje alias i własny zapis kolejności. W MySQL zwróciłbym uwagę również na group_concat_max_len. Dla Oracle LISTAGG sprawdziłbym obsługę przepełnienia. Nazwa odpowiednika nie gwarantuje identycznych limitów ani składni.
Ćwiczenie i granice zastosowania
Usuń w myślach wiersz z item_id = 2: dla projektu 10 zostanie Beta, Alpha. Następnie zastanów się, czy wiersz z pustym tekstem powinien być widoczny, pominięty czy zastąpiony opisem. Odpowiedź wynika z definicji listy, nie z chęci pozbycia się przecinka.
Agregacji użyłbym do prezentacji, zachowując osobne wiersze jako podstawę dalszych obliczeń. Jeżeli etykiety zawierają przecinki, taki tekst nie staje się automatycznie poprawnym CSV ani JSON-em. Do wymiany danych wybrałbym właściwy format i jego reguły kodowania. Dla samego łączenia fragmentów jednego wiersza zajrzyj do hasła CONCAT.
Najważniejsze w pigułce
Czy sortowanie STRING_AGG jest identyczne w SQL Server i PostgreSQL?
Nie. SQL Server używa WITHIN GROUP (ORDER BY ...) po wywołaniu funkcji. PostgreSQL umieszcza ORDER BY wewnątrz nawiasów funkcji, po separatorze.
Czy STRING_AGG usuwa duplikaty?
Zwykłe wywołanie zachowuje powtórzone wartości. PostgreSQL pozwala użyć DISTINCT w agregacji; w SQL Server można najpierw przygotować unikalne wiersze w podzapytaniu.
Co dzieje się z NULL?
W pokazanych wywołaniach NULL jest pomijane wraz z należnym mu separatorem. Grupa zawierająca wyłącznie NULL daje NULL. Pusty tekst jest inną wartością i może tworzyć widoczne separatory.
Przy pracy nad artykułem użyto w zautomatyzowany sposób sztucznej inteligencji. Jeśli też chcesz tak umieć, sprawdź ten projekt.
