쿼리에서 스칼라 UDF를 한 번만 평가하도록하려면 어떻게해야합니까?


12

스칼라 UDF 결과에 대해 필터링 해야하는 쿼리가 있습니다. 쿼리는 단일 문으로 전송되어야하며 (UDF 결과를 로컬 변수에 할당 할 수 없음) TVF를 사용할 수 없습니다. 스칼라 UDF로 인한 성능 문제를 알고 있습니다. 스칼라 UDF는 전체 계획을 순차적으로 강제 실행, 과도한 메모리 부여, 카디널리티 추정 문제 및 인라인 부족을 포함합니다. 이 질문에 대해 스칼라 UDF를 사용해야한다고 가정하십시오.

UDF 자체는 호출하는 데 비용이 많이 들지만 이론적으로 쿼리는 함수를 한 번만 계산하면되는 방식으로 옵티 마이저에 의해 논리적으로 쿼리를 구현할 수 있습니다. 이 질문에 대해 크게 단순화 된 예를 모의했습니다. 다음 쿼리는 내 컴퓨터에서 실행하는 데 6152ms가 걸립니다.

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;

코드를 실행하는 데 약 18 초가 걸리지 만 위 예제 의 db 바이올린 링크 는 다음과 같습니다 .

경우에 따라 공급 업체에서 제공 한 함수 코드를 편집하지 못할 수도 있습니다. 다른 경우에는 변경할 수 있습니다. 쿼리에서 스칼라 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;

질문의 쿼리는 64ms에서 실행됩니다.

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

쿼리 계획에 더 이상 필터 연산자가 없습니다.

쿼리 계획 1

한 번만 실행되도록하려면 SQL Server 2016에 릴리스 된 새로운 sys.dm_exec_function_stats DMV를 사용할 수 있습니다 .

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가 한 번 평가되고 중첩 루프 조인에서 외부 테이블로 사용될 것으로 예상됩니다. 그러나 쿼리가 내 컴퓨터에서 실행되는 데 6446ms가 걸립니다. 따르면 sys.dm_exec_function_stats함수 100 회 실행 하였다. 그것이 어떻게 가능합니까? " 컴퓨팅 스칼라, 식 및 실행 계획 성능 "에서 Paul White는 Compute Scalar 연산자가 지연 될 수 있다고 지적합니다.

종종 Compute Scalar는 단순히 표현식을 정의합니다. 실제 계산은 나중에 실행 계획의 결과가 필요할 때까지 연기됩니다.

이 쿼리의 경우, UDF 호출이 필요할 때까지 지연되어 100 회 평가 된 것처럼 보입니다.

흥미롭게도 CTE 예제 SCHEMABINDING는 원래 질문에서 와 같이 UDF가로 정의되지 않은 경우 내 컴퓨터에서 71ms 안에 실행됩니다 . 이 함수는 쿼리가 실행될 때 한 번만 실행됩니다. 이에 대한 쿼리 계획은 다음과 같습니다.

쿼리 계획 3

Compute Scalar가 연기되지 않은 이유는 확실하지 않습니다. 함수의 비결 정성이 쿼리 최적화 프로그램이 수행 할 수있는 연산자의 재 배열을 제한하기 때문일 수 있습니다.

다른 방법은 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옵티 마이저에 대한 불확실성 이 추가됩니다. 테이블에 0 개의 행이 있으면 CTE는 0 개의 행을 반환합니다. 어쨌든 옵티마이 저는 UDF가 결정적이지 않은 경우 CTE가 하나의 행을 리턴하도록 보장 할 수 없으므로 결합 전에 UDF가 평가 될 것 같습니다. 옵티마이 저가 scan dbo.X_ONE_ROW_TABLE, 스트림 집계를 사용하여 반환 된 하나의 행의 최대 값 (함수를 평가해야 함)을 가져오고 dbo.X_100_INTEGERS주 쿼리에서 중첩 루프 조인의 외부 테이블로 사용하려고 합니다. . 이것은 일어나는 것처럼 보입니다 .

쿼리 계획 4

쿼리는 내 컴퓨터에서 약 110ms 후에 실행되며 UDF는에 따라 한 번만 평가됩니다 sys.dm_exec_function_stats. 쿼리 옵티마이 저가 UDF를 한 번만 강제로 평가한다고 말하는 것은 올바르지 않습니다. 그러나 UDF 및 계산 스칼라 원가 계산에 대한 제한 사항이 있어도 쿼리 비용을 낮추는 옵티 마이저 다시 작성을 상상하기는 어렵습니다.

요약하자면, 결정적 함수 ( SCHEMABINDING옵션을 포함해야 함 )의 경우 가능한 간단한 방법으로 쿼리를 작성하십시오. SQL Server 2016 이상 버전에서을 사용하여 함수가 한 번만 실행되었는지 확인하십시오 sys.dm_exec_function_stats. 그런 점에서 실행 계획이 오도 될 수 있습니다.

SCHEMABINDING옵션 이없는 것을 포함하여 SQL Server에 의해 결정되지 않은 함수를 결정하기 위해서는 UDF를 신중하게 조작 된 CTE 또는 파생 테이블에 넣는 방법이 있습니다. 이를 위해서는 약간의주의가 필요하지만 동일한 CTE가 결정적 기능과 비 결정적 기능 모두에서 작동 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.