소개
IN 목록은 N1QL 쿼리에서 일반적으로 사용됩니다. Couchbase Server 6.5에는 N1QL 쿼리에서 IN 목록 처리에 대한 몇 가지 향상된 기능이 도입되었습니다.
대규모 IN 목록 처리
Couchbase Server 6.5 이전에는 N1QL이 런타임에 IN 목록 평가를 간단한 방식으로 처리하여 일치하는 항목이 발견될 때까지 IN 목록의 각 요소와 차례로 비교하는 방식으로 처리했습니다. IN 목록이 큰 경우, 즉 IN 목록에 많은 요소가 있는 경우 이 방식은 비효율적일 수 있습니다. 카우치베이스 서버 6.5에서는 런타임에 큰 IN 목록 처리를 위한 개선 사항이 도입되었습니다. IN 목록의 요소 수가 임계값(현재 16개)을 초과하면 IN 목록의 모든 요소로 해시 테이블을 생성합니다. 이렇게 하면 IN 목록 술어의 각 평가가 IN 목록의 각 요소와 많은 비교를 수행하는 대신 해시 테이블의 프로브가 됩니다.
"정적" IN 목록
이 최적화가 작동하려면 IN 목록이 "정적"이어야 합니다. 즉, 쿼리를 한 번 실행하는 동안 IN 목록의 각 요소 값이 변경되지 않아야 합니다. 예를 들어 모든 요소가 상수이거나 쿼리 매개변수인 IN 목록을 가질 수 있습니다:
1 2 3 |
문서.필드 IN [1, 2, 3, ......, 1024] 문서.필드 IN [$1, $2, $3, ......, $128] |
위의 ...... 는 약어일 뿐이며 쿼리를 실행하려면 실제로 모든 값을 입력해야 합니다.
동일한 IN 목록에서 상수와 쿼리 매개변수를 혼합할 수 있습니다:
1 |
문서.필드 IN [1, 2, 3, 4, 5, $1, $2, $3, $4, $5, 6, 7, 8, 9, 10, ......, 512] |
전체 IN 목록에 단일 쿼리 매개변수를 사용할 수도 있습니다:
1 |
문서.필드 IN $인리스트 |
를 클릭하고 실제 IN 목록을 쿼리 매개변수로 전달합니다.
IN 목록 평가의 최적화는 IN 목록이 "정적"이 아닌 경우(예: IN 목록의 요소가 문서의 어떤 필드를 참조하는 경우) 적용되지 않습니다:
1 |
문서.필드 IN [문서.c1, 2, ......, 1024] |
이 경우 실행 중에 IN 목록의 첫 번째 요소 값이 변경되므로 이 IN 목록의 평가에 해시 테이블 최적화를 사용할 수 없습니다.
하위 쿼리가 있는 IN 절
IN 목록의 특수한 경우로 하위 쿼리가 있는 IN 절을 가질 수 있습니다:
1 |
문서.필드 IN (선택 RAW <expr> FROM ...... 어디 ......) |
RAW 키워드가 사용되고 투영 목록에 단일 표현식이 있는 경우 하위 쿼리의 결과는 값의 배열입니다. 하위 쿼리가 상관 관계가 없는 경우, 즉 상위 쿼리의 어떤 필드에 대한 참조도 없는 경우, IN 목록 술어 평가에 해시 테이블을 사용하는 새로운 최적화가 여기에 적용될 수 있습니다. 런타임에 하위 쿼리가 실행되고 하위 쿼리 실행 결과가 알려진 후 결과 집합에 임계값보다 많은 값이 포함된 경우, 결과 집합의 모든 값으로 해시 테이블이 생성되고 이후 하위 쿼리 술어의 평가는 해시 테이블에 대한 프로브가 됩니다. 이 IN 목록 최적화는 중첩된 하위 쿼리의 경우 잠재적으로 여러 번 적용될 수 있습니다:
1 2 3 4 |
document1.field1 IN (선택 RAW <expr1> FROM ...... 어디 document2.field2 IN (선택 RAW <expr2> FROM ...... 어디 ......) ) |
두 하위 쿼리가 상관관계가 없고 각 하위 쿼리의 결과 크기가 임계값을 초과하지 않는 한.
취급하지 않음
IN 리스트 평가에 해시 테이블을 사용하는 최적화는 NOT IN 조건문에도 적용됩니다. NOT IN 조건문은 IN 조건문과 동일한 방식으로 평가되며 결과 부울 값은 단순히 반전됩니다.
IN 목록 평가를 위한 해시 테이블 최적화 가시성 확인
해시 테이블을 사용하여 큰 IN 목록을 평가하기 위한 최적화는 순전히 내부적인 최적화입니다. 쿼리 실행 속도가 빨라지는 것 외에는 외부에서 눈에 보이는 효과는 거의 없습니다. 해시 테이블을 사용하기 때문에 쿼리를 실행하는 동안 메모리 사용량도 약간 증가합니다. 추가 메모리 소비량은 해시 테이블의 크기에 따라 달라지며, 이는 다시 IN 목록의 요소 수에 따라 달라집니다.
쿼리 매개 변수를 IN 목록으로 사용하기 위한 동적 인덱스 범위 생성
IN 목록 술어의 경우 적절한 인덱스가 존재하는 경우 플래너는 IN 목록의 각 요소에 대해 하나씩 여러 스팬으로 인덱스 스캔을 생성할 수 있습니다. 예를 들어
1 2 3 |
만들기 INDEX ix1 켜기 기본값(c1); 설명 선택 1 FROM 기본값 어디 c1 IN [1, 2, 3]; |
인덱스 스캔에는 3개의 스팬이 생성됩니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
"spans": [ { "정확한": true, "범위": [ { "high": "1", "포함": 3, "low": "1" } ] }, { "정확한": true, "범위": [ { "high": "2", "포함": 3, "low": "2" } ] }, { "정확한": true, "범위": [ { "high": "3", "포함": 3, "low": "3" } ] } ], |
쿼리 매개변수가 IN 목록에서 개별 요소로 사용되는 경우에도 마찬가지입니다:
1 |
설명 선택 1 FROM 기본값 어디 c1 IN [$1, $2, $3]; |
다음과 같은 인덱스 범위가 생성됩니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
"spans": [ { "정확한": true, "범위": [ { "high": "$1", "포함": 3, "low": "$1" } ] }, { "정확한": true, "범위": [ { "high": "$2", "포함": 3, "low": "$2" } ] }, { "정확한": true, "범위": [ { "high": "$3", "포함": 3, "low": "$3" } ] } ], |
그러나 쿼리 매개변수가 전체 IN 목록으로 사용되는 경우:
1 |
설명 선택 1 FROM 기본값 어디 c1 IN $인리스트; |
그러면 단일 인덱스 스팬이 생성됩니다:
1 2 3 4 5 6 7 8 9 10 11 12 |
"spans": [ { "정확한": true, "범위": [ { "high": "array_max($inlist)", "포함": 3, "low": "array_min($inlist)" } ] } ], |
인덱스 범위의 하한과 상한은 각각 array_min($inlist) 및 array_max($inlist)입니다. 쿼리 매개변수가 전체 IN 목록에 사용되는 경우, 컴파일 시점에 IN 목록의 크기를 알 수 없으므로 IN 목록의 각 요소에 대한 인덱스 범위를 미리 생성할 수 없습니다. 현재 생성된 인덱스 범위를 사용하여 배열 요소의 최소값과 최대값 사이에서 인덱스 스캔을 수행해야 합니다. 예를 들어 쿼리 실행을 위해 제공된 쿼리 매개변수가 [1, 1000, 1000000]인 경우 1과 1000000 사이의 모든 것이 인덱스에 의해 스캔됩니다. 이는 매우 비효율적이며 불필요한 작업이 많이 수반됩니다.
카우치베이스 서버 6.5에서는 런타임에 인덱스 스팬을 동적으로 생성하여 이러한 경우의 처리가 개선되었습니다. 쿼리가 실행될 때 이제 쿼리 매개변수의 값을 알 수 있으므로 IN 목록의 요소 수 또한 알 수 있습니다. 이제 실행 엔진은 컴파일 시점에 IN 목록이 알려졌을 때 생성된 인덱스 스팬을 모방하려고 시도하며, IN 목록의 각 요소에 대해 별도의 인덱스 스팬을 동적으로 생성합니다. 예를 들어, 쿼리 실행을 위해 [1, 1000, 1000000]이 $inlist로 지정된 경우, 런타임 실행 엔진은 쿼리에서 [1, 1000, 1000000]이 상수로 지정된 경우와 유사하게 3개의 인덱스 스팬을 생성합니다. 이렇게 하면 인덱스 스캔이 훨씬 더 정확해지고 인덱스의 불필요한 영역을 스캔하는 것을 방지할 수 있습니다.
쿼리에 대한 설명 계획은 동일하게 유지되며, 인덱스 범위의 하한과 상한으로 array_min($inlist) 및 array_max($inlist)가 여전히 표시됩니다. 인덱스 스팬에 새로운 "dynamic_in" 표시기가 추가됩니다:
1 2 3 4 5 6 7 8 9 10 11 12 |
"spans": [ { "범위": [ { "dynamic_in": true, "high": "array_max($inlist)", "포함": 3, "low": "array_min($inlist)" } ] } ], |
동적 인덱스 스팬 생성은 런타임에 이루어집니다. 잠재적인 성능 문제를 피하기 위해 인덱스 스팬이 동적으로 생성될 때 8192개의 스팬으로 제한됩니다. 쿼리 매개변수로 전달된 IN 목록에 8192개 이상의 요소가 있는 경우, 이 최적화는 적용되지 않습니다.
하위 쿼리 처리 및 잠재적 쿼리 재작성
위의 예는 IN 목록이 쿼리 매개변수일 때 인덱스 스팬을 동적으로 생성하는 최적화를 보여줍니다. 이 최적화는 IN 목록이 WITH 변수 또는 함수 매개변수인 경우에도 적용됩니다. WITH 변수는 Couchbase Server 6.5의 또 다른 새로운 기능인 일반 테이블 표현식에서 제공됩니다( https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/with.html); 함수 매개 변수는 Couchbase Server 6.5의 또 다른 새로운 개발자 프리뷰 기능인 사용자 정의 함수에서 제공됩니다( https://docs.couchbase.com/server/6.5/n1ql/n1ql-language-reference/userfun.html).
그러나 IN 절이 하위 쿼리를 사용하는 경우에는 이 최적화가 적용되지 않습니다. 예를 들어
1 2 3 4 5 6 7 8 |
선택 DISTINCT RAW 경로.항공사 FROM `여행-샘플` AS 경로 어디 경로.유형 = "경로" AND 경로.소스공항 IN (선택 RAW 공항.faa FROM `여행-샘플` AS 공항 어디 공항.유형 = "공항" AND 공항.공항명 = "시애틀 타코마 국제 공항"); |
이 예제에서는 IN 절의 오른쪽에 하위 쿼리가 있습니다. 다음과 같은 인덱스가 정의되어 있습니다:
1 2 |
만들기 INDEX ix_route_1 켜기 `여행-샘플`(소스공항, 항공사) 어디 유형 = "경로"; 만들기 INDEX ix_airport_1 켜기 `여행-샘플`(공항명, faa) 어디 유형 = "공항"; |
그런 다음 위 쿼리에 대한 설명은 다음 스팬을 사용하여 ix_route_1에 대한 인덱스 스캔을 보여줍니다:
1 2 3 4 5 6 7 8 9 10 |
"spans": [ { "범위": [ { "포함": 0, "low": "null" } ] } ], |
즉, 인덱스는 NULL이 아닌 모든 값을 스캔합니다.
위 쿼리를 수동으로 다시 작성하여 WITH 절을 사용할 수 있습니다:
1 2 3 4 5 6 7 8 |
WITH FAAS AS (선택 RAW 공항.faa FROM `여행-샘플` AS 공항 어디 공항.유형="공항" AND 공항.공항명 = "시애틀 타코마 국제 공항") 선택 DISTINCT RAW 경로.항공사 FROM `여행-샘플` AS 경로 어디 경로.유형 = "경로" AND 경로.소스공항 IN FAAS; |
이 재작성된 쿼리에 대한 설명을 살펴보면, 이제 ix_route_1의 인덱스 스캔은 다음과 같은 스팬을 갖습니다:
1 2 3 4 5 6 7 8 9 10 11 12 |
"spans": [ { "범위": [ { "dynamic_in": true, "high": "array_max(`faas`)", "포함": 3, "low": "array_min(`FAAS`)" } ] } ], |
이제 재작성된 쿼리의 경우 최적화를 활용하여 런타임에 인덱스 스팬을 동적으로 생성할 수 있습니다.
동적 인덱스 스팬 생성의 새로운 최적화를 활용하기 위한 이 수동 쿼리 재작성은 하위 쿼리가 상관관계가 없는 경우에만 가능합니다. 수동 쿼리 재작성은 단일 하위 쿼리에만 국한되지 않습니다. 관련된 하위 쿼리가 상호 연관성이 없는 한, 중첩된 하위 쿼리에 대해서도 유사한 수동 쿼리 재작성을 수행할 수 있습니다. 예를 들어 다음 쿼리가 있다고 가정해 보겠습니다:
1 2 3 4 5 6 7 8 9 10 11 12 |
선택 RAW 항공사.콜사인 FROM `여행-샘플` AS 항공사 어디 항공사.유형 = "항공사" AND 항공사.iATA IN (선택 DISTINCT RAW 경로.항공사 FROM `여행-샘플` AS 경로 어디 경로.유형 = "경로" AND 경로.소스공항 IN (선택 RAW 공항.faa FROM `여행-샘플` AS 공항 어디 공항.유형 = "공항" AND 공항.공항명 = "시애틀 타코마 국제 공항")); |
인덱스 스팬을 동적으로 생성하는 최적화를 활용할 수 있도록 다음 쿼리로 재작성할 수 있습니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
WITH 항공사 AS ( WITH FAAS AS (선택 RAW 공항.faa FROM `여행-샘플` AS 공항 어디 공항.유형 = "공항" AND 공항.공항명 = "시애틀 타코마 국제 공항") 선택 DISTINCT RAW 경로.항공사 FROM `여행-샘플` AS 경로 어디 경로.유형 = "경로" AND 경로.소스공항 IN FAAS) 선택 RAW 항공사.콜사인 FROM `여행-샘플` AS 항공사 어디 항공사.유형 = "항공사" AND 항공사.iATA IN 항공사; |
다음 인덱스가 정의되어 있는 경우:
1 |
만들기 INDEX ix_airline_1 켜기 `여행-샘플`(iATA, 콜사인) 어디 유형 = "항공사"; |
재작성된 쿼리에 대한 설명은 인덱스 ix_airline_1에서 유사한 인덱스 범위를 보여줍니다:
1 2 3 4 5 6 7 8 9 10 11 12 |
"spans": [ { "범위": [ { "dynamic_in": true, "high": "array_max(`airlines`)", "포함": 3, "low": "array_min(`airlines`)" } ] } ], |
동적 인덱스 스팬 생성의 최적화가 이루어지고 있음을 나타냅니다.
요약
카우치베이스 서버 6.5는 IN 목록 처리를 위한 몇 가지 향상된 기능을 도입했습니다. IN 목록이 큰 경우, 런타임에 해시 테이블이 IN 목록 평가를 위해 사용됩니다. 쿼리 매개변수가 전체 IN 목록으로 지정된 경우, 런타임에 동적 인덱스 스팬 생성을 통해 보다 효율적인 인덱스 스캔을 수행할 수 있습니다. 이 두 가지 최적화를 통해 IN 리스트의 처리가 훨씬 더 효율적으로 이루어집니다. IN 절에 상관관계가 없는 하위 쿼리가 있는 경우, 런타임에 동적 인덱스 범위 생성의 최적화를 활용하기 위해 일반적인 테이블 표현식을 사용하여 쿼리를 수동으로 다시 작성할 수 있습니다.
블로그를 커뮤니케이션 매체로 사용해서 죄송합니다만... 사이트의 문의 양식이 작동하지 않고 '회원'만 이메일을 보낼 수 있다고 문의 이메일이 반송됩니다.
카우치베이스가 아직 살아 있나요? :/?
누구와도 대화할 수 있었나요?
안녕하세요 데니스.
빙지에로부터 연락을 받았습니다.
여기에 답장해 주셔서 감사합니다 :)
특히 쿼리 최적화를 위한 해시 테이블과 CTE 등 매우 유용한 정보를 제공합니다.