Table value functions vs Inline functions

Table value functionYou may not know that there are two different ways of writing user-defined functions that return a recordset: Table value functions and Inline functions, and they both come with a number of benefits and limitations with regard to performance and programmability.

Table value function

A table value function is essentially a stored procedure that accepts arguments and returns a table recordset, defined as a table variable. The table value function can contain multiple statements and flow control logic, just like a regular stored procedure. The last statement of the procedure needs to be a RETURN statement, at which point the declared table variable is returned to the calling context.

Here’s an example of a table value function that returns a set of sales order rows, along with product data, for a specific sales order.

CREATE FUNCTION Sales.fn_OrderDetails_TF(@SalesOrderID int)
RETURNS @rows TABLE (
    OrderRow        bigint NOT NULL,
    OrderQty        smallint NOT NULL,
    UnitPrice       money NOT NULL,
    LineTotal       numeric(38, 6) NOT NULL,
    ProductName     nvarchar(50) NOT NULL,
    Color           nvarchar(30) NULL,
    StandardCost    money NOT NULL,
    [Weight]        decimal(8, 2) NULL,
    PRIMARY KEY CLUSTERED (OrderRow))
AS

BEGIN;

    INSERT INTO @rows (OrderRow, OrderQty, UnitPrice, LineTotal,
        ProductName, Color, StandardCost, [Weight])
    SELECT ROW_NUMBER() OVER (
            PARTITION BY detail.SalesOrderID
            ORDER BY SalesOrderDetailID) AS OrderRow,
        detail.OrderQty, detail.UnitPrice, detail.LineTotal,
        prod.[Name] AS ProductName, prod.Color,
        prod.StandardCost, prod.[Weight]
    FROM Sales.SalesOrderDetail AS detail
    INNER JOIN Production.Product AS prod ON
        detail.ProductID=prod.ProductID
    WHERE detail.SalesOrderID=@SalesOrderID;

    RETURN;
END;

GO

Inline function

If a table value function could be said to work like a stored procedure, an inline function is similar to a view. This means that an inline function can only contain a single SELECT statement, and the columns in the SELECT statement implicitly define the columns of the returned table set of the function.

CREATE FUNCTION Sales.fn_OrderDetails_IF(@SalesOrderID int)
RETURNS TABLE
AS

RETURN(
    SELECT ROW_NUMBER() OVER (
            PARTITION BY detail.SalesOrderID
            ORDER BY SalesOrderDetailID) AS OrderRow,
        detail.OrderQty, detail.UnitPrice, detail.LineTotal,
        prod.[Name] AS ProductName, prod.Color,
        prod.StandardCost, prod.[Weight]
    FROM Sales.SalesOrderDetail AS detail
    INNER JOIN Production.Product AS prod ON
        detail.ProductID=prod.ProductID
    WHERE detail.SalesOrderID=@SalesOrderID
);

GO

Performance comparison

The difference in how table value functions and inline functions work (comparable to a stored procedure and a view, respectively) affects how these two types of functions are evaluated and optimized in the query plan.

There’s no given when a table value function or an inline function is a better choice from a performance perspective – it depends on all those other factors in database performance tuning, such as indexing, data volumes, etc. However, there are clear performance differences between the two methods.

Here is a test query that selects all sales order rows for a given sales rep. The first statement uses the table value function we created earlier:

--- Table value function query
SELECT hdr.SalesOrderID, fn.*
FROM Sales.SalesOrderHeader AS hdr
CROSS APPLY Sales.fn_OrderDetails_TF(hdr.SalesOrderID) AS fn
WHERE hdr.SalesPersonID=279;

Table value function

With the table value function, the execution happens in two steps: First, the return table is populated, after that, the returned table variable is joined with Sales.SalesOrderHeader.

But with an inline function, the function is “expanded” like a view, even though it is called on like a function using CROSS APPLY.

--- Inline table function query
SELECT hdr.SalesOrderID, fn.*
FROM Sales.SalesOrderHeader AS hdr
CROSS APPLY Sales.fn_OrderDetails_IF(hdr.SalesOrderID) AS fn
WHERE hdr.SalesPersonID=279;

How the function is expanded is clearly visible in the query plan:

Inline function

In this case, the expanded view approach of the inline function carries a marked performance penalty.

For a fair comparison, we’ll also try to expand the query ourselves:

--- Regular query without functions
SELECT hdr.SalesOrderID, ROW_NUMBER() OVER (
        PARTITION BY detail.SalesOrderID
        ORDER BY SalesOrderDetailID) AS OrderRow,
    detail.OrderQty, detail.UnitPrice, detail.LineTotal,
    prod.[Name] AS ProductName, prod.Color,
    prod.StandardCost, prod.[Weight]
FROM Sales.SalesOrderHeader AS hdr
INNER JOIN Sales.SalesOrderDetail AS detail ON
    hdr.SalesOrderID=detail.SalesOrderID
INNER JOIN Production.Product AS prod ON
    detail.ProductID=prod.ProductID
WHERE hdr.SalesPersonID=279;

The query plan of this statement looks rather similar to that of the inline function plan:

No function

Conclusion

Table value functions work like stored procedures, where the returned dataset is stored in a table variable (which can be indexed). On the other hand, inline functions are more similar to views, in that the query optimizer expands the function to be included in the query.

Performance will vary between the two approaches, but which method is the more efficient will depend on each individual situation.

5 comments

Leave a comment

Your email address will not be published. Required fields are marked *