COALESCECOALESCE akceptuje serie wartości z listy wyników, które mogą być puste (NULL) potem ona zwraca pierwszą nie pustą wartość z tej listy.

Taka funkcjonalność daje wiele kreatywnych użyć w  bazie SQL Server.

Oto proste przykłady.

image

Tabelka PeopleNames zawiera kolumnę “Middle Name”, która może być pusta w końcu nie każda osoba ma drugie imię. Naszym celem jest dodanie wszystkie napisów uwzględniając przy tym drugie imię.

Dodawanie napisów COALESCE

Skrypt tworzący tabelkę znajduję się poniżej.

CREATE TABLE [dbo].[PeopleNames](
	[Id] [int] NOT NULL,
	[FirstName] [nvarchar](50) NOT NULL,
	[MiddleName] [nvarchar](50) NULL,
	[LastName] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_PeopleNames] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

Tabelkę trzeba uzupełnić danymi.

INSERT INTO [dbo].[PeopleNames]
           ([Id]
           ,[FirstName]
           ,[MiddleName]
           ,[LastName])
     VALUES
           (1,'Cezary',NULL,'Walenciuk'),
           (2,'Stefan','Viktor','Kowalski'),
           (3,'Franko','Cyryl','Domanowski'),
           (4,'Zdzisław',NULL,'Bohaterowicz')

Funkcja COALESCE pozwala nam wstawić naszą wartość w przypadku wystąpienia elementu pustego. Gdy drugie imię będzie puste zostanie wstawiony pusty znak.

SELECT  FirstName + ' ' +COALESCE(MiddleName,'')+ ' ' + LastName FROM PeopleNames

Co pozwoli nam dodać wszystkie wartości kolumn bez żadnych problemów.

image

Operacja dodawania napisów, gdy jeden z parametrów jest NULL zwróci NULL.Jakiekolwiek wyrażenie matematyczne na typach liczbowych zwróci NULL, jeśli jeden z parametrów jest NULL.

SELECT  FirstName + ' ' +MiddleName + ' ' + LastName FROM PeopleNames

Dlatego bez użycia funkcji  COALESCE bezpośrednie dodanie kolumn przy pustym drugi imieniu zwróci NULL.

image

Jak zauważyłeś przy poprawnym wyniku, gdy drugie imię jest puste występuje jeden  znak spacji za dużo. Jednak to nie jest duży problem wystarczy tylko trochę zmodyfikować zapytanie.

SELECT  FirstName + COALESCE(' ' + MiddleName,'')+ ' ' + LastName FROM PeopleNames

Teraz zawsze występuje jeden znak spacji bez względu na to, czy drugie imię jest puste, czy nie.

image

Matematyka z COALESCE

Oto drugi przykład z tabelą Package.

image

Tabela ta określa ilość zapłaty za przesyłkę, która jest zależna od wagi paczki od dystansu oraz od kosztu specjalnego. Wszystkie te pola jednak mogą  być puste. Czyli cena za przesyłkę może być liczona  poprzez kombinację tych pól w zależności od tego, czy są puste, czy nie.

Jak widać tabelka też zawiera pola bitowe które określają z jakiej kombinacji te pola będą liczone ,ale oczywiście nie będziemy liczyć ceny za pomocą wyrażenie IF i ELSE (to nie C#).

CREATE TABLE [dbo].[Package](
	[PackagedId] [int] NOT NULL,
	[WeightKg] [decimal](18, 2) NULL,
	[PricePerKg] [decimal](18, 2) NULL,
	[DistanceKm] [decimal](18, 2) NULL,
	[PricePerKm] [decimal](18, 2) NULL,
	[AdditionalCost] [decimal](18, 2) NULL,
	[CalculateWeight] [bit] NOT NULL,
	[CalculateDistance] [bit] NOT NULL,
	[CalculateAdditionalCost] [bit] NOT NULL,
 CONSTRAINT [PK_Package] PRIMARY KEY CLUSTERED 
(
	[PackagedId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

Jak więc wygląda rozwiązanie przy użyciu wyrażenia COALESCE . Uzupełnimy tabelkę i się przekonajmy.

INSERT INTO [dbo].[Package]
           ([PackagedId]
           ,[WeightKg]
           ,[PricePerKg]
           ,[DistanceKm]
           ,[PricePerKm]
           ,[AdditionalCost]
           ,[CalculateWeight]
           ,[CalculateDistance]
           ,[CalculateAdditionalCost])
     VALUES
           (1,NULL,NULL,NULL,NULL,10.50,0,0,1),
           (2,15.5,5.25,NULL,NULL,5.50,1,0,1),
           (3,25.5,7.25,975,0.25,5.50,1,1,1),
           (4,NULL,NULL,1342,0.25,NULL,0,1,0),
           (5,NULL,NULL,2128.5,0.15,20.63,0,1,1),
           (6,NULL,NULL,538.7,0.30,15.25,0,1,1),
           (7,55.5,4.25,NULL,NULL,NULL,1,0,0)

image

Wyrażenie mnożenia zwróci NULL, jeśli chociaż jeden z parametrów też jest NULL co oznacza ,że całe wyrażenie mnożenia możemy umieścić wewnątrz funkcji COALESCE. Z logiki tej tabelki jasno wynika ,że jeśli wartość ceny za kilogram jest puste to  kolumna  kilogramy też powinna być pusta. Czyli nie powinnyśmy uwzględniać ceny za kilogramy w dodawaniu, jeśli chociażby jedna z tych kolumn ma pustą wartość.

Gdy wyrażenie mnożenia będzie NULL funkcja  COALESCE zwróci wartość zero.

SELECT 
      PackagedId,
      COALESCE(WeightKg * PricePerKg,0)+
      COALESCE(DistanceKm * PricePerKm,0)+
      COALESCE(AdditionalCost,0) AS Pay
FROM dbo.Package

W ten sposób obliczamy cena przesyłkę bez względu na to, które pola są puste ,a które nie.

image

Te wyrażenie może być przydatne w kilku miejscach w twojej aplikacji wciąż nie jest to jednak wydajne rozwiązanie.

W takim wypadku warto stworzyć kolumnę, która ma już i będzie miała przy nowych rekordach  skalkulowaną tą wartość.

Ta wartość jest dynamicznie obliczana przez SQL więc nie trzeba jej aktualizować przy zmianie innych kolumn. Dzieje się to automatycznie.

ALTER TABLE dbo.Package
ADD Payment AS
      COALESCE(WeightKg * PricePerKg,0)+
      COALESCE(DistanceKm * PricePerKm,0)+
      COALESCE(AdditionalCost,0)

Teraz zwykłe wyrażenie SELECT zwróci cena całkowitą za przesyłkę paczki.

WHERE z COALESCE

COALESCE ma też oczywiście inne praktyczne użycia niż dodawanie różnych wartości.

Oto procedura, która zwróci imiona osób z poprzedniej tabelki. Dzięki COALESCE parametry @FirstName oraz @LastName mogą być puste co daje dużo elastyczność.

CREATE  PROCEDURE [dbo].[uspReadPeopleNames]
@FirstName VARCHAR(50) = NULL,
@LastName VARCHAR(50) = NULL
AS
BEGIN
	SELECT [Id]
		  ,[FirstName]
		  ,[MiddleName]
		  ,[LastName]
	 FROM [dbo].[PeopleNames]
	 WHERE [FirstName] LIKE COALESCE(@FirstName,[FirstName]) OR
	 [LastName] LIKE COALESCE(@LastName,[LastName])
	 
END

Gdy parametr nie zostanie podany COALESC wstawi w wrażeniu logicznym tą samą wartość kolumny z którą jest on porównywany. Takie wyrażenie  zawsze zwróci prawdę przynajmniej dla kolumn bez wartości  pustych.

Warto też zwrócić uwagę na różnice warunków OR  i AND, gdyż w obecnym przypadku gdy podamy tylko jeden parametr wciąż otrzymamy wszystkie wyniki.  W końcu zwracam wyniki pozytywne dla obu warunków.

Dopiero po podaniu dwóch parametrów wybór zostanie ograniczony.

EXEC	@return_value = [dbo].[uspReadPeopleNames]
		@FirstName = N'F%',
		@LastName =  N'W%'

image

EXEC	@return_value = [dbo].[uspReadPeopleNames]

Jak widać, jeśli nie podamy żadnych parametrów zostaną zwrócone wszystkie wyniki.

image

Problem NULL = NULL

A jak wybrać pole “MiddleName”. Dla przypomnienia NULL w bazie danych oznacza wartość nieokreśloną. Nie określoność nie określoności nie równa . Czyli przyrównanie NULL = NULL zawsze zwróci wartość logiczną false.

image

W takim wypadku funkcja COALESCE w niczym nam nie pomoże. Wręcz nawet utrudni zapytanie. Musimy uwzględnić scenariusz pustej kolumny oraz pustego parametru. W takim wypadku  lepiej  zastosować proste wyrażenie logiczne.

ALTER PROCEDURE [dbo].[uspReadPeopleNames]
@FirstName VARCHAR(50) = NULL,
@LastName VARCHAR(50) = NULL,
@MiddleName VARCHAR(50) = NULL
AS
BEGIN
	SELECT [Id]
		  ,[FirstName]
		  ,[MiddleName]
		  ,[LastName]

	 FROM [dbo].[PeopleNames]
	 WHERE 
	 [FirstName] LIKE COALESCE(@FirstName,[FirstName]) AND
	 [LastName] LIKE COALESCE(@LastName,[LastName]) AND
	 ([MiddleName] IS NULL AND @MiddleName IS NULL) OR ([MiddleName] LIKE @MiddleName) 
END

Zmienić też operacje logiczne na AND. Teraz będą selekcją będzie uwzględniać cześć wspólną wszystkich warunków.  Pokaże wyniki dwóch zapytań dowodzących ,że wyrażenie powyżej jest dobrze napisane, ponieważ mam dziwne przeczucie ,że tak nie jest.

EXEC	@return_value = [dbo].[uspReadPeopleNames]
		@FirstName = N'C%',
		@LastName =  N'W%'

image

EXEC	@return_value = [dbo].[uspReadPeopleNames]
		@MiddleName = N'V%',
		@FirstName = N'S%',
		@LastName = N'K%'

image

Wszystko wydaje się w porządku ,ale gdy nie podamy żadnego parametru powinienem otrzymać wszystkie wyniki jednak tak nie jest. Teraz wiesz jak to fajnie być testerem swojej aplikacji.

image

Jak widać obecna funkcja dla parametru  MiddelName  z wartością NULL domyślnie wykluczę pola nie puste gdy nie podamy parametru. Trzeba to poprawić.

ALTER PROCEDURE [dbo].[uspReadPeopleNames]
@FirstName VARCHAR(50) = NULL,
@LastName VARCHAR(50) = NULL,
@MiddleName VARCHAR(50) = NULL
AS
BEGIN
	SELECT [Id]
		  ,[FirstName]
		  ,[MiddleName]
		  ,[LastName]

	 FROM [dbo].[PeopleNames]
	 WHERE 
	 [FirstName] LIKE COALESCE(@FirstName,[FirstName]) AND
	 [LastName] LIKE COALESCE(@LastName,[LastName]) AND
	 (([MiddleName] IS NULL AND @MiddleName IS NULL) OR ([MiddleName] LIKE @MiddleName) 
	 OR (@MiddleName IS NULL))
END

Sprawdź działanie procedury raz jeszcze.

EXEC	@return_value = [dbo].[uspReadPeopleNames]
EXEC	@return_value = [dbo].[uspReadPeopleNames]
		@FirstName = N'Stefan'
EXEC	@return_value = [dbo].[uspReadPeopleNames]
		@FirstName = N'Cezary'

image

Teraz wszystko działa jak należy.

Mechanizm OR

Jeśli z jakiegoś powodu chcesz mieć poprzedni mechanizm OR, który zwraca sumę wszystkich pozytywnych wyrażenie logiczny oto poniżej zmieniony mechanizm tego kodu.

ALTER PROCEDURE [dbo].[uspReadPeopleNames]
@FirstName VARCHAR(50) = NULL,
@LastName VARCHAR(50) = NULL,
@MiddleName VARCHAR(50) = NULL
AS
BEGIN
	SELECT [Id]
		  ,[FirstName]
		  ,[MiddleName]
		  ,[LastName]

	 FROM [dbo].[PeopleNames]
	 WHERE 
	 [FirstName] LIKE COALESCE(@FirstName,[FirstName]) OR
	 [LastName] LIKE COALESCE(@LastName,[LastName]) OR
	 (([MiddleName] IS NULL AND @MiddleName IS NULL) 
	 OR ([MiddleName] LIKE @MiddleName) 
	 OR ([MiddleName] IS NOT NULL AND  @MiddleName IS NULL))
 
END

Wypadałoby skróci te wyrażenie algebrą boola o ile to możliwe dla wydajności.

Gdy nie podam parametru MiddelName zostaną zwrócone wszystkie wyniki.

image

Natomiast gdy określę wszystkie parametry otrzymam ich sumę spełnionych warunków.

EXEC	@return_value = [dbo].[uspReadPeopleNames]
		@MiddleName = N'V%',
		@FirstName = N'Stefan',
		@LastName = N'Walenciuk'

image

Kowalski został wybrany, ponieważ jego drugie imię zaczyna się na “V” oraz jego imię to “Stefan”. Cezary został wybrany, ponieważ jego nazwisko to “Walenciuk”.