Skip to content

Latest commit

 

History

History
1213 lines (708 loc) · 50 KB

File metadata and controls

1213 lines (708 loc) · 50 KB

9장 옵티마이저와 힌트

여행을 할 때 계획을 세워서 비용과 이동거리를 최소화하여 경로를 결정하듯이, MySQL에서도 쿼리를 최적으로 실행하기 위해서 통계 정보를 참조하여 실행 계획을 수립하는 작업이 필요하다. 많은 DBMS에서 옵티마이저가 그 기능을 담당한다.

9.1 개요

옵티마이저는 어렵다. 옵티마이저가 만드는 실행 계획 또한 어렵다. 하지만 이해해야 최적화된 쿼리 작성이 가능하다.

References:

9.1.1 쿼리 실행 절차

MySQL 서버에서 쿼리가 실행되는 과정

  1. SQL Parsing

요청된 SQL 문장을 MySQL 서버가 이해할 수 있는 수준으로 분리(Parse Tree)

  • MySQL 서버의 SQL Parser가 SQL 문법을 검증한다.
  • SQL Parse Tree가 생성된다. (MySQL 서버는 이를 기준으로 쿼리를 실행)
  1. 최적화 및 계획수립

SQL의 파싱 정보(Parse Tree)를 확인하면서 테이블, 인덱스를 읽는 순서를 선택

  • 불필요한 조건 제거 및 연산의 단순화
  • 여러 테이블의 조인에서 테이블 읽는 순서 결정
  • 각 테이블에 사용된 조건과 인덱스 통계 정보를 이용해 사용할 인덱스 결정
  • 가져온 레코드들을 임시 테이블에 넣고 다시 한 번 가공할지 결정
  1. 수립된 계획대로 레코드를 읽음

결정된 테이블의 읽기 순서나 선택된 인덱스를 이용해 스토리지 엔진으로부터 데이터를 가져옴 MySQL 엔진과 스토리지 엔진이 동시에 참여해서 처리

9.1.2 옵티마이저의 종류

옵티마이저는 DB 서버에서 두뇌 역할

비용 기반 최적화(Cost-Based Optimizer, CBO)

  • 쿼리를 처리하기 위한 여러 가지 가능한 방법을 만들고
  • 각 단위 작업의 비용(부하) + 대상 테이블의 예측된 통계 정보 → 실행 계획별 비용 산출
  • 산출된 실행 방법별 비용이 최소가 되는 쿼리 선택

규칙 기반 최적화(Rule-Based Optimizer, RBO)

  • 대상 테이블 레코드 건수나 선택도 등 통계 정보 고려 x
  • 옵티마이저에 내장도니 우선순위에 따라 고정된 실행 계획 수립
  • 사용자 데이터 분포도는 매우 다양하므로 거의 사용 x

9.2 기본 데이터 처리

RDBMS는 데이터를 정렬하거나 그루핑하는 기본 데이터 가공 기능을 가진다. 하지만 그 과정은 모두 다르다.

9.2.1 풀 테이블 스캔과 풀 인덱스 스캔

풀 테이블 스캔

인덱스 없이 전체를 읽는다.

풀 테이블 스캔의 조건
  • 테이블 레코드 건수가 너무 작은 경우
  • WHERE 절이나 ON 절에 인덱스를 이용할 조건이 없는 경우
  • 두 조건을 만족해도 옵티마이저가 판단한 조건 일치 레코드 건수가 너무 많은 경우
디스크 읽기 단위
  • MyISAM: 페이지를 하나씩 읽어옴
  • InnoDB: 특정 테이블의 연속된 데이터 페이지가 읽히면 Read Ahead가 수행
Read Ahead

앞으로 읽을 데이터를 예측해서 미리 가져오는 작업 (지역성을 고려) innodb_read_ahead_threashold 시스템 변수를 이용해 InnoDB 스토리지 엔진이 언제 Read Ahead를 시작할지 임계값을 설정할 수 있다.

SELECT COUNT(*) FROM employees;를 실행하면 풀 인덱스 스캔 SELECT * FROM employees;를 실행하면 레코드에만 있는 컬럼이 필요하므로 플 테이블 스캔

References:

Pyro: 가우시안 분포를 이용해서 지역성을 고려하는데 근처에 있는게 얼마나 히트될것인지 계산. read ahead가 커질수록 좋아지는게 로그함수를 따르는데 일정이상 커지면 별차이가 없어진다.

9.2.2 병렬 처리

MySQL 8.0부터 하나의 쿼리를 여러 스레드가 동시에 처리하는게 가능해짐

innodb_parallel_read_threads 시스템 변수를 이용해 하나의 쿼리를 최대 몇 개의 스레드로 처리할지 결정.

9.2.3 ORDER BY 처리(Using filesort)

정렬을 처리하는 방법

장점 단점
인덱스 이용 INSERT, UPDATE, DELETE 시 이미 인덱스가 정렬되어 매우 빠름 INSERT, UPDATE, DELETE 작업 시 인덱스 추가/삭제 작업 필요, 디스크 공간, 메모리 많이 필요
Filesort 이용 인덱스 부가 작업과 공간이 필요 x, 정렬해야할 레코드 적으면 메모리에서 Filesort되어 빠름 정렬 작업이 쿼리 실행 시 처리되어 레코드가 많아지면 응답 느림

Pyro: CUD PK 주소가 필요하다. 그래서 인덱스가 걸려있으면 빠르다. PK인덱스 기준이 없다면 다른 기준으로 풀 스캔해서 찾아서 지우든지 해야해서 느릴 수 있다.

정렬에 인덱스를 이용하기 어려운 경우

  • 정렬 기준이 너무 많아서 요건별로 모두 인덱스를 생성하는 것이 불가능한 경우
  • GROUP BY의 결과 or DISTINCT 처리 결과를 정렬해야하는 경우
  • UNION 결과와 같이 임시 테이블의 결과를 다시 정렬해야 하는 경우
  • 랜덤하게 결과 레코드를 가져와야 하는 경우

9.2.3.1 소트 버퍼

  • MySQL은 정렬을 수행하기 위해 별도의 메모리 공간을 할당받아서 사용 -> 소트 버퍼
  • 정렬이 필요한 경우에만 할당
  • 버퍼의 크기는 정렬해야 할 레코드 크기에 따라 가변적으로 증가
  • 최대 가용 소트 버퍼 크기는 sort_buffer_size로 설정 가능
  • 소트 버퍼용 메모리 공간은 쿼리 실행 완료 즉시 시스템으로 반납
정렬할 레코드 건수가 소트 버퍼 공간보다 큰 경우

메모리의 소트 버퍼에서 정렬을 수행하고, 그 결과를 임시로 디스크에 기록 그리고 다음 레코드를 가져와서 다시 정렬, 반복적으로 디스크에 임시 저장 각 버퍼 크기만큼 정렬된 레코드를 다시 병합하면서 정렬 수행

소트 버퍼 크기에 따른 성능

sort_buffer_size가 크다고 무조건 성능이 좋은게 아니다. 소트 버퍼 크기가 크면 디스크 I/O를 줄일 수 있지만, 서버의 메모리가 부족해질 수 있다. 따라서 적절한 임계값(56KB ~ 1MB가 적절)을 지정하는게 좋다.

Pyro: 정렬하기 위해 데이터를 메모리에 올려놓는데, 정렬 대상 데이터의 크기가 다를 수 있다. auto_incremented는 고정값이지만, varchar라면 메모리에 올릴 수 있는 양이 다를 수 있어서 들쑥날쑥 할수도 있겠다.

9.2.3.2 정렬 알고리즘

레코드를 정렬할 때 레코드 전체를 소트 버퍼에 담을지 or 정렬 기준 컬럼만 담을지에 따라 2가지 정렬 모드로 나뉜다.

9.2.3.2.1 싱글 패스 정렬 방식

소트 버퍼에 정렬 기준 컬럼을 포함해 SELECT 대상이 되는 컬럼 전부를 담아서 정렬을 수행하는 정렬 방식

SELECT emp_no, first_name, last_name
FROM employees
ORDER BY first_name;

emp_no, first_name, last_name 컬럼 조회 → 소트 버퍼에 담고 정렬 → 정렬 버퍼의 내용을 클라이언트로 반환 (멀티 머지가 필요하면 중간과정에서 수행)

9.2.3.2.2 투 패스 정렬 방식

정렬 대상 컬럼과 PK값만 소트 버퍼에 담아서 정렬을 수행하고, 정렬된 순서대로 다시 PK로 테이블을 읽어서 SELECT할 컬럼을 가져오는 정렬 방식

emp_no, first_name 컬럼만 조회 → 소트 버퍼에 담고 정렬 → 정렬 버퍼의 내용을 기반으로 employees 테이블을 한 번 더 읽어서 last_name을 가져오고 클라이언트로 반환 (멀티 머지가 필요하면 중간과정에서 수행)

투 패스 정렬 방식을 사용하는 경우

  • 레코드의 크기가 max_length_for_sort_data값 보다 큰 경우
  • BLOB나 TEXT 타입의 컬럼이 SELECT 대상에 포함되는 경우

SELECT * FROM employees가 아니라 SELECT emp_no, first_name, last_name FROM employees를 사용해야 하는 이유가 바로 이것이다. 정렬이 필요한 SELECT는 불필요한 컬럼을 SELECT할 경우 투 패스를 사용할 가능성이 커진다. 그리고 임시 테이블이 필요한 쿼리에서도 영향을 미친다.

Woody: 엔티티를 들고올때 와 SELECT * 와 동일한게 아닌가? 하나의 테이블에 대해서 엔티티가 여러개 사용될수도 있을것 같다.

Pyro: 하나의 테이블에 엔티티를 따로 만들자. @Column, @Table(name = "")

9.2.3.3 정렬 처리 방법

정렬 처리 방법 실행 계획의 Extra 컬럼 내용
인덱스를 사용한 정렬 별도 표기 없음
조인에서 드라이빙 테이블만 정렬 "Using filesort" 메시지가 표시
조인에서 조인 결과를 임시 테이블로 저장 후 정렬 "Using temporary; Using filesort"메시지가 표시

옵티마이저의 정렬 처리

인덱스 이용 여부 검토
→ 이용 가능하다면 별도의 "Filesort"없이 인덱스를 순서대로 읽어서 결과 반환 → 이용 불가능하다면 별도의 WHERE 조건에 일치하는 레코드를 검색해 정렬 버퍼에 저장하면서 정렬을 처리(Filesort)

이때 정렬 대상 레코드를 최소화하기 위해 2가지 방법 중 선택

  • 조인의 드라이빙 테이블만 정렬한 다음 조인을 수행
  • 조인이 끝나고 일치하는 레코드를 모두 가져온 후 정렬을 수행

일반적으로 조인이 수행되면서 레코드 건수와 레코드 크기는 매우 커지므로 가능한한 드라이빙 테이블만 정렬한 다음 조인을 수행하는게 효율적이다.

9.2.3.3.1 인덱스를 이용한 정렬

인덱스를 이용한 정렬의 조건

  • ORDER BY에 명시된 컬럼이 제일 먼저 읽는 테이블에 속한다.
  • ORDER BY의 순서대로 생성된 인덱스가 있어야한다.
  • WHERE절에 첫 번째로 읽는 테이블의 컬럼에 대한 조건이 있다면 그 조건과 ORDER BY는 같은 인덱스를 사용할 수 있어야 한다.
  • 여러 테이블이 조인되는 경우에는 Nested-loop 조인에서만 가능하다.
SELECT *
FROM employees e, salaries s
WHERE s.emp_no = e.emp_no
AND e.emp_no BETWEEN 100002 AND 100020
ORDER BY e.emp_no; -- 옵티마이저가 알아서 하니까 일부러 지우진 말자.

employees 테이블의 PK를 읽고, salaries 테이블을 조인했기 때문에 employees의 PK인 emp_no로 정렬이 유지된다.

References:

9.2.3.3.2 조인의 드라이빙 테이블만 정렬

조인이 수행되면 레코드가 커지므로 조인을 실행하기 전에 테이블을 정렬하고 다음 조인을 실행하는 것이 차선책이다. 이 방법으로 정렬하려면 첫 번째 테이블(드라이빙 테이블)의 컬럼만으로 ORDER BY 절을 작성해야 한다.

SELECT *
FROM employees e, salaries s
WHERE s.emp_no = e.emp_no -- emp_no 컬럼에 인덱스가 있다.
AND e.emp_no BETWEEN 100002 AND 100010 -- employees 테이블의 PK를 이용해 작업량을 줄일 수 있다.
ORDER BY e.last_name;

두 조건으로 옵티마이저는 employees를 드라이빙 테이블로 선택한다.

  • WHERE 절의 검색 조건은 employees 테이블의 PK를 이용해 작업량을 줄일 수 있다.
  • 드리븐 테이블(salaries)의 조인 컬럼인 emp_no 컬럼에 인덱스가 있다.

검색은 인덱스 레인지 스캔으로 처리할 수 있지만 ORDER BY에 명시된 컬럼은 employees 테이블의 PK와 연관이 없으므로 인덱스를 이용한 정렬은 불가능하다. 그런데 ORDER BY 절의 정렬 기준 컬럼이 드라이빙 테이블에 포함된 컬럼이다. 옵티마이저는 드라이빙 테이블만 검색해서 정렬을 먼저 수행하고, 그 결과와 salaries 테이블을 조인한 것이다.

  1. 인덱스를 이용해 조건을 만족하는 레코드 검색
  2. 검색 결과를 last_name 컬럼으로 정렬 수행(Filesort)
  3. 정렬된 결과를 순서대로 읽으면서 salaries 테이블과 조인을 수행해 최종 결과를 가져옴
9.2.3.3.3 임시 테이블을 이용한 정렬

쿼리가 여러 개가 아닌 하나의 테이블로부터 SELECT해서 정렬하는겨우라면 임시 테이블이 필요하지 않을 수 있다. 하지만 2개 이상을 조인해서 정렬해야한다면 임시테이블이 필요할 수 있다.

앞의 경우 외에는 항상 조인의 결과를 임시 테이블에 저장하고, 그 결과를 다시 정렬하는 과정을 거친다. 이 방법은 정렬할 레코드가 가장 많아서 가장 느리다.

SELECT *
FROM employees e, salaries s
WHERE s.emp_no = e.emp_no
AND e.emp_no BETWEEN 100002 AND 100010
ORDER BY s.salary; -- 정렬 기준 컬럼이 드리븐 테이블에 있다.

정렬이 수행되기 전에 salaries 테이블을 읽어야 하므로, 이 쿼리는 조인된 데이터를 가지고 정렬할 수 밖에 없다.

August: 드라이빙 테이블이 무엇인가? FROM 절 앞에있는 테이블? ORDER BY 기준이 되는 컬럼을 가지고 있는 테이블? 드라이빙 테이블 선정 기준 조인 시 먼저 엑세스 되어 읽어지는 테이블, 하지만 어떻게 선택되는지는 좀 더 조사해보자..

Pyro: 드라이빙 테이블 선택에 따라 성능의 차이가 있느냐? -> 있다.

Woody: DB에서 소팅 vs App에서 소팅 인덱스를 탈 수 있는 경우는 DB에서 소팅 인덱스를 탈 수 없는 경우는 복잡하고 조인도 많이 들어가고 쿼리가 너무 길어지면 앱에서 해도 나쁘지 않을것 같다. 그 기준은 서버 스펙에 맞춰서 메모리 용량을 따져봐야할듯

Pyro: 앱에서 정렬은 최대한 피하는 것 같다. 엔드포인트 커넥션 부하에 집중하고 정렬 등 앱의 핵심로직에 집중한다. 심지어 FE에서 정렬을 담당하기도 한다. 조인을 앱에서 하는 경우도 많다. 몽고 DB로 넘어가는 현상이 자꾸 발생

Woody: 앱에서 조인한다는게 각자 쿼리 날려서 가져온다음 앱에서 합친다? -> 네 n+1이 발생하지 않는가? -> 잘 하면..

Pyro: 드리븐 테이블에 FK가 있을텐데 그걸로 다시 쿼리 날리기 조인 조건과 드리븐테이블 조건이 있는데, 드리븐 테이블에서 where 조건 기준이 있다. 드리븐 테이블에 100개를 가져온 다음 in절로 두번째 테이블을 가져온다. 사실상 순수한 조인은 앱 상에서는 불가능할지도..

Dong: 모든 소팅을 서버에서 시키는데 이유는 DB확장이 어렵다. 샤딩이 어렵다. 이미 돌고있는 DB서버도 최고스펙이지만 select * 로 뻗는다. 서버에서 하면 확장은 쉽다. VM올리고 하면 확장이 용이 but DB는 확장이 어렵다. VM서버들도 VM 자체 성능이 올라가면 스펙이 정렬, 조인 성능에 그대로 반영된다. AWS는 물리 머신대비 90% 성능인데, IDC사용시 70% 성능으로 되기도 해서 앱에서 다 담당하고 있다.

9.2.3.3.4 정렬 처리 방법의 성능 비교

웹 서비스용 쿼리에서는 ORDER BY와 함께 LIMIT가 사용된다. (페이지네이션, 페이징) ORDER BY나 GROUP BY는 WHERE 조건을 만족하는 레코드를 LIMIT 만큼만 가져와서는 처리할 수 없다. 우선 조건을 만족하는 레코드를 모두 가져와서 정렬을 수행하거나 그루핑 작업을 해야만 LIMIT로 제한할 수 있다. WHERE 조건이 아무리 인덱스를 잘 활용하도록 튜닝해도 ORDER BY, GROUP BY로 느려질 수 있다.

스트리밍 방식

서버 쪽에서 처리할 데이터가 얼마인지에 관계없이 조건에 일치하는 레코드가 검색될 때마다 바로바로 클라이언트로 전송해주는 방식 클라이언트는 쿼리를 요청하고 바로 첫 번째 레코드를 전달받는다. 쿼리가 조회하는 레코드 양과 상관없이 빠른 응답시간을 보장해준다. LIMIT처럼 결과 건수를 제한하는 조건은 쿼리 실행시간을 줄여준다.

버퍼링 방식

ORDER BY나 GROUP BY 같은 처리는 쿼리의 결과가 스트리밍되는 것을 불가능하게 한다. MySQL 서버에서는 모든 레코드를 검색하고 정렬 작업을 하는 동안 클라이언트는 아무것도 하지않고 기다려야 하기 때문에 응답 속도가 느려진다. LIMIT처럼 결과 건수를 제한하는 조건이 있어도 성능에는 별로 도움이 되지 않는다.

MySQL이 스트리밍으로 보내줘봤자 JDBC가 버퍼링으로 처리해서 결국엔 클라이언트는 느린 응답을 받는다. 하지만 SQL 클라이언트 도구는 스트리밍 방식으로 보여주기 떄문에 비교적 빠르다. DataGrip이나 WorkBench가 월등히 빠르다.

SELECT *
FROM tb_test1 t1, tb_test t2
WHERE t1.col1 = t2.col1
ORDER BY t1.col2
LIMIT 10;

tb_test1: 100 records tb_test2: 1000 records

어느 테이블이 먼저 드라이빙되어 조인되는지도 중요하지만, 어떤 정렬 방식으로 처리되는지가 더 중요 최소한 드라이빙 테이블만이라도 정렬해도 되는 수준으로 튜닝

인덱스를 사용하지 못하고 별도로 Filesort 작업을 거쳐야 하는 쿼리에서 LIMIT가 도움이 될수도 있다. 예를 들어, LIMIT 10인데 10개가 나왔다면 정렬을 멈추고 반환할수도 있다. 하지만 정렬 알고리즘에 따라 별 차이가 없다.

9.2.3.4 정렬 관련 상태 변수

상태변수 설명
Sort_merge_passes 멀티 머지 처리 횟수
Sort_range 인덱스 레인지 스캔을 통해 검색된 결과에 대한 정렬 작업 횟수
Sort_scan 풀 테이블 스캔을 통해 검색된 결과에 대한 정렬 작업 횟수
Sort_rows 지금까지 정렬한 전체 레코드 건수

9.2.4 GROUP BY 처리

GROUP BY도 스트리밍 처리를 할 수 없게 하는 처리 HAVING은 GROUP BY 결과에 대해 필터링 GROUP BY에 사용된 조건은 인덱스를 사용해서 처리될 수 없으므로 HAVING 절을 튜닝하려고 애쓸 필요 x

9.2.4.1 인덱스 스캔을 이용하는 GROUP BY(타이트 인덱스 스캔)

조인의 드라이빙 테이블에 속한 컬럼만 이용해 그루핑할 때 GROUP BY 컬럼으로 이미 인덱스가 있다면 그 인덱스를 차례대로 읽으면서 그루핑 작업을 수행하고 그 결과로 조인을 처리한다.

9.2.4.2 루스 인덱스 스캔을 이용하는 GROUP BY

루스(Loose) 인덱스 스캔 방식은 인덱스의 레코드를 건너뛰면서 필요한 부분만 읽어서 가져오는 것 옵티마이저가 루스 인덱스 스캔을 사용할 때는 실행 계획의 Extra 컬럼에 "Using index for group-by" 표시

EXPLAIN
    SELECT emp_no
    FROM salaries
    WHERE from_date = '1985-03-01'
    GROUP BY emp_no;

August: 쿼리 실제로 이것저것 해보면서 추가하면 이해가 될듯

9.2.4.3 임시 테이블을 사용하는 GROUP BY

GROUP BY의 기준 컬럼이 드라이빙 테이블에 있든 드리븐 테이블에 있든 관계없이 인덱스를 전혀 사용하지 못할 때는 이 방식으로 처리된다.

EXPLAIN
    SELECT e.last_name, AVG(s.salary)
    FROM employees e, salaries s
    WHERE s.emp_no = e.emp_no
    GROUP BY e.last_name;
    -- ORDER BY e.last_name;

"Using filesort"가 아니라 "Using temporary"만 표시 GROUP BY에는 있지만 ORDER BY가 없는 쿼리에 대해서 기본적으로 그루핑 컬럼(last_name)으로 정렬된 결과를 반환했지만, 8.0 부터는 묵시적인 정렬 x

9.2.5 DISTINCT 처리

DISTINCT는 집계함수의 사용여부에 따라 처리가 달라진다.

9.2.5.1 SELECT DISTINCT ...

GROUP BY와 동일한 방식으로 처리 8.0 부터는 두 쿼리가 동일

SELECT DISTINCT emp_no FROM salaries;
SELECT emp_no FROM salaries GROUP BY emp_no;

DISTINCT는 특정 컬럼만 유니크하게 조회하는 것이 아니라, 조합 전체(row)가 유니크한 레코드를 가져온다. SELECT절에 사용된 DISTINCT 키워드는 조회되는 모든 컬럼에 영향을 미친다.

SELECT DISTINCT first_name, last_name FROM employees;
SELECT DISTINCT(first_name), last_name FROM employees; -- 결과는 동일

August: GROUP BY 하면 최상단의 레코드로 채워지는가? -> 아마도 QueryDSL에서 서브쿼리에서 SELECT하는데 LIMIT 1 이 무시된다. SELECT (SELECT ~ LIMIT 1)

9.2.5.2 집합 함수와 함께 사용된 DISTINCT

집합 함수 내에서 DISTINCT는 그 집합 함수의 인자로 전달된 컬럼값이 유니크한 것들을 가져온다.

EXPLAIN
    SELECT COUNT(DISTINCT s.salary)
    FROM employees e, salaries s
    WHERE e.emp_no = s.emp_no
    AND e.emp_no BETWEEN 100001 AND 100100;

내부적으로 임시 테이블을 사용한다.

    SELECT COUNT(DISTINCT s.salary),
           COUNT(DISTINCT e.last_name)
    FROM employees e, salaries s
    WHERE e.emp_no = s.emp_no
    AND e.emp_no BETWEEN 100001 AND 100100;

내부적으로 임시 테이블 2개를 사용한다.

SELECT COUNT(DISTINCT emp_no) FROM employees;
SELECT COUNT(DISTINCT emp_no) FROM dept_emp GROUP BY dept_no;

인덱스된 컬럼에 DISTINCT 처리를 수행할 때는 인덱스를 풀 스캔하거나 레인지 스캔, 임시 테이블 없이 최적화 가능

9.2.6 내부 임시 테이블 활용

MySQL 엔진이 스토리지 엔진으로부터 받아온 레코드를 정렬하거나 그루핑할 때는 내부적인 임시 테이블을 사용한다.

  • "CREATE TEMPORARY TABLE" 명령으로 만든 임시 테이블과 다르다.
  • 다른 세션이나 다른 쿼리에서 볼 수 x, 사용 x
  • 쿼리의 처리가 완료되면 자동 삭제

9.2.6.1 메모리 임시 테이블과 디스크 임시 테이블

MySQL 8.0 부터는 internal_tmp_mem_storage_engine 시스템 변수로 메모리용 임시 테이블을 MEMORY와 TempTable(기본값) 중에 선택할 수 있게 한다.

9.2.6.2 임시 테이블이 필요한 쿼리

  • ORDER BY와 GROUP BY에 명시된 컬럼이 다른 쿼리
  • ORDER BY나 GROUP BY에 명시된 컬럼이 조인의 순서상 첫 번째 테이블이 아닌 쿼리
  • DISTINCT와 ORDER BY가 동시에 쿼리에 존재하는 경우 or DISTINCT가 인덱스로 처리되지 못하는 쿼리
  • UNION이나 UNION DISTINCT가 사용된 쿼리 (select_type 컬럼이 UNION RESULT인 경우)
  • 쿼리의 실행 계획에서 select_type이 DERIVED인 쿼리

9.2.6.3 임시 테이블이 디스크에 생성되는 경우

  • UNION이나 UNION ALL에서 SELECT되는 컬럼 중에서 길이가 512Bytes 이상인 크기의 컬럼이 있는 경우
  • GROUP BY나 DISTINCT 컬럼에서 512Bytes 이상인 크기의 컬럼이 있는 경우
  • 메모리 임시 테이블의 크기가 tmp_table_size 또는 max_heap_table_size 보다 크거나 temptable_max_ram 보다 큰 경우

9.2.6.4 임시 테이블 관련 상태 변수

상태변수 설명
Created_tmp_tables 쿼리의 처리를 위해 만들어진 내부 임시 테이블의 개수를 누적하는 상태값
Created_tmp_disk_tables 디스크에 내부 임시 테이블이 만들어진 개수만 누적해서 가지고 있는 상태값

9.3 고급 최적화

9.3.1 옵티마이저 스위치 옵션

MySQL 서버의 옵티마이저가 실행 계획을 수립할 때 통계 정보와 옵티마이저 옵션을 결합해서 최적의 실행 계획을 수립하게 된다.

옵티마이저 옵션 구분

  1. 조인과 관련된 옵티마이저 옵션

  2. 옵티마이저 스위치

    • MySQL 서버의 고급 최적화 기능들을 활성화할지를 제어하는 용도
    • optimizer_switch 시스템 변수를 이용해서 제어

9.3.1.1 MRR과 배치 키 액세스(mrr & batched_key_access)

Nested Loop Join
  • MySQL 서버에서 사용되는 대부분의 조인

  • 프로그래밍 언어에서 마치 중첩된 반복 명령을 사용하는 것처럼 작동한다고 해서 네스티드 루프 조인이라고 함.

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

  • 드라이빙 테이블의 레코드를 한 건 읽어서 드리븐 테이블의 일치하는 레코드를 찾아서 조인을 수행

    • 드라이빙 테이블: 조인에서 제일 먼저 읽는 테이블
    • 드리븐 테이블: 조인되는 테이블에서 드라이빙 테이블이 아닌 테이블들
  • 단점

    • 드라이빙 테이블의 레코드 건별로 드리븐 테이블의 레코드를 찾으면, 레코드를 찾고 읽는 스토리지 엔진에서는 아무런 최적화를 할 수 없음.
    • 조인 칼럼 인덱스가 없는 경우, 드라이빙 테이블에서 일치하는 레코드의 건수만큼 드리븐 테이블을 풀 테이블 스캔하면서 처리
      • ORACLE과 PostgreSQL과 같은 DBMS는 해시 조인 또는 머지 조인을 사용
MRR (Multi-Range-Read)
  • 조인 버퍼에 레코드가 가득 차면 MySQL 엔진이 버퍼링된 레코드를 스토리지 엔진으로 한 번에 요청해서 읽는 방식

    • 드라이빙 테이블의 레코드를 읽어서 드리븐 테이블과의 조인을 즉시 실행하지 않고 조인 대상을 조인 버퍼에 버퍼링
  • 이렇게 함으로써 스토리지 엔진은 읽어야 할 레코드들을 데이터 페이지에 정렬된 순서로 접근해서 디스크의 데이터 페이지 읽기를 최소화 할 수 있음.

BKA (Batched Key Access)
(1) 먼저 읽혀지는 선행 테이블(Driving Table)에서 조회되는 값들을 먼저 조인 버퍼(join_buffer_size)에 채운다.
(2) 조인 버퍼에 채워진 내용을 기준으로, 조인되는 테이블(Joined Table)의 rowid(primary key) 값을 구한다. (인덱스 Lookup)
(3) 빌드된 rowid(primary key) 값으로 조인되는 테이블(Joined Table)을 대상으로 Multi Range Read(MRR) 연산을 수행한다.
(4) 결과 값을 조인 버퍼(join_buffer_size)에 채워진 선행 테이블(Driving Table) 값과 연결한다.
https://blog.naver.com/seuis398/70159183472

정렬 -> i/o 묶어서 실행

9.3.1.2 블록 네스티드 루프 조인(block_nested_loop)

  • 드리븐 테이블을 반복적으로 풀스캔하는 Nested Loop Join의 성능을 보완하기 위한 차선책

  • 드라이빙 테이블에서 읽은 레코드를 조인 버퍼 캐시한 후 드리븐 테이블과 이 조인 버퍼를 조인하여 스캔 횟수를 줄일 수 있음.

  • 네스티드 루프 조인과 블록 네스티드 루프 조인의 가장 큰 차이

    • 조인 버퍼 사용
    • 드라이빙 테이블과 드리븐 테이블의 조인 순서
      • 조인 버퍼가 사용되는 조인에서는 드리븐 테이블을 먼저 읽고 조인 버퍼에서 일치하는 레코드를 찾아 결과의 정렬 순서가 흐트러질 수 있음.

데이터 페이지(디스크)를 덜 읽기위해 조인버퍼(메모리)를 사용

9.3.1.3 인덱스 컨디션 푸시다운(index_condition_pushdown)

  • MhSQL 5.5 버전까지는 인덱스를 범위 제한 조건으로 사용하지 못하는 first_name 조건은 MySQL 엔진이 스토리지 엔진으로 전달해주지 않아 불필요한 테이블 읽기가 추가로 수행됨.

  • MySQL 5.6 버전부터는 인덱스를 범위 제한 조건으로 사용하지 못한다고 하더라도 인덱스에 포함된 칼럼의 조건이 있다면 모두 같이 모아서 스토리지 엔진으로 전달할 수 있게 핸들러 API가 개선됨.

인덱스 레코드로만 판별해서 레코드를 읽는 방법 데이터 레코드까지 가느냐 안 가느냐가 핵심!

9.3.1.4 인덱스 확장(use_index_extentions)

  • InnoDB 스토리지 엔진을 사용하는 테이블에서 세컨더리 인덱스에 자동으로 추가된 프라이머리 키를 활용할 수 있게 할지를 결정하는 옵션

use_index_extentions on => pk 인덱스 활용

9.3.1.5 인덱스 머지(index_merge)

  • 하나의 테이블에 대해 2개 이상의 인덱스를 이용해 쿼리를 처리하는 옵션
    • 쿼리에 사용된 각각의 조건이 서로 다른 인덱스를 사용할 수 있고, 그 조건을 만족하는 레코드 건수가 많을 것으로 예상될 때 MySQL 서버는 인덱스 머지 실행 계획을 선택
  • 어떤 방식으로 병합할지에 따라 다음과 같이 3개의 세부 실행 계획으로 나누어 볼 수 있음.
    • index_merge_intersection
    • index_merge_sort_union
    • index_merge_union

9.3.1.6 인덱스 머지 - 교집합(index_merge_intersection)

  • 여러 개의 인덱스를 각각 검색해서 그 결과의 교집합만 반환하는 실행 계획

AND 의 경우 하나라도 조건이 걸리면 index range scan이 가능할 수 있다.

9.3.1.7 인덱스 머지 - 합집합(index_merge_union)

  • 2개 이상의 조건이 각각의 인덱스를 사용하되 OR 연산자로 연결된 경우에 사용되는 최적화
  • 각각의 인덱스 검색 결과 집합의 중복을 제거하기 위해 정렬된 결과를 필요로 함.
    • MySQL 서버는 별도의 정렬 작업 없이 프라이머리 키 칼럼으로 정렬된 집합의 결과를 하나씩 가져와 중복 제거를 수행할 수 있음
      • 이때 사용된 알고리즘을 우선순위 큐라고 함.(?!)

머지소트 하면서 중복 제거?!

OR 의 경우 조건 중 하나라도 index가 없으면 풀테이블 스캔으로 처리될 가능성이 높다.

9.3.1.8 인덱스 머지 - 정렬 후 합집합(index_merge_sort_union)

  • 인덱스 머지 작업을 하는 도중에 결과의 정렬이 필요한 경우, 인덱스 머지 최적화의 Sort union 알고리즘 사용

9.3.1.9 세미 조인(semijoin)

SELECT *
FROM employees e
WHERE e.emp_no IN (SELECT de.emp_no FROM dept_emp de WHERE de.from_date = '1995-01-01');
  • 다른 테이블과 실제 조인을 수행하지는 않고, 단지 다른 테이블에서 조건에 일치하는 레코드가 있는지 없는지만 체크하는 형태의 쿼리

  • 세미 조인 최적화 기능이 없었을 때는 테이블을 풀 스캔하면서 한 건 한 건 서브쿼리 조건에 일치하는지 비교했음.

  • 세미 조인 쿼리 최적화

    1. 세미 조인 형태의 쿼리 = (subquery), IN (subquery)

      • 세미 조인 최적화

        • Table Pull-out / Duplicate Weed-out / First Match / Loose Scan / Materialization
      • IN-to-EXISTS 최적화

      • MATERIALIZATION 최적화

    2. 안티 세미 조인 형태의 쿼리 <> (subquery), NOT IN (subquery)

      • IN-to-EXISTS 최적화

      • MATERIALIZATION 최적화

9.3.1.10 테이블 풀-아웃(Table Pull-out)

  • 사용 가능하면 항상 세미 조인보다는 좋은 성능을 내기 때문에 별도로 제어하는 옵티마이저 옵션을 제공하지 않음.

  • 세미 조인의 서브쿼리에 사용된 테이블을 아우터 쿼리로 끄집어낸 후에 쿼리를 조인 쿼리로 재작성하는 형태의 쿼리

  • 제한 사항과 특성

    • 별도의 Using table pullout과 같은 문구가 출력되지는 않는다.
    • 서브 쿼리 부분이 유니크 인덱스나 프라이머리 키 룩업으로 결과가 1건인 경우에만 사용 가능
mysql> EXPLAIN
    SELECT * FROM employees e
    WHERE e.emp_no IN (SELECT de.emp_no FROM dept_emp de WHERE de.dept_no='d009');


mysql> SHOW WARNINGS \G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `employees`.`e`.`emp_no` AS `emp_no`,`employees`.`e`.`birth_date` AS `birth_date`,`employees`.`e`.`first_name` AS `first_name`,`employees`.`e`.`last_name` AS `last_name`,`employees`.`e`.`gender` AS `gender`,`employees`.`e`.`hire_date` AS `hire_date` from `employees`.`dept_emp` `de` join `employees`.`employees` `e` where ((`employees`.`e`.`emp_no` = `employees`.`de`.`emp_no`) and (`employees`.`de`.`dept_no` = 'd009'))
1 row in set (0.00 sec)

최대한 서브쿼리를 조인으로 풀어서 사용해라! 이유: (서브쿼리 시간 복잡도 n^2, join은 in절 1번, 아우터 1번 => 조인)

9.3.1.11 퍼스트 매치(firstmatch)

  • IN (subquery) 형태의 세미 조인을 EXIST (subquery) 형태로 튜닝 한 것

  • 서브쿼리 패턴으로 실행되지 않고, 조인으로 처리

  • FirstMatch(e) 는 일치하는 레코드 1건만 찾으면 더이상 employees 테이블 검색을 하지 않는 다는 것을 의미

    • 의미론적으로는 EXISTS (subquery)와 동일하게 처리된 것
  • 제한사항과 특성

    • 서브쿼리가 참조하는 모든 아우터 테이블이 먼저 조회된 이후에 서브쿼리가 실행된다.
    • GROUP BY나 집합 함수가 사용된 서브쿼리의 최적화에는 사용할 수 없다.

9.3.1.12 루스 스캔(loosescan)

  • 루스 스캔 최적화는 루스 인덱스 스캔으로 서브쿼리 테이블을 읽고, 그다음으로 아우터 테이블을 드리븐으로 사용해서 조인을 수행한다.

  • MySQL 내부적으로 조인처리됨.

  • 제한사항

    • 서브쿼리 부분이 루스 인덱스 스캔을 사용할 수 있는 조건이 갖춰져야 사용할 수 있는 최적화

9.3.1.13 구체화(Materialization)

  • Materialization 최적화는 세미 조인에 사용된 서브쿼리를 통째로 구체화해서 쿼리를 최적화 한다는 의미

  • 서브쿼리는 GROUP BY나 집합 함수들이 사용돼도 구체화를 할 수 있다.

  • 구체화가 사용된 경우에는 내부 임시 테이블이 사용된다.

9.3.1.14 중복 제거(Duplicated Weed-out)

  • 세미 조인 서브쿼리를 일반적인 INNER JOIN 쿼리로 바꿔서 실행하고 마지막에 중복된 레코드를 제거하는 방법으로 처리되는 최적화 알고리즘

  • 제한사항

    • 서브 쿼리가 GROUP BY나 집합 함수가 사용된 경우에는 사용할 수 없다.

9.3.1.15 컨디션 팬아웃(condition_fanout_filter)

  • JOIN 상황 시 WHERE 절이 있을 때 인덱스를 사용할 수 없는 조건에 대해서도, 최적화를 할 수 있도록 함
  • 최적화를 위해 "인덱스" 와 "히스토그램"을 활용함
  • 컨디션 팬아웃이 비활성화되면, 무엇이 드라이빙인가에 따라서 성능이 많이 달라지는데, 컨디션 팬아웃을 활성화하면 잘못된 테이블 순서로 인한 성능저하를 옵티마이저가 최적화를 해준다.
  • 한 마디로, 빠르게 소량의 데이터를 읽어보고 어떤 순서로 조인할지 결정 (350p '참고'를 참고했습니다)
  • 옵티마이저는 정교한 계산을 위해 쿼리의 실행 계획 수립에 많은 시간과 컴퓨터 자원 사용

9.3.1.16 파생 테이블 머지(derived_merge)

  • 파생 테이블(Derived Table)

    • FROM 절에 사용된 서브쿼리
    • MySQL 서버는 내부적으로 임시 테이블을 생성
  • 레코드가 많아진다면 임시 테이블로 레코드를 복사하고, 읽으 오버헤드로 인해 쿼리 성능이 많이 느려질 것임.

  • MySQL 5.7 부터는 파생 테이블로 만들어지는 서브쿼리를 외부 쿼리와 병합해서 서브쿼리 부분을 제거하는 최적화가 도입됨.

  • 제한사항

    • 집계 함수와 윈도우 함수가 사용된 서브쿼리에서 사용 불가
    • DISTNCT가 사용된 쿼리
    • GROUP BY나 HAVING이 사용된 쿼리
    • LIMIT이 사용된 쿼리
    • SELECT 절에 사용된 쿼리
    • 값이 변경되는 사용자 변수가 사용된 서브 쿼리
    • LIMIT이 사용된 서브 쿼리

9.3.1.17 인비저블 인덱스(use�_invisible_indexes)

  • MySQL 8.0 버전부터는 인덱스의 가용 상태를 제어할 수 있는 기능이 추가됨.
    • 인덱스를 삭제하지 않고, 옵티마이저가 실행계획을 수립할 때 해당 인덱스를 사용하지 못하도록 제어하는 기능

9.3.1.18 스킵 스캔(skip_scan)

  • 인덱스를 구성하는 칼럼의 순서에 대한 제약 사항을 제한적이지만 뛰어넘을 수 있는 최적화 기법

루스 스캔과 같은 것? 앞 카디널리티가 작은 게 루스 스캔이 효율적으로 동작

9.3.1.19 해시 조인(hash_join)

개요
  • 8.0.18 부터 지원
  • 네스티드 루프 조인보다 해시조인이 빠르다, but not always
  • OLTP는 응답 속도가 중요
    • 때문에, nested loop join 이 낫다
    • Q1) 마지막 레코드를 찾은 시점에 쿼리 처리가 완료인데 어쨌든 hash join이 좋은 것이 아닌가요?

우디) B에서 최종적으로 내려준는 게 아닌가요? 그니까 nested loop join이 더 느린게 아닌가요?

K) 301p 스트림방식은 응답을 빨리받을수있는데 버퍼릴ㅇ방식응ㄴ 응답을 늦게 받겠죠 데이터자체는 스트림으로 받지만 JDBC 경우에 클라이언트 단에서 자체적으로 버퍼링하는 형태로 되어있다 어플리케이션에서 응답을 바로받았으니까 처리할수있는 시간이 더 여유롭다

파이로) JPA ㅆ ㅡ면서 느낀건데 JPA 버퍼링은 걍 느리다 ordering 과 index가 문제다 데이터가 많은 경우에서 해시조인이 유리하ㅏ다고하는ㄴㄴㄴ 쿼리가 느린것보다 JPA 가 객체를 생성하고 메모리할당하는게 훨씬 느리다 그래서 버퍼링만을는 제한적이다 JPA도 스트림을제공해서 이걸쓰려면 카프카를

k) JPA가 성능적으로 문제가있따면 JPA를 사용하지 않는게 맞다 그정도 대용량처리가 이 ㅆ따면 RDB를 쓰지않는것도 방법이죠~

파이로) SQL Lite에서 날짜에 인덱스를 안걸ㅇ놔서 맨마지막에 가졍는 로직이 ㅣㅇ썽ㅆ는데 갯수가 1500개 정도 넘어가니까 뭐가 느려졋따 로컬디비인데도 불구하고

k) SQL Lite 해쉬조인 있어요

  • 분석 서비스는 처리 소요 시간이 중요
    • 때문에, hash join 이 낫다
  • MySQL은 OLTP 처리를 위한 DB
    • it means...??
    • hash join을 쓰는 조건은 한정적, fallback strategy
    • 때문에, 옵티마이저 힌트로 강제로 쿼리 실행계획 hash join으로 쓰면 안된다
block nested join
  • 8.0.17 까지 사용 hash jojn대신
설명
  • build-phase, probe-phase 로 나뉘어 처리 (358p)

파이로) 빌드는 해쉬테이블을 만드는 단계 프로브는 레코드를 탐색하는 것이다.

우디) 그림 9.17은 임픞로이 검색조인으로 되어있는데 385ㅈ explain inner hash join 조건이 두개라 그림이 잘못됐을수도 있겠다

9.3.1.20 인덱스 정렬 선호(prefer_ordering_index) (363p)

  • 8.0.20 까지 옵티마이저가 실수하지 않도록 IGNORE INDEX 힌트 사용
  • 8.0.21 부터 ORDER BY에 가중치를 너무 부여하지 않도록 prefer_ordering_index가 추가되어 on/off 설정 가능

K) 보통 1번이 훨씬 효율적인데 옵티마이저가 잘못된 선택을 한다 그 경우에는 이전엔 ignore index를 썼고 최근은 prefer_ordering_index가 추가됐다.

9.3.2 조인 최적화 알고리즘 (364p)

  • 5.0 부터 존재
  • 문제: 조인되는 테이블 많으면 실행 계획 수립에 몇분, 몇시간, 며칠로 늘어날수 있다

9.3.2.1 Exhaustive 검색 알고리즘 (365p)

  • 5.0과 그 이전
  • 테이블 하나하나 모두 순서 바꿔서 최적 1개를 찾는다

9.3.2.2 Greedy 검색 알고리즘 (366p)

  • 5.0부터 도입

어거스트) 이그죠스티는 모든 경우의수 -> 가장 최적의 비용을 찾는다 brute force? 시스템변수에 그 값을 미리 설정해서 가장 빠른거 선택하는 식의 그런 느낌적인 느낌

K) 시스템변수 갯수만큼 이그죠스티를 검색한다음에 젤 작은 경우의 시작 테이블을 선택한다 예를 들면, 1차 경우에 t1,2,3,4 가 2로 설정된건데, (2개까지 조인한걸 탐색) 거기에서 젤 작은값이 t3->t2라 t3를 선택한다

2차에서 t3기준으로 다음껄 어떤걸 선택할건지 전체 이그죠스티를 찾는다

그리디알고리즘? 현재조건에서 가장 좋은것만 찾아간다 뎁스가 1개라면 바로 1개만 보고서 찾을것이다 예를 들면, t1을 검색하면 비용4, t2 비용2, t3 비용4, t4 비용5 가장작은 t2를 찾을것이다

어거스트) 보통 그리디하면 500원짜리 100원짜리 10언짜리 동전이있는데 가장 적게 사용하는 방법이 무엇인가? 500원짜리가 가장 먼저 선택된다고 보는것이다, 실제로는 그렇지 않을 수있ㄷ는데. 현재상황에서 가장 최적이라는 것을 찾아 한단계한단계 나아가는것. 그래서 이그죠스티는 brute force하는거고 그리디는 뎁스끝까지 안가고 하나하나 최적이라 생각드는것으로 가는것

Dong) (++수저얹기) 그리디 알고리즘이 최선이 아닌 경우 어거스트 예시에서 더하면 400원짜리 동전이 있을때, 800원 지불하는 경우

파이로) 쿼리할때 옵티마이저는 빠르게 조인해주는 플랜을 짜는 것이다. 어떻게 순서를 정할것인가 그 연산이 조인보다 옵티마이저가 하는 연산이 오래걸린다 때문에, 다 돌아다니느거(이그죠스티)보다 적당히 빨라보이는거(그리디) 선택해서 돌리는게 빠르다

K) 꽤나 높은 확률로 합리적인 선택을 하는것이 그리디이다 그리디는 한개씩 찾아가서 최선이 아닌 경우도 발생할수 있지만 그걸 조금이라도 줄이기위해 시스템변수 serach_depth를 설정할 수 있다, 시간은 늘어날수있지만 최악의 경우를 피할수있다

예시

  • t1~t4찾을때 탐색비용이 100미만이다
  • t2 탐색했을때 비용이 100이상이다
  • 그다음껄 탐색하면 당연히 100이상이니까 선택안할것이다
  • 그 옵션이 optiomizer_prune_level = 1

파이로) 요즘 조인많으면 성능이 끔찍해져서 NoSQL로 가는 추세이다 (?)


++샤딩 정규화 - 컬럼 쪼갠다 샤딩 - row를 쪼갠다

제인) 예를들어 로우가 많을떄 샤딩키라는게 있는데 해쉬함수사용하는거처럼 특정함수에넣어서 파티션별로 데이터자체를 분할해두는것 데이터를 여러곳에 쪼개져있을때 안에 들어가있는 메타정보는 똑같다.

K) 샤딩은 디비서버를 나누는것이다


질문: 그러면 이게 CQRS 랑은 개념이 다른건가요?

DONG) CQRS 적용예가 살짝 쓰기동작은 RDB 에 바로 저장하지 않고 5분마다 한번씩 배치 돌려서 저장하고, 읽기동작은 몽고디비에서 읽어오기를 하는데, 데이터 동기화는 매일 새벽에 RDB 서버에서 몽고디비에다가 퍼부어준다 요런거 더라구요

K) 도메인 - 해결해야할 문제 쿼리라는 도메인, 커맨드라는 도메인을 분리하는 걸 CQRS

디비를 심지어 하나놔둘수있지만 해결방식을 나눌수있는것이다 도메인이 나뉘어져있따는것은 디비를 아예 분리할 "수"도 있다

샤딩은 단순히 데이터를 저장하는 방식을 말합니다 대용량을 분산하는 방식중에 하나를 말하는 것입니다

마스터-레플리카 방식 같은.

어거스트) CQRS란 어플리케이션에서 나오는 이야기. 샤딩이랑은 다른이야기 https://always-kimkim.tistory.com/entry/cqrs-pattern

제인) 디비구조랑은 별개입니다

K,제인,어거스트) RDB에서 샤딩이 구리다. -> 성능이 안나온다? -> 만드는거 자체가 힘들어요, 조인하는거 자체가 힘들어서 실서비스에서 조인을 하면 조인 필요한부분을 비정규화하거나 레디스처럼 그부분에대해 nosql처럼 사용해서 하는방법을 쓰기 떄문에 의미가 사라진다

샤딩을 좀 잘해주는 게 ORACLE 정도

파이로) RDB 데이터 일정이상있을때 어떻게 ㅎ가장하냐? 샤딩하는 이유가 데이터가 많이 쌓인건데 -> 그래서 MSA가 나왔따

K) MSA가 되는 이유중 첫번쨰는 조직적인 문제가 있을 수도 있다. 서로의 조직들간의 개발이 다르니까 협업하다 보니 MSA가 된다. 그게 젤 클것이다.

두번쨰는 이유를 붙이는것 분산처리, 확장성, MSA간의 배포가 따로되고.

어거스트) 어떤 데이터가 샤딩할만큼 쌓일수있을까? 페북정도? 일반적인 우리나라 회사규모는 로그성데이터나 죽은데이터 등.

제인) 일본웹툰같은 라인망가가 했다는 글을 봤어요. https://engineering.linecorp.com/ko/blog/line-manga-server-side/

DONG) 이거 보시면 샤딩이 조인이 필요없는 분야에서는 쓰는거같긴 해영 https://youtu.be/8Eb_n7JA1yA


9.4 쿼리 힌트 (370p)

  • 옵티마이저한테 힌트를 주는거임
  • 인덱스 힌트: 예전부터 사용해오던 힌트
  • 옵티마이저 힌트: 5.6부터

9.4.1 인덱스 힌트

  • 가능하면 옵티마이저 힌트 사용하세요
  • SELECT, UPDATE 에서만 사용가능

9.4.1.1 STRAIGHT_JOIN

  • SELECT, UPDATE, DELETE에서 여러 테이블 조인시 순서 고정 역할
  • 조인 순서 변경하고 싶으면 STRAIGHT_JOIN 힌트 사용 가능
  • 어떤 기준으로 순서 정할까?
    • 임시테이블 드라이빙 테이블로 하는게 좋음
    • 대부분 옵티마이저가 적절한 조인 순서를 선택하기때문에...
    • 임시테이블끼리 조인할때 크기가 작은 테이블을 드라이빙으로
    • 일반테이블끼리 조인할때 양쪽 모두 조인칼럼에 인덱스있거나 없으면 레코드 건수적은걸로, 그이외는 조인컬럼에 인덱스없는 테이블을 드라이빙으로

9.4.1.2 USE_INDEX / FORCE INDEX / IGNORE INDEX

  • 사용법: 인덱스를 가지는 테이블 뒤에 힌트를 명시
  • USE INDEX: 옵티마이저에게 특정 테이블 인덱스 사용하도록 권장하는 힌트
  • FORCE INDEX: 위와 비슷함
  • IGNORE INDEX: 특정 인덱스 사용못하게, 주로 풀테이블스캔 유도용

어거스트) full scan이 꼭 앞에서 배운것처럼 안좋은경우가 있는 것이 아니어서 만약에 full scan이 필요한 경우에 사용할수있또록하는게 IGNORE INDEX이다

K) full text search가 있는경우 b-tree index보다 가중치가 더 높다

9.4.1.3 SQL_CALC_FOUND_ROWS

  • LIMIT에 관계없이 찾는다 1
  • 레코드 카운터용 쿼리와 데이터를 조회하는 쿼리는 분리하는 것이 더 효율적일것이다

K) 페이징 할때 사용한다 전체게시판 전체조건이 3만개인데 그중 10개보여주는거 그런 요구사항이 비즈니스요구사항에 있을때 개꿀인것 같지만 쓰지마시고 count()쓰세요

9.4.2 옵티마이저 힌트

9.4.2.1 옵티마이저 힌트 종류

영향 범위에 따라 4종류

  • 인덱스: 테이블명이 선행되어야함 /*+ INDEX(employees ix_firstname) */
  • 테이블
  • 쿼리 블록: 서브쿼리영역
  • 글로벌

9.4.2.2 MAX_EXECITION_TIME

  • 단순히 쿼리의 최대실행시간을 정함

9.4.2.3 SET_VAR (382p)

  • 시스템 변수를 조정하여 조인 버퍼나 정렬 버퍼 크기를 일시 늘려 대용량 처리 쿼리 성능 향상 용도로 사용 가능

9.4.2.4 SEMIJOIN & NO_SEMIJOIN

335p 세미조인이란

K) Table pull out - 쿼리를 완전히 변경한다 - 조인으로 최적화한다 원래는 밖에 쿼리한번하고 안에서브쿼리를 한다 n x n materialization은 서브쿼리를 임시테이블로 만들어서 하게 해준다 그게 가능하려면 외부-내부 쿼리가 연관관계가 없어야한다

9.4.2.5 SUBQUERY

  • 세미조인 최적화가 사용되지 못할 때

9.4.2.6 BNL & NO_BNL & HASHJOIN & NO_HASHJOIN

  • 8.0.20 이후부턴 해쉬조인을 하고 싶으면 BNL이나 NO_BNL을 넣어야한다

9.4.2.7 JOIN_FIXED_ORDER & JOIN_ORDER & JOIN_PREFIX & JOIN_SUFFIX

  • STRAIGHT_JOIN 보완 - 모든 명시된 테이블을 강제해서 그것을 보완

9.4.2.8 MERGE & NO_MERGE

  • 서브쿼리는 불필요한 임시 테이블을 남김 -> 불필요한 자원소모
    • MERGE -> FROM 절의 서브쿼리를 와부 쿼리와 병합하는 방법

9.4.2.9 INDEX_MERGE & NO_INDEX_MERGE

  • 원래 가능하다면 테이블당 하나의 인덱스만을 이용해 쿼리를 처리하는데
  • 하나의 테이블에 대해서 여러개의 인덱스를 동시에 사용

9.4.2.10 NO_ICP

  • 옵티마이저는 ICP를 항상 사용하는 방향으로 계싼
  • 근데 가끔 잘못된 실행계획 수립
  • 데이터분포에따라 A인덱스가 효율적인데 실제서비스에서 B인덱스가 효율적일 경우가 있음
  • 이떄 OFF

9.4.2.11 SKIP_SCAN & NO_SKIP_SCAN (390p)

K) gender 가 일반적이면 남녀로 찾아야하는데 이경우엔 skip_scan은 left most 조건이 없어도 예를들어 서비스가 성별을 10만개라면 skip_scan이 비효율적이 된다 그런 경우에 off를 한다.

9.4.2.12 INDEX & NO_INDEX