Neben den reinen Neuerungen in komprimierter Form erwarten interessierte Leser und Leserinnen also auch ein – sicherlich persönlich geprägtes – Minifazit. Sollte dies von den eigenen Anforderungen oder der eigenen Meinung abweichen, so mögen Sie es dem Autor verzeihen. Kenntnisse älterer SQL-Server-Versionen sind hilfreich.
Unterscheidbar oder nicht? Filtern mit NULL
Zu prüfen, ob zwei Werte gleich sind oder nicht, klingt im ersten Moment trivial. Und zwar genau so lange, bis einer (oder gar beide) NULL sein können. Ab diesem Moment muss mit einer Kombination von gleich und ungleich sowie mit dem IS-Operator gearbeitet werden. (Zur Erinnerung: NULL = NULL OR NULL <> NULL ist immer falsch, auch wenn es dem einen oder anderen erst einmal widersinnig erscheint). Aus diesem Grund bietet SQL Server 2022 nun die Syntax A IS DISTINCT FROM B beziehungsweise A IS NOT DISTINCT FROM B. Eine Abfrage kann so aussehen:
SELECT * FROM <Tabelle/ Sicht> WHERE A IS DISTINCT FROM B; SELECT * FROM <Tabelle/ Sicht> WHERE A IS NOT DISTINCT FROM B;
Abbildung 1 zeigt, wie der Server dies umsetzt – etwas umfangreich, aber korrekt und nötig. Sicher ist, dass es sich bei A/B um Ausdrücke oder Spalten handeln kann.
Abb. 1: Decoding der neuen IS (NOT) DISTINCT FROM-Syntax
Minifazit: Wer bereit ist, statt A=B einfach A IS NOT DISTINCT B zu schreiben, bekommt ein Ergebnis, das in jedem Fall korrekt ist. Mehr ist nicht nötig. Notlösungen mit der ISNULL()-Funktion sollten damit endgültig der Vergangenheit angehören.
RTRIM(), LTRIM()? Besser TRIM()!
Das Entfernen von Zeichen von Anfang und/oder Ende einer Zeichenkette ist eine Grundfunktionalität, für die es eigentlich für jedes Framework eine Lösung gibt. Nun auch für T-SQL mit der Angabe, welche Zeichen entfernt werden sollen. Whitespaces sind also nicht mehr Pflicht. Außerdem kann nun für die TRIM()-Funktion bestimmt werden, ob sie den Anfang oder das Ende einer Zeichenkette berücksichtigen soll oder beides. Letzteres wird nicht über einen Parameter, sondern den Zusatz LEADING, TAILING oder BOTH gesteuert. Listing 1 zeigt, wie das genau aussieht.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Listing 1 DECLARE @caption VARCHAR(100) = '-== SQL Server 2022 ==-'; SELECT LTRIM(@caption, '-= ') AS [LTRIM], RTRIM(@caption, '-= ') AS [RTRIM]; SELECT TRIM(LEADING '-= ' FROM @caption) AS [LEADING], TRIM(TRAILING '-= ' FROM @caption) AS [TRAILING], TRIM(BOTH '-= ' FROM @caption) AS [BOTH];
Minifazit: Praktisch, dass genau bestimmt werden kann, welche Zeichen (‘-= ‘ in Listing 1) entfernt werden sollen. Nur an die neue Syntax der TRIM()-Funktion muss man sich sicherlich erst gewöhnen.
Horizontales Maxi-/Minimum
Um den größten beziehungsweise kleinsten Wert einer Reihe von Werten und Ausdrücken zu ermitteln, waren bisweilen „interessante“ Lösungen notwendig. Die dabei naheliegenden Aggregate MIN() und MAX() funktionieren nicht, da diese nur einen Parameter akzeptieren und vertikal (aus Spalten) arbeiten. GREATEST() und LEAST() kann man nun eine Menge an kompatiblen Werten übergeben und erhält den größten oder den kleinsten von diesen. Folgender Abschnitt zeigt ein paar Aufrufe und das daraus folgende Ergebnis:
SELECT GREATEST('6.62', 3.1415, N'7') AS 'GREATEST', -- 7.0000 LEAST( '6.62', 3.1415, N'7') AS 'LEAST'; -- 3.1415 SELECT GREATEST('A', 'B', 'C') AS 'GREATEST', -- C LEAST('A', 'B', 'C') AS 'LEAST'; -- A
Minifazit: Klein, aber sehr hilfreich. Wer hatte so eine Anforderung noch nicht?
Zahlenreihen erzeugen
Ebenfalls öfter mal benötigt und nun leicht zu erzeugen: eine Zahlenreihe von einem Start- bis zu einem Endwert; auf Wunsch auch mit Schrittweite. Damit ist der konkrete Datentyp unerheblich, so lange er nur numerisch ist oder als solcher interpretiert wird. In der Praxis bedeutet dies Zahlen mit oder ohne Komma. Beides akzeptiert die neue GENERATE_SERIES()-Funktion. Neben den obligatorischen Werten für Start und Ende ist der Wert für die Schrittweite optional und SQL Server verwendet ohne Angabe 1, bzw. -1. Listing 2 zeigt ein paar Aufrufe mit der Funktion.
Listing 2 -- Von 1 bis <=50 in 5er Schritten => 1..6..11..41..46 SELECT value FROM GENERATE_SERIES(/* Start */ 1, /*STOP*/ 50, /*STEP*/ 5); -- Auch andere (numerische) Datentypen sind erlaubt SELECT value FROM GENERATE_SERIES(/* Start */ 1.0, /*STOP*/ 10.0, /*STEP*/ .5); -- Alle Kalenderwochen 2022 SELECT CAST(DATEADD(WEEK, value - 1, '2022-01-03') AS DATE), value FROM GENERATE_SERIES(1, 52);
Soll eine Serie mit anderen Datentypen erzeugt werden, so ist das auf Basis der Zahlenreihe natürlich auch kein Problem, wie die letzte Abfrage aus Listing 2 zeigt. Die Ausgabe ist in Abbildung 2 zu sehen.
Abb. 2: Kalenderwochen mit der GENERATE_SERIES()-Funktion
Minifazit: Ziemlich hilfreiche kleine Funktion für vielfältige Einsätze.
DATETRUNC()
Die neue DATETRUNC()-Funktion schneidet alle Details eines Datums/einer Uhrzeit ab der übergebenen Einheit ab. So liefert DATETRUNC(YEAR, ‘2022-10-25 19:45:32’) schlicht ‘2022-01-01 00:00:00.0000000’. Für Month wäre es ‘2022-10-01 00:00:00.0000000’. Listing 3 zeigt einige Varianten mit deren jeweiligen Ausgaben.
Listing 3 DECLARE @d DATETIME2 = GETDATE(); SELECT 'Year', DATETRUNC(YEAR, @d); -- 2022-01-01 00:00:00.0000000 SELECT 'Quarter', DATETRUNC(QUARTER, @d); --2022-10-01 00:00:00.0000000 SELECT 'Month', DATETRUNC(MONTH, @d); -- 2022-12-01 00:00:00.0000000 SELECT 'Week', DATETRUNC(WEEK, @d); -- 2022-12-11 00:00:00.0000000 SELECT 'Iso_week', DATETRUNC(ISO_WEEK, @d); -- 2022-12-12 00:00:00.0000000 SELECT 'DayOfYear', DATETRUNC(DAYOFYEAR, @d); -- 2022-12-17 00:00:00.0000000 SELECT 'Day', DATETRUNC(DAY, @d); -- 2022-12-17 00:00:00.0000000 SELECT 'Hour', DATETRUNC(HOUR, @d); -- 2022-12-17 19:00:00.0000000 SELECT 'Minute', DATETRUNC(MINUTE, @d); -- 2022-12-17 19:45:00.0000000 SELECT 'Second', DATETRUNC(SECOND, @d); -- 2022-12-17 19:45:17.0000000 SELECT 'Millisecond', DATETRUNC(MILLISECOND, @d); -- 2022-12-17 19:45:17.5800000 SELECT 'Microsecond', DATETRUNC(MICROSECOND, @d); -- 2022-12-17 19:45:17.5800000
Minifazit: Diese Funktion sorgt für einfacheren und lesbareren Code, wenn ein vollständiges Datum oder eine vollständige Uhrzeit vorliegt (z. B. von GETDATE() oder GETUTCDATE()), aber nur ein Teil davon tatsächlich benötigt wird.
SELECT … WINDOW-Klausel
Wer viel mit Window-Funktionen oder Aggregaten bei seinen Abfragen arbeitet, wird diese kleine Verbesserung zu schätzen wissen, da sich so repetitiver Code vermeiden lässt. Mit der WINDOW-Klausel lässt sich PARTITION BY und ORDER BY pro Abfrage einmal definieren, mit einem Namen versehen und dann mehrfach verwenden. Statt also eine Abfrage wie in Listing 4 ist nun eine wie in Listing 5 möglich.
Listing 4 SELECT SalesOrderID, ProductID, OrderQty ,SUM(OrderQty) OVER (PARTITION BY SalesOrderID) AS Total ,AVG(OrderQty) OVER (PARTITION BY SalesOrderID) AS "Avg" ,COUNT(OrderQty) OVER (PARTITION BY SalesOrderID) AS "Count" ,MIN(OrderQty) OVER (PARTITION BY SalesOrderID) AS "Min" ,MAX(OrderQty) OVER (PARTITION BY SalesOrderID) AS "Max" FROM Sales.SalesOrderDetail WHERE SalesOrderID IN(43659,43664);
Listing 5 SELECT SalesOrderID, ProductID, OrderQty ,SUM(OrderQty) OVER win AS Total ,AVG(OrderQty) OVER win AS "Avg" ,COUNT(OrderQty) OVER win AS "Count" ,MIN(OrderQty) OVER win AS "Min" ,MAX(OrderQty) OVER win AS "Max" FROM Sales.SalesOrderDetail WHERE SalesOrderID IN(43659,43664) WINDOW win AS (PARTITION BY SalesOrderID);
Die Abfrage in Listing 5 ist kompakter und Fehler bei Änderungen haben ebenfalls weniger Chancen.
Minifazit: Praktisch, wenn man viel mit umfangreichen Window-Funktionen oder Aggregaten arbeitet. Sonst eher hinderlich; dafür handelt es sich aber auch nur um eine Option, die alte Syntax existiert natürlich nach wie vor.
FIRST_VALUE() und LAST_VALUE()
Die Funktionen FIRST()_VALUE und LAST_VALUE() sind bei weitem nicht neu, sondern schon seit SQL Server 2012 im Portfolio. Nun sind sie um eine Syntax erweitert worden, die steuert, ob NULL berücksichtigt wird (RESPECT NULLS) oder nicht (IGNORE NULLS). Listing 6 zeigt beide nun möglichen Varianten.
Listing 6 SELECT Name, ListPrice, FIRST_VALUE(Name) RESPECT NULLS OVER (ORDER BY ListPrice ASC) AS LeastExpensive FROM Production.Product WHERE ProductSubcategoryID = 37; SELECT Name, ListPrice, FIRST_VALUE(Name) IGNORE NULLS OVER (ORDER BY ListPrice ASC) AS LeastExpensive FROM Production.Product WHERE ProductSubcategoryID = 37;
Minifazit: Die neue Syntax erspart eine zusätzliche Unterabfrage und macht damit die Abfrage an sich einfacher, lesbarer und performanter. Wer die beiden Funktionen bis dato noch nicht verwenden konnte, wird allerdings auch nicht bekehrt werden.
STRING_SPLIT() mit Ordinal (Index)
Auch die STRING_SPLIT()-Funktion ist nicht neu, wurde aber mit SQL Server 2022 um einen optionalen Parameter erweitert, der neben den aufgeteilten Elementen der Zeichenketten auch den entsprechenden Indexwert (beginnend mit 1) in einer zusätzlichen Spalte liefert – zumindest wenn eine 1 als Wert übergeben wird. Die folgende Abfrage liefert das Ergebnis, das in Abbildung 3 gezeigt wird:
SELECT * FROM STRING_SPLIT('Lorem ipsum dolor sit amet.', ' ', 1);
Abb. 3: STRING_SPLIT()-Funktion mit Ordinal (Index)
Minifazit: Wahrlich eine Detailerweiterung mit wenig Einsatzpotenzial, oder?
Genäherte Aggregate
An sich sind die grundlegenden Funktionen PERCENTILE_CONT() und PERCENTILE_DISC() ebenfalls nicht neu. APPROX_PERCENTILE_CONT() und APPROX_PERCENTILE_DISC() sind nun Varianten, die auch bei größeren Datenmengen sehr performant sind, dafür aber nur genähert und so mit einer Abweichung versehen sind. Listing 7 zeigt diese beiden Funktionen.
Listing 7 -- Kontinuierliche Werte SELECT DeptId, APPROX_PERCENTILE_CONT(0.10) WITHIN GROUP(ORDER BY Salary) AS 'P10', APPROX_PERCENTILE_CONT(0.90) WITHIN GROUP(ORDER BY Salary) AS 'P90' FROM #Employee GROUP BY DeptId; -- Diskrete Werte SELECT DISTINCT DeptId, PERCENTILE_DISC(0.10) WITHIN GROUP (ORDER BY Salary) OVER (PARTITION BY DeptId) AS 'P10', PERCENTILE_DISC(0.90) WITHIN GROUP (ORDER BY Salary) OVER (PARTITION BY DeptId) AS 'P90' FROM #Employee;
Minifazit: Ebenfalls eine Detailerweiterung für die wenigen, die ausgiebig mit PERCENTILE_CONT() und PERCENTILE_DISC() arbeiten – für den Rest eher uninteressant.
Zeitliche Abschnitte mit der DATE_BUCKET()-Funktion
Für das Erzeugen von zeitlichen Abschnitten steht ab SQL Server 2022 die DATE_BUCKET()-Funktion bereit. Sie liefert den Anfang eines Abschnittes (Woche, Monat, Jahr etc.) basierend auf einem Datum. Und dabei muss es sich nicht um den ersten Abschnitt handeln, er kann auch per Index bestimmt werden. Listing 8 zeigt den Aufruf.
Listing 8 DECLARE @date DATETIME2 = '2022-02-24 00:00:00'; SELECT 'WEEK', DATE_BUCKET(WEEK, 1, @date); -- 2022-02-21 00:00:00.0000000 SELECT 'MONTH', DATE_BUCKET(MONTH, 1, @date); -- 2022-02-21 00:00:00.0000000 SELECT 'YEAR', DATE_BUCKET(YEAR, 1, @date); -- 2022-01-01 00:00:00.0000000 DECLARE @date DATETIME2 = '2022-02-24 00:00:00'; DECLARE @origin1 DATETIME2 = '2021-01-01 00:00:00'; DECLARE @origin2 DATETIME2 = '2021-02-22 00:00:00'; SELECT 'WEEK', DATE_BUCKET(WEEK, 1, @date, @origin1); -- 2022-02-18 00:00:00.0000000 SELECT 'WEEK', DATE_BUCKET(WEEK, 1, @date, @origin2); -- 2022-02-21 00:00:00.0000000
Minifazit: Eine sehr spezielle Funktion, die für viele keine Rolle spielen wird.
Bit-Funktionen
Gleich eine Handvoll neuer Funktionen für die Arbeit mit Bit-Werten bietet SQL Server 2022, auch wenn es wenige Gelegenheiten gibt, im T-SQL-Umfeld damit zu arbeiten – die UPDATE()-Funktion im Zuge von DML-Triggern ist eine der wenigen davon. Tabelle 1 zeigt die einzelnen Funktionen zusammen mit einer kleinen Erläuterung. Listing 9 zeigt die Verwendung dieser Funktionen in einem kurzen Beispiel.
Funktion | Erläuterung |
RIGHT_SHIFT() | Verschiebt den numerischen Wert binär nach rechts |
LEFT_SHIFT() | Verschiebt den numerischen Wert binär nach links |
BIT_COUNT () | Liefert die Anzahl der (aus 1) gesetzten Bits |
GET_BIT() | Liefert den Status (0 oder 1) eines bestimmten Bits |
SET_BUT() | Setzt den Status (0 oder 1) eines bestimmten Bits |
Tabelle 1: Neue Bit-Funktionen
Listing 9 -- RIGHT_SHIFT()/ LEFT_SHIFT() SELECT @number, RIGHT_SHIFT(@number, 1) '>> 1', RIGHT_SHIFT(@number, 2) '>> 2', RIGHT_SHIFT(@number, 3) '>> 3'; -- 10011010010 1001101001 100110100 10011010 SELECT @number, LEFT_SHIFT(@number, 1) '<< 1', LEFT_SHIFT(@number, 2) '<< 2', LEFT_SHIFT(@number, 3) '<< 3'; -- 10011010010 100110100100 1001101001000 10011010010000 -- BIT_COUNT() SELECT @number, BIT_COUNT(@number) 'Number of 1s'; -- 10011010010 5 -- SET_BIT()/ GET_BIT() SELECT GET_BIT(@number, 0) 'Pos 0', GET_BIT(@number, 1) 'Pos 1';
Minifazit: Wie bereits erwähnt, gibt es nur wenige Stellen, an denen in T-SQL mit Bit-Operationen sinnvoll gearbeitet werden kann. Aber wenn man es mit so einer Stelle zu tun hat, machen die Neuerungen vieles einfacher.
Arbeiten mit JSON
Und vielleicht das Beste zum Schluss: Arbeiten mit JSON ist seit SQL Server 2016 möglich und wird nun mit SQL Server 2022 essenziell erweitert. Um genau zu sein, gibt es drei neue Funktionen und eine Erweiterung einer bestehenden Funktion. Tabelle 2 zeigt diese Funktionen mit einer kleinen Erläuterung für ihren jeweiligen Zweck. Einige Funktionen arbeiten dabei mit unterschiedlichen JSON-Typen, die in Tabelle 3 aufgelistet sind. Listing 10 zeigt den Einsatz der JSON-Funktion im Beispiel.
Funktion | Zweck |
JSON_ARRAY() | Erzeugt ein JSON-Array (Neu) |
JSON_OBJECT() | Erzeugt ein einfaches JSON-Objekt (Neu) |
ISJSON() | Erweitert um die neue JSON-Typangabe |
JSON_PATH_EXIST() | Prüft, ob ein JSON-Objekt den angegebenen Pfad besitzt (Neu) |
Tabelle 2: Neue und erweiterte JSON-Funktionen
JSON-Type | Beschreibung |
VALUE | Testet auf einen Wert (object, array, number, string, true, false, null |
ARRAY | Testet auf ein JSON-Array |
OBJECT | Testet auf ein JSON-Objekt |
SCALAR | Testet auf einen gültigen Skalarwert – Zahl, String etc. |
Tabelle 3: Unterstützte JSON-Typen
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Listing 10 DECLARE @jsonInfo NVARCHAR(MAX)=N'{"info":{"address":[{"town":"Nidderau-Erbstadt"},{"town":"Rom"}]}}'; -- JSON_ARRAY SELECT JSON_ARRAY(); SELECT JSON_ARRAY('a', 1, 'b', 2); SELECT JSON_ARRAY('name', name, 'id', database_id) FROM sys.databases; GO -- JSON_OBJECT SELECT JSON_OBJECT('name':'value', 'type':null, 'abc': 3); SELECT JSON_OBJECT('name':'value', 'type':null, 'abc': 3 NULL ON NULL /* Standard */ ); SELECT JSON_OBJECT('name':'value', 'type':null, 'abc': 3 ABSENT ON NULL); GO -- ISJSON DECLARE @json VARCHAR(MAX) = '["a",1,"b",2]'; SELECT ISJSON(@json, ARRAY); -- 1 SELECT ISJSON(@json, SCALAR); -- 0 -- ARRAY, SCALAR, VALUE, OBJECT GO -- JSON_PATH_EXISTS DECLARE @jsonInfo NVARCHAR(MAX)=N'{"info":{"address":[{"town":"Nidderau-Erbstadt"},{"town":"Rom"}]}}'; SELECT JSON_PATH_EXISTS(@jsonInfo,'$.info.address'); -- 1 SELECT JSON_PATH_EXISTS(@jsonInfo,'$.info.email'); -- 0 GO
Minifazit: Sinnvolle und nützliche Funktionen und Erweiterungen, für die man bei der Arbeit mit JSON schnell einen Einsatz findet. Leider ist (immer noch) kein JSON-Datentyp vorhanden, um mit großen Datenmengen im JSON-Format performant arbeiten zu können.
Gesamtfazit
SQL Server 2022 bietet einiges Neue für T-SQL: IS [not ]DISTINCT FROM, RTRIM(), LTRIM() & TRIM(), GREATEST() & LEAST() und DATETRUNC() sind nützliche Verbesserungen, die schnell Verwendung finden. Andere Neuerungen sind recht speziell, finden aber bestimmt auch ihre Anwender:innen. Insgesamt sicher keine echte Revolution, aber eine Evolution ist es schon. Vielleicht schafft es Microsoft sogar noch, T-SQL sprachlich in die Zukunft zu führen.