ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Real MySQL 8.0 - 9.3 고급 최적화 - MySQL의 조인 방식들
    ✏️ 스터디 모음집/RealMySQL 스터디 2023. 3. 26.

    테스트 데이터 생성

    DELIMITER $$
    DROP PROCEDURE IF EXISTS gen_data$$
    CREATE PROCEDURE gen_data(
    IN v_rows INT
    )
    BEGIN
        DECLARE i INT DEFAULT 1;
        
        create table tb_test1(
        no bigint,
        col1 varchar(100),
        col2 varchar(100),
        col3 varchar(100)
        );
        WHILE i <= v_rows DO     
            INSERT INTO tb_test1 (no , col1, col2 , col3 )
              VALUES(concat(i), concat('col1-',i), concat('col2-',i), concat('col3-',i));
            SET i = i + 1;
        END WHILE;
    END$$
    DELIMITER ;
    
    call gen_data(1000000);
    
    create table tb_test2
       as
       select * from tb_test1;
       
    CREATE INDEX idx_col1
    ON tb_test1 (col1);
    
    CREATE INDEX idx_col1
    ON tb_test2 (col1);

     

     

    1. Nested Loop Join

    조인의 연결 조건이 되는 컬럼에 모두 인덱스가 있는 경우 사용되는 조인 방식

    중첩 for 문 방식

     

    https://www.youtube.com/watch?v=pJWCwfv983Q&t=88s 

    explain select * 
    from tb_test1 a INNER JOIN tb_test2 b 
    ON a.col1=b.col1 -- 두 테이블 모두 col1 에는 인덱스 설정 된 상태
    where a.no > 1 and b.no > 2;

    Nested Loop Join의 경우 실행계획의 Extra 컬럼에 별도 표시되는 항목이 없다.

     

     

    😱 Nested Loop Join에서 주의 할 점

    어떤 테이블이 먼저 액세스 되느냐에 따라서 속도의 차이가 크게 날 수 있다. 그렇기 때문에 어느 테이블을 드라이빙 테이블로 설정 하는지, 드라이빙 테이블이 너무 많은 데이터를 읽는것은 아닌지 확인 하는 것이 중요하다.

     

    -- 테스트1 네스티드 루프조인, 드라이빙 조건에 걸리는 테이블 row 많은 경우 -- 38s
    explain select * 
    from tb_test1 a INNER JOIN tb_test2 b 
    ON a.col1=b.col1 
    where a.col1 >= 'col1-100' and b.col1 >= 'col1-100';
    
    -- 테스트2 네스티드 루프조인, 드라이빙 테이블 row 적은 경우 -- 14ms
    select * -- 35.6ms
    from tb_test1 a INNER JOIN tb_test2 b 
    ON a.col1=b.col1 
    where a.col1 >= 'col1-999900' and b.col1 >= 'col1-100';

    드라이빙 테이블의 검색 조건에 걸리는 데이터가 많은 경우(테스트1 약 999,900개) 적은 경우보다 처리에 속도가 오래 걸리는 것을 확인 할 수 있다.

     

     

    2. Block Nested Loop Join

    ⇒ 드리븐 테이블의 연결 조건에 인덱스를 사용할 수 없는 경우 사용되는 조인 방식

    드리븐 테이블 연결 조건에 인덱스를 사용할 수 없는 경우, Driving 테이블의 추출 건수만큼 Driven(Inner) 테이블 Full Scan 을 반복적으로 스캔을 해야하는 상황이 발생하는데 이러한 문제를 개선 하고자 내부적으로 내부적으로 메모리 버퍼를 활용하는 방식

    드라이빙 테이블 row 하나 하나 마다 드리븐 테이블을 순회 할 필요 없이

    드라이빙 테이블 row 여러개를 메모리에 저장해두고, 드리븐 테이블의 데이터를 한번 가지고 온 다음, 메모리에서 드라빙 테이블과 드리븐 테이블 데이터를 조인처리

    데이터 스캔 횟수가

    드라이빙 테이블 row 갯수 X 드리븐 테이블 row 갯수 당 조인 1번에서

    (드라이빙 테이블 row 갯수 X 드리븐 테이블 row 갯수) / 버퍼크기 당 1번으로 조인 횟수가 줄어들게 된다.

     

    실행 계획 확인

    explain select * 
    from tb_test1 a JOIN tb_test2 b
    on a.col2=b.col2 // 인덱스가 아닌 컬럼이 조인의 연결 조건으로 사용 되는 경우 작동
    where a.no < 10 and b.no > 10;

     

    MySQL 5.7 버전의 경우

     

    MySQL 8.0.20 버전 부터 더이상 사용되지 않고, 해시조인 알고리즘이 대체되어 사용된다.

     

    3. Hash Join

    MySQL 8.0.20 버전 부터 드리븐 테이블의 연결 조건에 인덱스를 사용할 수 없는 경우,

    Block Nested Loop Join대신 사용되는 조인 방식

     

    어느 테이블로 해시를 구성했는지 확인하기

    explain format=tree
    select * 
    from tb_test1 a JOIN tb_test2 b
    on a.col2=b.col2 // 인덱스가 아닌 컬럼이 조인의 연결 조건으로 사용 되는 경우 작동
    where a.no < 10 and b.no > 10;

    explain format=tree 로 실행계획을 분석할때, 제일 하단에 명시된 테이블이 해시로 구성된 테이블이다.

    이렇게 해시로 구성되는 테이블을 빌드테이블, 구성된 해시와 조인되는 테이블을 프로브 테이블이라고 한다.

    -> Filter: (b.col2 = a.col2)  (cost=124311.64 rows=37278)
        -> Inner hash join (<hash>(b.col2)=<hash>(a.col2))  (cost=124311.64 rows=37278)
            -> Filter: (b.`no` > 10)  (cost=3.43 rows=334)
                -> Table scan on b  (cost=3.43 rows=10034)
            -> Hash
                -> Filter: (a.`no` < 10)  (cost=1027.65 rows=3344)
                    -> Table scan on a  (cost=1027.65 rows=10034)

     

    메모리(조인버퍼)의 크기가 조인처리해야할 데이터보다 작을 경우, 위 그림 처럼 빌드 테이블과 프로브 테이블의 데이터를 여러 청크를 구성해 나누어 조인 작업을 진행한다.

     

    Nested Loop Join과 성능 비교

    위 그림에서 A 지점은 MySQL 서버가 첫번째 레코드를 찾아낸 시점, B는 마지막 레코드를 찾아낸 시점이다.

    해시조인의 경우 첫번째 지점을 찾기 까지는 오래걸리지만, 전체적인 실행시간이 Nested Loop Join 보다 적게 걸리는 것을 확인 할 수 있다.

     

    MySQL 서버는 조인 조건의 인덱스 컬럼이 없거나, 조인 대상 테이블 중 일부의 레코드 건수가 매우 적은 경우등에 대해서만 해시 조인 알고리즘을 사용하도록 설계 되어있다.

     

    일반적인 OLTP 환경의 경우 빠른 응답속도가 중요하기 때문에 해시조인이 더 빠르다고 옵티마이저 힌트를 사용해서 강제로 쿼리 실행계획을 해시 조인으로 유도하는 것은 좋지 않다.

     

    정리 

    조인 조건 컬럼에 인덱스 있다면 인덱스네스티드 루프 조인 → 인덱스 못쓰는 경우 “블록” 네스티드 루프조인 → 더 빠른 해시 조인으로 대체

     

    4. Batch Key Access JOIN(BKA JOIN)

    Multi Range Read 방식을 적용한 조인 방식

     

    MMR 방식이란?

    Multi Range Read(MRR)는 메모리 버퍼를 사용하여 Random I/O를 Sequential I/O로 처리할 수 있도록 도와주는 기능

    MMR의 경우 BKA 조인, Index range scan 에서 사용될 수 있다.

     

    1. Index range scan 에서 MMR이 사용되는 경우

     

    Non-Clustered Index에서 Range Scan을 사용하여 테이블의 행을 읽게 되는 경우 테이블에 대한 랜덤 디스크 액세스가 많이 발생할 수 있게 된다.

    Index range scan에서 MMR이 적용되지 않은 경우

     

    MMR기능이 켜진 경우 버퍼 내에서 한번 데이터의 정렬이 일어나기 때문에 랜덤 I/O를 줄일 수 있다.

    Index range scan에서 MMR이 적용된 경우

     

    Index range scan 에서 MMR 기능 사용해 보기

     

    [TEST1] MMR 사용, Non-Clustered Index에서 Range Scan

    SET profiling=1;
    
    -- MMR 사용, Non-Clustered Index에서 Range Scan
    -- 조회되는 건수에 따라서 MRR 동작 대신 Index Condition Pushdown 이 동작될 수 도 있는데 테스트를 위해 그 기능 off
    set optimizer_switch='index_condition_pushdown=off';
    set optimizer_switch='mrr_cost_based=off';
    
    -- MMR on 설정
    set optimizer_switch='mrr=on';
    
    select * 
    from tb_test1 force index(idx_col1)
    where col1 between 'col1-1' and 'col1-900000';
    
    SHOW profiles;

    MMR 옵션이 켜진 경우, Extra 컬럼에 정보가 표시 된다.

    → 17초 소요

     

    [TEST2] MRR 사용 X, Non-Clustered Index에서 Range Scan

    SET profiling=1;
    
    -- MMR 사용 x, Non-Clustered Index에서 Range Scan
    set optimizer_switch='index_condition_pushdown=on';
    set optimizer_switch='mrr_cost_based=on';
    set optimizer_switch='mrr=off';
    
    explain
    select * 
    from tb_test1 force index(idx_col1)
    where col1 between 'col1-1000' and 'col1-500000';
    
    SHOW profiles;

    → 16초 소요

    ?? 오히려 MMR 기능을 켰을때 더 시간이 걸렸는데, SSD라서 랜덤 읽기 I/O를 줄인다고 해도 별 성능 이점이 없었던 것일까?

     

    4. Batch Key Access JOIN(BKA JOIN)에서 사용되는 경우

     

    일반적인 Nested Loop 조인(NLJ) 의 경우 Driving Table(선행 테이블)에서 조회되는 값으로 조인되는 Driven Table(Inner 테이블,후행 테이블) 을 접근하게 되며 조인시 데이터 저장 순서대로 조회를 하는 것이 아니기 때문에 랜덤 액세스, 랜덤 I/O가 발생되게 된다.

     

     

    BKA 조인의 경우 메모리에서 드라이빙 테이블에서 읽어온 데이터를 부가적으로 정렬하기 때문에 랜덤 I/O를 줄일 수 있다.

    하지만 이 부가적인 정렬이 오버헤드로 작용 할 수 도 있기 때문에 무조건적으로 성능의 이점이 있는것은 아니다.

     

    출처

    https://hoing.io/archives/24491

     

    MySQL - Block Nested-Loop and Batched Key Access Joins

     

    hoing.io

     

    댓글

GitHub: https://github.com/Yesung-Han