Status: Reviewed (deep-dive v1)
Stack note: UDFs live in SQL; PowerShell/Blazor should call stored procs for app paths — keep UDFs for set-friendly reuse inside T-SQL, not as the Blazor API surface.
Goal: Know scalar vs multi-statement TVF vs inline TVF, when each is safe, and the performance traps.
| Need | Prefer | Avoid |
|---|---|---|
| Reuse a expression in queries (can be inlined) | Inline TVF (RETURNS TABLE AS RETURN SELECT …) |
Scalar UDF in SELECT/WHERE over big sets |
| Parameterized view-like shape | Inline TVF | Multi-statement TVF if a single SELECT works |
| Complex procedural logic with temp tables / many statements | Stored procedure (or carefully a multi-statement TVF) | Scalar UDF called per row |
| Compute once per row in app/API | Do it in proc or app layer | Scalar UDF from Blazor via ad-hoc SQL |
RETURNS datatype)CREATE FUNCTION dbo.udf_AddTax (@Amount money)
RETURNS money
AS BEGIN
RETURN @Amount * 1.0825;
END;
Pros: Simple, familiar.
Cons (classic): Historically executed like a black box per row; blocks parallelism; hides cost in plans. Newer SQL Server versions can inline some scalar UDFs (Froid) — don’t rely on that for estate-wide habit.
Rule of thumb: OK for rare/admin scripts. Bad as SELECT dbo.udf_X(col) FROM bigtable. Prefer inline expression, computed column, or inline TVF joined/applied.
CREATE FUNCTION dbo.itvf_OrdersForCustomer (@CustomerId int)
RETURNS TABLE
AS RETURN
(
SELECT OrderId, OrderDate, Total
FROM Sales.Orders
WHERE CustomerId = @CustomerId
);
Pros: Optimizer can expand like a view; set-based; usually best UDF style for querying.
Cons: Single RETURN SELECT only — no procedural body.
Use: Parameterized reusable queries; CROSS APPLY dbo.itvf_…(t.Id).
CREATE FUNCTION dbo.tvf_ReportRows (@AsOf date)
RETURNS @t TABLE (Id int, Label nvarchar(100))
AS BEGIN
INSERT @t SELECT …;
-- more logic
RETURN;
END;
Pros: Multi-step logic in one function.
Cons: Often poor cardinality estimates (table variable); can be much slower than inline TVF or a proc. Treat as last resort vs proc + temp table.
SELECT/JOIN/WHERE on large sets #temp WITH SCHEMABINDING on UDFs when you want to lock dependencies / allow some optimizations GETDATE() inside UDF limits indexed view / persisted computed use | Scenario | Choice |
|---|---|
| Blazor needs data | Stored proc (usp_…) — matches SPIKE pattern |
| Same filter reused in many reports/procs | Inline TVF or shared view |
| One-off formatting / mask for DBA script | Scalar UDF acceptable |
| ETL row-by-row helper | Fix set-based design; don’t “UDF your way out” |
| Maintenance warehouse transforms | Inline TVF or proc steps in Agent job |
udf_ / itvf_ / tvf_ prefix (or team standard) + schema dbo or domain schema EXECUTE on scalar; SELECT on TVFs to roles — least privilege | Symptom | Likely cause |
|---|---|
| Serial plan / high CPU on simple query | Scalar UDF per row |
| TVF returns slow with wrong memory grant | mTVF table-variable estimates |
| “Unexpected” GETDATE() values | Non-deterministic UDF in weird contexts |
| Can’t create indexed view | Non-schema-bound / non-deterministic function |
SQL Dude — SQL-11 T-SQL User-Defined Functions v1