Sposoby na łączenie tekstów w SQL

SQL
Analityka danych

Spis treści:

Łączenie tekstów to jedna z najczęściej wykonywanych operacji podczas pracy z językiem SQL. Każdy programista czy analityk baz danych wcześniej czy później staje przed koniecznością połączenia kilku kolumn tekstowych w jedną całość. W praktyce może to oznaczać np. tworzenie pełnych nazw z imienia i nazwiska (co też będziemy robić w tym artykule), generowanie adresów z poszczególnych komponentów czy budowanie złożonych kolumn. SQL oferuje kilka różnych sposobów na realizację tego zadania. Co więcej, niektóre sposoby są bardziej uniwersalne i działają w różnych programach do pracy z językiem SQL, podczas gdy inne są specyficzne dla konkretnych implementacji. W tym artykule przeanalizujemy 4 główne sposoby łączenia tekstów w Microsoft SQL Server: operator + oraz funkcje CONCAT, CONCAT_WS i STRING_AGG. Każdy z nich ma swoje miejsce w arsenale narzędzi bazodanowca, a zrozumienie różnic pomiędzy nimi pozwoli na świadome podejmowanie decyzji o wyborze najlepszego rozwiązania. Omówimy także praktyczne aspekty stosowania każdej metody, w tym obsługę wartości nieokreślonych, czyli NULL. Dzięki szczegółowym przykładom i porównaniom będziesz mógł drogi czytelniku nie tylko zrozumieć mechanizm działania każdej z nich, ale także nauczyć się, kiedy którą stosować.

Przygotowanie danych do pracy

Przed rozpoczęciem analizy wspomnianych sposobów łączenia tekstów musimy przygotować odpowiednie dane testowe. Stworzymy przykładową bazę danych z tabelą pracowników, która będzie zawierać różne typy danych tekstowych oraz wartości NULL. Taki zestaw danych pozwoli nam na realistyczne przetestowanie każdej metody w różnych scenariuszach i będzie reprezentować typowe sytuacje spotykane w prawdziwych projektach – od prostych połączeń dwóch kolumn, po bardziej złożone scenariusze z wieloma kolumnami. Zachęcamy do zainstalowania wspomnianego oprogramowania i przetestowania kodu również we własnym zakresie.

-- utworzenie przykładowej bazy danych:
---------------------------------------
USE master;
GO
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'ImperiumSzkolenioweDB')
BEGIN
DROP DATABASE ImperiumSzkolenioweDB;
END;
GO
CREATE DATABASE ImperiumSzkolenioweDB;
GO
USE ImperiumSzkolenioweDB;
GO
-- utworzenie przykładowej tabeli:
---------------------------------------
-- usunięcie tabeli jeśli istnieje:
IF OBJECT_ID('ImperiumSzkolenioweDB.dbo.Employees') IS NOT NULL
BEGIN
DROP TABLE ImperiumSzkolenioweDB.dbo.Employees;
END;
-- utworzenie tabeli:
CREATE TABLE ImperiumSzkolenioweDB.dbo.Employees
(
ID INT IDENTITY(1,1),
FirstName VARCHAR(50),
LastName VARCHAR(50),
JobTitle VARCHAR(100),
Department VARCHAR(50),
City VARCHAR(50),
Phone VARCHAR(20)
);
-- wstawienie danych:
---------------------------------------
INSERT INTO ImperiumSzkolenioweDB.dbo.Employees (FirstName, LastName, JobTitle, 
Department, City, Phone)
VALUES 
('Jan', 'Kowalski', 'Senior Developer', 'IT', 'Warszawa', '123-456-789'),
('Anna', 'Nowak', 'Project Manager', 'IT', 'Kraków', '987-654-321'),
('Piotr', 'Wiśniewski', NULL, 'HR', 'Gdańsk', '555-123-456'),
('Maria', NULL, 'Analyst', 'Finance', 'Wrocław', NULL),
('Tomasz', 'Kowalczyk', 'Junior Developer', NULL, 'Poznań', '111-222-333'),
(NULL, 'Zieliński', 'Manager', 'Sales', 'Szczecin', '444-555-666'),
('Katarzyna', 'Dąbrowska', 'Tester', 'QA', NULL, '777-888-999')
;
-- podgląd danych:
---------------------------------------
SELECT * FROM ImperiumSzkolenioweDB.dbo.Employees;

Przygotowane dane stanowią przykładowy zestaw informacji, który pozwoli nam na kompleksowe przetestowanie różnych sposobów łączenia tekstów. Tabela Employees zawiera siedem wierszy, z których każdy jest nieco inny. Mamy tu pracowników z kompletnymi danymi, jak również takich, dla których brakuje pewnych informacji, co jest bardzo częste w bazach danych stosowanych komercyjnie. Dane zostały celowo zaprojektowane tak, aby pokazać typowe wyzwania związane z konkatenacją w języku SQL. W praktyce często spotykamy się z sytuacjami, gdzie nie wszystkie kolumny są wypełnione, a nasze zapytania muszą prawidłowo obsłużyć takie przypadki, aby działały. Niektóre metody łączenia tekstów mogą spowodować, że cały wynik stanie się NULL-em, jeśli choć jedna z łączonych wartości jest pusta, podczas gdy inne metody potrafią elegancko pominąć puste wartości. Tak przygotowane dane są znacznie bardziej reprezentatywne niż często spotykane w przykładach proste łańcuchy tekstowe składające się z pojedynczych słów. W rzeczywistych aplikacjach rzadko pracujemy z tak uproszczonymi danymi, dlatego ważne jest, aby nasze testy były jak najbardziej zbliżone do prawdziwych scenariuszy użycia.

Sposób nr 1: operator +

Operator + jest najprostszym sposobem łączenia tekstów w Microsoft SQL Server. Może być używany zarówno do dodawania liczb, jak i do konkatenacji ciągów znaków – tak jak w naszym przypadku. W kontekście łączenia tekstów, operator + po prostu skleja dwa łańcuchy jeden za drugim tworząc nowy, dłuższy tekst. Jego składnia jest niezwykle prosta – wystarczy umieścić go między wartościami, które chcemy połączyć.

-- Przykład użycia operatora +:
SELECT 
FirstName + ' ' + LastName AS [Full Name 1],
FirstName + '-' + LastName AS [Full Name 2]
FROM ImperiumSzkolenioweDB.dbo.Employees;

Największą zaletą operatora + jest jego prostota i uniwersalność. Każdy bazodanowiec, niezależnie od poziomu doświadczenia natychmiast zrozumie co robi takie zapytanie. Nie wymaga wywoływania żadnych funkcji. Ma też jedną istotną wadę – jego zachowanie w kontekście wartości NULL. Jeśli którakolwiek z łączonych wartości jest NULL, cały wynik operacji również staje się NULL. Oznacza to, że jeśli pracownik ma puste pole FirstName lub LastName, wynik zapytania będzie NULL zamiast częściowo wypełnionej wartości.

Takie zachowanie można zaobserwować w przypadku powyższego zapytania. Dla pracownika o ID 4 (Maria z pustym LastName) wynik będzie NULL, mimo że imię jest dostępne. Podobnie, dla pracownika o ID 6 (pusty FirstName) także otrzymamy NULL. To oznacza, że operator + nie jest najlepszym wyborem, gdy mamy do czynienia z danymi, które mogą zawierać wartości NULL i chcemy zachować dostępne informacje. Dodatkowo, operator ten nie oferuje żadnych wbudowanych mechanizmów obsługi separatorów czy formatowania. Jeśli chcemy dodać spacje, przecinki czy inne znaki między łączonymi wartościami, musimy je jawnie dodać w zapytaniu. Mimo tych ograniczeń, operator + pozostaje popularnym wyborem ze względu na swoją prostotę i szybkość działania.

Sposób nr 2: funkcja CONCAT

Funkcja CONCAT stanowi małą poprawę w porównaniu do operatora +. Jej główną zaletą jest inteligentna obsługa wartości NULL – funkcja automatycznie traktuje je jako pusty string, co pozwala na uzyskanie sensownych wyników nawet gdy niektóre z łączonych wartości są puste. CONCAT może przyjmować wiele argumentów, co czyni ją bardzo elastyczną w użyciu.

-- Przykład użycia funkcji CONCAT:
SELECT 
CONCAT(FirstName, ' ', LastName) AS Full_Name_1,
CONCAT(FirstName, ' ', LastName) AS Full_Name_2,
CONCAT(FirstName, ' ', LastName, ' (', Department, ')') 
AS Full_Name_With_Department
FROM ImperiumSzkolenioweDB.dbo.Employees;

Główną przewagą funkcji CONCAT jest jej tolerancja na wartości NULL, co możemy zaobserwować w naszym przykładzie. Jeśli którakolwiek z przekazanych wartości jest NULL, funkcja po prostu ją ignoruje i kontynuuje łączenie pozostałych wartości. W praktyce oznacza to, że dla pracownika o ID 4 (Maria z pustym LastName) otrzymamy samo imię zamiast NULL, co jest znacznie bardziej użyteczne. Podobnie, dla pracownika o ID 6 z pustym FirstName otrzymamy samo nazwisko. Funkcja te jest również bardzo czytelna i intuicyjna w użyciu. Jej nazwa jednoznacznie wskazuje na przeznaczenie, a składnia z listą argumentów oddzielonych przecinkami jest naturalna dla większości użytkowników baz danych. Dodatkowo, funkcja może przyjmować dowolną liczbę argumentów, co eliminuje potrzebę zagnieżdżania wielu operacji łączenia dla bardziej złożonych scenariuszy.

Jedyną wadą funkcji CONCAT jest to, że nie oferuje wbudowanej obsługi separatorów. Jeśli chcemy dodać spacje, przecinki czy inne znaki między łączonymi wartościami, musimy je jawnie podać jako osobne argumenty. W przypadku długich list wartości może to sprawić, że kod stanie się mniej czytelny.

Szkolenie Język SQL (MS SQL)

900  netto

Szkolenie dedykujemy wszystkim rozpoczynającym pracę z językiem SQL. Dzięki szkoleniu poznasz jego...
Zobacz szkolenie

Sposób nr 3: funkcja CONCAT_WS

Funkcja CONCAT_WS (ang. CONCAT With Separator) rozwiązuje główne ograniczenie funkcji CONCAT. Pierwszy argument to separator, który zostanie automatycznie wstawiony między wszystkie łączone wartości. Funkcja nie tylko zachowuje wszystkie zalety CONCAT, włączając inteligentną obsługę NULL-i, ale dodatkowo oferuje eleganckie rozwiązanie problemu separatorów. Chcąc połączyć ze sobą większą liczbę kolumn wystarczy, że zadeklarujemy separator tylko raz, zaraz na początku.

-- Przykład użycia funkcji CONCAT_WS:
SELECT 
CONCAT_WS(' ', FirstName, LastName) AS Full_Name_1,
CONCAT_WS('-', FirstName, LastName) AS Full_Name_2,
CONCAT_WS(', ', FirstName, LastName, Department, City) AS Full_Information
FROM ImperiumSzkolenioweDB.dbo.Employees;

Jak możemy zaobserwować, największą zaletą CONCAT_WS jest zarządzanie separatorami. Funkcja inteligentnie wstawia je tylko między niepustymi wartościami, co oznacza, że nie otrzymamy zbędnych separatorów na początku, na końcu czy w miejscach, gdzie występują wartości NULL. Na przykład, jeśli łączymy imię, nazwisko i stanowisko, a stanowisko jest NULL, funkcja automatycznie połączy tylko imię i nazwisko z odpowiednim separatorem, bez dodawania pustego separatora na końcu. Obsługa NULL-i w CONCAT_WS jest jeszcze bardziej zaawansowana niż w CONCAT. Funkcja nie tylko je ignoruje, ale także automatycznie dostosowuje separatory, aby wynik był estetyczny i poprawny. W przypadku naszych danych testowych, dla pracownika o ID 4 (Maria z pustym LastName) otrzymamy po prostu „Maria”, bez żadnych zbędnych spacji czy separatorów. Czytelność kodu z CONCAT_WS jest wyraźnie lepsza niż w przypadku poprzednich metod, szczególnie przy łączeniu większej liczby wartości. Zamiast przeplatać wartości separatorami, definiujemy separator raz na początku, a następnie wymieniamy wszystkie łączone wartości. To podejście jest mniej podatne na błędy i łatwiejsze w konserwacji. Dodatkowo, zmiana separatora wymaga modyfikacji tylko jednego miejsca w kodzie. Jej jedyną drobną wadą może być fakt, że funkcja jest dostępna dopiero od wersji Microsoft SQL Server 2017 lub nowszych, więc nie można jej używać w starszych wersjach systemu.

Sposób nr 4: STRING_AGG

STRING_AGG tym różni się od metod omawianych wcześniej, że jest funkcją agregującą – łączy wartości z wielu wierszy w jeden ciąg znaków i działa podobnie do funkcji takich jak np. SUM sumującej wartości liczbowe lub AVG obliczającej średnią arytmetyczną. Podobnie jak funkcja CONCAT_WS została wprowadzona w wersji Microsoft SQL Server z 2017 lub nowszych, a ponadto wymaga użycia klauzuli GROUP BY.

-- Przykład użycia funkcji STRING_AGG:
SELECT
Department,
STRING_AGG(FirstName + ' ' + LastName, ', ') AS EmployeeList,
STRING_AGG(CONCAT(FirstName, ' ', LastName), '; ') AS EmployeeListConcat
FROM ImperiumSzkolenioweDB.dbo.Employees
WHERE Department IS NOT NULL
GROUP BY Department;

Jej zaletą jest możliwość łączenia wartości z wielu wierszy w jeden ciąg znaków. To sprawia, że funkcja jest niezastąpiona przy tworzeniu raportów, gdzie chcemy przedstawić powiązane dane w formie listy. Na przykład, możemy łatwo utworzyć listę wszystkich pracowników w danym departamencie, wszystkich produktów w kategorii, czy wszystkich zamówień klienta w jednym wierszu. W naszym przykładzie najlepiej obrazuje to przypadek działu IT do którego jest przypisanych 2 pracowników. Obsługa NULL-i w STRING_AGG jest podobna do CONCAT_WS – funkcja ignoruje wartości NULL i nie włącza ich do wynikowego stringa. Jednak należy pamiętać, że jeśli używamy złożonych wyrażeń (jak np. FirstName + ’ ’ + LastName), a którakolwiek z wartości jest NULL, całe wyrażenie również stanie się NULL i zostanie pominięte. Dlatego często lepiej jest łączyć STRING_AGG z CONCAT lub CONCAT_WS.

Porównanie omówionych sposobów dla 2 kolumn

Porównując różne sposoby łączenia tekstów na przykładzie dwóch kolumn (FirstName i LastName), możemy wyraźnie zobaczyć różnice w zachowaniu każdego z nich. Ten podstawowy scenariusz jest jednoczenie typowym przypadkiem użycia w praktycznych aplikacjach – łączenie imienia i nazwiska w całość jest jedną z najczęściej wykonywanych operacji konkatenacji w systemach bazodanowych.

-- Porównanie wszystkich metod dla 2 kolumn:
SELECT 
ID,
FirstName,
LastName,
FirstName + '-' + LastName AS [Operator +],
CONCAT(FirstName, '-', LastName) AS [Funkcja CONCAT],
CONCAT_WS('-', FirstName, LastName) AS [Funkcja CONCAT_WS]
FROM ImperiumSzkolenioweDB.dbo.Employees
ORDER BY ID;

Operator + pokazuje swoje główne ograniczenie – wszędzie tam, gdzie FirstName lub LastName jest NULL, wynik również staje się NULL. Dla pracownika o ID 4 (Maria z pustym LastName) oraz pracownika o ID 6 (Zieliński z pustym FirstName) otrzymujemy NULL zamiast części informacji. To zachowanie może być problematyczne w aplikacjach, gdzie chcemy wyświetlić przynajmniej częściowe informacje.

Funkcja CONCAT radzi sobie znacznie lepiej z wartościami NULL. Dla Marii (ID 4) otrzymujemy „Maria-” (z myślnikiem na końcu), a dla pracownika o ID 6 otrzymujemy samo nazwisko „-Zieliński”. Wyniki są użyteczne, choć mogą zawierać zbędne spacje lub znaki, gdy jedna z wartości jest pusta – tak jak w naszym przypadku. To zachowanie jest raczej akceptowalne, ale może wymagać dodatkowej obróbki.

CONCAT_WS pokazuje swoją przewagę w eleganckim zarządzaniu separatorami. Dla Marii otrzymujemy po prostu „Maria” bez zbędnego myślnika na końcu, a dla pracownika o ID 6 otrzymujemy „Zieliński” bez myślnika na początku. Funkcja automatycznie dostosowuje separatory do dostępnych wartości, co daje najczystsze i najbardziej profesjonalne wyniki. To sprawia, że CONCAT_WS jest często najlepszym wyborem dla tego typu operacji.

Wszystkie trzy metody działają identycznie dla rekordów z kompletnymi danymi bez NULL-i – dla Jana Kowalskiego, Anny Nowak czy Tomasza Kowalczyka otrzymujemy identyczne wyniki w formie „Imię Nazwisko”. Różnice ujawniają się dopiero w przypadku niepełnych danych, gdzie strategia obsługi NULL-i staje się kluczowa.

Porównanie sposobów dla 5 kolumn

Łączenie większej liczby kolumn ujawnia dodatkowe różnice między przedstawionymi metodami oraz pokazuje, jak każda z nich radzi sobie z bardziej złożonymi scenariuszami. W tym przypadku połączymy pięć kolumn: FirstName, LastName, JobTitle, Department i City, aby utworzyć kompletny opis pracownika. Ten scenariusz jest reprezentatywny dla rzeczywistych aplikacji, gdzie często musimy tworzyć złożone opisy czy etykiety z wielu pól.

-- Porównanie wszystkich metod dla 5 kolumn
SELECT 
	ID,
	FirstName + ' ' + LastName + ' - ' + JobTitle + 
' (' + Department + ', ' + City + ')' AS [Operator_Plus],
	CONCAT(FirstName, ' ', LastName, ' - ', JobTitle, 
' (', Department, ', ', City, ')') AS [Funkcja CONCAT],
	CONCAT_WS(' ', FirstName, LastName, '-', JobTitle, 
'(' + Department + ',', City + ')') AS [Funkcja CONCAT_WS i Operator +],
	CONCAT(FirstName, ' ', LastName, ' - ', JobTitle, 
' (', CONCAT_WS(', ', Department, City), ')') AS [Funkcje CONCAT i CONCAT_WS]
FROM ImperiumSzkolenioweDB.dbo.Employees
ORDER BY ID;

Operator + ponownie pokazuje swoje największe ograniczenie w tym scenariuszu – jeśli którakolwiek z pięciu kolumn jest NULL, cały wynik staje się NULL. Oznacza to, że dla większości naszych testowych rekordów (które zawierają przynajmniej jedną wartość NULL) nie otrzymamy żadnych użytecznych informacji. To sprawia, że operator + jest praktycznie bezużyteczny w scenariuszach z większą liczbą kolumn, gdzie prawdopodobieństwo wystąpienia NULL-a jest wysokie.

Funkcja CONCAT radzi sobie znacznie lepiej, ignorując wartości NULL i łącząc dostępne informacje. Jednak wyniki mogą zawierać zbędne separatory i nieesteyczne formatowanie. Na przykład, dla pracownika bez JobTitle możemy otrzymać „Piotr Wiśniewski –  (HR, Gdańsk)” z podwójną spacją po myślniku, tak jak w naszym przypadku.

CONCAT_WS w czystej formie nie jest idealna dla tego scenariusza, ponieważ używa tego samego separatora dla wszystkich wartości. Aby uzyskać poprawne formatowanie z różnymi separatorami (spacje, myślniki, nawiasy), musimy łączyć CONCAT_WS z innymi metodami lub używać zagnieżdżonych wywołań. Powyższy przykład pokazuje hybrydowe podejście, gdzie główna struktura jest budowana przez operator + lub funkcję CONCAT, a CONCAT_WS jest używana do formatowania fragmentów.

Najbardziej praktycznym rozwiązaniem dla złożonych scenariuszy jest często kombinacja różnych metod. Takie podejście oferuje maksymalną kontrolę nad formatowaniem przy zachowaniu korzyści z inteligentnej obsługi NULL-i.

Podsumowanie

Łączenie tekstów w SQL to fundamentalna umiejętność, którą każda osoba pracująca z bazami danych powinna opanować. Przeanalizowaliśmy cztery główne sposoby, z których każdy ma swoje miejsce w arsenale narzędzi developera. Wybór odpowiedniej metody nie jest przypadkowy – zależy to od konkretnych wymagań projektu, wersji Microsoft SQL Server oraz tego, jak chcemy obsługiwać wartości NULL i formatowanie wyników.

Podsumowując – operator + pozostaje najprostszym rozwiązaniem dla podstawowych operacji łączenia, szczególnie gdy mamy pewność, że dane nie zawierają wartości NULL. Jego główną zaletą jest prostota i szybkość, ale znaczące ograniczenie w postaci obsługi NULL-i czyni go problematycznym w wielu praktycznych scenariuszach. Warto go stosować tylko w kontrolowanych środowiskach z gwarantowaną jakością danych. Funkcja CONCAT stanowi znaczący krok naprzód w porównaniu do operatora +, oferując inteligentną obsługę NULL-i przy zachowaniu dobrej wydajności. Jest to uniwersalne rozwiązanie, które sprawdza się w większości przypadków użycia. Jednak brak wbudowanej obsługi separatorów może sprawić, że kod stanie się mniej czytelny przy łączeniu wielu wartości z różnymi separatorami. CONCAT_WS to często najlepszy wybór dla nowoczesnych aplikacji w Microsoft SQL Server. Kombinuje zalety CONCAT z elegancką obsługą separatorów, co przekłada się na czystsze wyniki i bardziej czytelny kod. Automatyczne zarządzanie separatorami w kontekście NULL-i jest szczególnie cenną funkcjonalnością, która eliminuje typowe problemy z formatowaniem. Natomiast funkcja STRING_AGG służy specjalnemu przypadkowi użycia – łączeniu wartości z wielu wierszy. Jest ona nowoczesnym i zalecanym rozwiązaniem dla nowszych wersji Microsoft SQL Server (2017+).

Kluczowym czynnikiem przy wyborze metody jest strategia obsługi NULL-i. W środowiskach produkcyjnych rzadko mamy do czynienia z idealnymi, kompletnymi danymi, dlatego wybór metody, która elegancko radzi sobie z pustymi wartościami, jest kluczowy dla jakości aplikacji. Dodatkowo, warto zawsze testować wybrane rozwiązanie na reprezentatywnych danych, aby upewnić się, że wyniki spełniają oczekiwania dotyczące formatowania i kompletności. W praktyce najlepszym podejściem jest często kombinowanie różnych metod w zależności od konkretnego scenariusza. Proste łączenia dwóch kolumn mogą używać operatora +, złożone formatowanie może wymagać CONCAT lub CONCAT_WS z warunkami, a agregacja danych z wielu wierszy będzie potrzebować STRING_AGG. Zrozumienie mocnych i słabych stron każdej metody pozwala na podejmowanie świadomych decyzji projektowych i tworzenie wydajnych, niezawodnych rozwiązań bazodanowych.

Podobne artykuły

Wszystkie artykuły