<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. SQL
  3. Programmability
All lessons in this course

Lesson 39 of 54 16 minAdvanced

Scalar and table-valued functions

Your own functions: scalar functions that return one value, inline table-valued functions that work like views with parameters, multi-statement table-valued functions, what functions are not allowed to do, and why inline functions are usually the fast choice.

A user-defined function (UDF) takes parameters and returns either one value or a table. Unlike a procedure, it is used inside a query: in the SELECT list, in WHERE, or in FROM.

KindReturnsUsed in
Scalar functionone valueSELECT, WHERE, ORDER BY, CHECK constraints …
Inline table-valued functiona table, from one SELECTFROM and APPLY, like a view with parameters
Multi-statement table-valued functiona table variable it fills step by stepFROM and APPLY

Scalar functions

Query.sql
CREATE OR ALTER FUNCTION dbo.PriceWithVat (@Price decimal(10,2))RETURNS decimal(10,2)ASBEGIN    RETURN ROUND(@Price * 1.20, 2);END;GOSELECT Name, Price, dbo.PriceWithVat(Price) AS WithVatFROM dbo.ProductsWHERE dbo.PriceWithVat(Price) > 50ORDER BY Price DESC;
Output
Name                 Price  WithVat
-------------------  -----  -------
Cordless drill       89.00   106.80
Masonry paint 10 L   54.00    64.80
Plywood sheet 18 mm  42.00    50.40

The body is a BEGIN … END block that must end with RETURN. When you call the function you must include the schema. Without it, SQL Server looks for a built-in function of that name:

Query.sql
SELECT Name, PriceWithVat(Price) AS WithVat FROM dbo.Products;
Output
Msg 195, Level 15, State 10, Line 1
'PriceWithVat' is not a recognized built-in function name.

Inline table-valued functions

An inline table-valued function is a single SELECT with parameters. Think of it as a view you can pass values to:

Query.sql
CREATE OR ALTER FUNCTION dbo.OrdersFor (@CustomerId int)RETURNS TABLEASRETURN    SELECT o.OrderId, o.OrderDate, o.Status,           SUM(i.Quantity * i.UnitPrice) AS Total    FROM dbo.Orders AS o    JOIN dbo.OrderItems AS i ON i.OrderId = o.OrderId    WHERE o.CustomerId = @CustomerId    GROUP BY o.OrderId, o.OrderDate, o.Status;GOSELECT * FROM dbo.OrdersFor(4) ORDER BY OrderDate;
Output
OrderId  OrderDate   Status    Total
-------  ----------  -------  ------
      4  2024-03-15  Shipped  524.00
      7  2024-05-05  Shipped  199.50

Because it returns a table, it goes in FROM, and it can be joined, filtered and grouped like one. With CROSS APPLY it runs once per row of another table, which you met in the APPLY lesson:

Query.sql
SELECT c.FirstName, f.OrderId, f.TotalFROM dbo.Customers AS cCROSS APPLY dbo.OrdersFor(c.CustomerId) AS fWHERE f.Status = 'Shipped' AND f.Total > 100ORDER BY f.Total DESC;
Output
FirstName  OrderId   Total
---------  -------  ------
Dev              4  524.00
Dev              7  199.50
Farid            5  120.50
Ben              2  113.00
Asha             3  112.80

SQL Server expands an inline function into the calling query, as it does with a view. The optimizer sees the whole thing and can use indexes, so an inline function costs nothing extra.

Interactive

Where can each function go?

In the SELECT listIn FROMWith CROSS APPLYWith EXEC
Scalar
Inline table-valued
Multi-statement
CREATE FUNCTION dbo.OrdersFor (@CustomerId int)
RETURNS TABLE
AS RETURN SELECT o.OrderId, o.OrderDate, o.Status, SUM(…) AS Total
          FROM dbo.Orders AS o JOIN dbo.OrderItems AS i … GROUP BY …;
SELECT c.FirstName, f.OrderId, f.Total
FROM dbo.Customers AS c
CROSS APPLY dbo.OrdersFor(c.CustomerId) AS f
WHERE c.City = N'York';
FirstNameOrderIdTotal
1Ben2113.00
2Farid5120.50
3Ben918.00
4Farid1091.35
APPLY runs it once per outer row, with that row’s value. Customers without orders drop out; OUTER APPLY would keep them.

Multi-statement table-valued functions

When the result needs several steps, a multi-statement function declares a table variable, fills it, and returns it:

Query.sql
CREATE OR ALTER FUNCTION dbo.StockReport (@Below int)RETURNS @Report TABLE (ProductId int, Name nvarchar(60), Stock int, Advice nvarchar(20))ASBEGIN    INSERT INTO @Report (ProductId, Name, Stock)    SELECT ProductId, Name, Stock FROM dbo.Products WHERE Stock < @Below;     UPDATE @Report SET Advice = N'Reorder now' WHERE Stock = 0;    UPDATE @Report SET Advice = N'Reorder soon' WHERE Advice IS NULL;    RETURN;END;GOSELECT * FROM dbo.StockReport(30) ORDER BY Stock;
Output
ProductId  Name                Stock  Advice
---------  ------------------  -----  ------------
        9  Wall plugs (100)        0  Reorder now
       12  Safety goggles          0  Reorder now
        2  Cordless drill         12  Reorder soon
       10  Masonry paint 10 L     25  Reorder soon

This is easy to read, but the function is a black box to the optimizer: it runs the body, fills the table variable, and only then carries on. Most multi-statement functions can be rewritten as one SELECT (here a CASE would do). Prefer the inline form.

What functions cannot do

A function must not change anything outside itself. It cannot INSERT, UPDATE or DELETE real tables, call a procedure that does, or use TRY … CATCH. Changing its own table variable, as above, is allowed. The rule is checked when you create the function:

Query.sql
CREATE OR ALTER FUNCTION dbo.TakeStock (@ProductId int)RETURNS intASBEGIN    UPDATE dbo.Products SET Stock = Stock - 1 WHERE ProductId = @ProductId;    RETURN 1;END;
Output
Msg 443, Level 16, State 15, Procedure TakeStock, Line 5
Invalid use of a side-effecting operator 'UPDATE' within a function.

Use a stored procedure when you need to change data.

Scalar functions and performance

A scalar function used in a query used to run once per row, as a separate call, and it stopped the query using parallel plans. On a million rows that was very slow. SQL Server 2019 added scalar UDF inlining: simple scalar functions are now merged into the query like inline functions. Not every function qualifies. One that reads the current time, for example, cannot be inlined, and sys.sql_modules tells you which ones can:

Query.sql
CREATE OR ALTER FUNCTION dbo.DaysSince (@d date)RETURNS intASBEGIN    RETURN DATEDIFF(day, @d, GETDATE());END;GOSELECT OBJECT_NAME(object_id) AS FunctionName, is_inlineableFROM sys.sql_modulesWHERE OBJECT_NAME(object_id) IN (N'PriceWithVat', N'DaysSince')ORDER BY FunctionName;
Output
FunctionName  is_inlineable
------------  -------------
DaysSince     0
PriceWithVat  1
Stored procedureFunction
Called withEXECinside a query
Can change datayesno
Returnsresult sets, OUTPUT parameters, a return codeone value, or one table
TRY … CATCH, transactionsyesno
Use in FROM or WHEREnoyes
Quick check

Why does SELECT PriceWithVat(Price) FROM dbo.Products fail when dbo.PriceWithVat exists?

Quick check

Which kind of function does the optimizer always expand into the calling query, like a view?

Practice

Write an inline table-valued function dbo.ProductsInCategory(@CategoryName nvarchar(40)) that returns the Name and Price of the products in the category with that name. Use it to list the Timber products, cheapest first.

Show a hint

RETURNS TABLE AS RETURN SELECT … with a join to Categories and WHERE c.Name = @CategoryName. The ORDER BY goes in the query that calls it.

Show the solution
Solution.sql
CREATE OR ALTER FUNCTION dbo.ProductsInCategory (@CategoryName nvarchar(40))RETURNS TABLEASRETURN    SELECT p.Name, p.Price    FROM dbo.Products AS p    JOIN dbo.Categories AS c ON c.CategoryId = p.CategoryId    WHERE c.Name = @CategoryName;GOSELECT Name, PriceFROM dbo.ProductsInCategory(N'Timber')ORDER BY Price;
Output
Name                 Price
-------------------  -----
Timber 2x4 2.4 m      6.80
Plywood sheet 18 mm  42.00

Remember

  • Scalar functions return one value; table-valued functions return a table and go in FROM or APPLY.
  • Call scalar UDFs with their schema (dbo.Fn), or SQL Server looks for a built-in (Msg 195).
  • An inline TVF is one SELECT with parameters, a view with parameters, and is expanded into the query for free.
  • A multi-statement TVF fills a table variable step by step; the optimizer cannot see inside it.
  • Functions cannot change data, call procedures that do, or use TRY … CATCH (Msg 443).
  • SQL Server 2019+ inlines simple scalar functions; sys.sql_modules.is_inlineable shows which.
  • When in doubt, write an inline table-valued function.
0 of 54 lessons completed