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

阅读数:6593 2019 年 1 月 6 日 07:00

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

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论