WHERE IN을 사용하여 삭제 조작 중 예상치 않은 스캔


40

다음과 같은 쿼리가 있습니다.

DELETE FROM tblFEStatsBrowsers WHERE BrowserID NOT IN (
    SELECT DISTINCT BrowserID FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID IS NOT NULL
)

tblFEStatsBrowsers에 553 개의 행이 있습니다.
tblFEStatsPaperHits에 47.974.301 개의 행이 있습니다.

브라우저 :

CREATE TABLE [dbo].[tblFEStatsBrowsers](
    [BrowserID] [smallint] IDENTITY(1,1) NOT NULL,
    [Browser] [varchar](50) NOT NULL,
    [Name] [varchar](40) NOT NULL,
    [Version] [varchar](10) NOT NULL,
    CONSTRAINT [PK_tblFEStatsBrowsers] PRIMARY KEY CLUSTERED ([BrowserID] ASC)
)

tblFEStatsPaperHits :

CREATE TABLE [dbo].[tblFEStatsPaperHits](
    [PaperID] [int] NOT NULL,
    [Created] [smalldatetime] NOT NULL,
    [IP] [binary](4) NULL,
    [PlatformID] [tinyint] NULL,
    [BrowserID] [smallint] NULL,
    [ReferrerID] [int] NULL,
    [UserLanguage] [char](2) NULL
)

tblFEStatsPaperHits에는 BrowserID가 포함되지 않은 클러스터형 인덱스가 있습니다. 따라서 내부 쿼리를 수행하려면 tblFEStatsPaperHits의 전체 테이블 스캔이 필요합니다.

현재 tblFEStatsBrowsers의 각 행에 대해 전체 스캔이 실행됩니다. 즉, tblFEStatsPaperHits에 대한 553 개의 전체 테이블 스캔이 있습니다.

기존 위치에 다시 쓰더라도 계획은 변경되지 않습니다.

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
)

그러나 Adam Machanic이 제안한대로 HASH JOIN 옵션을 추가하면 최적의 실행 계획이 생성됩니다 (tblFEStatsPaperHits의 단일 스캔).

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
) OPTION (HASH JOIN)

이제 이것은이 문제를 해결하는 방법에 대한 질문이 아닙니다 .OPTION (HASH JOIN)을 사용하거나 임시 테이블을 수동으로 만들 수 있습니다. 쿼리 최적화 프로그램이 현재 계획을 사용하는 이유가 더 궁금합니다.

QO에는 BrowserID 열에 통계가 없으므로 최악의 5 천만 개의 고유 값을 가정하므로 상당히 큰 메모리 내 / tempdb 작업 테이블이 필요하다고 생각합니다. 따라서 가장 안전한 방법은 tblFEStatsBrowsers에서 각 행에 대한 스캔을 수행하는 것입니다. 두 테이블의 BrowserID 열간에 외래 키 관계가 없으므로 QO는 tblFEStatsBrowsers에서 정보를 빼낼 수 없습니다.

이것이 들리는 것처럼 간단합니까?

업데이트 1
몇 가지 통계를 제공하려면 : OPTION (HASH JOIN) :
208.711 논리적 읽기 (12 회 스캔)

옵션 (루프 가입, 해시 그룹) :
11.008.698 논리적 읽기 (~ 브라우저 ID 당 스캔 (339))

옵션 없음 :
11.008.775 논리적 읽기 (브라우저 ID 당 ~ scan (339))

업데이트 2
훌륭한 답변, 모두 감사합니다! 하나만 고르기가 힘들다. Martin은 처음이었고 Remus는 훌륭한 솔루션을 제공하지만 세부 사항을 염두에두고 키위에 제공해야합니다. :)


5
한 서버에서 다른 서버로 통계 복사에 따라 통계를 스크립팅하여 복제 할 수 있습니까?
Mark Storey-Smith

2
MarkStorey 스미스 물론 @ - pastebin.com/9HHRPFgK은 당신이 빈 데이터베이스에서 스크립트를 실행 가정하면,이 실행 계획을 보여주는 포함 할 때 문제가있는 쿼리를 재현하는 나를 수 있습니다. 두 쿼리 모두 스크립트 끝에 포함됩니다.
Mark S. Rasmussen 2018 년

답변:


61

"쿼리 옵티마이 저가 현재 계획을 사용하는 이유가 더 궁금합니다."

다시 말해, 다음 계획이 대안에 비해 (최적화가 많은 ) 최적화 계획에 비해 저렴한 이유는 무엇입니까 ?

원래 계획

조인의 내부는 기본적으로 상관 된 각 값에 대해 다음과 같은 형식의 쿼리를 실행합니다 BrowserID.

DECLARE @BrowserID smallint;

SELECT 
    tfsph.BrowserID 
FROM dbo.tblFEStatsPaperHits AS tfsph 
WHERE 
    tfsph.BrowserID = @BrowserID 
OPTION (MAXDOP 1);

용지 조회수 스캔

예상 행 수 있음을 유의 185220 (안 289013 평등 비교 암시 적으로 제외 이후) NULL(하지 않는 ANSI_NULLS것입니다 OFF). 위 계획의 예상 비용은 206.8 단위입니다.

이제 TOP (1)절을 추가하자 :

DECLARE @BrowserID smallint;

SELECT TOP (1)
    tfsph.BrowserID 
FROM dbo.tblFEStatsPaperHits AS tfsph 
WHERE 
    tfsph.BrowserID = @BrowserID 
OPTION (MAXDOP 1);

TOP으로 (1)

추정 비용은 이제 0.00452 단위입니다. Top 물리 연산자를 추가하면 Top 연산자에서 행 목표 를 1 행으로 설정합니다. 문제는 클러스터형 인덱스 스캔을위한 '행 목표'를 도출하는 방법이됩니다. 즉, 한 행이 BrowserID술어 와 일치하기 전에 스캔이 처리 할 행 수는 몇 개 입니까?

사용 가능한 통계 정보는 166 개의 고유 BrowserID값을 보여줍니다 (1 / [All Density] = 1 / 0.006024096 = 166). 원가 계산에서는 고유 한 값이 실제 행에 균일하게 분산되어 있다고 가정하므로 군집 인덱스 스캔의 행 목표는 166.302 로 설정됩니다 (샘플링 된 통계를 수집 한 이후 테이블 카디널리티의 변경 을 고려함 ).

예상되는 166 개의 행을 스캔하는 데 드는 예상 비용은 그리 크지 않습니다 (각 변경마다 한 번씩 339 회 실행 됨 BrowserID). 클러스터 된 인덱스 스캔은 1.3219 단위 의 예상 비용을 보여 주므로 행 목표의 스케일링 효과를 보여줍니다. I / O 및 CPU의 비 눈금 조작 비용으로 나타낸다 153.93152.8698 각각 :

행 목표 규모 예상 비용

실제로, 색인에서 스캔 된 처음 166 개의 행 (반환되는 순서에 관계없이)이 가능한 각 값을 하나씩 포함 할 가능성 은 거의 없습니다 BrowserID. 그럼에도 불구하고, DELETE계획은 총 1.40921 단위 로 비용이 들며 , 이러한 이유로 옵티마이 저가 선택합니다. Bart Duncan은 최근에 Row Goals Gone Rogue 라는 제목의 게시물에이 유형의 다른 예를 보여줍니다 .

실행 계획의 최상위 운영자가 반반 결합과 관련 이 없음 (특히 '단락'마틴 언급) 도 주목할 만하다 . 먼저 GbAggToConstScanOrTop 이라는 탐색 규칙을 비활성화하여 Top의 출처를 알 수 있습니다 .

DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
GO
DBCC RULEON ('GbAggToConstScanOrTop');

GbAggToConstScanOrTop 비활성화

이 계획의 예상 비용은 364.912 이며 Top은 Group By Aggregate를 대체했습니다 (상관 된 열별로 그룹화 BrowserID). 집계는 조회 텍스트 의 중복으로 인한 것이 아닙니다 . LASJNtoLASJNonDistLASJOnLclDistDISTINCT 두 가지 탐색 규칙에 의해 도입 될 수있는 최적화입니다 . 이 두 가지를 비활성화하면이 계획이 생성됩니다.

DBCC RULEOFF ('LASJNtoLASJNonDist');
DBCC RULEOFF ('LASJOnLclDist');
DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
GO
DBCC RULEON ('LASJNtoLASJNonDist');
DBCC RULEON ('LASJOnLclDist');
DBCC RULEON ('GbAggToConstScanOrTop');

스풀 계획

이 계획의 예상 비용은 40729.3 입니다.

Group By에서 Top으로의 변환없이 옵티마이 저는 BrowserID반 자연 조인 전에 집계 가 포함 된 해시 조인 계획을 '자연스럽게'선택합니다 .

DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, RECOMPILE);
GO
DBCC RULEON ('GbAggToConstScanOrTop');

상위 DOP 1 계획 없음

MAXDOP 1 제한이없는 병렬 계획 :

최고 병렬 계획 없음

원래 쿼리를 '수정'하는 다른 방법 BrowserID은 실행 계획 보고서 에 누락 된 인덱스를 작성하는 것 입니다. 중첩 루프는 내부가 색인 될 때 ​​가장 잘 작동합니다. 세미 조인에 대한 카디널리티 추정은 최상의시기에 어렵습니다. 적절한 인덱싱이 없으면 (큰 테이블에는 고유 키가 없습니다!) 전혀 도움이되지 않습니다.


3
나는 당신에게 절을했고, 당신은 내가 전에는 본 적이없는 몇 가지 새로운 개념을 소개했습니다. 당신이 무언가를 알고 있다고 느낄 때, 누군가가 당신을 내려 놓을 것입니다-좋은 방법으로 :) 색인을 추가하면 분명히 도움이 될 것입니다. 그러나이 일회성 작업 외에도 필드는 BrowserID 열에 의해 액세스 / 집계되지 않으므로 테이블이 상당히 크기 때문에이 바이트를 저장하고 싶습니다 (이것은 많은 동일한 데이터베이스 중 하나 일뿐입니다). 테이블에는 고유 한 고유성이 없으므로 테이블에는 고유 한 키가 없습니다. 모든 선택은 PaperID 및 선택적으로 마침표로 이루어집니다.
Mark S. Rasmussen

22

통계 전용 데이터베이스와 질문의 쿼리를 작성하기 위해 스크립트를 실행할 때 다음 계획을 얻습니다.

계획

계획에 표시된 테이블 카디널리티는

  • tblFEStatsPaperHits: 48063400
  • tblFEStatsBrowsers : 339

따라서 tblFEStatsPaperHits339 번에 스캔을 수행해야한다고 추정합니다 . 각 스캔에는 tblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULL스캔 연산자로 푸시 다운되는 상관 술어 가 있습니다.

그러나이 계획이 339 건의 전체 스캔을 의미하지는 않습니다. 각 스캔에서 첫 번째로 일치하는 행이 발견 되 자마자 반반 결합 연산자 아래에 있으므로 나머지 부분을 단락시킬 수 있습니다. 이 노드의 예상 서브 트리 비용은 1.32603이며 전체 계획 비용은 1.41337입니다.

해시 조인의 경우 아래 계획을 제공합니다.

해시 조인

전체 계획에 비용이 산정됩니다 418.415에 하나의 전체 클러스터 된 인덱스 스캔과 (중첩 루프 계획보다 더 많은 비용이 약 300 배) tblFEStatsPaperHits에 비용이 산정 206.8혼자. 이것을 1.32603이전에 제공된 339 개의 부분 스캔에 대한 추정치 와 비교하십시오 (평균 부분 스캔 추정 비용 = 0.003911592).

따라서 이는 전체 스캔보다 53,000 배 적은 비용으로 각 부분 스캔 비용이 발생 함을 나타냅니다. 원가 계산이 행 수와 선형으로 확장되는 경우, 평균적으로 일치하는 행을 찾고 단락 될 수 있기 전에 각 반복에서 900 개의 행만 처리하면된다고 가정합니다.

그러나 원가 계산이 그 선형 방식으로 확장되지는 않는다고 생각합니다. 그들은 또한 고정 된 시작 비용의 일부 요소를 통합한다고 생각합니다. TOP다음 쿼리에서 다양한 값 시도

SELECT TOP 147 BrowserID 
FROM [dbo].[tblFEStatsPaperHits] 

147가장 가까운 추정 하위 트리 비용 제공 0.003911592에를 0.0039113. 어느 쪽이든 각 스캔이 수백만 행이 아닌 수백 행의 순서로 테이블의 작은 비율 만 처리하면된다는 가정에 근거하여 비용을 계산한다는 것이 분명합니다.

나는이 가정에 근거한 수학이 무엇인지 확실하지 않으며 계획의 나머지 부분에서 행 수 추정치를 실제로 합치 지 않습니다 (중첩 루프 조인에서 나온 236 개의 추정 행은 236이 있음을 암시합니다 일치하는 행이없고 전체 스캔이 필요한 경우). 모델링 모델링 가정이 다소 떨어지고 중첩 루프 계획을 비용이 많이 드는 상태로 남겨 둔 경우라고 가정합니다.


20

내 책 에서 50M 행의 한 번의 스캔 조차도 용납 할 수 없습니다 ... 나의 일반적인 트릭은 고유 한 값을 구체화하고 최신 상태로 엔진을 위임하는 것입니다.

create view [dbo].[vwFEStatsPaperHitsBrowserID]
with schemabinding
as
select BrowserID, COUNT_BIG(*) as big_count
from [dbo].[tblFEStatsPaperHits]
group by [BrowserID];
go

create unique clustered index [cdxVwFEStatsPaperHitsBrowserID] 
  on [vwFEStatsPaperHitsBrowserID]([BrowserID]);
go

이렇게하면 BrowserID 당 한 행씩 구체화 된 인덱스가 제공되므로 50M 개의 행을 스캔 할 필요가 없습니다. 엔진이 엔진을 유지 관리하고 QO는 게시 한 설명에 힌트를 '있는 그대로'사용합니다 (힌트 나 쿼리 재 작성없이).

단점은 물론 경합입니다. 모든 삽입 또는 삭제 작업 tblFEStatsPaperHits(그리고 무거운 삽입이있는 로깅 테이블이라고 생각합니다)은 주어진 BrowserID에 대한 액세스를 직렬화해야합니다. 기꺼이 구매하려는 경우이 기능을 수행 할 수있는 방법 (지연된 업데이트, 2 단계 로깅 등)이 있습니다.


큰 스캔은 일반적으로 허용되지 않습니다. 이 경우 일부 일회성 데이터 정리 작업이므로 추가 인덱스를 만들지 않기로 결정합니다 (시스템을 중단 할 때 일시적으로 수행 할 수 없음). 나는 EE가 없지만 이것이 일회성이라는 것을 감안할 때 힌트는 괜찮을 것입니다. 내 주요 호기심은 QO가 계획을 어떻게 달성했는지에 관한 것이 었습니다. :) 테이블은 로깅 테이블이며 무거운 삽입물이 있습니다. 나중에 별도의 비동기 로깅 테이블이 있기 때문에 나중에 tblFEStatsPaperHits의 행을 업데이트하므로 필요한 경우 직접 관리 할 수 ​​있습니다.
Mark S. Rasmussen 2018 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.