Reputation: 7977
I am using SQL Server 2008 and I have the following query:
SELECT [Id] FROM [dbo].[Products] WHERE [dbo].GetNumOnOrder([Id]) = 0
With the following "GetNumOnOrder" Scalar-valued function:
CREATE FUNCTION [dbo].[GetNumOnOrder]
(
@ProductId INT
)
RETURNS INT
AS
BEGIN
DECLARE @NumOnOrder INT
SELECT @NumOnOrder = SUM([NumOrdered] - [NumReceived])
FROM [dbo].[PurchaseOrderDetails]
INNER JOIN [dbo].[PurchaseOrders]
ON [PurchaseOrderDetails].[PurchaseOrderId] = [PurchaseOrders].[Id]
WHERE [PurchaseOrders].[StatusId] <> 5
AND [PurchaseOrderDetails].[ProductId] = @ProductId
RETURN CASE WHEN @NumOnOrder IS NOT NULL THEN @NumOnOrder ELSE 0 END
END
However it takes around 6 seconds to execute. Unfortunately I have no control over the initial SQL generated but I can change the function. Is there any way the function can be modified to speed this up? I'd appreciate the help. Thanks
Upvotes: 0
Views: 735
Reputation: 1715
If you have the rights to add indexes to the tables (and dependant on the version of SQL Server you are using), I would investigate what performance gain adding the following would have:-
create index newindex1 on PurchaseOrders (id)
include (StatusId);
create index newindex2 on PurchaseOrderDetails (PurchaseOrderId)
include (ProductId,NumOrdered,NumReceived);
You probably already have indexes on these columns - but the indexes above will support just the query in your function in the most efficient way possible (reducing the number of page reads to a minimum). If the performance of this function is important enough, you could also consider adding a calculated column into your table - for NumOrdered-NumReceived
(and then only include the result column in the index above - and your query). You could also consider doing this in an indexed view rather than the table - but schema binding a view can by tiresome and inconvenient. Obviously, the wider the tables in question are - the greater the improvement in performance will be.
Upvotes: 1
Reputation: 14915
If you still want to use a function and can not live without it, use a in-line table value version. It is a-lot faster. Check out these articles from some experts.
I have had a couple MVP friends say that this the only function they ever write since scalar functions are treated as a bunch of Stored Procedure calls.
Re-write using in-line table value function. Check the syntax since I did not. Use the Coalesce function to convert NULL to Zero.
--
-- Table value function
--
CREATE FUNCTION [dbo].[GetNumOnOrder] ( @ProductId INT )
RETURNS TABLE
AS
RETURN
(
SELECT
COALESCE(SUM([NumOrdered] - [NumReceived]), 0) AS Num
FROM
[dbo].[PurchaseOrderDetails]
INNER JOIN [dbo].[PurchaseOrders]
ON [PurchaseOrderDetails].[PurchaseOrderId] = [PurchaseOrders].[Id]
WHERE [PurchaseOrders].[StatusId] <> 5
AND [PurchaseOrderDetails].[ProductId] = @ProductId
);
--
-- Sample call with cross apply
--
SELECT [Id]
FROM [dbo].[Products] P
CROSS APPLY [dbo].[GetNumOnOrder] (C.Id) AS CI
WHERE CI.Num = 0;
Upvotes: 1
Reputation: 10264
If the data is unevenly distributed data in table PurchaseOrderDetails
then cached query plans might impact your query performance. This is a case where "Parameter Sniffing
" might create bad query plans. Actually SQL Server supports an optimization called "parameter sniffing", where it will choose different plan based on the particular values in @ProductId
variable.
So to improve performance of your query you can re-write your function as:
CREATE FUNCTION [dbo].[GetNumOnOrder]
(
@ProductId INT
)
RETURNS INT
AS
BEGIN
DECLARE @NumOnOrder INT,@v_ProductId INT
SET @v_ProductId = @ProductId;
SELECT @NumOnOrder = SUM([NumOrdered] - [NumReceived])
FROM [dbo].[PurchaseOrderDetails]
INNER JOIN [dbo].[PurchaseOrders]
ON [PurchaseOrderDetails].[PurchaseOrderId] = [PurchaseOrders].[Id]
WHERE [PurchaseOrders].[StatusId] <> 5
AND [PurchaseOrderDetails].[ProductId] = @v_ProductId
RETURN CASE WHEN @NumOnOrder IS NOT NULL THEN @NumOnOrder ELSE 0 END
END
or you can include a Recomplie hint.
Upvotes: 0