SQL Server 2019 修复函数内联 bug,速度提高 1000 倍
栏目: 数据库 · SQL Server · 发布时间: 5年前
内容简介:与过去几十年出现的大多数数据库一样,SQL Server 允许开发人员通过创建函数来扩展数据库。但在即将发布的 SQL Server 2019 之前,SQL Server 在执行标量函数时,速度要慢 1000 倍。与过去几十年出现的大多数数据库一样,SQL Server 允许开发人员通过创建函数来扩展数据库。但在即将发布的 SQL Server 2019 之前,SQL Server 对标量函数的支持存在很多问题。在 SQL Server 中,返回单个值的函数被称为“标量 UDF”或“标量用户定义函数”。这些
与过去几十年出现的大多数数据库一样,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 建议不要在对性能要求较高的代码中使用标量函数,尽管将标量函数的内容粘贴到需要它的每个查询、视图和存储过程中会导致大量代码重复。于是,我们经常会听到诸如“除了视图之外的代码重用不适合数据库”这样的说法。
标量函数的性能损失不容小觑,例如这个简单的函数:
复制代码
CREATEFUNCTIONdbo.discount_price(@priceDECIMAL(12,2),@discountDECIMAL(12,2)) RETURNSDECIMAL(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。
-
DECLARE、SET:变量声明和赋值。
- 不调用任何与时间相关的内部函数(例如 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
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 内联(inline)函数与虚函数(virtual)的讨论
- c++中函数的参数传递,内联函数和默认实参的理解
- 内联函数和编译器对 Go 代码的优化
- iframe内联框架之巧妙跨域
- 提升go编译器内联程度
- 智能合约基础语言(十):Solidity内联汇编
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。