User-Defined Functions
Build custom functions that extend SQL's built-in capabilities for domain-specific logic.
- Scalar Functions β Return a single value for use in SELECT, WHERE, and JOIN clauses
- Table-Valued Functions β Return a result set that can be used like a table in FROM clauses
- Deterministic Logic β Encapsulate calculations that produce consistent, repeatable results Functions let you write clean, testable, and reusable SQL without duplicating logic.
What Is a User-Defined Function?
DfUser-Defined Function (UDF)
A named, reusable SQL routine that accepts parameters, performs calculations or lookups, and returns a result. Unlike stored procedures, functions must return a value and can be embedded directly in SQL expressions.
UDFs are particularly useful when you need the same calculation across multiple queries. Instead of repeating complex logic, you define it once and reference it by name β just like built-in functions such as GETDATE() or SUM().
-- Create a scalar function
CREATE FUNCTION dbo.CalculateAge(@birth_date DATE)
RETURNS INT
AS
BEGIN
DECLARE @age INT;
SET @age = DATEDIFF(YEAR, @birth_date, GETDATE())
- CASE
WHEN DATEADD(YEAR, DATEDIFF(YEAR, @birth_date, GETDATE()), @birth_date) > GETDATE()
THEN 1 ELSE 0
END;
RETURN @age;
END;
-- Use it in a query
SELECT name, birth_date, dbo.CalculateAge(birth_date) AS age
FROM employees;
Scalar Functions
Scalar functions return a single value. They can be used anywhere an expression is valid β SELECT lists, WHERE clauses, CHECK constraints, and computed columns.
CREATE FUNCTION dbo.FormatPhoneNumber(@phone NVARCHAR(20))
RETURNS NVARCHAR(20)
AS
BEGIN
DECLARE @digits NVARCHAR(20) = REPLACE(REPLACE(REPLACE(@phone, '-', ''), '(', ''), ')', '');
IF LEN(@digits) = 10
RETURN '(' + SUBSTRING(@digits, 1, 3) + ') '
+ SUBSTRING(@digits, 4, 3) + '-'
+ SUBSTRING(@digits, 7, 4);
ELSE IF LEN(@digits) = 11
RETURN '+' + SUBSTRING(@digits, 1, 1) + ' ('
+ SUBSTRING(@digits, 2, 3) + ') '
+ SUBSTRING(@digits, 5, 3) + '-'
+ SUBSTRING(@digits, 8, 4);
ELSE
RETURN @phone;
END;
-- Use in queries
SELECT name, dbo.FormatPhoneNumber(phone) AS formatted_phone
FROM contacts;
Inline Table-Valued Functions
DfInline Table-Valued Function (TVF)
A function that returns a result set defined by a single SELECT statement. It behaves like a parameterized view β the query plan is merged with the calling query for optimal performance.
CREATE FUNCTION dbo.GetOrdersByDateRange(@start_date DATE, @end_date DATE)
RETURNS TABLE
AS
RETURN
(
SELECT order_id, customer_id, order_date, total, status
FROM orders
WHERE order_date BETWEEN @start_date AND @end_date
);
-- Use like a table
SELECT * FROM dbo.GetOrdersByDateRange('2024-01-01', '2024-06-30');
-- Join with other tables
SELECT c.name, o.order_id, o.total
FROM customers c
INNER JOIN dbo.GetOrdersByDateRange('2024-01-01', '2024-12-31') o
ON c.customer_id = o.customer_id;
Multi-Statement Table-Valued Functions
DfMulti-Statement TVF
A function that returns a result set constructed through multiple SQL statements. It allows variables, loops, and conditional logic to build the result set incrementally.
CREATE FUNCTION dbo.GetSalesReport(@year INT)
RETURNS @report TABLE
(
month_name NVARCHAR(15),
total_orders INT,
total_revenue DECIMAL(12,2),
avg_order_value DECIMAL(10,2)
)
AS
BEGIN
INSERT INTO @report
SELECT
DATENAME(MONTH, order_date) AS month_name,
COUNT(*) AS total_orders,
ISNULL(SUM(total), 0) AS total_revenue,
ISNULL(AVG(total), 0) AS avg_order_value
FROM orders
WHERE YEAR(order_date) = @year
GROUP BY MONTH(order_date), DATENAME(MONTH, order_date)
ORDER BY MONTH(order_date);
RETURN;
END;
-- Use the report
SELECT * FROM dbo.GetSalesReport(2024);
Deterministic vs Non-Deterministic Functions
| Type | Behavior | Example |
|---|---|---|
| Deterministic | Same input always produces same output | dbo.CalculateAge(birth_date) |
| Non-Deterministic | Same input may produce different output | dbo.GetRandomNumber() |
-- Deterministic: always returns the same result for the same input
CREATE FUNCTION dbo.FahrenheitToCelsius(@fahrenheit DECIMAL(5,2))
RETURNS DECIMAL(5,2)
AS
BEGIN
RETURN ROUND((@fahrenheit - 32) * 5.0 / 9.0, 2);
END;
-- Non-deterministic: uses GETDATE(), so output varies
CREATE FUNCTION dbo.GetBusinessDayStatus()
RETURNS NVARCHAR(10)
AS
BEGIN
IF DATEPART(WEEKDAY, GETDATE()) IN (1, 7)
RETURN 'WEEKEND';
ELSE
RETURN 'WEEKDAY';
END;
SQL Server can automatically detect determinism. If a function is deterministic and marked as such with SCHEMABINDING, it can be used in computed columns and indexed views.
Functions vs Stored Procedures
| Feature | Function | Stored Procedure |
|---|---|---|
| Return Value | Must return a value (scalar or table) | Optional (OUTPUT parameters) |
| Used In | SELECT, WHERE, JOIN, computed columns | EXEC statement only |
| Side Effects | Cannot modify database state (no INSERT/UPDATE/DELETE on tables) | Can perform any DML operation |
| Parameters | Input only | Input, output, and return values |
| Error Handling | Limited (no TRY/CATCH in some RDBMS) | Full TRY/CATCH support |
| Use Case | Calculations, transformations, lookups | Business processes, batch operations |
-- Function: pure calculation, no side effects
CREATE FUNCTION dbo.CalculateDiscount(@total DECIMAL(10,2), @tier NVARCHAR(20))
RETURNS DECIMAL(10,2)
AS
BEGIN
RETURN CASE @tier
WHEN 'GOLD' THEN @total * 0.20
WHEN 'SILVER' THEN @total * 0.10
WHEN 'BRONZE' THEN @total * 0.05
ELSE 0
END;
END;
-- Stored procedure: modifies data
CREATE PROCEDURE ApplyDiscount
@order_id INT,
@tier NVARCHAR(20)
AS
BEGIN
DECLARE @total DECIMAL(10,2);
SELECT @total = total FROM orders WHERE order_id = @order_id;
UPDATE orders
SET discount = dbo.CalculateDiscount(@total, @tier),
total = total - dbo.CalculateDiscount(@total, @tier)
WHERE order_id = @order_id;
END;
Performance Considerations
Scalar functions applied to every row in a query can cause performance issues. SQL Server executes scalar UDFs row-by-row (RBAR), which is significantly slower than set-based operations. Prefer inline TVFs or computed columns when possible.
-- Slow: scalar function called per row
SELECT name, dbo.CalculateAge(birth_date) AS age
FROM employees; -- executes function once per row
-- Faster: inline TVF with set-based logic
CREATE FUNCTION dbo.GetEmployeesByAgeRange(@min_age INT, @max_age INT)
RETURNS TABLE
AS
RETURN (
SELECT employee_id, name, birth_date,
DATEDIFF(YEAR, birth_date, GETDATE()) AS age
FROM employees
WHERE DATEDIFF(YEAR, birth_date, GETDATE()) BETWEEN @min_age AND @max_age
);
SELECT * FROM dbo.GetEmployeesByAgeRange(25, 35); -- single execution
Common Patterns
-- Pattern: Parameterized view (inline TVF)
CREATE FUNCTION dbo.SearchEmployees(@department NVARCHAR(50), @min_salary DECIMAL(10,2))
RETURNS TABLE
AS
RETURN (
SELECT e.employee_id, e.name, e.salary, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.department_id
WHERE (@department IS NULL OR d.department_name = @department)
AND e.salary >= @min_salary
);
-- Pattern: Lookup function
CREATE FUNCTION dbo.GetDepartmentName(@dept_id INT)
RETURNS NVARCHAR(50)
AS
BEGIN
DECLARE @name NVARCHAR(50);
SELECT @name = department_name FROM departments WHERE department_id = @dept_id;
RETURN ISNULL(@name, 'Unknown');
END;
-- Use in SELECT
SELECT name, dbo.GetDepartmentName(department_id) AS department
FROM employees;
Key Takeaways
- User-defined functions encapsulate reusable logic that can be called from any SQL statement
- Scalar functions return a single value; table-valued functions return result sets
- Inline TVFs are generally more performant than scalar UDFs because their logic is merged with the calling query
- Functions cannot modify database state β they are read-only by design
- Avoid scalar UDFs in large result sets due to row-by-row execution overhead
- Use SCHEMABINDING when possible to enable optimization and prevent schema changes