병렬 처리를 방해하지 않는 방식으로 사용자 정의 스칼라 함수 에뮬레이션


12

쿼리에 특정 계획을 사용하도록 SQL Server를 속이는 방법이 있는지 확인하려고합니다.

1. 환경

다른 프로세스간에 공유되는 데이터가 있다고 가정하십시오. 따라서 많은 공간을 차지하는 실험 결과가 있다고 가정합니다. 그런 다음 각 프로세스에 대해 사용하려는 실험 결과의 년 / 월을 알고 있습니다.

if object_id('dbo.SharedData') is not null
    drop table SharedData

create table dbo.SharedData (
    experiment_year int,
    experiment_month int,
    rn int,
    calculated_number int,
    primary key (experiment_year, experiment_month, rn)
)
go

이제 모든 프로세스에 대해 테이블에 매개 변수가 저장되었습니다.

if object_id('dbo.Params') is not null
    drop table dbo.Params

create table dbo.Params (
    session_id int,
    experiment_year int,
    experiment_month int,
    primary key (session_id)
)
go

2. 시험 데이터

테스트 데이터를 추가하자 :

insert into dbo.Params (session_id, experiment_year, experiment_month)
select 1, 2014, 3 union all
select 2, 2014, 4 
go

insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
    2014, 3, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
    cross join master.dbo.spt_values as v2
go

insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
    2014, 4, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
    cross join master.dbo.spt_values as v2
go

3. 페치 결과

이제 다음과 같은 방법으로 실험 결과를 얻는 것이 매우 쉽습니다 @experiment_year/@experiment_month.

create or alter function dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.SharedData as d
    where
        d.experiment_year = @experiment_year and
        d.experiment_month = @experiment_month
)
go

계획은 훌륭하고 평행입니다.

select
    calculated_number,
    count(*)
from dbo.f_GetSharedData(2014, 4)
group by
    calculated_number

쿼리 0 계획

여기에 이미지 설명을 입력하십시오

4. 문제

그러나 데이터를 좀 더 일반적으로 사용하려면 다른 기능을 원합니다 dbo.f_GetSharedDataBySession(@session_id int). 그래서, 간단한 방법이 스칼라 함수, 번역을 생성하는 것입니다 @session_id-> @experiment_year/@experiment_month:

create or alter function dbo.fn_GetExperimentYear(@session_id int)
returns int
as
begin
    return (
        select
            p.experiment_year
        from dbo.Params as p
        where
            p.session_id = @session_id
    )
end
go

create or alter function dbo.fn_GetExperimentMonth(@session_id int)
returns int
as
begin
    return (
        select
            p.experiment_month
        from dbo.Params as p
        where
            p.session_id = @session_id
    )
end
go

이제 함수를 만들 수 있습니다 :

create or alter function dbo.f_GetSharedDataBySession1(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.f_GetSharedData(
        dbo.fn_GetExperimentYear(@session_id),
        dbo.fn_GetExperimentMonth(@session_id)
    ) as d
)
go

쿼리 1 계획

여기에 이미지 설명을 입력하십시오

데이터 액세스를 수행하는 스칼라 함수가 전체 계획을 serial로 만들기 때문에 계획은 물론 병렬이 아니라는 점을 제외하면 동일합니다 .

그래서 스칼라 함수 대신 하위 쿼리를 사용하는 것과 같은 여러 가지 접근법을 시도했습니다.

create or alter function dbo.f_GetSharedDataBySession2(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.f_GetSharedData(
       (select p.experiment_year from dbo.Params as p where p.session_id = @session_id),
       (select p.experiment_month from dbo.Params as p where p.session_id = @session_id)
    ) as d
)
go

쿼리 2 계획

여기에 이미지 설명을 입력하십시오

또는 사용 cross apply

create or alter function dbo.f_GetSharedDataBySession3(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.Params as p
        cross apply dbo.f_GetSharedData(
            p.experiment_year,
            p.experiment_month
        ) as d
    where
        p.session_id = @session_id
)
go

쿼리 3 계획

여기에 이미지 설명을 입력하십시오

그러나이 쿼리를 스칼라 함수를 사용하는 쿼리만큼 훌륭하게 작성하는 방법을 찾을 수 없습니다.

몇 가지 생각 :

  1. 기본적으로 원하는 것은 SQL Server에 특정 값을 미리 계산 한 다음 상수로 더 전달하도록 지시하는 것입니다.
  2. 도움이 될 수있는 것은 중간 구체화 힌트 가있는 경우 입니다. 나는 몇 가지 변형 (멀티 스테이트먼트 TVF 또는 cte와 상단)을 확인했지만 지금까지 스칼라 함수를 사용하는 것만 큼 좋은 계획은 없습니다.
  3. SQL Server 2017- Froid : 관계형 데이터베이스의 명령형 프로그램 최적화 개선에 대해 알고 있지만 도움이 될지 확실하지 않습니다. 그래도 여기서 잘못 입증 된 것이 좋았을 것입니다.

추가 정보

일반적으로 @session_id매개 변수 가있는 많은 다른 쿼리에서 사용하기가 훨씬 쉽기 때문에 테이블에서 직접 데이터를 선택하지 않고 함수를 사용 하고 있습니다.

실제 실행 시간을 비교하라는 요청을 받았습니다. 이 특별한 경우

  • 쿼리 0은 ~ 500ms 동안 실행됩니다.
  • 쿼리 1은 ~ 1500ms 동안 실행됩니다.
  • 쿼리 1 ~ 1500ms 동안 실행
  • 쿼리 3은 ~ 2000ms 동안 실행됩니다.

계획 # 2에는 탐색 대신 인덱스 스캔이 있으며 중첩 루프의 술어에 의해 필터링됩니다. 계획 # 3은 그렇게 나쁘지는 않지만 여전히 계획 # 0보다 더 많은 작업을 수행하고 느리게 작동합니다.

dbo.Params거의 변경되지 않는다고 가정 하고 일반적으로 약 1-200 개의 행이 있으며 2000 개가 예상된다고 가정 해 봅시다. 현재 약 10 열이며 열을 너무 자주 추가하지는 않습니다.

Params의 행 수는 고정되어 있지 않으므로 모든 @session_id행이 있습니다. 거기에 고정 된 열 수는 dbo.f_GetSharedData(@experiment_year int, @experiment_month int)어디에서나 호출하지 않으려는 이유 중 하나 이므로이 쿼리에 내부적으로 새 열을 추가 할 수 있습니다. 제한이 있더라도 이에 대한 의견이나 제안을 듣고 기쁩니다.


Froid의 쿼리 계획은 위의 query2의 쿼리 계획과 비슷하므로이 경우 달성하려는 솔루션으로 이동하지 않습니다.
Karthik

답변:


13

오늘날 SQL Server에서 원하는 것을 정확히 안전하게 얻을 수는 없습니다. 즉, 질문에 명시된 제한 내에서 (단 하나의 문장과 병렬 실행으로) (인식 할 때).

그래서 간단한 대답은 ' 아니요' 입니다. 이 답변의 나머지 부분은 주로 왜 그것이 관심이 있는지에 대한 토론입니다.

질문에 명시된 바와 같이 병렬 계획을 얻는 것이 가능하지만 두 가지 주요 품종이 있습니다.

  1. 상위 수준의 라운드 로빈 분배 스트림과 함께 상관 된 중첩 루프가 결합됩니다. Params특정 session_id값에 대해 단일 행이 보장되는 경우 병렬 처리 아이콘으로 표시되어 있어도 내부는 단일 스레드에서 실행됩니다. 이것이 명백한 병렬 계획 3 이 잘 수행되지 않는 이유이다 . 실제로는 연속적입니다.

  2. 다른 대안은 중첩 루프 조인의 내부에서 독립적 인 병렬 처리를위한 것입니다. 여기서 독립적 이라는 것은 중첩 루프 결합의 외부를 실행하는 것과 동일한 스레드가 아니라 내부에서 스레드가 시작됨을 의미합니다. 한 외측 행있을 보장되지 않는 경우 SQL Server는 독립적 내측 중첩 루프 병렬 처리를 지원 하고 파라미터 (가입 아무런 상관이 계획 2 ).

따라서 원하는 상관 값을 가진 일련의 (하나의 스레드로 인해) 병렬 계획을 선택할 수 있습니다. 또는 검색 할 매개 변수가 없으므로 스캔해야하는 내부 병렬 계획입니다. (제외 : 정확히 하나 의 상관 된 매개 변수 세트를 사용하여 내부 측 병렬 처리를 수행 할 수 있어야 하지만, 아마도 적절한 이유로 구현 된 적이 없습니다).

자연스러운 질문은 왜 우리가 상관 파라미터를 필요로 하는가? SQL Server가 하위 쿼리와 같은 스칼라 값을 직접 찾지 못하는 이유는 무엇입니까?

SQL Server는 상수, 변수, 열 또는 식 참조와 같은 간단한 스칼라 참조 만 사용하여 '인덱스 탐색'만 할 수 있습니다 (따라서 스칼라 함수 결과도 규정 할 수 있음). 하위 쿼리 (또는 다른 유사한 구성)는 스토리지 엔진 전체로 들어가기에는 너무 복잡하고 안전하지 않을 수 있습니다. 따라서 별도의 쿼리 계획 연산자가 필요합니다. 이것은 차례대로 상관 관계가 필요하므로 원하는 종류의 병렬 처리가 필요하지 않습니다.

대체로 현재 조회 값을 변수에 할당 한 다음 함수 매개 변수의 값을 별도의 명령문으로 사용하는 것과 같은 방법보다 더 나은 해결책은 없습니다.

이제 년과 월의 현재 값을 캐싱하는 것이 의미가있는 특정 지역 고려 사항이있을 수 있습니다 SESSION_CONTEXT.

SELECT FGSD.calculated_number, COUNT_BIG(*)
FROM dbo.f_GetSharedData
(
    CONVERT(integer, SESSION_CONTEXT(N'experiment_year')), 
    CONVERT(integer, SESSION_CONTEXT(N'experiment_month'))
) AS FGSD
GROUP BY FGSD.calculated_number;

그러나 이것은 대안의 범주에 속합니다.

반면 집계 성능이 가장 중요한 경우 인라인 함수를 사용하고 테이블에 열 저장소 인덱스 (기본 또는 보조)를 만드는 것을 고려할 수 있습니다. 열 저장소 스토리지, 배치 모드 처리 및 집계 푸시 다운의 이점은 행 모드 병렬 탐색보다 더 큰 이점을 제공합니다.

그러나 별도의 행 모드 필터에서 행 단위로 평가되는 함수로 끝나기 쉽기 때문에 특히 columnstore 스토리지에서 스칼라 T-SQL 함수를주의하십시오. SQL Server가 스칼라를 평가하기로 선택한 횟수를 보장하는 것은 일반적으로 매우 까다 롭고 시도하지 않는 것이 좋습니다.


고마워, 폴, 좋은 대답! 나는 사용에 대해 생각 session_context했지만 그것이 나에게 너무 미친 아이디어라고 결정하고 그것이 현재의 아키텍처에 어떻게 적용되는지 확실하지 않습니다. 그래도 유용한 것은 최적화 프로그램이 하위 쿼리의 결과를 간단한 스칼라 참조와 같이 처리해야한다는 것을 옵티 마이저에게 알려주는 데 사용할 수있는 힌트 일 수 있습니다.
로마 Pekar

8

원하는 계획 형태를 아는 한 T-SQL로는 불가능합니다. 함수의 하위 쿼리가 클러스터 된 인덱스 스캔에 대해 직접 필터로 적용되는 원래 계획 모양 (쿼리 0 계획)을 원하는 것 같습니다. 스칼라 함수의 리턴 값을 보유하기 위해 로컬 변수를 사용하지 않으면 이와 같은 쿼리 계획을 얻지 못할 것입니다. 필터링은 대신 중첩 루프 조인으로 구현됩니다. 루프 조인을 구현할 수있는 세 가지 방법 (병렬 관점에서)이 있습니다.

  1. 전체 계획은 순차적입니다. 허용되지 않습니다. 이것이 쿼리 1에 대한 계획입니다.
  2. 루프 결합은 직렬로 실행됩니다. 이 경우 내부가 병렬로 실행될 수 있다고 생각하지만 술어를 전달할 수는 없습니다. 따라서 대부분의 작업은 병렬로 수행되지만 전체 테이블을 스캔하고 부분 집계는 이전보다 훨씬 비쌉니다. 이것이 쿼리 2에 대한 계획입니다.
  3. 루프 결합은 병렬로 실행됩니다. 병렬 중첩 루프 조인을 사용하면 루프의 내부가 직렬로 실행되지만 내부에서 한 번에 최대 DOP 스레드를 실행할 수 있습니다. 외부 결과 집합은 하나의 행만 가지므로 병렬 계획은 효과적으로 직렬화됩니다. 이것이 쿼리 3에 대한 계획입니다.

그것들은 내가 아는 유일한 계획 형태입니다. 임시 테이블을 사용하는 경우 다른 것을 얻을 수 있지만 쿼리 성능을 쿼리 0과 동일하게 유지하려는 경우 근본적인 문제를 해결하는 것은 없습니다.

스칼라 UDF를 사용하여 리턴 값을 로컬 변수에 지정하고 해당 로컬 변수를 쿼리에 사용하여 동등한 쿼리 성능을 얻을 수 있습니다. 유지 보수성 문제를 피하기 위해 스토어드 프로 시저 또는 다중 명령문 UDF로 해당 코드를 랩핑 할 수 있습니다. 예를 들면 다음과 같습니다.

DECLARE @experiment_year int = dbo.fn_GetExperimentYear(@session_id);
DECLARE @experiment_month int = dbo.fn_GetExperimentMonth(@session_id);

select
    calculated_number,
    count(*)
from dbo.f_GetSharedData(@experiment_year, @experiment_month)
group by
    calculated_number;

스칼라 UDF가 병렬 처리를 원하는 조회 외부로 이동되었습니다. 내가 얻는 쿼리 계획이 원하는 것으로 보입니다.

병렬 쿼리 계획

다른 쿼리에서이 결과 집합을 사용해야하는 경우 두 방법 모두 단점이 있습니다. 저장 프로 시저에 직접 참여할 수 없습니다. 자체 문제 세트가있는 임시 테이블에 결과를 저장해야합니다. MS-TVF에 가입 할 수 있지만 SQL Server 2016에서는 카디널리티 예상 문제가 나타날 수 있습니다. SQL Server 2017은 MS-TVF에 대해 인터리브 된 실행을 제공 하여 문제를 완전히 해결할 수 있습니다.

T-SQL Scalar UDF는 항상 병렬 처리를 금지하며 Microsoft는 SQL Server 2017에서 FROID를 사용할 수 있다고 말하지 않았습니다.


SQL 2017의 Froid 관련-왜 그것이 있다고 생각했는지 모르겠습니다. vNext-brentozar.com/archive/2018/01/…에 있음이 확인되었습니다
Roman

4

이것은 대부분 SQLCLR을 사용하여 수행 할 수 있습니다. SQLCLR Scalar UDF의 한 가지 이점은 데이터 액세스를 수행 하지 않는 경우 (때로는 "결정적"으로 표시되어야 함) 병렬 처리를 방지 하지 않는다는 것입니다. 그렇다면 작업 자체에 데이터 액세스가 필요할 때 데이터 액세스가 필요하지 않은 것을 어떻게 사용합니까?

글쎄, dbo.Params테이블은 다음과 같이 예상 되기 때문에 :

  1. 일반적으로 행이 2000 개를 넘지 않아야합니다.
  2. 구조를 거의 바꾸지 않고
  3. (현재) 두 개의 INT열만 있으면됩니다

세 개의 열을 캐시에 저장하는 것은 가능합니다 session_id, experiment_year int, experiment_month. 정적 컬렉션 (예 : 사전)은 프로세스 외부에 채워져 experiment_year int있고 experiment_month값 을 가져 오는 Scalar UDF에서 읽습니다 . "프로세스 외부"의 의미 dbo.Params는 정적 콜렉션을 채우기 위해 테이블 에서 데이터 액세스 및 읽기를 수행 할 수있는 완전히 분리 된 SQLCLR Scalar UDF 또는 스토어드 프로 시저를 가질 수 있다는 것 입니다. "연도"및 "월"값을 얻는 UDF를 사용하기 전에 "연도"및 "월"값을 얻는 UDF가 DB 데이터 액세스를 수행하지 않는 방식으로 UDF 또는 저장 프로 시저가 실행됩니다.

데이터를 읽는 UDF 또는 스토어드 프로시 저는 먼저 콜렉션에 0 개의 항목이 있는지 확인한 후 채우고 그렇지 않으면 건너 뛸 수 있습니다. 채워진 시간을 추적하고 X 분 (또는 이와 유사한 것)을 초과 한 경우 컬렉션에 항목이 있어도 지우고 다시 채울 수 있습니다. 그러나 모집단을 건너 뛰면 두 주 UDF가 항상 값을 가져 오도록 채우기 위해 자주 실행해야하므로 도움이됩니다.

주된 관심사는 SQL Server가 어떤 이유로 든 App Domain을 언로드하기로 결정할 때입니다 (또는 사용하여 무언가에 의해 트리거 됨 DBCC FREESYSTEMCACHE('ALL');). "인구"UDF 또는 저장 프로 시저의 실행과 "연도"및 "월"값을 얻기 위해 UDF간에 콜렉션이 지워질 위험이 없습니다. 이 경우 콜렉션이 비어 있으면 예외를 발생시키기 위해 두 UDF의 맨 처음에 점검 할 수 있습니다. 잘못된 결과를 제공하는 것보다 오류가 더 낫기 때문입니다.

물론, 위에서 언급 한 우려는 총회가로 표시되기를 원한다고 가정합니다 SAFE. Assembly를로 표시 할 수 있으면 EXTERNAL_ACCESS정적 생성자가 데이터를 읽고 컬렉션을 채우는 메서드를 실행하도록 할 수 있으므로 행을 새로 고치려면 수동으로 실행해야하지만 항상 채워집니다. 정적 클래스 생성자는 클래스가로드 될 때 항상 실행되기 때문에이 클래스의 메소드는 다시 시작한 후 또는 App Domain이 언로드 된 후 실행될 때마다 발생합니다. 이를 위해서는 프로세스 내 컨텍스트 연결 (정적 생성자가 사용할 수 없으므로)이 아닌 일반 연결을 사용해야합니다 EXTERNAL_ACCESS.

참고 : 어셈블리를로 UNSAFE표시하지 않아도하려면 정적 클래스 변수를로 표시해야합니다 readonly. 이것은 최소한 컬렉션을 의미합니다. 읽기 전용 컬렉션에는 항목을 추가하거나 제거 할 수 있으므로 생성자 또는 초기로드 외부에서 초기화 할 수 없으므로 문제가되지 않습니다. static readonly DateTime클래스 변수는 생성자 외부 또는 초기로드 외부에서 변경할 수 없으므로 X 분 후에 만료 될 목적으로 콜렉션이로드 된 시간을 추적하는 것이 더 까다 롭습니다 . 이 제한 사항을 해결하려면 DateTime값 이 하나 인 단일 항목이 포함 된 정적 읽기 전용 콜렉션을 사용해야 새로 고칠 때이를 제거하고 다시 추가 할 수 있습니다.


누군가 왜 이것을 다운 피트했는지 몰라요. 매우 일반적인 것은 아니지만 현재 사례에 적용 할 수 있다고 생각합니다. 순수한 SQL 솔루션을 선호하고 싶지만, 이것에 대해 자세히 살펴보고 작동하는지 확인해 보겠습니다.
Roman Pekar

@RomanPekar 확실하지 않지만, SQLCLR을 방지하는 사람들이 많이 있습니다. 그리고 아마도 일부는 anti-me ;-)입니다. 어느 쪽이든, 나는이 솔루션이 작동하지 않는 이유를 생각할 수 없습니다. 순수한 T-SQL에 대한 선호도를 이해하고 있지만이를 수행하는 방법을 모르겠으며 경쟁 답변이 없으면 다른 누구도 할 수 없습니다. 메모리 최적화 테이블과 기본적으로 컴파일 된 UDF가 여기서 더 잘 작동하는지 모르겠습니다. 또한 방금 명심해야 할 구현 메모가 포함 된 단락을 추가했습니다.
Solomon Rutzky

1
readonly staticsSQLCLR에서 사용 이 안전하거나 현명하다고 확신하지 못했습니다 . 훨씬 덜 나는 그하여 시스템을 바보에가는 것을 확신 readonly당신이 다음 가서 참조 형, 변화 . 나에게 절대적인 의지 tbh를 제공합니다.
Paul White 9

@PaulWhite 이해하고 몇 년 전에 개인 대화에서이 일이 떠오른 것을 기억합니다. staticSQL Server에서 응용 프로그램 도메인 (및 개체) 의 공유 특성을 고려할 때 경쟁 조건이 발생할 위험이 있습니다. 그렇기 때문에 처음으로 OP에서이 데이터가 최소화되고 안정적이라고 판단한 이유와이 접근 방식을 "거의 변경"이 필요한 것으로 규정 한 이유는 무엇입니까? 사용 사례 에서는 위험이 많지 않습니다. 몇 년 전 읽기 전용 컬렉션을 의도적으로 업데이트하는 기능에 대해 알았습니다 (C #에서는 토론이 없습니다 : SQLCLR). 그것을 찾으려고 노력할 것입니다.
Solomon Rutzky

2
필요하지 않습니다. 공식 SQL Server 문서를 제외 하고는이 문제를 해결할 수있는 방법이 없습니다. 괜찮습니다.
Paul White 9
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.