This is best Fabric, Power BI, SQL and AI community event. How do we know? The last event sold out! Save €200 with code FABCMTY200.
Register nowA new Data Days event is coming soon! This time we’re going bigger than ever. Fabric, Power BI, SQL, AI and more. Don't miss out.
In our previous blog post Inline Scalar user-defined functions in Microsoft Fabric Warehouse (Preview) we have announced the availability of SQL native scalar UDFs. We also emphasized the importance of inlining and how that can affect scenarios in which UDF can be used.
In this post, we aim to highlight common patterns that prevent inlining and offer suggestions on how you can rewrite your code to enable more use cases. We acknowledge that this is not a comprehensive list and that some of the techniques described here may not be applicable to all scenarios.
There are three main reasons:
While reason (1) is self-explanatory, reason (2) and (3) refer to scalar UDFs which are nominally inlineable, but user query fails. Continue on for more details.
Typical body shapes which cannot be inlined are:
While UDF eligible for inlining is prerequisite to use such function in combination with a User table (SELECT … FROM query shape), there are still cases where inlining is not possible, such as:
Typically, this is an edge case that can occur when a user query plan becomes too complex after inlining. One possible reason for this could be the complexity of the query itself.
Use this query for a sanity check and aim to achieve is_inlineable equal to 1.
SELECT SCHEMA_NAME(o.schema_id) as function_schema, OBJECT_NAME(m.object_id) as function_name, is_inlineable
FROM sys.sql_modules as m
JOIN sys.objects as o on m.object_id=o.object_id
WHERE SCHEMA_NAME(o.schema_id)='your_function_schema'
AND OBJECT_NAME(m.object_id)='your_function_name'
GO
All these methods need a rewrite of a function code.
Tip: Consider adding a local variable(s) to keep the result and returning only final variable value.
For example, in this case we have a function which is returning different discount rates depending on Customer Type.
CREATE OR ALTER FUNCTION dbo.GetDiscount (@CustomerType VARCHAR(50))
RETURNS DECIMAL(5, 2)
AS
BEGIN
IF @CustomerType = 'Regular'
RETURN 0.05;
IF @CustomerType = 'Premium'
RETURN 0.10;
IF @CustomerType = 'VIP'
RETURN 0.15;
RETURN 0.01; -- Default discount for unknown customer types
END;
GO
Consider an alternative approach to avoid multiple returns.
CREATE OR ALTER FUNCTION dbo.GetDiscount (@CustomerType VARCHAR(50))
RETURNS DECIMAL(5, 2)
AS
BEGIN
DECLARE @Discount DECIMAL(5, 2);
SET @Discount = CASE
WHEN @CustomerType = 'Regular' THEN 0.05
WHEN @CustomerType = 'Premium' THEN 0.10
WHEN @CustomerType = 'VIP' THEN 0.15
ELSE 0.01
END;
RETURN @Discount;
END;
GO
Tip: Consider passing GETDATE(), and similar functions, as input parameters to a scalar UDF.
For example, in this case we have a function which is returning the current UTC date in a certain text format.
CREATE OR ALTER FUNCTION dbo.FormatCurrentDateTime()
RETURNS VARCHAR(50)
AS
BEGIN
DECLARE @CurrentDateTime DATETIME;
SET @CurrentDateTime = GETUTCDATE();
RETURN CONVERT(VARCHAR(50), @CurrentDateTime, 120); -- Format as 'yyyy-mm-dd hh:mi:ss'
END;
GO
Consider an alternative approach to avoid explicit calls of non-determinant functions within the body.
CREATE OR ALTER FUNCTION dbo.FormatCurrentDateTime(@InputDateTime DATETIME2)
RETURNS VARCHAR(50)
AS
BEGIN
RETURN CONVERT(VARCHAR(50), @InputDateTime, 120); -- Format as 'yyyy-mm-dd hh:mi:ss'
END;
GO
Tip: Consider passing NEWID as input parameter to a scalar UDF. Notice that nowadays NEWID cannot be used directly in the body.
For example, instead of:
CREATE OR ALTER FUNCTION dbo.GetNewID()
RETURNS VARCHAR(50)
AS
BEGIN
DECLARE @NewID UNIQUEIDENTIFIER;
SET @NewID = NEWID();
RETURN CONVERT(VARCHAR(50), @NewID);
END;
GO
Consider passing NEWID() as input parameter:
CREATE OR ALTER FUNCTION dbo.GetNewID(@NEWID UNIQUEIDENTIFIER)
RETURNS VARCHAR(50)
AS
BEGIN
RETURN CONVERT(VARCHAR(50), @NewID);
END;
GO
Tip: Use local variables and other supported TSQL constructs.
For example, in this case we have a function which is doing a check does product exists in a table:
CREATE OR ALTER FUNCTION dbo.CheckProductExists(@ProductId INT)
RETURNS BIT
AS
BEGIN
DECLARE @Exists BIT;
-- Simple check if rows are affected
SELECT @Exists=1
FROM Products
WHERE ProductID = @ProductId;
IF @@ROWCOUNT > 0
SET @Exists = 1;
ELSE
SET @Exists = 0;
RETURN @Exists;
END;
GO
Consider an alternative approach to avoid ROWCOUNT.
CREATE OR ALTER FUNCTION dbo.CheckProductExists(@ProductId INT)
RETURNS BIT
AS
BEGIN
DECLARE @Exists BIT;
-- Check existence using EXISTS
IF EXISTS (SELECT 1 FROM Products WHERE ProductID = @ProductId)
SET @Exists = 1;
ELSE
SET @Exists = 0;
RETURN @Exists;
END;
GO
Tip: Consider rewriting this logic to a single CASE WHEN statement.
We'll use previously created inlineable version of the GetDiscount scalar UDF to explain these scenarios. The same principles apply to any inlineable scalar UDF. We'll also create a compact Customer table to demonstrate behaviors.
-- Create Customer table, and load few examples
-- Create the Customer table
CREATE TABLE Customer (
CustomerID INT NOT NULL,
CustomerName VARCHAR(100),
CustomerType VARCHAR(50)
);
GO
-- Insert sample data into the table
INSERT INTO Customer (CustomerID, CustomerName, CustomerType) VALUES
(1, 'Alice Johnson', 'Regular'),
(2, 'Bob Smith', 'Premium'),
(3, 'Charlie Lee', 'VIP'),
(4, 'Diana Prince', 'Type0'),
(5, 'Evan Davis', 'VIP'),
(6, 'Fiona Green', 'Premium');
GO
-- Verify the inserted data
SELECT * FROM Customer;
GO
-- Create scalar UDF
CREATE OR ALTER FUNCTION dbo.GetDiscount (@CustomerType VARCHAR(50))
RETURNS DECIMAL(5, 2)
AS
BEGIN
DECLARE @Discount DECIMAL(5, 2);
SET @Discount = CASE
WHEN @CustomerType = 'Regular' THEN 0.05
WHEN @CustomerType = 'Premium' THEN 0.10
WHEN @CustomerType = 'VIP' THEN 0.15
ELSE 0.01
END;
RETURN @Discount;
END;
GO
Tip: Consider using column order, or column alias instead of explicit UDF call in an ORDER BY.
In case you need to order by a function output, instead of this query shape:
SELECT CustomerID, CustomerName, dbo.GetDiscount (CustomerType) as DiscountRate
FROM Customer
ORDER BY dbo.GetDiscount (CustomerType)
GO
Consider using any of the following:
-- Option 1 – Via column order
SELECT CustomerID, CustomerName, dbo.GetDiscount (CustomerType) as DiscountRate
FROM Customer
ORDER BY 3
GO
-- Option 2- Via column alias
SELECT CustomerID, CustomerName, dbo.GetDiscount (CustomerType) as DiscountRate
FROM Customer
ORDER BY DiscountRate
GO
Tip: Consider doing GROUP BY in an outer query.
In case your query pattern requires group by a function output, for example:
SELECT dbo.GetDiscount (CustomerType) as DiscountRate, count(*) as NumberOfRows
FROM Customer
GROUP BY dbo.GetDiscount (CustomerType)
GO
Consider enabling this scenario by appplying GROUP BY on an outer query.
SELECT DiscountRate, count(*) as number_of_rows
FROM
(
SELECT dbo.GetDiscount (CustomerType) as DiscountRate
FROM Customer
)_outer
GROUP BY DiscountRate
In this case, alternative is to replace CTE with different query shape where possible.
For example:
WITH CustomerDiscounts AS (
SELECT
CustomerID,
CustomerName,
CustomerType,
dbo.GetDiscount(CustomerType) AS Discount
FROM Customer
)
SELECT * FROM CustomerDiscounts
ORDER BY Discount DESC;
GO
Consider rewriting as:
SELECT
CustomerID,
CustomerName,
CustomerType,
dbo.GetDiscount(CustomerType) AS Discount
FROM Customer
ORDER BY Discount DESC;
GO
Tip: Consider creating separate query column for each individual UDF call and doing an expression in an outer query.
For example - instead of:
SELECT udf1 + udf2 + …+ udfn as Result FROM Table
Use:
SELECT udf1_output+ud2_output+…+udfn_output as Result
FROM
(
SELECT udf1 as udf1_output, ud2 as udf2_output,…, udfn as udfn_output
FROM Table
)_outer
This capability is available now in preview. To learn more about it, refer to the official documentation Create function in Microsoft Fabric Data Warehouse.
Submit your feedback on Fabric Ideas and join the conversation on the Fabric Community.
In case you have specific questions, suggestion or scenario, email Fabric DW functions feedback.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.