ฉันจะบังคับให้ประเมินค่าสเกลาร์ UDF เพียงครั้งเดียวในแบบสอบถามได้อย่างไร


12

ฉันมีข้อความค้นหาที่ต้องการกรองผลลัพธ์ของสเกลาร์ UDF ต้องส่งเคียวรีเป็นคำสั่งเดียว (ดังนั้นฉันไม่สามารถกำหนดผลลัพธ์ UDF ให้กับตัวแปรโลคอล) และฉันไม่สามารถใช้ TVF ได้ ฉันรับรู้ถึงปัญหาประสิทธิภาพที่เกิดจาก UDF แบบเกลาซึ่งรวมถึงการบังคับให้แผนทั้งหมดทำงานอย่างต่อเนื่องหน่วยความจำที่มากเกินไปปัญหาการประมาณค่าทาง cardinality และการขาด inlining สำหรับคำถามนี้โปรดสมมติว่าฉันต้องใช้สเกลาร์ UDF

UDF นั้นค่อนข้างแพงในการโทร แต่ในทางทฤษฎีแล้วแบบสอบถามสามารถนำไปใช้อย่างมีเหตุผลโดยเครื่องมือเพิ่มประสิทธิภาพในลักษณะที่ฟังก์ชันจะต้องคำนวณเพียงครั้งเดียว ฉันล้อเลียนตัวอย่างที่ง่ายมากสำหรับคำถามนี้ แบบสอบถามต่อไปนี้ใช้เวลา 6152 ms ในการดำเนินการบนเครื่องของฉัน:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

ตัวดำเนินการตัวกรองในแผนแบบสอบถามชี้ให้เห็นว่าฟังก์ชันนั้นถูกประเมินหนึ่งครั้งสำหรับแต่ละแถว:

แผนแบบสอบถาม 1

DDL และการเตรียมข้อมูล:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
AS
BEGIN
    DECLARE @tbl TABLE (VAL VARCHAR(5));

    -- make the function expensive to call
    INSERT INTO @tbl
    SELECT [VALUE]
    FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');

    RETURN 1;
END;

GO

DROP TABLE IF EXISTS dbo.X_100_INTEGERS;

CREATE TABLE dbo.X_100_INTEGERS (ID INT NOT NULL);

-- insert 100 integers from 1 - 100
WITH
    L0   AS(SELECT 1 AS c UNION ALL SELECT 1),
    L1   AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
    L2   AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
    L3   AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
    L4   AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
    L5   AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
    Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO dbo.X_100_INTEGERS WITH (TABLOCK)
SELECT n FROM Nums WHERE n <= 100;

นี่คือลิงค์ซอ dbสำหรับตัวอย่างข้างต้นถึงแม้ว่ารหัสจะใช้เวลาประมาณ 18 วินาทีในการดำเนินการที่นั่น

ในบางกรณีฉันอาจไม่สามารถแก้ไขรหัสของฟังก์ชั่นได้เนื่องจากมีการจัดจำหน่ายโดยผู้จำหน่าย ในกรณีอื่น ๆ ฉันสามารถทำการเปลี่ยนแปลงได้ ฉันจะบังคับให้ประเมินค่าสเกลาร์ UDF เพียงครั้งเดียวในแบบสอบถามได้อย่างไร

คำตอบ:


17

ในท้ายที่สุดมันเป็นไปไม่ได้ที่จะบังคับให้ SQL Server ประเมินค่าสเกลาร์ UDF เพียงครั้งเดียวในแบบสอบถาม อย่างไรก็ตามมีบางขั้นตอนที่สามารถดำเนินการเพื่อสนับสนุนมัน จากการทดสอบเราเชื่อว่าคุณจะได้รับสิ่งที่ทำงานกับ SQL Server รุ่นปัจจุบัน แต่เป็นไปได้ว่าการเปลี่ยนแปลงในอนาคตจะทำให้คุณต้องทบทวนรหัสของคุณอีกครั้ง

หากเป็นไปได้ที่จะแก้ไขโค้ดสิ่งแรกที่ควรลองคือการทำให้ฟังก์ชั่นกำหนดค่าได้ถ้าเป็นไปได้ Paul White ชี้ให้เห็นที่นี่ว่าฟังก์ชั่นจะต้องสร้างขึ้นด้วยSCHEMABINDINGตัวเลือกและรหัสฟังก์ชั่นจะต้องกำหนดขึ้น

หลังจากทำการเปลี่ยนแปลงต่อไปนี้:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @tbl TABLE (VAL VARCHAR(5));

    -- make the function expensive to call
    INSERT INTO @tbl
    SELECT [VALUE]
    FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');

    RETURN 1;
END;

แบบสอบถามจากคำถามจะถูกดำเนินการใน 64 ms:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

แผนแบบสอบถามไม่มีตัวดำเนินการตัวกรองอีกต่อไป:

แผนแบบสอบถาม 1

เพื่อให้แน่ใจว่าดำเนินการเพียงครั้งเดียวเราสามารถใช้sys.dm_exec_function_stats DMV ใหม่ที่วางจำหน่ายใน SQL Server 2016:

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');

การออกALTERฟังก์ชันจะเป็นการรีเซ็ตexecution_countสำหรับวัตถุนั้น แบบสอบถามด้านบนส่งคืน 1 ซึ่งหมายถึงฟังก์ชั่นถูกดำเนินการเพียงครั้งเดียว

โปรดทราบว่าเพียงเพราะฟังก์ชั่นที่กำหนดไว้ไม่ได้หมายความว่ามันจะถูกประเมินเพียงครั้งเดียวสำหรับแบบสอบถามใด ๆ ในความเป็นจริงการเพิ่มข้อความค้นหาบางอย่างSCHEMABINDINGอาจทำให้ประสิทธิภาพลดลง พิจารณาคำถามต่อไปนี้:

WITH cte (UDF_VALUE) AS
(
    SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;

เพิ่มความไม่จำเป็นDISTINCTเพื่อกำจัดตัวดำเนินการตัวกรอง แผนดูมีแนวโน้ม:

แผนแบบสอบถาม 2

จากนั้นจะคาดว่า UDF จะได้รับการประเมินหนึ่งครั้งและจะใช้เป็นตารางด้านนอกในการเข้าร่วมลูปที่ซ้อนกัน อย่างไรก็ตามแบบสอบถามใช้เวลา 6446 ms ในการทำงานบนเครื่องของฉัน ตามsys.dm_exec_function_statsฟังก์ชั่นถูกดำเนินการ 100 ครั้ง เป็นไปได้อย่างไร? ใน " Compute Scalars, Expression and Execution Plan Performance ", Paul White ชี้ให้เห็นว่าตัวดำเนินการคำนวณ Scalar สามารถเลื่อนออกได้:

บ่อยครั้งที่การคำนวณสเกลาร์คำนวณการแสดงออกเพียงอย่างเดียว การคำนวณจริงจะถูกเลื่อนออกไปจนกว่าจะมีบางสิ่งในภายหลังในแผนการดำเนินการที่ต้องการผลลัพธ์

สำหรับเคียวรี่นี้ดูเหมือนว่าการโทร UDF จะถูกเลื่อนออกไปจนกว่าจะมีความจำเป็น ณ จุดนี้มันถูกประเมิน 100 ครั้ง

น่าสนใจตัวอย่าง CTE รันใน 71 ms บนเครื่องของฉันเมื่อไม่ได้กำหนด UDF ด้วยSCHEMABINDINGเช่นเดียวกับคำถามเดิม ฟังก์ชั่นจะดำเนินการเพียงครั้งเดียวเมื่อมีการเรียกใช้แบบสอบถาม นี่คือแผนแบบสอบถามสำหรับที่:

แผนแบบสอบถาม 3

ยังไม่ชัดเจนว่าทำไม Compute Scalar จึงไม่ถูกเลื่อนออกไป อาจเป็นเพราะ nondeterminism ของฟังก์ชัน จำกัด การจัดเรียงตัวดำเนินการใหม่ที่ตัวเพิ่มประสิทธิภาพการสืบค้นสามารถทำได้

อีกวิธีหนึ่งคือการเพิ่มตารางเล็ก ๆ ลงใน CTE และเพื่อค้นหาแถวเดียวในตารางนั้น โต๊ะเล็ก ๆ จะทำอะไร แต่ลองใช้สิ่งต่อไปนี้:

CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);

INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);

แบบสอบถามจะกลายเป็น:

WITH cte (UDF_VALUE) AS
(       
    SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
    FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;

การdbo.X_ONE_ROW_TABLEเพิ่มความไม่แน่นอนเพิ่มขึ้นสำหรับเครื่องมือเพิ่มประสิทธิภาพ ถ้าตารางมีศูนย์แถว CTE จะส่งคืน 0 แถว ไม่ว่าในกรณีใดเครื่องมือเพิ่มประสิทธิภาพไม่สามารถรับประกันได้ว่า CTE จะส่งคืนหนึ่งแถวหาก UDF ไม่ได้กำหนดไว้ดังนั้นจึงดูเหมือนว่า UDF จะได้รับการประเมินก่อนเข้าร่วม ฉันคาดว่าเครื่องมือเพิ่มประสิทธิภาพในการสแกนdbo.X_ONE_ROW_TABLEใช้สตรีมมวลรวมเพื่อรับค่าสูงสุดของแถวที่ส่งคืนหนึ่งแถว (ซึ่งต้องใช้ฟังก์ชันที่ต้องประเมินผล) และใช้เป็นตารางด้านนอกสำหรับการวนซ้ำซ้อนรวมdbo.X_100_INTEGERSในแบบสอบถามหลัก . สิ่งนี้ดูเหมือนจะเป็นสิ่งที่เกิดขึ้น :

แผนแบบสอบถาม 4

รันแบบสอบถามในประมาณ 110 มิลลิวินาทีในเครื่องของฉันและ UDF sys.dm_exec_function_statsจะถูกประเมินเพียงครั้งเดียวตาม มันจะไม่ถูกต้องที่จะบอกว่าเครื่องมือเพิ่มประสิทธิภาพแบบสอบถามถูกบังคับให้ประเมิน UDF เพียงครั้งเดียว อย่างไรก็ตามมันเป็นเรื่องยากที่จะจินตนาการว่าเครื่องมือเพิ่มประสิทธิภาพการเขียนใหม่ที่จะนำไปสู่การสอบถามราคาที่ต่ำกว่าแม้จะมีข้อ จำกัด เกี่ยวกับ UDF และการคำนวณต้นทุนสเกลาร์

โดยสรุปสำหรับฟังก์ชั่นที่กำหนดขึ้นเอง (ซึ่งต้องมีSCHEMABINDINGตัวเลือก) ให้ลองเขียนคำถามด้วยวิธีที่ง่ายที่สุดเท่าที่จะทำได้ หากใน SQL Server 2016 หรือรุ่นที่ใหม่กว่าให้ยืนยันว่าฟังก์ชั่นนั้นใช้งานได้เพียงครั้งเดียวsys.dm_exec_function_statsเท่านั้น แผนการดำเนินการอาจทำให้เข้าใจผิดในเรื่องนั้น

สำหรับฟังก์ชั่นที่ไม่ได้รับการพิจารณาโดย SQL Server ให้กำหนดค่าได้รวมถึงสิ่งใดก็ตามที่ไม่มีSCHEMABINDINGตัวเลือกแนวทางหนึ่งคือใส่ UDF ใน CTE หรือตารางที่ได้รับการออกแบบมาอย่างระมัดระวัง สิ่งนี้ต้องการการดูแลเพียงเล็กน้อย แต่ CTE เดียวกันสามารถทำงานได้ทั้งฟังก์ชั่นที่กำหนดไว้ล่วงหน้าและไม่ระบุชื่อ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.