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

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