왜 이것이 더 빠르고 사용하기에 안전합니까? (첫 글자가 알파벳 인 곳에)


10

간단히 말해서, 우리는 작은 사람들의 테이블을 매우 큰 사람들의 테이블로 업데이트하고 있습니다. 최근 테스트에서이 업데이트를 실행하는 데 약 5 분이 걸립니다.

우리는 가능한 한 가장 최적화 된 것처럼 보이는 것을 보았습니다. 동일한 쿼리가 이제 2 분 이내에 실행되며 동일한 결과를 완벽하게 생성합니다.

다음은 쿼리입니다. 마지막 줄은 "최적화"로 추가됩니다. 쿼리 시간이 크게 줄어드는 이유는 무엇입니까? 뭔가 빠졌습니까? 이것이 미래에 문제를 일으킬 수 있습니까?

UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
    AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')

기술 노트 : 테스트 할 문자 목록에 몇 개의 문자가 더 필요할 수 있습니다. 또한 "DIFFERENCE"를 사용할 때 명백한 오류 마진을 알고 있습니다.

쿼리 계획 (일반) : https://www.brentozar.com/pastetheplan/?id=rypV84y7V
쿼리 계획 ( "최적화"포함) : https://www.brentozar.com/pastetheplan/?id=r1aC2my7E


4
기술 노트에 대한 작은 답변 : AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AI모든 문자를 나열하지 않고 읽기 어려운 코드를 작성하지 않고도 원하는 작업을 수행해야합니다.
Erik A

의 최종 조건 WHERE이 거짓 인 행 이 있습니까? 특히 비교는 대소 문자를 구분할 수 있습니다.
jpmc26

@ErikvonAsmuth는 훌륭한 지적입니다. 그러나 간단한 기술 정보 : SQL Server 2008 및 2008 R2의 경우 문화 / 로캘에 사용할 수있는 경우 버전 "100"데이터 정렬을 사용하는 것이 가장 좋습니다. 그렇습니다 Latin1_General_100_CI_AI. 또한 SQL Server 2012 이상 (최소한 SQL Server 2019를 통해)에서는 사용중인 로캘에 대해 보조 문자 사용 데이터 정렬을 최고 버전으로 사용하는 것이 가장 좋습니다. 그래서 것 Latin1_General_100_CI_AI_SC,이 경우. 100보다 큰 버전 (지금까지는 일본어 만 해당)이 없습니다 _SC(예 :) Japanese_XJIS_140_CI_AI.
Solomon Rutzky

답변:


9

그것은 테이블의 데이터, 인덱스, ...에 달려 있습니다. 실행 계획 / io + 시간 통계를 비교할 수 없어 말하기 어렵습니다.

내가 기대할 수있는 차이점은 두 테이블 사이에 참여하기 전에 추가 필터링이 발생한다는 것입니다. 이 예에서는 테이블을 재사용하도록 업데이트를 변경했습니다.

"최적화"를 포함한 실행 계획 여기에 이미지 설명을 입력하십시오

실행 계획

필자는 테스트 데이터에서 필터링 작업이 수행되는 레코드가 없으며 결과적으로 개선되지 않은 필터 작업이 있음을 분명히 알 수 있습니다.

"최적화"가없는 실행 계획 여기에 이미지 설명을 입력하십시오

실행 계획

필터가 사라 졌으므로 불필요한 레코드를 필터링하려면 조인에 의존해야합니다.

다른 이유 쿼리를 변경 한 또 다른 이유 / 결과는 쿼리를 변경할 때 새로운 실행 계획이 만들어 져서 더 빠르기 때문일 수 있습니다. 이에 대한 예는 다른 Join 연산자를 선택하는 엔진이지만이 시점에서는 추측입니다.

편집하다:

두 가지 쿼리 계획을 얻은 후 설명 :

쿼리가 큰 테이블에서 550M 행을 읽고 필터링합니다. 여기에 이미지 설명을 입력하십시오

술어는 탐색 술어가 아니라 대부분의 필터링을 수행하는 것임을 의미합니다. 데이터를 읽지 만 결과는 덜 리턴됩니다.

SQL Server에서 다른 인덱스 (쿼리 계획)를 사용하도록하거나 인덱스를 추가하면이 문제를 해결할 수 있습니다.

그렇다면 최적화 쿼리에 왜 같은 문제가 없습니까?

다른 쿼리 계획이 사용되기 때문에 탐색 대신 스캔이 사용됩니다.

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

검색을 수행하지 않고 작업 할 4M 행만 리턴합니다.

다음 차이점

업데이트 차이를 무시하고 (최적화 된 쿼리에서 아무것도 업데이트되지 않음) 해시 일치는 최적화 된 쿼리에서 사용됩니다.

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

최적화되지 않은 중첩 루프 조인 대신 :

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

중첩 루프는 한 테이블이 작고 다른 테이블이 클 때 가장 좋습니다. 둘 다 같은 크기에 가깝기 때문에이 경우 해시 일치가 더 나은 선택이라고 주장합니다.

개요

최적화 된 쿼리 여기에 이미지 설명을 입력하십시오

최적화 된 쿼리 계획에는 병렬이 있으며 해시 일치 조인을 사용하며 잔여 IO 필터링을 줄여야합니다. 또한 비트 맵을 사용하여 조인 행을 생성 할 수없는 키 값을 제거합니다. (또한 아무것도 업데이트되지 않습니다)

최적화되지 않은 쿼리 최적화 여기에 이미지 설명을 입력하십시오 되지 않은 쿼리의 계획에는 병렬이 없으며 중첩 루프 조인을 사용하며 550M 레코드에 대한 잔여 IO 필터링을 수행해야합니다. (또한 업데이트가 진행 중입니다)

최적화되지 않은 쿼리를 개선하기 위해 무엇을 할 수 있습니까?

  • 키 열 목록에서 first_name & last_name을 갖도록 색인 변경 :

    INDEX IX_largeTableOfPeople_birth_date_first_name_last_name on dbo.largeTableOfPeople (birth_date, first_name, last_name) include (id) 작성

그러나 함수를 사용하고이 테이블이 크면 최적의 솔루션이 아닐 수 있습니다.

  • 더 나은 계획을 세우고 다시 컴파일하기 위해 재 컴파일을 사용하여 통계 업데이트
  • (HASH JOIN, MERGE JOIN)쿼리에 OPTION 추가
  • ...

테스트 데이터 + 사용 된 쿼리

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;

8

두 번째 쿼리가 실제로 개선 된 것은 분명하지 않습니다.

실행 계획에는 질문에 명시된 것보다 훨씬 적은 차이를 보여주는 QueryTimeStats가 포함됩니다.

느린 계획의 경과 시간은 257,556 ms4 분 17 초였습니다. 빠른 계획은 190,992 ms3의 병렬 처리 수준으로 실행 했음에도 불구하고 경과 시간 (3 분 11 초)이었습니다.

또한 두 번째 계획은 조인 후 수행 할 작업이없는 데이터베이스에서 실행되었습니다.

첫 번째 계획

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

두 번째 계획

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

따라서 350 만 행을 업데이트하는 데 필요한 작업으로 추가 시간을 잘 설명 할 수 있습니다 (업데이트 연산자가이 행을 찾고, 페이지를 잠그고, 페이지에 업데이트를 작성하고 트랜잭션 로그를 무시하는 데 필요한 작업)

경우 다음과 같은과 같이 비교할 때이 사실의 재현에있는 설명은 당신이 다만이 경우에는 운이 가지고 있다는 것입니다.

37 개의 IN조건을 가진 필터 는 테이블의 4,008,334 개 중 51 개의 행만 제거했지만 옵티마이 저는 훨씬 더 많은 것을 제거 할 것이라고 생각했습니다.

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

   LEFT(TRIM(largeTbl.last_name), 1) IN ( 'a', 'à', 'á', 'b',
                                          'c', 'd', 'e', 'è',
                                          'é', 'f', 'g', 'h',
                                          'i', 'j', 'k', 'l',
                                          'm', 'n', 'o', 'ô',
                                          'ö', 'p', 'q', 'r',
                                          's', 't', 'u', 'ü',
                                          'v', 'w', 'x', 'y',
                                          'z', 'æ', 'ä', 'ø', 'å' ) 

이러한 잘못된 카디널리티 추정은 일반적으로 나쁜 것입니다. 이 경우에는 과소 평가로 인한 해시 유출에도 불구하고 분명히 (?) 더 잘 작동하는 다른 모양의 (및 병렬) 계획을 생성했습니다.

TRIMSQL Server가 없으면 기본 열 히스토그램에서이를 범위 간격으로 변환하고 훨씬 더 정확한 추정치를 제공 할 수 있지만 TRIM추측만으로도 가능합니다.

추측의 본질은 다를 수 있지만 하나의 조건에 대한 추정에 LEFT(TRIM(largeTbl.last_name), 1)어떤 상황이에 * 단지로 추정 table_cardinality/estimated_number_of_distinct_column_values.

데이터 크기가 어떤 역할을하는 것 같은지 정확히 모르겠습니다. 나는 여기에서 와 같이 넓은 고정 길이 데이터 유형 으로 이것을 재현 할 수 있었지만 다른 더 높은 추측을 얻었습니다 varchar(평평한 10 % 추측과 100,000 행을 추정했습니다). @Solomon Rutzky는 이 경우 지적 varchar(100)후행 공백 패딩위한 공교롭게도 char낮은 추정치가 사용

IN목록 밖으로 확장 OR및 SQL 서버는 사용 지수 백 오프를 고려 4 술어 최대. 따라서 219.707추정치는 다음과 같습니다.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

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