sql server尽量避免滥用影响性能的标量函数

相信很多新手学了 函数的用法就不可避免的想把学到的东西用起来,然而这个函数使用却有坑, 在实际用的时候我发现一个简单的计算封装 ,不用函数和用函数执行耗时差太多了。

能避免列上进行函数则尽量避免,这是在实际上遇到的坑 ,封装成函数和直接计算效果差太多。

行中函数(Scalar-valued functions)在 SQL Server 中的性能通常较差,主要原因是它们在查询执行过程中被视为"黑盒",使得 SQL Server 优化器无法有效优化这些函数的执行。下面是一些针对行中函数优化的建议和替代方法:

-- 原始标量值函数
CREATE FUNCTION dbo.GetDiscount (@ProductID INT)
RETURNS DECIMAL(10, 2)
AS
BEGIN
    DECLARE @Discount DECIMAL(10, 2)
    SELECT @Discount = Discount
    FROM Products
    WHERE ProductID = @ProductID
    RETURN @Discount
END

-- 转换为内联表值函数
CREATE FUNCTION dbo.GetDiscountInline (@ProductID INT)
RETURNS TABLE
AS
RETURN
(
    SELECT Discount
    FROM Products
    WHERE ProductID = @ProductID
)

使用内联表值函数时,你可以通过 JOIN 或 CROSS APPLY 来调用它,而不会丢失性能优势。

SELECT p.ProductID, p.ProductName, d.Discount
FROM Products p
CROSS APPLY dbo.GetDiscountInline(p.ProductID) d

避免在查询中的列上使用标量函数

-- 性能较差的写法
SELECT OrderID, dbo.CalculateTax(OrderAmount) AS TaxAmount
FROM Orders

-- 性能更好的写法(将计算逻辑直接写入查询)
SELECT OrderID, OrderAmount * 0.08 AS TaxAmount
FROM Orders

使用计算列(Computed Columns)

ALTER TABLE Orders
ADD TaxAmount AS OrderAmount * 0.08 PERSISTED

使用 CASE 语句代替简单的函数

如果标量函数只涉及简单的逻辑判断,可以考虑使用 CASE 语句直接在查询中实现。

-- 使用 CASE 语句替代简单函数
SELECT OrderID, 
       CASE 
           WHEN OrderAmount > 100 THEN OrderAmount * 0.1
           ELSE OrderAmount * 0.05
       END AS Discount
FROM Orders

消除标量子查询

标量函数在 WHERE 或 JOIN 条件中使用时会影响性能,可以考虑将其转换为 JOIN 操作。

-- 性能较差的标量子查询
SELECT OrderID
FROM Orders
WHERE dbo.GetCustomerStatus(CustomerID) = 'Active'

-- 性能更好的 JOIN 替代
SELECT o.OrderID
FROM Orders o
JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE c.Status = 'Active'

使用存储过程替代复杂的标量函数

对于复杂的逻辑,可以使用存储过程来代替标量函数,因为存储过程的执行效率通常较高。

标量值函数在 SQL Server 中的性能瓶颈通常可以通过以下方式解决:

总结的优化:

转换为内联表值函数(ITVF)

在查询中内联计算逻辑

使用计算列

使用 CASE 语句

使用存储过程

表值函数和内联表值函数的区别

CREATE FUNCTION dbo.GetOrdersByCustomer (@CustomerID INT)
RETURNS TABLE
AS
RETURN (
    SELECT OrderID, OrderDate, TotalAmount
    FROM Orders
    WHERE CustomerID = @CustomerID
)

多语句表值函数(MSTVF)

CREATE FUNCTION dbo.GetOrdersByCustomerMulti (@CustomerID INT)
RETURNS @OrderTable TABLE
(
    OrderID INT,
    OrderDate DATETIME,
    TotalAmount DECIMAL(18, 2)
)
AS
BEGIN
    INSERT INTO @OrderTable
    SELECT OrderID, OrderDate, TotalAmount
    FROM Orders
    WHERE CustomerID = @CustomerID

    RETURN
END

性能表现

内联表值函数(ITVF):

性能更高,因为它们直接嵌入到调用查询中,与视图类似。

SQL Server 优化器能够完全展开内联表值函数,并将其优化为与查询其他部分一起执行的一个执行计划。

没有额外的计算开销,因为它不使用表变量。

多语句表值函数(MSTVF):

性能通常较差,因为 SQL Server 优化器无法提前知道函数内的具体逻辑。

由于使用了表变量,可能会影响查询性能,因为表变量不会生成统计信息,这限制了优化器的能力。

对于复杂的逻辑和多个步骤的计算,MSTVF 的灵活性更高,但执行效率往往不如 ITVF。

适用场景

内联表值函数(ITVF):

适合简单的查询逻辑。

用于那些查询不需要复杂处理逻辑的场景。

性能要求较高的情况下,应该优先选择使用 ITVF。

多语句表值函数(MSTVF):

适合复杂的业务逻辑和多步骤处理。

当需要多个 SQL 语句来生成最终结果时,可以使用 MSTVF。

如果需要在函数中执行复杂的数据操作(如条件判断、循环等),MSTVF 是更好的选择。

SQL Server 优化器支持

ITVF:因为是单个查询,优化器可以将 ITVF 中的逻辑与主查询一起优化。SQL Server 能够生成更高效的执行计划。

MSTVF:由于多语句表值函数的逻辑是一个"黑盒",优化器在执行之前无法知道其中包含的具体内容,这会导致它生成一个次优的执行计划。

为啥标量值函数尽量避免使用

标量值函数(Scalar-valued functions)在 SQL Server 中的性能往往较差,通常建议尽量避免使用。原因如下:

1. 逐行执行

标量值函数在查询中被调用时,会对每一行数据逐一执行。这种逐行处理(Row-by-row execution)方式会导致性能显著下降,尤其是当查询结果集非常大时。相比之下,SQL Server 通常更擅长处理批量操作。

示例:假设有一个返回税额的标量函数 dbo.CalculateTax:

sql

SELECT OrderID, dbo.CalculateTax(OrderAmount) AS TaxAmount
FROM Orders

在此查询中,如果 Orders 表有一百万行记录,SQL Server 会为每一行调用一次 CalculateTax 函数,导致性能极差。

2. 阻碍查询优化器优化

SQL Server 的查询优化器在生成查询计划时,无法有效地优化标量值函数。标量函数的逻辑对于优化器来说是一个"黑盒",无法提前知道函数内的执行逻辑,因此优化器无法进行充分的优化。这就限制了查询的性能提升。

相比之下,内联表值函数(Inline Table-Valued Functions, ITVF)中的逻辑会被直接嵌入到查询计划中,优化器可以根据整体查询来选择最优的执行计划。

3. 不会生成执行计划并行化

标量值函数通常会导致查询计划的并行化被禁用。SQL Server 优化器会倾向于将使用标量函数的查询设计为单线程执行,这在处理大量数据时,会显著降低性能。

4. 隐藏了真正的计算成本

标量值函数中的操作很容易被忽略,因为它们的执行是隐藏在函数调用中的。这使得查询执行时间的分析和调优变得更加困难。使用标量函数时,开发者可能低估了计算成本,从而导致性能问题。

5. 带来额外的上下文切换开销

标量值函数在执行时会频繁地在 SQL Server 的上下文和函数自身的上下文之间进行切换。每次调用函数时都需要这种开销,在处理大量数据时,这种开销会被放大,从而影响查询性能。

替代方案

为了避免标量值函数的性能问题,可以考虑以下替代方案:

使用内联表值函数(ITVF):它们的性能更好,因为优化器可以将它们直接嵌入到主查询中进行优化。

将计算逻辑直接写在查询中:将简单的计算逻辑内联到查询中,避免使用函数封装。

使用计算列(Computed Columns):对于简单的计算,可以在表中创建计算列,并根据需要为其创建索引。

使用 CASE 语句:对于简单的条件判断,CASE 语句可以替代标量函数,实现相同的逻辑。