SQL Server 2019修复函数内联bug,速度提高1000倍

与过去几十年出现的大多数数据库一样,SQL Server允许开发人员通过创建函数来扩展数据库。但在即将发布的SQL Server 2019之前,SQL Server在执行标量函数时,速度要慢1000倍。

与过去几十年出现的大多数数据库一样,SQL Server允许开发人员通过创建函数来扩展数据库。但在即将发布的SQL Server 2019之前,SQL Server对标量函数的支持存在很多问题。

在SQL Server中,返回单个值的函数被称为“标量UDF”或“标量用户定义函数”。这些标量UDF由数据库自动归类为确定性或非确定性的。例如,非确定性函数用于读取当前时间或从表中获取数据。确定性函数始终为给定的参数集返回相同的值。理论上,数据库如果知道正在使用的是确定性UDF,那么就可以针对这些函数进行优化。

遗憾的是,SQL Server从未将标量UDF支持完全集成到执行计划生成器中。因此,它经常会做一些不必​​要的工作,例如会在每一行上执行确定性函数,但其实如果只针对每个唯一值执行一次函数会更快。

重复使用之前的值多少次以上才会带来实际的好处?这个很难说。除非数据是预先排序的,或者它知道可能的输入数量是有限的,否则缓存函数参数和结果的成本可能会超过收益。而这并不是标量UDF存在的唯一问题。

SQL Server标量UDF的另一个问题是它们给并行化带来了阻碍。跨多个CPU分发复杂查询的能力是SQL Server的主要卖点。(很多开源替代品几乎没有并行支持或者只能依赖分布式数据库)。如果没有并行化,就很难证明SQL Server对得起它的价格。

说到价格,我们根本无法估计一个SQL Server标量函数究竟有多贵。所有的标量函数,不管是简单的还是复杂的,在执行计划中都会被赋予一个默认的成本。

标量UDF与调用它们的查询之间是单独进行解释的。根据微软的说法,对于每一行数据,都涉及到查询与函数之间的上下文切换。我们可以假设上下文切换的成本可能会超过函数本身的成本。

出于这些原因,很多开发人员和DBA建议不要在对性能要求较高的代码中使用标量函数,尽管将标量函数的内容粘贴到需要它的每个查询、视图和存储过程中会导致大量代码重复。于是,我们经常会听到诸如“除了视图之外的代码重用不适合数据库”这样的说法。

标量函数的性能损失不容小觑,例如这个简单的函数:

CREATE FUNCTION dbo.discount_price(@price DECIMAL(12,2), @discount DECIMAL(12,2))
RETURNS DECIMAL (12,2) AS
BEGIN
    RETURN @price * (1 - @discount);
END

微软研究员Karthik Ramachandra表示,这个标量函数可能会导致一个通常只需要1.6秒的查询变成29分11秒那么久。虽然没有语义差异,速度却慢了1000倍。

解决方法是使用“内联表值函数”或“内联TVF”代替标量函数。表值函数通常会返回一组行,但也可以将它们改写成只返回一行。这个时候,可以使用CROSS APPLY运算符模拟正常的标量函数调用。

SQL Server 2019中的函数内联

从SQL Server 2019开始,可以内联用使用T-SQL编写的标量函数。这意味着它们可以被嵌入到查询中,并且不会有UDF那样的开销。在查看执行计划时,包含逻辑代码的查询与使用内联标量函数的查询之间是没有区别的。

这个新功能不仅限于简单的表达式。一些多语句UDF也可以被内联,甚至可以内联涉及从表中读取数据的非确定性UDF。它还以推断出是否需要添加JOIN或GROUP BY运算符,以便将查询中的表与函数中的表组合在一起。

当然,并非所有函数都可以被内联。要进行内联,UDF需要满足以下要求:

满足以下所有条件的标量T-SQL UDF可以被内联:

  • 使用以下构造编写的UDF:
    • DECLARE、SET:变量声明和赋值。
      SELECT:具有单/多变量赋值的SQL查询。
    • IF/ELSE:具有任意级别的嵌套分支。
      RETURN:单个或多个返回语句。
    • UDF:嵌套/递归函数调用。
    • 其他:关系操作,如EXISTS、ISNULL。
  • 不调用任何与时间相关的内部函数(例如GETDATE())或具有副作用的函数(例如NEWSEQUENTIALID())的UFD。
  • 使用EXECUTE AS CALLER子句(如果未指定EXECUTE AS子句,则这个为默认行为)的UDF。
  • 不引用表变量或表值参数的UDF。
  • 调用了标量UDF,但其GROUP BY子句中不引用标量UDF调用的查询。
  • 不是原生编译的UDF。
  • 不是被用在计算列或检查约束定义中的UDF。
  • 不引用用户定义类型的UDF。
  • 没有添加任何签名的UDF。
  • 不是用于分区的UDF。

对于每个T-SQL标量UDF,sys.sql_modules视图中都有对应的is_inlineable属性,这个属性用于指示UDF是否可以内联。值为1表示它是可内联的,0表示不可以内联。对于所有内联TVF,这个属性的值均为1。对于所有其他模块,该值为0。

可以通过将数据库兼容级别设置为小于150或者将TSQL_SCALAR_UDF_INLINING的作用域配置设置为OFF来禁用UDF内联。

也可以通过OPTION (USE HINT(‘DISABLE_TSQL_SCALAR_UDF_INLINING’))在给定查询上禁用内联。

你也可以在声明函数时使用WITH INLINE = OFF来表示永久禁用内联。

Froid项目和未来的机会

如果没有Froid研究项目,可能就无法将该功能添加到SQL Server。Froid项目被描述为:

用于优化关系数据库命令式程序的可扩展框架。Froid自动将整个用户定义函数(UDF)转换为关系代数表达式,并将它们嵌入到SQL查询中。这种形式可以进行基于成本的优化,并且可以生成有效的、面向集合的并行计划,而不是UDF那种低效、迭代、串行的执行过程。Froid还为UDF带来了很多编译器优化,而不需要进行额外的实现。我们介绍了Froid的设计,并展示了我们的实验评估,它在实际工作负载上带来了多达数个数量级的性能改进。

目前只知道Froid框架支持T-SQL,但论文中也提到了C#、Java、Python和R语言。由于SQL Server现在支持这四门语言中的三门,因此将函数内联扩展到其他语言会带来很大好处。

查看英文原文:https://www.infoq.com/news/2019/01/SQL-Server-Scalar-UDF-Inlining

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章