Tag: Table-Valued Functions

  • Scalar vs Inline TVF vs Multi-Statement TVF: Choosing the Right SQL Server Function Type

    Scalar vs Inline TVF vs Multi-Statement TVF: Choosing the Right SQL Server Function Type

    Three function types, three very different performance profiles — now that you’ve built one of each, here’s the decision made simple, plus the stored-procedure exit ramp that applies whenever none of the three actually fit.

    Which function do you need?Need a valueor a table back?(start here)Scalar Function→ one value outprofile before WHERE useInline TVF→ a table, 1 SELECTDEFAULT PICKMulti-Statement TVF→ a table, multi-stepblack-box row estimatemodify data?Stored ProcedureINSERT / UPDATE / DELETEtransactions, TRY/CATCHRule of thumb:default to iTVF.Everything else is adeliberate exception. 📌

    Side by Side

    Scalar Inline TVF Multi-statement TVF
    Returns One value A table A table
    Optimizer visibility Limited (improved 2019+) Full — inlines like a view None — black box, fixed row estimate
    Procedural logic Yes No, single SELECT only Yes
    Usable inside a JOIN N/A (scalar value) Yes, joins like a table Yes, joins like a table
    Default preference 3rd choice 1st choice Last resort — consider a procedure first

    The Decision Flow

    Can 1 SELECT do it? Yes → Inline TVF No → need one value? No → mTVF or Procedure Yes → Scalar Function

    The Real Decision Isn’t Always Among These Three

    The most important line in the flowchart is the last branch: “mTVF or Procedure.” If you need to modify data (INSERT/UPDATE/DELETE), manage a transaction, or use TRY/CATCH error handling — none of which any function type can do — that’s your unambiguous signal to write a stored procedure instead, which is exactly where the next chapter picks up. Functions are for computing and returning values; procedures are for doing things, including things that change data.

    -- FAILS: functions cannot modify data outside a local table variable
    CREATE FUNCTION dbo.BadIdea (@id INT) RETURNS INT AS
    BEGIN
        UPDATE dbo.Employee SET last_login = GETDATE() WHERE employee_id = @id; -- not allowed
        RETURN 1;
    END;
    -- Msg 443: Invalid use of a side-effecting operator 'UPDATE' within a function.

    This restriction isn’t arbitrary — it’s what allows functions to be safely called from inside a SELECT list or WHERE clause at all. If functions could silently modify data, using one inside a SELECT would make the query’s meaning depend on evaluation order, which SQL’s set-based model deliberately doesn’t guarantee.

    Key Takeaways

    • Default to iTVF whenever a single SELECT expresses the logic — it’s the best-performing option, with zero downside versus writing the JOIN by hand
    • Scalar functions are fine for reusable expressions on modest data volumes, called outside a large table’s WHERE clause
    • mTVFs and stored procedures both handle procedural logic — if you need to modify data or manage transactions, that’s your firm signal to reach for a procedure instead, since no function type permits it
    Practice tip: Take one real reporting need from your own work or a hobby project, and walk it through this decision flow explicitly before writing any code — “can one SELECT do it? do I need one value or a table? am I modifying data?” Making this a conscious three-question checklist, rather than an instinct, is what actually sticks.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • Multi-Statement Table-Valued Functions in SQL Server: When You Need Procedural Logic

    Multi-Statement Table-Valued Functions in SQL Server: When You Need Procedural Logic

    Sometimes a single SELECT genuinely can’t express what you need — the previous lesson’s exact limitation. That’s what multi-statement TVFs (mTVFs) are for, at a real, measurable cost that’s worth understanding before reaching for one.

    Estimated 100… actual 3(the mTVF row-estimate problem)mTVFblack box,fixed guessEstimated100 rowsActual3 rows!bad join plan chosennested loop, when hash joinwould’ve been dramatically fasterNot your indexes. Not stale stats.The row estimate is just… made up. 🔍Profile before blaming the wrong thing.

    Writing One

    CREATE FUNCTION dbo.GetSalaryBands ()
    RETURNS @Bands TABLE (
        band_name NVARCHAR(20),
        employee_count INT,
        avg_salary DECIMAL(10,2)
    )
    AS
    BEGIN
        INSERT INTO @Bands
        SELECT
            CASE WHEN salary < 65000 THEN 'Junior'
                 WHEN salary BETWEEN 65000 AND 85000 THEN 'Mid'
                 ELSE 'Senior' END,
            COUNT(*), AVG(salary)
        FROM dbo.Employee
        GROUP BY CASE WHEN salary < 65000 THEN 'Junior'
                      WHEN salary BETWEEN 65000 AND 85000 THEN 'Mid'
                      ELSE 'Senior' END;
        RETURN;
    END;
    GO
    
    SELECT * FROM dbo.GetSalaryBands();

    Notice the return type: RETURNS @Bands TABLE (...) defines an explicit table-variable shape, populated with ordinary INSERT statements inside a BEGIN/END body — fundamentally different from an iTVF's single implicit RETURN (SELECT ...). This structure is exactly what buys you procedural freedom (multiple statements, variables, even loops) at the cost described below.

    The Real Cost

    mTVFs lose the inlining benefit of an iTVF entirely — SQL Server treats the whole thing as a black box with a fixed row-count estimate (historically 1 row on older versions, 100 on newer defaults, version-dependent), regardless of how many rows the function actually returns. If a query joins the result of an mTVF to a large table, the optimizer's wildly wrong row estimate can lead it to choose a bad join strategy — a nested loop where a hash join would've been dramatically faster, for instance — for reasons that have nothing to do with your indexes or statistics being stale.

    -- Confirm the black-box estimate yourself: enable the actual execution plan
    -- and hover over the mTVF call in the plan — the Estimated Number of Rows
    -- will not match reality, even though this specific function returns exactly 3 rows.
    SELECT * FROM dbo.GetSalaryBands();

    The Rule of Thumb

    Prefer an iTVF whenever a single SELECT can express the logic. Reach for an mTVF only when you genuinely need procedural steps — and even then, seriously consider whether a stored procedure is actually the better fit, since mTVFs can't be indexed, can't be updated through, and carry the poor cardinality estimation shown above. A stored procedure returning a result set has none of these specific limitations, at the cost of not being directly usable inside a larger SELECT/JOIN the way a table-valued function is.

    Common mistake: Reaching for an mTVF purely out of habit because it feels more "function-like" than a stored procedure, without weighing that the black-box row estimate can silently degrade a much larger surrounding query's plan. If the mTVF's result is ever joined to another sizable table, this isn't a theoretical concern — profile it.
    Practice tip: Rewrite GetSalaryBands as a stored procedure instead (using a plain SELECT with the same CASE/GROUP BY logic, no RETURNS TABLE). Compare how you'd call each one — SELECT * FROM dbo.GetSalaryBands() vs EXEC dbo.GetSalaryBandsProc — and notice the procedure can't be directly joined into another query the way the function can. That tradeoff, not raw performance alone, is often the deciding factor in practice.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it's published, and share it with a developer who'd find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • Inline Table-Valued Functions in SQL Server: Parameterized Views That Actually Perform

    Inline Table-Valued Functions in SQL Server: Parameterized Views That Actually Perform

    An inline TVF (iTVF) is essentially a parameterized view — a single SELECT statement wrapped in a function, returning a table. Unlike scalar functions, these get inlined into the calling query’s execution plan, which is why this lesson comes with none of the previous one’s performance warnings.

    Inlined into the plan, like a viewDept: SalesDept: EngDept: HRCROSS APPLYGetEmployeesByDept()single SELECT bodyinlines = same plan as the JOINscalar / mTVFoptimizer treats it as aBLACK BOXProof, not faith: in SSMS pressCtrl+M and compare plans — a trueiTVF’s plan is IDENTICAL to the JOIN. 🔍

    Writing and Using One

    CREATE FUNCTION dbo.GetEmployeesByDepartment (@dept NVARCHAR(50))
    RETURNS TABLE
    AS
    RETURN (
        SELECT employee_id, first_name, last_name, salary
        FROM dbo.Employee
        WHERE department = @dept
    );
    GO
    
    SELECT * FROM dbo.GetEmployeesByDepartment('Engineering');
    
    -- Can be joined just like a table
    SELECT e.first_name, e.salary
    FROM dbo.GetEmployeesByDepartment('Sales') e
    WHERE e.salary > 60000;
    
    -- Can even be CROSS APPLY'd per row of another table (covered fully in Chapter 6)
    SELECT d.department_name, top_earner.first_name
    FROM dbo.Department d
    CROSS APPLY (SELECT TOP 1 * FROM dbo.GetEmployeesByDepartment(d.department_name) ORDER BY salary DESC) top_earner;

    That last example is worth pausing on: an iTVF can take a value from the outer query as its parameter, once combined with CROSS APPLY — something a plain view can never do, since a view has no parameters at all. This is one of the most useful, distinctly-SQL-Server patterns in the whole language.

    Why iTVFs Perform Well

    Inline TVF Single SELECT body Optimizer sees inside it Scalar / mTVF Procedural body Optimizer treats as a black box

    Because SQL Server can see straight into an iTVF’s single SELECT, it substitutes the function call with the equivalent SQL directly — conceptually similar to how a compiler inlines a small function — and produces accurate row estimates, exactly as if you’d hand-written the JOIN yourself. No separate execution step, no performance penalty, no black-box row-count guessing.

    Proving the Inlining, Not Just Trusting It

    -- Compare the execution plan of the function call...
    SELECT * FROM dbo.GetEmployeesByDepartment('Engineering') WHERE salary > 70000;
    
    -- ...against the hand-written equivalent. In SSMS, enable "Include Actual Execution Plan"
    -- (Ctrl+M) for both and compare — for a true iTVF, the plans are identical.
    SELECT employee_id, first_name, last_name, salary
    FROM dbo.Employee WHERE department = 'Engineering' AND salary > 70000;
    Practice tip: Run both queries above with the actual execution plan visible and confirm they’re the same shape. This is the single best way to build real trust in “iTVFs are free” instead of just accepting it as a rule to memorize.

    The One Real Limitation

    An iTVF’s body must be exactly one SELECT statement — no variables, no IF/ELSE, no intermediate steps. The moment your logic needs a second statement (populate a temp result, then filter it based on something computed in step one), you’ve outgrown an iTVF and need either a multi-statement TVF (next lesson) or a restructured single query using a CTE, which often accomplishes the same goal without leaving iTVF territory at all.


    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.