Persisted Computed 열에 대한 인덱스는 계산 된 표현식에서 열을 가져 오려면 키 조회가 필요합니다.


24

나는 단순히 연결된 열로 구성된 테이블에 지속 계산 열을 가지고 있습니다.

CREATE TABLE dbo.T 
(   
    ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
    A VARCHAR(20) NOT NULL,
    B VARCHAR(20) NOT NULL,
    C VARCHAR(20) NOT NULL,
    D DATE NULL,
    E VARCHAR(20) NULL,
    Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL 
);

이것은 Comp고유하지 않으며 D는의 각 조합의 날짜에서 유효 A, B, C하므로 다음 쿼리를 사용하여 각 종료 날짜를 가져옵니다 A, B, C(기본적으로 동일한 Comp 값의 다음 시작 날짜).

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1
WHERE   t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;

그런 다음이 쿼리 (및 다른 쿼리)를 돕기 위해 계산 열에 인덱스를 추가했습니다.

CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;

그러나 쿼리 계획은 나를 놀라게했습니다. where 절이 있고을 기준으로 D IS NOT NULL정렬 Comp하고 계산 된 열의 인덱스를 사용하여 t1 및 t2를 스캔하는 데 사용할 수있는 인덱스 외부의 열을 참조하지 않는다고 생각했지만 클러스터 된 인덱스를 보았습니다. 주사.

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

따라서이 인덱스를 사용하여 더 나은 계획을 얻었는지 확인했습니다.

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;

이 계획은

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

키 조회가 사용되고 있음을 보여줍니다. 자세한 내용은 다음과 같습니다.

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

이제 SQL-Server 설명서에 따르면 :

CREATE TABLE 또는 ALTER TABLE 문에서 컬럼이 PERSISTED로 표시되면 결정적이지만 부정확 한 표현식으로 정의 된 계산 컬럼에서 인덱스를 작성할 수 있습니다. 즉, 데이터베이스 엔진은 계산 된 값을 테이블에 저장하고 계산 된 열이 종속 된 다른 열이 업데이트 될 때 해당 값을 업데이트합니다. 데이터베이스 엔진은 열에서 인덱스를 만들 때와 인덱스가 쿼리에서 참조 될 때 이러한 지속 된 값을 사용합니다. 이 옵션을 사용하면 데이터베이스 엔진이 계산 된 열 식을 반환하는 함수, 특히 .NET Framework에서 생성 된 CLR 함수가 결정적이고 정확한지 여부를 정확하게 증명할 수없는 경우 계산 된 열에 대한 인덱스를 만들 수 있습니다.

따라서 문서에서 "데이터베이스 엔진이 계산 된 값을 테이블에 저장하고" 값이 인덱스에 저장되어있는 경우 키를 참조하지 않을 때 A, B 및 C를 가져 오는 이유는 무엇입니까? 전혀 쿼리? 나는 그들이 Comp를 계산하는 데 사용되고 있다고 가정하지만 왜 그럴까요? 또한 쿼리에서 인덱스를 사용할 수는 t2있지만 인덱스를 사용할 수없는 이유는 t1무엇입니까?

SQL Fiddle의 쿼리 및 DDL

NB SQL Server 2008에 내 주요 문제가있는 버전이기 때문에 태그를 추가했지만 2012 년에도 같은 동작을합니다.

답변:


20

쿼리에서 전혀 참조되지 않을 때 A, B 및 C를 가져 오기 위해 키 조회가 필요한 이유는 무엇입니까? 나는 그들이 Comp를 계산하는 데 사용되고 있다고 가정하지만 왜 그럴까요?

A, B, and C 쿼리 계획에서 참조되며 검색시 사용됩니다 T2.

또한 왜 쿼리가 t2의 인덱스를 사용할 수 있지만 t1의 인덱스를 사용할 수 없습니까?

옵티마이 저는 클러스터 된 인덱스를 스캔하는 것이 필터링 된 비 클러스터형 인덱스를 스캔 한 다음 조회를 수행하여 열 A, B 및 C에 대한 값을 검색하는 것보다 저렴하다고 판단했습니다.

설명

실제 질문은 옵티마이 저가 인덱스 탐색을 위해 A, B 및 C를 검색 할 필요성을 느낀 이유입니다. Comp비 클러스터형 인덱스 스캔을 사용 하여 열 을 읽은 다음 동일한 인덱스 (별칭 T2)에서 검색을 수행하여 최상위 1 레코드를 찾습니다.

쿼리 최적화 프로그램은 최적화가 시작되기 전에 계산 된 열 참조를 확장하여 다양한 쿼리 계획의 비용을 평가할 수있는 기회를 제공합니다. 일부 쿼리의 경우 계산 열의 정의를 확장하면 옵티마이 저가보다 효율적인 계획을 찾을 수 있습니다.

옵티마이 저가 상관 서브 쿼리를 발견하면 추론하기 쉬운 양식으로 '해제'를 시도합니다. 보다 효과적인 단순화를 찾을 수 없으면 상관 하위 쿼리를 적용 (상관 된 조인)으로 다시 작성해야합니다.

다시 쓰기 적용

이 적용 언 롤링은 논리적 쿼리 트리를 프로젝트 정규화 ( 잘못 된 일반 단계를 계산 열과 비교하는 후반부)에서 제대로 작동하지 않는 형태로 만듭니다.

귀하의 경우, 쿼리 작성 방법은 확장 식 정의가 계산 열과 다시 일치하지 않도록 최적화 프로그램의 내부 세부 사항과 상호 작용하고 계산 열 A, B, and C대신 열을 참조하는 탐색으로 끝납니다 Comp. 이것이 근본 원인입니다.

해결 방법

이 부작용을 해결하기위한 한 가지 아이디어는 쿼리를 수동으로 적용으로 작성하는 것입니다.

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

불행하게도,이 쿼리는 우리가 원하는 것처럼 필터링 된 인덱스를 사용하지 않습니다. Dapply 내부의 열에 대한 부등식 테스트는 거부 NULLs되므로 명백하게 중복되는 술어 WHERE T1.D IS NOT NULL가 최적화됩니다.

명시 적 술어가 없으면 필터링 된 인덱스 일치 논리가 필터링 된 인덱스를 사용할 수 없다고 결정합니다. 이 두 번째 부작용을 해결하는 방법에는 여러 가지가 있지만 가장 쉬운 방법은 크로스 적용을 외부 적용으로 변경하는 것입니다 (상관 된 하위 쿼리에서 이전에 수행 한 최적화 프로그램 다시 쓰기의 논리를 반영 함).

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

이제 옵티마이 저는 적용 재 작성 자체를 사용할 필요가 없으므로 (계산 된 컬럼 일치가 예상대로 작동 함) 술어도 최적화되지 않으므로 필터링 된 인덱스를 두 데이터 액세스 조작 모두에 사용할 수 있으며 탐색은 Comp컬럼을 사용합니다. 양쪽에 :

외부 지원 계획

이는 일반적으로 A, B 및 C를 INCLUDEd필터링 된 인덱스의 열로 추가하는 것보다 선호됩니다 . 문제의 근본 원인을 해결하고 불필요하게 인덱스를 확장 할 필요가 없기 때문입니다.

지속 계산 열

참고로 PERSISTED, CHECK제약 조건 에서 정의를 반복하지 않아도되는 경우 계산 열을로 표시 할 필요는 없습니다 .

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    A varchar(20) NOT NULL,
    B varchar(20) NOT NULL,
    C varchar(20) NOT NULL,
    D date NULL,
    E varchar(20) NULL,
    Comp AS A + '-' + B + '-' + C,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

제약 조건 PERSISTED을 사용하거나 NOT NULL제약 조건 Comp에서 정의를 반복하지 않고 열을 직접 참조 하려는 경우 계산 열은 이 경우 에만 필요합니다 CHECK.


2
+1 BTW 나는 당신이 관심을 가질 수도 있고 그렇지 않을 수도있는 이것을 보는 동안 불필요한 조회의 다른 경우를 발견했습니다. SQL 바이올린 .
Martin Smith

@MartinSmith 네, 흥미 롭습니다. 다른 일반적인 규칙 다시 작성 ( FOJNtoLSJNandLASJN)으로 인해 원하는대로 작동하지 않고 일부 유형의 계획 (예 : 커서)에 유용하지만 여기서는 필요하지 않은 정크 (BaseRow / Checksum)가 남습니다.
Paul White에 따르면 GoFundMonica는

Chk체크섬입니다! 고마워 나는 그것에 대해 확신하지 못했습니다. 원래 나는 그것이 체크 제한과 관련이 있다고 생각했습니다.
Martin Smith

6

테스트 데이터의 인위적인 특성으로 인해 이것이 약간의 우연의 일치 일 수 있지만 SQL 2012를 언급 한 것처럼 다시 작성해 보았습니다.

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

이를 통해 인덱스를 사용하여 다른 옵션보다 훨씬 낮은 판독 값과 테스트 데이터에 대해 동일한 결과를 제공하는 저렴한 저비용 계획을 만들었습니다.

네 가지 옵션에 대한 탐색기 비용 계획 : 원본;  힌트가있는 원본;  외부 적용 및 리드

실제 데이터가 더 복잡하다고 생각 되므로이 쿼리가 의미 론적으로 다르게 작동하는 시나리오가있을 수 있지만 때로는 새로운 기능이 실제로 차이를 만들 수 있음을 보여줍니다.

좀 더 다양한 데이터를 실험 해 보았고 일부 시나리오는 일치하지만 일부는 그렇지 않습니다.

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'

1
글쎄, 그것은 색인을 사용하지만 한 지점까지만 사용합니다. 경우 comp계산 열 수 없습니다 당신은 종류를 볼 수 없습니다.
Martin Smith

감사. 내 실제 시나리오는 훨씬 복잡하지 않으며 LEAD함수는 2012 express의 로컬 인스턴스에서 원하는대로 정확하게 작동했습니다. 불행히도, 나에게이 작은 불편 함은 아직 프로덕션 서버를 업그레이드 할만한 충분한 이유가 아닌 것으로 여겨졌다 ...
GarethD

-1

동일한 작업을 수행하려고 할 때 다른 결과를 얻었습니다. 먼저 인덱스가없는 테이블의 실행 계획은 다음과 같습니다.여기에 이미지 설명을 입력하십시오

클러스터형 인덱스 스캔 (t2)에서 알 수 있듯이 조건자는 조건으로 인해 반환 될 필요한 행을 결정하는 데 사용됩니다.

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

WITH 연산자로 정의했는지 여부에 관계없이 인덱스가 추가되면 실행 계획은 다음과 같이됩니다.

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

보다시피, Clustered Index Scan은 Index Scan으로 대체됩니다. 위에서 보았 듯이 SQL Server는 계산 된 열의 원본 열을 사용하여 중첩 된 쿼리의 일치를 수행합니다. 클러스터형 인덱스 스캔 중에이 모든 값을 동시에 획득 할 수 있습니다 (추가 작업이 필요하지 않음). 인덱스가 추가되면 테이블에서 필요한 행 (기본 선택)의 필터링이 인덱스에 따라 수행되지만 계산 된 열의 소스 열 값을 comp가져와야합니다 (마지막 연산 Nested Loop). .

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

이 때문에 키 조회 작업이 사용되어 계산 된 소스 열의 데이터를 가져옵니다.

PS SQL Server의 버그 인 것 같습니다.

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