37.15. 연산자 최적화 정보

37.15.1. COMMUTATOR
37.15.2. NEGATOR
37.15.3. RESTRICT
37.15.4. JOIN
37.15.5. HASHES
37.15.6. MERGES

PostgreSQL에서 사용하는 연산자는 자신이 어떻게 동작하는지에 대한 여러 정보를 서버에서 제공하는 여러 옵션을 포함하여 정의된다. 이 옵션 정의는 쿼리 처리 속도를 빠르게 하기 때문에, 적절히 잘 사용해야 한다! 잘못된 옵션 정의는 오히려 쿼리 처리 속도를 더 늦게 만들 수도 있고, 의도하지 않은 출력 결과가 나올 수도 있고, 그 외 다른 문제점을 일으킬 수도 있다. 물론 아래에서 설명하는 옵션들을 정확히 모르면, 아애 정의하지 않을 수도 있다; 단지 더 빠르게 할 수 있는 것을 못할 뿐이다.

아래에서 설명하고 있는 최적화 옵션들 외에도 향후 PostgreSQL 버전에서 보다 많은 최적화 옵션들이 추가 되어야 할 것이다. 여기서 소개하고 있는 옵션들은 13.3 버전 기준에서 사용 가능한 것들이다.

It is also possible to attach a planner support function to the function that underlies an operator, providing another way of telling the system about the behavior of the operator. See 37.11절 for more information.

37.15.1. COMMUTATOR

COMMUTATOR 절에 (제공한다면) 해당 연산자에 대한 교환 연산자 이름을 지정한다. 입력된 x, y 값에 대한 (x A y) 식과 (y B x) 식의 결과값이 같다면, A 연산자를 B 연산자의 교환 연산자라 한다. 물론 A 연산자의 교환 연산자는 B이다. 예를 들어, 특정 자료형에서 <, > 두 연산자는 서로 교환 연산자이며, + 연산자는 자기 스스로가 교환 연산자이지만, - 연산자는 자기 스스로가 교환 연산자가 되지 못하기도 한다.

교환 연산자는 왼쪽, 오른쪽 자료형이 서로 같아야 한다. 그래서, PostgreSQL에서는 COMMUTATOR 절에서 단지 그 교환 연산자의 이름만 정의한다. (즉, 해당 자료형을 사용하는 연산자가 먼저 정의 되어 있어야 한다. - 옮긴이)

교환 연산자 정의는 인덱스 사용과 조인 구문에서 아주 중요한 영향을 미친다. 왜냐하면, 이 정의가 없다면, 쿼리 최적화기의 항 바꾸기 작업을 건너띄기 때문이다. 예를 들어, WHERE절에 tab1.x = tab2.y 형태의 구문이 있고, tab1.x, tab2.y 둘 다 사용자 정의 자료형이고, tab2.y 칼럼에 대해서 인덱스를 만들어 두었다. 이 경우, 최적화기는 이 구문에서 인덱스를 사용하지 않는다. 인덱스 검색 기능은 우선적으로 왼쪽 칼럼에 대해서 인덱스가 있는가를 조사하기 때문이다. PostgreSQL에서는 그냥 막 항을 서로 바꾸지 않는다 — 즉, 사용자가 그 자료형에 대한 = 연산자를 만들 때, 각 항이 서로 바뀌어도 결과가 같은가를 정의하는 교환 연산자 정의도 바르게 해야 한다.

교환 연산자가 만드려고 하는 연산자와 같은 경우라면, 그냥 그 연산자로 교환 연산자 정의를 하면 되지만, 이 두 개의 연산자가 서로 쌍인 경우는 약간의 꼼수가 필요하다: 왜냐하면, 교환 연산자가 이미 만들어져 있어야 하는데, 그 연산자의 교환 연산자를 아직 만들지 않았기 때문이다. 다음은 이 문제를 푸는 두 가지 방법이다:

  • 첫번째 방법은 일단 하나의 연산자에서 COMMUTATOR절 을 빼고 만들고, 다음 연산자에서는 앞에서 만든 연산자를 교환 연산자로 지정하여 만든다. PostgreSQL에서는 교환 연산자는 한 쌍을 가지기 때문에, 두번째 만들어진 연산자에서 첫번째 만들어진 연산자를 교환 연산자로 지정했다면, 자동으로 첫번째 연산자의 교환 연산자로 두번째 연산자를 다시 지정한다.

  • 다른 방법은 보다 직설적으로 그냥 두 연산자 모두 COMMUTATOR절을 포함해서 만든다. 이 경우 PostgreSQL에서는 첫번째 연산자를 만들 때 지정한 교환 연산자가 없는 것을 알고, 그냥 시스템 카탈로그에 그 정보만 기록해 두고 연산자를 만든다. 교환 연산자는 만드려고 하는 연산자에서 사용하는 양쪽 자료형이 서로 같기 때문에, 임의의 가상 연산자를 만들 수 있다. 그 다음 두번째 연산자를 정상적으로 만들고, 두번째 연산자가 만들어지면서 교환 연산자 정의 쌍이 마무리 된다. 즉, 두번째 연산자가 만들어지기 전에 첫번째 연산자를 사용하면 오류를 낸다.

37.15.2. NEGATOR

NEGATOR절에 (제공한다면) 해당 연산자의 부정 연산자 이름을 지정한다. 입력된 x, y 값에 대해서 (x A y) 식의 결과값(불리언 자료형)과 NOT (x B y) 식의 결과값이 같을 때, A 연산자를 B 연산자의 부정 연산자라고 한다. 물론 B 연산자는 A 연산자의 부정 연산자이기도 하다. 예를 들어, < 연산자와 >= 연산자는 대부분 자료형에서 부정 연산자 쌍이다. 자기 스스로가 부정 연산자인 경우는 없다.

교환 연산자와 달리, 단항 연산자의 쌍이 서로의 부정 연산자가 되기도 한다. 이 말은 (A x) 결과값이 NOT (B x)의 결과값과 같음을 의미한다. 또한 오른쪽 단항 연산자에서도 마찬가지다.

한 연산자의 부정 연산자에서 사용하는 자료형도 그 연산자가 사용하는 자료형과 같아야 한다. 그래서, COMMUTATOR절과 마찬가지로 NEGATOR절에서도 연산자 이름만 정의하면 된다.

부정 연산자는 최적화기가 NOT (x = y) 이런 구문을 x <> y 구문으로 단순화하는데 유용하게 사용된다. NOT 연산은 다른 재구성 결과로 추가될 수 있기 때문에, 이런 단순화 작업은 생각보다 빈번하게 일어난다.

한 쌍의 부정 연산자 정의에 있어 충돌 문제도 위에서 설명한 교환 연산자 문제와 같이 처리하면 된다.

37.15.3. RESTRICT

RESTRICT절에는 (제공한다면) 해당 연산자의 제한 선택 추정 함수 이름을 지정한다. (이 이름이 연산자 이름이 아니라, 함수 이름임을 주의해야 한다) RESTRICT절은 연산자가 이항 연산자이고 그 결과가 boolean형인 경우에만 효과가 있다. 이 제한 선택 추정이란 WHERE절에서 다음과 같은 형태로 연산자를 사용할 경우 해당 테이블에서 추출될 로우수를 추정하는 것을 말한다:

칼럼 연산자 상수

여기서 연산자는 지금 이야기하는 해당 연산자이며, 상수는 특정 상수다. 이 함수는 최적화기가 이 WHERE절에서 지정한 조건에 맞는 로우수를 예측하는데 도움을 준다. (물론 위와 같은 식에서 왼쪽 항에 상수가 올 경우는 상황이 복잡해진다. 하지만, 앞 COMMUTATOR절에서 지정한 교환 연산자를 통해서 그 위치를 서로 바꾼 다음 처리할 것이다.)

새로운 제한 선택 추정 함수를 만드는 방법에 대해서는 이 장의 범위를 벗어나지만, 대부분의 사용자 정의 연산자에서 쓸 수 있는 시스템 표준 추정 함수들을 미리 만들어 두었다. 다음은 이 함수들이다:

= 연산자에서는 eqsel
<> 연산자에서는 neqsel
< 연산자에서는 scalarltsel
<= 연산자에서는 scalarlesel
> 연산자에서는 scalargtsel
>= 연산자에서는 scalargesel

실 결과가 정말 같은지 다른지에 관계 없이, 아주 많거나, 아주 적은 조건 만족 결과가 예상되는 eqsel, neqsel 함수 사용은 의미가 없다고 판단하여 종종 사용하지 않을 수도 있다. 예를 들어, 대략 같다를 처리하는 도형 연산자인 경우 해당 테이블에서 일치하는 자료가 적을 것으로 간주하고, 그 연산자의 제한 선택 추정 함수로 eqsel 함수를 지정한다.

You can use scalarltsel, scalarlesel, scalargtsel and scalargesel for comparisons on data types that have some sensible means of being converted into numeric scalars for range comparisons. If possible, add the data type to those understood by the function convert_to_scalar() in src/backend/utils/adt/selfuncs.c. (향후, 이 함수는 pg_type 시스템 카탈로그의 한 칼럼에 일대일 대응되는 함수들로 대체될 것이다. 아직은 아님) 물론 이런 조건에 맞지 않아도 최적화기 쪽으로 참고 정보를 주겠지만, 좋은 실행 계획을 짤 근거 자료가 되지는 못한다.

Another useful built-in selectivity estimation function is matchingsel, which will work for almost any binary operator, if standard MCV and/or histogram statistics are collected for the input data type(s). Its default estimate is set to twice the default estimate used in eqsel, making it most suitable for comparison operators that are somewhat less strict than equality. (Or you could call the underlying generic_restriction_selectivity function, providing a different default estimate.)

추가로 도형 자료형에 대한 제한 선택 추정 함수로는 src/backend/utils/adt/geo_selfuncs.c 소스 코드에 다음과 같이 있다: areasel, positionsel, contsel. 이들은 기본 함수로 작성되었지만, 충분히 쓸만한 것도 있고, 개선 해야 할 것도 있다.

37.15.4. JOIN

JOIN절에서는 (제공한다면) 해당 연산자의 조인 선택 추정 함수 이름을 지정한다. (이 이름은 연산자 이름이 아니라, 함수 이름임을 주의해야 한다.) JOIN절은 연산자가 이항 연산자이고 그 결과가 boolean형인 경우에만 효과가 있다. 이 조인 선택 추정이란 WHERE절에서 다음과 같은 형태로 연산자를 사용할 경우 그 대상 로우수를 추정하는 것을 말한다:

table1.column1 연산자 table2.column2

RESTRICT절과 마찬가지로, 이 지정은 조인 작업을 소요되는 비용을 추정해서 최적화기가 최적의 실행계획을 짜도록 도움을 준다.

앞에서와 같이 이 함수를 만드는 방법에 대해서는 여기서 소개하지는 않고, 단지 미리 만들어진 표준 추정 함수를 소개한다:

= 연산자에서는 eqjoinsel
<> 연산자에서는 neqjoinsel
< 연산자에는 scalarltjoinsel
<= 연산자에는 scalarlejoinsel
> 연산자에는 scalargtjoinsel
>= 연산자에는 scalargejoinsel
일반 매칭 연산에는 matchingjoinsel
2D 영역 기반 비교 연산에서는 areajoinsel
2D 위치 기반 비교 연산에서는 positionjoinsel
2D 포함 기반 비교 연산에서는 contjoinsel

37.15.5. HASHES

HASHES절에서는 (제공한다면) 해당 연산자를 사용하는 부분에서 해시 조인을 사용할 수 있도록 한다. 이 지정은 그 연산자가 이항 연산자이며, 반환값이 boolean형이고, 각 항의 자료형이 서로 같거나, 같은 종류의 자료형이어야 작동한다.

해시 조인의 기본 개념은 조인 연산자 기준으로 양쪽 항의 그 값에 대한 해시값이 같은가를 조사하는 것이다. 이 두 값이 서로 다른 해시 버켓에 있다면, 그들은 처음부터 비교 대상에서 제외되며, 암묵적으로 서로 다른 값으로 판단해서 연산 결과값을 false로 반환한다. 따라서 두 값이 같은가를 조사하는 연산자가 아닌 경우는 이 HASHES 지정은 아무런 의미가 없다. 보통 이 기능은 양쪽 항 자료형이 같은 경우에 지정하는 것이 일반적이다. 반면에, 서로 다른 자료형일지라도 해시 함수가 그 값에 대한 해시값을 같게 만들도록 설계할 수도 있어, 이런 경우라면, 이 기능을 사용할 수 있다. 예를 들어, 정수 자료형에 대해서 그 길이가 달라도 그 값이 같다면, 같은 해시값을 사용한다.

HASHES 기능을 사용하려면, 먼저 그 해당 연산자가 해시 인덱스 연산자 가족의 일원이어야 한다. 연산자를 만들 때, 미리 이 연산자를 위한 연산자 가족이 아직 구성되지 않았기 때문에 이런 조건에 제한 없이 일단 만들 수 있다. 하지만, 이 연산자를 통해 해시 조인 작업을 할 경우 해당 연산자 가족이 없으면 실시간 오류를 낸다. 해당 자료형에 대한 해시 함수를 찾기 위해 연산자 가족 정보가 필요하기 때문이다. 물론 연산자 가족을 만들려면, 그 자료형에 대한 해시 함수를 먼저 만들어야 할 것이다.

해시 함수의 반환값이 기계에 따라 다를 수 있어, 해시 함수를 준비할 때 주의할 필요가 있다. 예를 들어, 사용할 자료형이 구조체이고, 이 안에 의미 없는 비트들이 채워진 경우, hash_any 함수를 사용해서 해시값을 구하면 위험하다. (함수와 연산자를 만들 때, 사용하지 않는 비트는 항상 0으로 처리하도록 작성하는 것이 안전하다.) 또 다른 예로, -0 값과, +0 값이 서로 다른 것으로 처리하는 IEEE 부동소수 표준 처리에 대한 기계별 차이가 있을 수 있다. 이런 경우를 대비해서 -0 값은 해시값을 구하기 전에 미리 +0 으로 바꾸는 작업이 필요하다.

해시 조인이 가능한 연산자는 교환 연산자를 해당 연산자 가족에 포함 시켜야 하며, 연산자를 만들 때 그것을 지정해야 한다( 교환 연산자는 연산 하는 두 자료형이 같은 경우는 그 연산자 자신이 될 것이고, 서로 다르다면, 그에 상응하는 또 다른 연산자일 것이다). 교환 연산자를 지정하지 않으면, 실행 계획기에서 오류를 낸다. 또한, (반드시 필요하지는 않지만) 해시 연산자 가족에 여러 자료형을 지원하는 그에 상응하는 다른 연산자들도 모두 포함시켜 놓는 것은 좋은 방법이다. 이렇게 함으로 최적화 작업이 더 좋아질 것이다.

참고

해시 조인이 가능한 연산자에 사용할 함수는 반드시 immutable 이거나 stable 함수여야 한다. volatile 함수면 해시 조인 자체를 시도하지 않는다.

참고

해시 조인이 가능한 연산자에 사용할 함수가 strict 인 경우, 입력값이 null일 지라도, 그 함수의 반환값은 true 또는 false 여야만 한다. 이렇게 하지 않으면, IN 동작의 해시 최적화가 잘못된 결과를 만든다. (특히, strict 함수는 입력값이 null 인 경우 무조건 null을 반환하기 때문에, 입력값으로 null이 오는 경우 default 처리를 하고, 함수 내용에서 그 default 값에 대한 예외처리를 하는 것이 안전하다.)

37.15.6. MERGES

MERGES절에서는 (제공한다면) 해당 연산자를 사용하는 부분에서 머지 조인을 사용할 수 있도록 한다. 이 지정은 그 연산자가 이항 연산자이며, 반환값이 boolean형이고, 각 항의 자료형이 서로 같거나, 같은 종류의 자료형이어야 작동한다.

머지 조인의 기본 개념은 조인할 칼럼의 양쪽 자료를 각각 정렬해서, 차례로 그 값이 같은지 비교하는 것이다. 그래서, 먼저 그 자료가 정렬될 수 있어야 하며, 양쪽 자료 모두 같은 정렬 기준으로 같은 장소에 같은 자료가 있어야 이들이 서로 같다고 간주할 수 있다. 하지만, 이 같은 장소는 논리적인 호환성을 뜻하기도 한다. 예를 들어 smallint 자료형과 integer 자료형의 머지 조인도 가능하다. 그래서, 양쪽 자료형에 대해서 논리적으로 같은 위치에 있도록 정렬하는 작업만 보장되면 된다.

MERGES 기능을 사용하려면, 이 연산자가 btree 인덱스 연산자 가족의 일원이어야 한다. 하지만, 연산자를 만들 때, 그 참조하는 연산자 가족을 아직 안 만들었을 수도 있기 때문에, 일단 연산자를 먼저 만들 수도 있다. 하지만 이 연산자가 연산자 가족으로 포함되어 있지 않다면, 머지 조인을 사용하지 못한다. MERGES 옵션은 실행계획기에게 사용할 연산자 가족을 찾기위한 힌트를 제공하는 것이다.

머지 조인이 가능한 연산자를 만들 때는 반드시 교환 연산자도 함께 지정해야 한다. (두 자료형이 같은 경우는 자기 자신이면 되고, 서로 다른 자료형이라면, 같은 역할을 하는 연산자를 지정한다.) 이 연산자 또한 같은 연산자 가족 구성원이여야 한다. 이렇게 만들지 않으면, 실행 계획기가 이 연산자를 사용할 때 오류를 낸다. 또한, (반드시 필요하지는 않지만) btree 연산자 가족에 여러 자료형을 지원하는 그에 상응하는 다른 연산자들도 모두 포함시켜 놓는 것은 좋은 방법이다. 이렇게 함으로 최적화 작업이 더 좋아질 것이다.

참고

머지 조인이 가능한 연산자에 사용할 함수는 반드시 immutable 이거나 stable 함수여야 한다. volatile 함수면 머지 조인 자체를 시도하지 않는다.