⚙️ T-SQL benutzerdefinierte Skalarfunktionen
Was sind benutzerdefinierte Skalarfunktionen?
GRUNDLAGEN-- Ganz einfache Logik: Berechnung des Bruttopreises mit 19 % MwSt.
CREATE FUNCTION dbo.fn_Bruttopreis(@Netto DECIMAL(10,2))
RETURNS DECIMAL(10,2)
AS
BEGIN
RETURN @Netto * 1.19;
END;
GO
-- Einfacher Aufruf im SELECT
SELECT
Artikelname,
Preis,
dbo.fn_Bruttopreis(Preis) AS Bruttopreis
FROM Produkte;
Syntax & grundlegender Aufbau
STRUKTUR-- Vollständige Syntax am Beispiel einer Funktion, die einen Kundenstatus ermittelt
CREATE FUNCTION dbo.fn_Kundenstatus (@Umsatz DECIMAL(10,2))
RETURNS VARCHAR(20)
AS
BEGIN
DECLARE @Status VARCHAR(20);
SET @Status =
CASE
WHEN @Umsatz > 10000 THEN 'Premium'
WHEN @Umsatz > 5000 THEN 'Gold'
ELSE 'Standard'
END;
RETURN @Status;
END;
GO
-- Optional: Ändern oder Anlegen mit CREATE OR ALTER
CREATE OR ALTER FUNCTION dbo.fn_Kundenstatus (@Umsatz DECIMAL(10,2))
RETURNS VARCHAR(20) AS BEGIN ... END;
Praktische Beispiele ohne Datenbankzugriff
MATHEMATIK & LOGIK-- Rabattpreis berechnen
CREATE FUNCTION dbo.fn_DiscountPrice(@price DECIMAL(12,2), @discount DECIMAL(12,2))
RETURNS DECIMAL(12,2)
AS
BEGIN
RETURN @price * (1 - @discount);
END;
GO
-- Volljährigkeit prüfen (wahr/falsch) auf Basis eines Geburtsdatums
CREATE FUNCTION dbo.fn_IsAdult(@BirthDate DATE)
RETURNS BIT
AS
BEGIN
DECLARE @Age INT = DATEDIFF(YEAR, @BirthDate, GETDATE());
IF (MONTH(@BirthDate) > MONTH(GETDATE()))
SET @Age = @Age - 1;
ELSE IF (MONTH(@BirthDate) = MONTH(GETDATE())
AND DAY(@BirthDate) > DAY(GETDATE()))
SET @Age = @Age - 1;
RETURN IIF(@Age >= 18, 1, 0);
END;
Mit Datenbankzugriff – Werte aus Tabellen ermitteln
TABELLENZUGRIFF-- Anzahl der Bestellungen eines Kunden
CREATE FUNCTION dbo.fn_CountOrdersForCustomer(@CustomerID INT)
RETURNS INT
AS
BEGIN
DECLARE @OrderCount INT;
SELECT @OrderCount = COUNT(OrderID)
FROM Sales.Orders
WHERE CustomerID = @CustomerID;
RETURN @OrderCount;
END;
GO
-- Verfügbarer Lagerbestand nach Abzug von Reservierungen
CREATE FUNCTION dbo.fn_AvailableStock(@ProductID INT)
RETURNS INT
AS
BEGIN
DECLARE @Stock INT;
SELECT @Stock = p.UnitsInStock - ISNULL(SUM(r.ReservedQuantity), 0)
FROM Production.Products p
LEFT JOIN Sales.Reservations r ON p.ProductID = r.ProductID
WHERE p.ProductID = @ProductID
GROUP BY p.UnitsInStock;
RETURN @Stock;
END;
Erweiterte Optionen: SCHEMABINDING & NULL‑Behandlung
SCHEMABINDING · NULL‑Input-- Schema‑gebundene Funktion (schützt vor Änderungen der Basistabellen)
CREATE FUNCTION dbo.fn_GetCustomerCity_withBinding(@CustomerID INT)
RETURNS VARCHAR(50)
WITH SCHEMABINDING, RETURNS NULL ON NULL INPUT
AS
BEGIN
DECLARE @City VARCHAR(50);
SELECT @City = City
FROM dbo.Customers
WHERE CustomerID = @CustomerID;
RETURN @City;
END;
GO
-- Explizite NULL‑Prüfung innerhalb der Logik
CREATE FUNCTION dbo.fn_SafeDivision(@Dividend DECIMAL(12,2), @Divisor DECIMAL(12,2))
RETURNS DECIMAL(12,2)
AS
BEGIN
RETURN
CASE
WHEN @Divisor IS NULL OR @Divisor = 0 THEN NULL
ELSE @Dividend / @Divisor
END;
END;
Performance & praxisnahe Alternativen
INLINING · Inline‑TVF-- Beispiel einer iTVF, die dasselbe Ergebnis liefert wie eine Skalarfunktion
CREATE FUNCTION dbo.itvf_GetAvailableCredit(@CustomerId INT)
RETURNS TABLE
AS
RETURN
(
SELECT ISNULL(c.CreditLimit - SUM(o.OrderTotal), 0) AS AvailableCredit
FROM dbo.Customers c
LEFT JOIN dbo.Orders o ON c.CustomerId = o.CustomerId AND o.IsPaid = 0
WHERE c.CustomerId = @CustomerId
GROUP BY c.CreditLimit
);
GO
-- Aufruf mit CROSS APPLY (set‑basiert statt zeilenweise)
SELECT
c.CustomerId,
c.Name,
ac.AvailableCredit
FROM dbo.Customers c
CROSS APPLY dbo.itvf_GetAvailableCredit(c.CustomerId) ac;
📌 Entscheidungshilfe: Wann Skalarfunktion, wann iTVF?
/* Skalarfunktion (traditionell) */
✅ sehr einfache, reine Rechenlogik ohne Datenbankzugriffe
✅ wiederverwendbare Formatierung / Umrechnung
✅ moderate Aufrufzahlen (wenige tausend Zeilen)
✅ Inlining‑fähige Funktion (MODIFIES SQL DATA vermeiden)
/* Inline‑Tabellenwertfunktion (iTVF) */
✅ komplexe Lookups mit Joins über mehrere Tabellen
✅ große Ergebnismengen (hunderttausende Zeilen)
✅ maximale Performance & Parallelität erwünscht
✅ einfaches Binden an vorhandene Indizes
⭐ Wichtigste Best Practices
- Verwenden Sie SCHEMABINDING – ermöglicht Inlining und schützt vor unbeabsichtigten Schema-Änderungen.
- Setzen Sie RETURNS NULL ON NULL INPUT, wenn die Funktion bei NULL‑Argumenten stets NULL liefern soll (das spart Ressourcen).
- Vermeiden Sie datenzugreifende Skalarfunktionen innerhalb großer WHERE‑Klauseln – das erzwingt row‑by‑row‑Ausführung.
- Prüfen Sie, ob die Funktion für Inlining geeignet ist: Nutzen Sie nicht nicht-inlineable Elemente wie Tabellenvariablen, dynamisches SQL oder TRY/CATCH.
- Erwägen Sie bei Performanceproblemen den Umbau in eine Inline‑Tabellenwertfunktion – oft der einfachste Weg zu drastisch besseren Laufzeiten.
✅ Fazit: Benutzerdefinierte Skalarfunktionen sind eine ausgezeichnete Wahl für modulare, wiederverwendbare Logik. Mit den modernen Optimierungen (Inlining) und der bewussten Wahl der Optionen behalten Sie sowohl sauberen Code als auch gute Performance.