Introduction

Scalar User-Defined Functions (UDFs) in SQL Server are convenient, but they are also one of the biggest hidden performance bottlenecks in transactional systems. They execute row-by-row, prevent the optimizer from using set-based strategies, and often force serial execution. As data grows, scalar UDFs can turn a fast query into a long-running one.

Microsoft has made scalar UDF inlining available in recent versions, but real-world systems still suffer because many scalar functions contain non-inlineable patterns. The most reliable solution is to rewrite slow scalar functions as Inline Table-Valued Functions (iTVFs) and let SQL Server fold them into the outer query for full set-based optimization.

This article provides a practical, step-by-step approach to identify, rewrite, and benchmark scalar functions using inline TVFs.

Why Scalar Functions Are Slow

Scalar UDFs introduce several performance issues:

  1. They run once per row (RBAR behaviour).

  2. They prevent parallelism.

  3. They use hidden iterative execution plans.

  4. They block index usage optimizations.

  5. They cause significant CPU overhead in high-volume queries.

Even with SQL Server UDF Inlining, many patterns remain non-inlineable:

Because of this, scalar functions remain one of the top causes of poor query performance.

Identifying Slow Scalar UDF Hotspots

To find candidate scalar functions, use the following queries:

1. Find most-used UDFs by query stats

SELECT
    qs.total_elapsed_time / qs.execution_count AS AvgTime,
    qs.execution_count,
    OBJECT_NAME(st.objectid) AS ObjectName
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE st.text LIKE '%dbo.%'
ORDER BY AvgTime DESC;

2. Find scalar UDFs used inside queries

SELECT OBJECT_NAME(referencing_id) AS UsingObject,
       OBJECT_NAME(referenced_id) AS FunctionUsed
FROM sys.sql_expression_dependencies
WHERE referenced_id IN (SELECT object_id FROM sys.objects WHERE type = 'FN');

3. Detect functions that disable parallelism

SELECT name
FROM sys.objects
WHERE type = 'FN';

All scalar functions shown here block parallelism.

Example Scenario: Slow Scalar Function

Assume we have a scalar function that calculates a customer’s available credit limit.

CREATE FUNCTION dbo.fn_GetAvailableCredit(@CustomerId INT)
RETURNS DECIMAL(18,2)
AS
BEGIN
    DECLARE @Credit DECIMAL(18,2);

    SELECT @Credit = c.CreditLimit - SUM(o.OrderTotal)
    FROM Customers c
    LEFT JOIN Orders o ON c.CustomerId = o.CustomerId
    WHERE c.CustomerId = @CustomerId
    GROUP BY c.CreditLimit;

    RETURN ISNULL(@Credit, 0);
END

This function runs inside a query like this:

SELECT CustomerId, Name, dbo.fn_GetAvailableCredit(CustomerId) AS AvailableCredit
FROM Customers;

If there are 200,000 customers, SQL Server executes the function 200,000 times.

Rewriting as an Inline Table-Valued Function

Here is the iTVF equivalent:

CREATE FUNCTION dbo.fn_GetAvailableCredit_iTVF(@CustomerId INT)
RETURNS TABLE
AS RETURN
(
    SELECT
        AvailableCredit = ISNULL(c.CreditLimit - SUM(o.OrderTotal), 0)
    FROM Customers c
    LEFT JOIN Orders o ON c.CustomerId = o.CustomerId
    WHERE c.CustomerId = @CustomerId
    GROUP BY c.CreditLimit
);

Now the main query becomes:

SELECT c.CustomerId, c.Name, ac.AvailableCredit
FROM Customers c
CROSS APPLY dbo.fn_GetAvailableCredit_iTVF(c.CustomerId) ac;

Because the function is inlineable:

Performance Comparison

Scalar Function

Inline TVF

In most real workloads, rewriting scalar UDFs eliminates 70 to 95 percent of the execution time.

Handling Multi-Step Logic During Rewrite

Sometimes the scalar function performs multiple calculations or conditions. For example:

IF @Type = 'Gold'
    RETURN @Amount * 0.15;
ELSE IF @Type = 'Silver'
    RETURN @Amount * 0.10;

The inline TVF version becomes:

CREATE FUNCTION dbo.fn_CalculateDiscount(@Type VARCHAR(20), @Amount DECIMAL(18,2))
RETURNS TABLE
AS RETURN
(
    SELECT Discount =
        CASE @Type
            WHEN 'Gold' THEN @Amount * 0.15
            WHEN 'Silver' THEN @Amount * 0.10
            ELSE 0
        END
);

All logic must be expressed in a single SELECT.

Converting Scalar Functions Used in Joins

Old pattern:

SELECT OrderId, dbo.fn_GetTax(TotalAmount) AS Tax
FROM Orders;

New pattern:

SELECT o.OrderId, t.Tax
FROM Orders o
CROSS APPLY dbo.fn_GetTax_iTVF(o.TotalAmount) t;

Always use CROSS APPLY for inline TVF integration.

Rewriting Multi-Statement Functions (MS-TVFs)

If you have multi-statement TVFs like:

RETURNS @Result TABLE (...)
BEGIN
    INSERT INTO @Result ...
    INSERT INTO @Result ...
    RETURN;
END

These should also be rewritten to inline TVFs. They are almost as slow as scalar UDFs because they use table variables.

Example rewrite:

RETURN
(
    SELECT ... UNION ALL SELECT ...
);

Ensuring the Function Stays Inlineable

Inline TVFs must follow these rules:

If any of these appear, SQL Server will not inline it.

Testing and Benchmarking

To measure performance before and after rewriting:

1. Use SET STATISTICS commands

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

2. Capture execution plans

Compare:

3. Load test on a subset of data

Use a workload simulator or generate synthetic rows.

Real-World Case Study Example

A financial system had a scalar function calculating overdue interest. It was executed 1.8 million times in a reporting query.

After rewriting it as an inline TVF:

Inline TVFs repeatedly deliver this level of improvement.

Patterns That Benefit Most From Inline Conversion

Rewrite scalar functions when they:

These are common in billing systems, inventory systems, financial calculations, and reporting logic.

Deployment Strategy for Live Systems

  1. Create a new inline TVF alongside existing scalar UDF.

  2. Update stored procedures and queries gradually.

  3. Switch application logic only after verifying performance.

  4. Remove old scalar functions once all references are replaced.

This avoids downtime and reduces risk.

Conclusion

Replacing slow scalar functions with inline table-valued functions is one of the most impactful SQL Server optimizations available to developers. This approach eliminates RBAR processing, improves parallelism, reduces CPU load, and dramatically speeds up reporting and transactional queries.

Inline TVFs provide the power of set-based execution with the convenience of modular logic, making them ideal for performance-critical systems.

If your system contains complex custom business calculations wrapped in scalar UDFs, rewriting them into inline TVFs can deliver immediate and lasting performance improvements.