다른 주석가들에게는 이것이 계산 비용이 많이 드는 문제라는 데 동의하지만, 사용중인 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에 대한 인덱스 검색 방법