조인 최적화

조인 최적화 (Optimizing Joins)

조인을 최적화하는 데 사용할 수 있는 팁과 트릭이에요.

참고: 조인이 어떻게 구현되는지 자세한 설명은 join operator 페이지를 읽어 보세요.

출처: 문서

본문

입력 관계의 순서가 중요해요

Apache Pinot는 조인 순서를 최적화하기 위해 테이블 통계에 의존하지 않아요. 대신 오른쪽에서 왼쪽으로 우선순위를 정해 입력 관계를 처리해요(SQL 쿼리의 테이블 순서 기반). 이 관계는 완전히 소비되어 인-메모리 해시 테이블을 만들며 모든 작업자에게 브로드캐스트될 수 있어요. 큰 테이블과 작은 테이블을 조인하는 것이 그 반대보다 비용이 적게 들기 때문에, 더 작은 관계를 오른쪽 입력으로 지정하는 것이 중요해요.

참고: 여기서 left는 explain 계획의 첫 번째 관계를, right는 두 번째를 의미해요. SQL에서 두 테이블을 조인할 때 왼쪽 관계는 먼저 지정하는 것이고 오른쪽은 두 번째예요. 하지만 세 개 이상의 테이블을 조인하면 더 복잡해져요. 어느 입력이 left이고 right인지 확실히 하기 위해 explain 계획을 사용하는 것을 강력히 권장해요.

예를 들어 이 쿼리는:

select largeTable.col1, smallTable.col2
from largeTable 
cross join smallTable

다음보다 더 효율적이에요:

select largeTable.col1, smallTable.col2
from smallTable 
cross join largeTable

조건 하향 푸시 (Predicate push-down)

보통 조인하기 전에 데이터를 필터링하는 것이 더 빨라요. Pinot는 변경이 의미론을 깨지 않음을 증명할 수 있을 때 조인하기 전에 조건을 각 개별 테이블로 자동으로 하향 푸시해요.

예를 들어 다음 쿼리를 고려해 보세요:

SELECT customer.c_address, orders.o_shippriority
FROM customer
JOIN orders
    ON customer.c_custkey = orders.o_custkey
WHERE customer.c_nationkey = 1

Pinot에 의해 자동으로 다음으로 변환돼요:

SELECT customer.c_address, orders.o_shippriority
FROM (customer WHERE c_nationkey = 1) as customer
JOIN orders
    ON customer.c_custkey = orders.o_custkey

이 최적화는 셔플되고 조인되어야 하는 데이터 양을 줄일 뿐만 아니라 인덱스를 사용해 쿼리를 빠르게 할 가능성도 열어요.

때로는 조건 하향 푸시가 불가능하다는 것을 기억하세요. 한 예는 입력 중 하나가 limit이 있는 서브쿼리일 때예요:

SELECT customer.c_address, orders.o_shippriority
FROM (select * from customer LIMIT 10) as customer
JOIN orders
    ON customer.c_custkey = orders.o
WHERE customer.c_nationkey = 1

이 경우 Pinot가 조건을 서브쿼리로 하향 푸시하지만, 원래 limit의 의미론을 깨뜨리기 때문에 조건을 서브쿼리의 테이블 스캔으로 하향 푸시할 수는 없어요.

따라서 최종 쿼리는 다음과 같아요:

SELECT customer.c_address, orders.o_shippriority
FROM (select * from 
        (select * from customer LIMIT 10) as temp where WHERE temp.c_nationkey = 1
     ) as customer
JOIN orders
    ON customer.c_custkey = orders.o

이 새 쿼리는 원래 쿼리와 동등하며 셔플되고 조인되어야 하는 데이터 양을 줄이지만 인덱스를 사용해 쿼리를 빠르게 하지는 못해요. limit 전에 필터를 적용하고 싶다면 쿼리를 다음과 같이 다시 작성할 수 있어요:

SELECT customer.c_address, orders.o_shippriority
FROM (select * from customer WHERE temp.c_nationkey = 1 LIMIT 10) as customer
JOIN orders
    ON customer.c_custkey = orders.o

이 최적화는 explain 계획에서 쉽게 볼 수 있어요. 필터 연산자가 조인의 한쪽으로 푸시된 것을 볼 수 있어요.

인덱스를 사용하도록 semi-join 최적화

Semi-joins는 조인의 특수한 경우예요. 조인의 결과가 조인 자체의 결과가 아니라 두 번째 테이블에서 일치하는 항목을 가진 첫 번째 테이블의 행이에요.

semi-join을 사용하는 쿼리는 보통 그렇게 작성되지 않고 WHERE 절에 서브쿼리가 있는 쿼리로 작성돼요. 예:

SELECT customer.c_address, customer.c_nationkey
FROM customer
WHERE EXISTS (SELECT 1 FROM orders WHERE customer.c_custkey = orders.o_custkey)

또는:

SELECT customer.c_address, customer.c_nationkey
FROM customer
WHERE c_custkey IN (SELECT o_custkey FROM orders)

인덱스를 사용하려면 Pinot가 최적화 시점에 서브쿼리의 실제 값을 알아야 해요. 그래서 Pinot가 내부적으로 하는 것은 먼저 서브쿼리를 실행한 다음 메인 쿼리에서 서브쿼리를 실제 값으로 대체하는 것이에요.

예를 들어 이전 예시의 서브쿼리가 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 값을 반환하면 쿼리는 다음으로 변환돼요:

SELECT customer.c_address, customer.c_nationkey
FROM customer
WHERE customer.c_custkey IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

그러면 인덱스를 사용해 최적화할 수 있어요.

현재 이 최적화는 Pinot explain 계획에서는 볼 수 없어요.

조인을 semi-join으로 재작성

때로는 내부 또는 왼쪽 조인을 semi-join으로 변환할 수 있어요. 구체적으로 요구사항은 다음과 같아요.

  1. 왼쪽의 컬럼만 프로젝션되어야 해요.
  2. 조인 조건이 등가(equality)여야 해요.
  3. 오른쪽이 유일해야 해요.

예를 들어 다음 두 쿼리는 동등해요:

SELECT customer.c_address, customer.c_nationkey
FROM customer
JOIN (SELECT DISTINCT o_custkey FROM orders) AS distinct_orders
on customer.c_custkey = distinct_orders.o_custkey
SELECT customer.c_address, customer.c_nationkey
FROM customer
WHERE c_custkey IN (SELECT o_custkey FROM orders)

하지만 distinct_orders 대신 orders를 사용한다면 동등하지 않아요. 이는 조인이 오른쪽에 반복이 있으면 왼쪽의 행을 반복하는 반면 semi-join은 행을 결코 반복하지 않기 때문이에요.

위에서 설명한 세 가지 조건이 충족되면 Pinot는 이 최적화를 자동으로 적용해요. Pinot에서 컬럼을 유일하다고 표시할 수 없기 때문에, 오른쪽이 유일함을 Pinot에 나타내는 유일한 방법은 조인 조건에 사용된 컬럼에 DISTINCT나 GROUP BY 같은 것을 보장하는 SQL 표현식을 적용하는 것이에요. 때로는 원래 SQL의 JOIN을 WHERE EXISTS로 대체하도록 다시 작성하는 것이 더 쉬울 수 있어요.

데이터 셔플 줄이기

Pinot는 다양한 유형의 join 전략을 지원해요. 그것들을 이해하고 가능하면 사용하려고 노력하는 것이 중요해요. 이 데이터 셔플은 비용이 크고 쿼리 성능의 병목이 될 수 있어요. stageStats(특히 mailbox send와 mailbox receive)와 다양한 explain plan 모드를 사용해 데이터가 어떻게 셔플되고 있는지 이해하는 것을 기억하세요.

더 알아보기 (Learn more)