하위 쿼리가있는 큰 테이블에서 느린 업데이트


16

함께 SourceTable> 가진 15MM 기록과 Bad_Phrase> 3K 기록을 가지고, 다음 쿼리는 SQL 서버 2005 SP4에서 실행되도록 약 10 시간이 소요됩니다.

UPDATE [SourceTable] 
SET 
    Bad_Count=
             (
               SELECT 
                  COUNT(*) 
               FROM Bad_Phrase 
               WHERE 
                  [SourceTable].Name like '%'+Bad_Phrase.PHRASE+'%'
             )

영어로,이 쿼리는 필드의 하위 문자열된다 Bad_Phrase에 나와있는 독특한 문구의 수를 세고 Name에서 SourceTable다음 필드에 그 결과를 배치를 Bad_Count.

이 쿼리를 상당히 빠르게 실행하는 방법에 대한 제안 사항이 있습니다.


3
따라서 테이블을 3K 번 스캔하고 15MM 행을 3K 시간마다 모두 업데이트하고 있으며 빠른 속도를 기대하십니까?
Aaron Bertrand

1
이름 열의 길이는 얼마입니까? 테스트 데이터를 생성하고 매우 느린 쿼리를 우리 중 누구와도 재생할 수있는 스크립트 또는 SQL 바이올린을 게시 할 수 있습니까? 어쩌면 나는 낙관론자 일지 모르지만 10 시간보다 훨씬 더 잘할 수 있다고 생각합니다. 나는 다른 주석가들에게 이것이 계산 비용이 많이 드는 문제라는 데 동의하지만, 우리가 왜 여전히 "더욱 빠른 속도"를 목표로 할 수 없는지 알 수 없습니다.
Geoff Patterson

3
매튜, 전체 텍스트 인덱싱을 고려 ​​했습니까? CONTAINS와 같은 것을 사용할 수 있지만 해당 검색에 대한 색인 생성의 이점을 얻을 수 있습니다.
swasheck

이 경우 행 기반 논리를 시도하는 것이 좋습니다 (즉, 15MM 행 1 업데이트 대신 SourceTable의 각 행 15MM 업데이트 또는 상대적으로 작은 청크 업데이트). 총 시간이 더 빠르지는 않지만 (이 경우에는 가능하더라도) 이러한 접근 방식은 시스템의 나머지 부분이 중단없이 계속 작동하게하고 트랜잭션 로그 크기를 제어합니다 (10k 업데이트마다 커밋). 모든 이전 업데이트를 잃지 않고 언제든지 업데이트 ...
a1ex07

2
@swasheck 전체 텍스트를 고려하는 것이 좋습니다 (2005 년에 새로워 졌으므로 여기에 적용 가능합니다). 그러나 전체 텍스트 인덱스 단어가 아니라 포스터에서 요구 한 것과 동일한 기능을 제공 할 수는 없습니다. 임의의 하위 문자열 달리 말하면, 전체 텍스트는 "환상적"이라는 단어에서 "개미"와 일치하는 것을 찾지 못할 것입니다. 그러나 비즈니스 요구 사항을 수정하여 전체 텍스트를 적용 할 수 있습니다.
Geoff Patterson

답변:


21

다른 주석가들에게는 이것이 계산 비용이 많이 드는 문제라는 데 동의하지만, 사용중인 SQL을 조정하여 개선의 여지가 많이 있다고 생각합니다. 예를 들어, 15MM 이름과 3K 구문으로 가짜 데이터 세트를 작성하고 이전 접근법을 실행하고 새로운 접근법을 실행했습니다.

가짜 데이터 세트를 생성하고 새로운 접근법을 시도하는 전체 스크립트

TL; DR

내 컴퓨터와이 가짜 데이터 세트에서 원래 접근 방식 을 실행 하는 데 약 4 시간이 걸립니다 . 제안 된 새로운 접근 방식은 약 10 분이 소요 되며 상당한 개선이 이루어집니다. 다음은 제안 된 접근 방식에 대한 간략한 요약입니다.

  • 각 이름에 대해 각 문자 오프셋에서 시작하고 최적화로 가장 긴 잘못된 문구의 길이에 제한을 두는 하위 문자열을 생성하십시오.
  • 이 하위 문자열에 클러스터형 인덱스를 만듭니다.
  • 각각의 나쁜 문구에 대해 일치하는 부분을 식별하기 위해이 부분 문자열을 탐색하십시오.
  • 각 원래 문자열에 대해 해당 문자열의 하나 이상의 하위 문자열과 일치하는 구별되는 나쁜 문구 수를 계산하십시오.


원래 접근 방식 : 알고리즘 분석

원래 UPDATE진술 의 계획에서 작업량은 이름 수 (15MM)와 문구 수 (3K)에 선형 적으로 비례한다는 것을 알 수 있습니다. 따라서 이름과 구의 수를 10으로 곱하면 전체 실행 시간이 ~ 100 배 느려집니다.

쿼리는 실제로 길이에 비례합니다 name. 이것은 쿼리 계획에 약간 숨겨져 있지만 테이블 스풀을 찾기 위해 "실행 횟수"를 통해 이루어집니다. 실제 계획에서이 작업은에 한 번만 발생하는 것이 name아니라 실제로 문자 내에서 한 번만 발생한다는 것을 알 수 있습니다 name. 따라서이 방법은 런타임 복잡성에서 O ( # names* # phrases* name length)입니다.

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


새로운 접근 방식 : 코드

이 코드는 전체 pastebin 에서도 사용할 수 있지만 편의를 위해 여기에 복사했습니다. 페이스트 빈에는 전체 프로 시저 정의가 있으며 여기에는 현재 배치의 경계를 정의하기 위해 아래에 표시되는 변수 @minId@maxId변수가 포함됩니다 .

-- For each name, generate the string at each offset
DECLARE @maxBadPhraseLen INT = (SELECT MAX(LEN(phrase)) FROM Bad_Phrase)
SELECT s.id, sub.sub_name
INTO #SubNames
FROM (SELECT * FROM SourceTable WHERE id BETWEEN @minId AND @maxId) s
CROSS APPLY (
    -- Create a row for each substring of the name, starting at each character
    -- offset within that string.  For example, if the name is "abcd", this CROSS APPLY
    -- will generate 4 rows, with values ("abcd"), ("bcd"), ("cd"), and ("d"). In order
    -- for the name to be LIKE the bad phrase, the bad phrase must match the leading X
    -- characters (where X is the length of the bad phrase) of at least one of these
    -- substrings. This can be efficiently computed after indexing the substrings.
    -- As an optimization, we only store @maxBadPhraseLen characters rather than
    -- storing the full remainder of the name from each offset; all other characters are
    -- simply extra space that isn't needed to determine whether a bad phrase matches.
    SELECT TOP(LEN(s.name)) SUBSTRING(s.name, n.n, @maxBadPhraseLen) AS sub_name 
    FROM Numbers n
    ORDER BY n.n
) sub
-- Create an index so that bad phrases can be quickly compared for a match
CREATE CLUSTERED INDEX IX_SubNames ON #SubNames (sub_name)

-- For each name, compute the number of distinct bad phrases that match
-- By "match", we mean that the a substring starting from one or more 
-- character offsets of the overall name starts with the bad phrase
SELECT s.id, COUNT(DISTINCT b.phrase) AS bad_count
INTO #tempBadCounts
FROM dbo.Bad_Phrase b
JOIN #SubNames s
    ON s.sub_name LIKE b.phrase + '%'
GROUP BY s.id

-- Perform the actual update into a "bad_count_new" field
-- For validation, we'll compare bad_count_new with the originally computed bad_count
UPDATE s
SET s.bad_count_new = COALESCE(b.bad_count, 0)
FROM dbo.SourceTable s
LEFT JOIN #tempBadCounts b
    ON b.id = s.id
WHERE s.id BETWEEN @minId AND @maxId


새로운 접근 방식 : 쿼리 계획

먼저 각 문자 오프셋에서 시작하는 하위 문자열을 생성합니다.

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

그런 다음이 하위 문자열에 클러스터형 인덱스를 만듭니다.

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

이제 각각의 나쁜 문구에 대해 일치하는 부분을 식별하기 위해 이러한 부분 문자열을 찾습니다. 그런 다음 해당 문자열의 하나 이상의 하위 문자열과 일치하는 별개의 잘못된 문구 수를 계산합니다. 이것이 실제로 핵심 단계입니다. 하위 문자열을 색인화하는 방식으로 인해 더 이상 잘못된 문구와 이름의 전체 교차 결과를 확인할 필요가 없습니다. 실제 계산을 수행하는이 단계는 실제 런타임의 약 10 % 만 차지합니다 (나머지는 하위 문자열의 사전 처리입니다).

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

마지막으로, a LEFT OUTER JOIN를 사용하여 잘못된 문구가 발견되지 않은 이름에 카운트 0을 할당 하여 실제 업데이트 문을 수행하십시오 .

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


새로운 접근법 : 알고리즘 분석

새로운 접근법은 전처리와 매칭의 두 단계로 나눌 수 있습니다. 다음 변수를 정의 해 봅시다 :

  • N = 이름 수
  • B = 잘못된 문구 수
  • L = 평균 이름 길이 (문자)

전처리 단계는 하위 문자열 O(N*L * LOG(N*L))을 생성 N*L한 후 정렬하는 것입니다.

실제 매칭은 O(B * LOG(N*L))각각의 나쁜 문구에 대한 부분 문자열을 찾기위한 것입니다.

이런 식으로, 우리는 3K 프레이즈 이상으로 확장 할 때 핵심 성능 잠금을 해제하는 불량 프레이즈 수에 따라 선형으로 확장되지 않는 알고리즘을 만들었습니다. 다른 방법으로 말하면, 원래 구현은 300 개의 나쁜 문구에서 3K 나쁜 문구로 이동하는 한 대략 10 배가 걸립니다. 마찬가지로 3K 나쁜 문구에서 30K로 가려면 10 배가 더 걸릴 것입니다. 그러나 새로운 구현은 하위 선형으로 확장되며 실제로 30K 나쁜 문구로 확장 될 때 3K 나쁜 문구에서 측정 된 시간의 2 배 미만이 소요됩니다.


가정 /주의

  • 전반적인 작업을 적당한 크기의 배치로 나누고 있습니다. 이것은 두 가지 방법 모두에 대한 좋은 생각 일 수 있지만 SORT하위 문자열 의 on이 각 배치에 대해 독립적이고 메모리에 쉽게 맞 도록 새로운 접근법에 특히 중요합니다 . 필요에 따라 배치 크기를 조작 할 수 있지만 한 배치에서 모든 15MM 행을 시도하는 것은 현명하지 않습니다.
  • SQL 2005 컴퓨터에 액세스 할 수 없으므로 SQL 2005가 아닌 SQL 2014를 사용하고 있습니다. SQL 2005에서는 사용할 수없는 구문을 사용하지 않도록주의를 기울 였지만 SQL 2012+ 의 tempdb 지연 쓰기 기능과 SQL 2014 의 병렬 SELECT INTO 기능을 여전히 활용할 수 있습니다 .
  • 이름과 구의 길이는 새로운 접근 방식에 상당히 중요합니다. 나는 나쁜 문구가 실제 사용 사례와 일치하기 때문에 일반적으로 상당히 짧다고 가정합니다. 이름은 나쁜 문구보다 약간 길지만 수천 자로 가정하지 않습니다. 나는 이것이 공정한 가정이라고 생각하고 이름 문자열이 길면 원래의 접근 방식이 느려질 것입니다.
  • 개선의 일부는 (그러나 모든 것에 근접하지는 않음) 새로운 접근 방식이 기존 방식 (단일 스레드 방식)보다 병렬 처리를보다 효과적으로 활용할 수 있기 때문입니다. 저는 쿼드 코어 랩톱을 사용하고 있으므로 이러한 코어를 사용할 수있는 접근 방식이 좋습니다.


관련 블로그 게시물

Aaron Bertrand 는 자신의 블로그 게시물에서 이러한 유형의 솔루션에 대해 자세히 살펴 보았습니다 . 주요 % wildcard에 대한 인덱스 검색 방법


6

Aaron Bertrand 가 제기 한 명백한 문제를 잠시 동안 주석으로 정리합시다 .

따라서 테이블을 3K 번 스캔하고 15MM 행을 3K 시간마다 모두 업데이트하고 있으며 빠른 속도를 기대하십니까?

하위 쿼리가 양쪽에서 와일드 카드를 사용한다는 사실 은 Sargability에 크게 영향을줍니다 . 해당 블로그 게시물에서 인용을하려면 :

즉, SQL Server는 Product 테이블의 모든 행을 읽고 이름의 어느 곳에 나“너트”가 있는지 확인한 다음 결과를 반환해야합니다.

에 대한 각 "나쁜 단어"와 "제품"에 대해 "너트"라는 단어를 교체 한 다음이 단어 SourceTable를 Aaron의 주석과 결합하면 현재 알고리즘을 사용하여 빠르게 실행되도록 하기가 매우 어려운 이유 (읽기 불가능)를 확인해야합니다.

몇 가지 옵션이 있습니다.

  1. 비즈니스가 전단력에 의한 쿼리를 극복 할 수있는 강력한 힘을 가진 몬스터 서버를 구매하도록 설득하십시오. (그렇지 않을 것이므로 다른 옵션이 더 좋습니다.
  2. 기존 알고리즘을 사용하여 고통을 한 번 받아 들인 다음 전파하십시오. 여기에는 삽입시 잘못된 단어를 계산하여 삽입 속도를 늦추고 새로운 잘못된 단어를 입력 / 발견 할 때만 전체 테이블을 업데이트합니다.
  3. Geoff의 답변을 받아들이십시오 . 이것은 훌륭한 알고리즘이며 내가 생각해 낸 것보다 훨씬 좋습니다.
  4. 옵션 2를 수행하되 알고리즘을 Geoff로 대체하십시오.

귀하의 요구 사항에 따라 옵션 3 또는 4를 권장합니다.


0

먼저 이상한 업데이트입니다

Update [SourceTable]  
   Set [SourceTable].[Bad_Count] = [fix].[count]
  from [SourceTable] 
  join ( Select count(*) 
           from [Bad_Phrase]  
          where [SourceTable].Name like '%' + [Bad_Phrase].[PHRASE] + '%')

'%'+ [Bad_Phrase]와 같이. [PHRASE] 님이 죽이고
있습니다. 색인을 사용할 수 없습니다

데이터 디자인이 속도에 적합하지 않습니다
[Bad_Phrase]. [PHRASE]를 단일 문구 / 단어로 나눌 수 있습니까?
같은 문구 / 단어가 두 개 이상 나타나는 경우 더 높은 개수를 원하면 두 번 이상 입력 할 수 있으므로
잘못된 구문의 행 수가 증가합니다. 가능
하면 훨씬 빠릅니다.

Update [SourceTable]  
   Set [SourceTable].[Bad_Count] = [fix].[count]
  from [SourceTable] 
  join ( select [PHRASE], count(*) as count 
           from [Bad_Phrase] 
          group by [PHRASE] 
       ) as [fix]
    on [fix].[PHRASE] = [SourceTable].[name]  
 where [SourceTable].[Bad_Count] <> [fix].[count]

2005가 지원하지만 전체 텍스트 색인을 지원하는지 확실하지 않으며 포함


1
OP가 잘못된 단어 테이블에서 잘못된 단어의 인스턴스를 계산하려고한다고 생각하지 않습니다. 소스 테이블에 숨겨진 잘못된 단어의 수를 계산하려고한다고 생각합니다. 예를 들어, 원래 코드는 "shitass"라는 이름으로 2를 계산하지만 코드는 0을 계산합니다.
Erik

1
@Erik "[Bad_Phrase]. [PHRASE]를 하나의 문구로 분리 할 수 ​​있습니까?" 실제로 데이터 디자인이 해결책이라고 생각하지 않습니까? 나쁜 물건을 찾는 것이 목적이라면 하나 이상의 카운트를 가진 "eriK"로 충분합니다.
paparazzo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.