디스크 읽기 방식, 인덱스 종류들, 외래키에 대해 알아보자
순차 I/O
는 3개의 페이지를 읽기 위해 1번 시스템 콜
요청 (왼)
랜덤 I/O
는 3개의 페이지를 읽기 위해 3번 시스템 콜
요청 (오)
인덱스
: 칼럼 값
과 해당 레코드가 저장된 주소
를 키와 값의 쌍(Key-Value Pair)
으로 만듦
=> 칼럼의 값을 미리 정렬
=> 데이터의 저장(Insert, Update, Delete)
성능을 희생하고 대신 읽기 속도
를 높이는 기능
프라이머리 키
: 레코드를 대표하는 칼럼의 값으로 만들어진 인덱스
=> Null 허용 X
=> 중복 허용X
세컨더리 인덱스
: 프라이머리 키 제외한 모든 인덱스
B-Tree 알고리즘
: 칼럼의 값을 변형하지 않고 원래의 값을 이용해 인덱싱
Hash 인덱스 알고리즘
: 칼럼의 값으로 해시값을 계산해서 인덱싱
=> 값을 변형
해서 인덱싱하므로 값 일부만 검색
하거나 범위 검색
할 때는 해시 인덱스 사용 불가
B-Tree
: 칼럼의 원래 값을 변형시키지 않고
인덱스 구조체 내에서 항상 정렬
된 상태로 유지
인덱스
는 테이블의 키 칼럼
만 가지고 있어 나머지 칼럼
을 읽으려면 데이터 파일
에서 해당 레코드
를 찾아야 함
InnoDB 테이블에서 인덱스
를 통해 레코드 읽을 경우 프라이머리 키
를 저장하고 있는 B-Tree
를 다시 한번 검색해야 함
저장될 키 값
을 이용해 B-Tree 상의 적절한 위치를 검색레코드 키 값
과 대상 레코드 주소 정보
를 B-Tree 리프 노드
에 저장리프 노드
가 꽉 차서 더는 저장 불가시 리프 노드 분리(Split)
리프 노드
를 찾아서 삭제 마크
만 하면 작업 완료삭제
한 후, 다시 새로운 키 값을 추가
하는 형태로 처리변형 가해진 경우
이용 불가100% 일치
또는 값의 앞부분
만 일치하는 경우 사용 가능하나의 인덱스 페이지(16KB)
에 몇 개의 키를 저장할 수 있을까?
=> 16*1024/(16+12)=585개
=> 자식 노드
를 585개
만약 키 값
이 커지면 어떻게 될까?
=> 32바이트
로 늘어났다면, 16*1024/(32+12) = 372
인덱스 키 값
의 평균 크기가 늘어나면, 추가로 인덱스 깊이
가 깊어져서 디스크 읽기
가 더 많이 필요해진다
모든 인덱스 키 값 가운데 유니크한 값의 수
=> 선택도가 높을수록
검색 대상이 줄어듦 (유니크한 값이 많아지니까)
인덱스
를 통해 읽어야 할 레코드의 건수가 전체 테이블 레코드
의 20~25%
를 넘어서면 인덱스를 이용하지 않고 테이블을 모두 직접 읽어서
필요한 레코드만 가려내는 방식으로 처리하는 것이 효율적
인덱스 레인지 스캔
: 검색해야 할 인덱스의 범위
가 결정됐을 때 사용
select *
from employees
where first_name between 'Ebbe' and 'Gad';
인덱스 레인지 스캔 단계
인덱스 탐색
인덱스 스캔
인덱스 키
와 레코드 주소
를 이용해 레코드가 저장된 페이지를 가져와, 최종 레코드 읽음커버링 인덱스
: 디스크의 레코드
를 읽지 않아도 되며, 위 과정 중 세번째 과정 제외한 것
-- 위 단계중 1번, 2번 단계 작업을 얼마나 수행했는지
show status like 'Handler_%';
Handler_read_key
: 위에서 1번 단계가 실행된 횟수Handler_read_next
와 Handler_read_prev
: 2번 단계로 읽은 레코드 건수정순
, 후자는 역순
Handler_read_first
와 Handler_read_last
: min()
또는 max()
와 같이 제일 큰 값 또는 제일 작은 값만 읽는 경우 증가인덱스 풀 스캔
: 인덱스의 처음
부터 끝
까지 모두 읽는 방식
=> 쿼리의 조건절에 사용된 칼럼이 인덱스의 첫 번째 칼럼이 아닌 경우
사용
=> 인덱스에 포함된 칼럼만
으로 쿼리를 처리할 수 있는 경우 효율적
루스 인덱스 스캔
: 인덱스 스캔을 느슨
하게 또는 듬성듬성
하게 읽는 것
=> Oracle의 인덱스 스킵 스캔
=> group by
또는 max() 또는 min()
에 대해 최적화하는 경우
-- 인덱스 : dept_no+emp_no
select dept_no, min(emp_no)
from dept_emp
where dept_no betwwen 'd002' and 'd004'
group by dept_no;
alter table employees add index ix_gender_birthdate(gender,birth_date);
위 인덱스를 사용하려면 gender 칼럼
에 대한 비교 조건이 필수였으나, 인덱스 스킵 스캔
을 이용해 gender 컬럼
을 건너뛰어서 birth_date 칼럼만
으로 인덱스 검색 가능하게 해줌
루스 인덱스 스캔
은 group by 작업
을 처리하기 위해 인덱스 사용하는 경우에만 적용 가능
--인덱스 스킵 스캔 비활성화
set optimizer_switch='skip_scan=off';
explain
select gender, birth_date
from employees
where birth_date>='1965-02-01';
type 칼럼
이 index
라고 표시된 것은 인덱스 풀 스캔
을 의미한다.
--인덱스 스킵 스캔 활성화
set optimizer_switch='skip_scan=on';
explain
select gender, birth_date
from employees
where birth_date>='1965-02-01';
type 칼럼
이 range
로 표시된 것은 인덱스에서 꼭 필요한 부분
만 읽었다는 것 의미
Using index for skip scan
으로 표시된 것은 인덱스 스킵 스캔
을 활용했다는 것을 의미
MySQL 옵티마이저는 gender 칼럼
에서 유니크한 값
을 모두 조회해 주어진 쿼리에 gender 칼럼 조건
을 추가해서 쿼리를 다시 실행
select gender, birth_date
from employees
where gender='M'
and birth_date>='1965-02-01';
select gender, birth_date
from employees
where gender='F'
and birth_date>='1965-02-01';
인덱스 스킵 스캔 단점
선행 칼럼
의 유니크한 값의 개수가 적어
야 함인덱스에 존재하는 칼럼만
으로 처리 가능해야 함 - 커버링 인덱스
다중 칼럼 인덱스
: 2개 이상의 칼럼을 포함하는 인덱스
--인덱스 정렬 순서 혼합 가능 (8.0 이상)
create index ix_teamname_userscore on employees (team_name asc, user_score desc);
인덱스 스캔 방향
select * from employees
order by first_name desc
limit 1;
위 쿼리의 경우 인덱스를 최댓값부터 거꾸로 읽으면
내림차순으로 값을 가져올 수 있음
내림차순 인덱스
오름차순 인덱스
: 작은 값
의 인덱스 키가 왼쪽
으로 정렬된 인덱스내림차순 인덱스
: 큰 값
의 인덱스 키가 왼쪽
으로 정렬된 인덱스인덱스 정순 스캔
: 리프 노드의 왼쪽
페이지 부터 오른쪽으로 스캔인덱스 역순 스캔
: 리프 노드의 오른쪽
페이지 부터 왼쪽으로 스캔인덱스 역순 스캔
이 인덱스 정순 스캔
에 비해 느릴 수
밖에 없는 이유
인덱스 정순 스캔
에 적합한 구조단방향
으로만 연결된 구조각 칼럼 순서
와 그 칼럼에 사용된 조건
이 동등 비교
인지 아니면 범위 조건
인지에 따라 활용 형태 달라짐
select * from dept_emp
where dept_no='d002' and emp_no>=10114;
--인덱스 dept_no+emp_no : 케이스 A
--인덱스 emp_no+dept_no : 케이스 B
필터링
: 인덱스를 통해 읽은 레코드가 나머지 조건
에 맞는지 비교하면서 취사선택하는 작업
작업 범위 결정 조건
: 케이스 A에서 처럼 작업의 범위를 결정하는 조건
필터링 조건
: 케이스 B의 dept_no
처럼 비교 작업 범위
를 줄이지 못하고 단순히 거름종이 역할만 하는 조건
인덱스 레인지 스캔
을 위해선 왼쪽 부분
이 꼭 있어야 함
--인덱스 : first_name
select * from employees where first_name like '%mer';
위 쿼리는 왼쪽 부분이 고정X(%mer)
이므로 인덱스 레인지 스캔 불가능
아래와 같은 경우 범위 조건
으로 사용할 수 없음
Not-Equal
로 비교된 경우Like %??
형태로 문자열 비교스토어드 함수
나 다른 연산자로 인덱스 칼럼이 변형
된 경우Not-Deterministic 속성
의 스토어드 함수
가 비교 조건에 사용된 경우서로 다른
비교문자열 데이터 타입
의 콜레이션
이 다른 경우where utf8_bin_char_column=euckr_bin_char_column
Null
값도 인덱스에 저장됨
다중 칼럼의 인덱스가 사용되는 조건과 사용할 수 없는 조건
-- index : column_1+column2+...+column_n)
범위 조건
으로 인덱스를 사용하지 못하는 경우
=> column_1
에 대한 조건이 없는 경우
=> column_1
의 비교 조건이 위의 인덱스 사용 불가 조건 중 하나인 경우
범위 조건
으로 인덱스 사용하는 경우
=> column_1~column_(i-1)칼럼
까지 동등 비교 형태로 비교
=> column_i 칼럼
에 대해 다음 연산자 중 하나로 비교
좌측 일치
패턴 위 두가지 조건을 모두 만족
하는 쿼리는 column_1
부터 column_i
까지는 범위 조건
, 그 이후는 필터링 조건
으로 사용
공간 인덱스(Spatial Index)
: R-Tree 인덱스 알고리즘
을 이용해 2차원의 데이터를 인덱싱하고 검색하는 인덱스
=> 위치 기반 서비스를 구현하기 위함
B-Tree
는 칼럼의 값이 1차원 스칼라 값
인 반면, R-Tree
는 2차원의 공간 개념 값
MySQL의 공간 확장
데이터 타입
공간 인덱스(R-Tree 알고리즘)
연산 함수
공간 정보의 저장
및 검색
을 위해 여러 가지 기하학적 도형(Geometry)
정보 관리할 수 있는 데이터 타입
제공
MBR(Minimum Bounding Rectangle)
: 해당 도형을 감싸는 최소 크기
의 사각형
위의 공간 데이터의 MBR
을 그려보면 아래와 같다.
루트 노드
브랜치 노드
리프 노드
R-Tree
는 Rectangle의 R
과 B-Tree의 Tree
를 섞어서 만들었으며, 공간 인덱스
라고도 함
=> 경도
, 위도
뿐만 아니라 좌표 시스템
에 기반을 둔 정보에 대해선 모두 적용 가능
=> ST_Contains()
또는 ST_Within()
처럼 포함 관계를 비교
하는 함수로 검색하는 경우만 인덱스 사용 가능
ex. 현재 사용자 위치로부터 반경 5km 이내
ST_Contains()
또는 ST_Within()
는 사각형 박스
와 같은 다각형으로만 연산이 가능해 원
을 포함하는 MBR
로 포함 관계 비교 수행
-- p6 음식점 제거 안해도 될 경우
-- 사각 상자에 포함된 좌표 Px만 검색
-- 사각 상자 : 포함 경계를 가진 도형 명시
-- px : 포함되는 도형(또는 점 좌표) 명시
select * from tb_location where ST_Contains(사각 상자, px);
select * from tb_location where ST_Within(px, 사각 상자);
-- p6 반드시 제거해야 할 경우
-- ST_Distance_Sphere() 함수로 필터링 필요
select * from tb_location
where ST_Contains(사각 상자, px) -- 공간 좌표 px가 시각 상자에 포함되는지 비교
and ST_Distance_Sphere(p,px)<=5*1000 -- 5km;
전문(Full Text) 검색
: 문서
의 내용 전체를 인덱스화해서 특정 키워드
가 포함된 문서를 검색
문서 본문의 내용에서 사용자가 검색하게 될 키워드
를 분석해 내고, 빠른 검색용으로 사용할 수 있게 이러한 키워드로 인덱스
구축
=> 어근 분석
=> n-gram 분석
아래 과정을 거쳐 색인 작업
수행
불용어(Stop Word) 처리
: 검색에서 별 가치가 없는 단어를 필터링해서 제거
어근 분석(Stemming)
: 검색어로 선정된 단어의 뿌리인 원형
을 찾는 작업본문을 무조건 몇 글자씩 잘라
서 인덱싱
=> n-gram
의 n
은 인덱싱할 키워드의 최소 글자 수
ex. To be or not to be. That is the question
띄어쓰기
와 마침표
를 기준으로 10개 단어로 구분
2글자씩 중첩
해서 토큰으로 분리
-- 내장된 불용어 확인
select *
from information_schema.INNODB_FT_DEFAULT_STOPWORD;
위 표에서 불용어
를 제외한 것들만 인덱스에 등록
ti
와 at
같은 단어는 a
와 i
가 불용어로 등록되어 있어 모두 걸러져 버려
짐
=> 불용어 처리를 무시
하거나 사용자가 직접 불용어
를 등록 권장
전문 검색 인덱스의 불용어 처리 무시
my.cnf
의 ft_stopword_file
에 빈 문자열
설정InnoDB
) innodb_ft_enable_stopword
시스템 변수를 OFF
로 설정사용자 정의 불용어 사용
my.cnf
의 ft_stopword_file
에 불용어 목록 파일
설정InnoDB
) 불용어 목록을 테이블
로 저장 -- inno_ft_server_stopword_table
create table my_stopword(
value varchar(30)
) engine=innodb;
insert into my_stopword(value) values ('MySQL');
set global innodb_ft_server_stopword_table='mydb/my_stopword';
alter table tb_bi_gram add fulltext index fx_title_body (title, body) with parser ngram;
-- 여러 전문 검색 인덱스가 다른 불용어 사용해야할 경우 innodb_ft_user_stopword_table 변수 사용
전문 검색 인덱스
사용 조건
match ... against...
) 사용전문 인덱스
보유-- 전문 검색 인덱스 구성 칼럼은 match절 괄호 안에 모두 명시돼야 함
select * from tb_test
where match(doc_body) against('애플' in boolean mode);
함수 기반 인덱스
: 칼럼의 값을 변형
해서 만들어진 값에 대해 인덱스 구축
함수 기반 인덱스 구현 방법
가상 칼럼
을 이용한 인덱스함수
를 이용한 인덱스create table user (
user_id bigint,
first_name varchar(10),
last_name varchar(10),
primary key(user_id)
);
위 테이블에서 first_name과 last_name을 합쳐서 검색해야 하는 요건이 있다면, 가상 칼럼
을 추가하고 그 가상 칼럼에 인덱스
를 생성
alter table user add full_name varchar(30) as (concat(first_name,' ',last_name)) virtual,
add index ix_fullname(full_name);
가상 칼럼
은 테이블에 새로운 칼럼을 추가하는 것과 같은 효과를 내어 실제 테이블 구조가 변경
됨
create table user (
user_id bigint,
first_name varchar(10),
last_name varchar(10),
primary key(user_id),
index ix_fullname ((concat(first_name,' ',last_name)))
);
멀티 밸류(Multi-Value) 인덱스
: 하나의 데이터 레코드가 여러 개의 키 값
을 가지는 형태의 인덱스
=> JSON
의 배열 타입의 필드에 저장된 원소들
에 대한 인덱스 요건
create table user (
user_id bigint auto_increment primary key,
first_name varchar(10),
last_name varchar(10),
credit_info json,
index mx_creditscores ((cast(credit_info->'$.credit_scores' as unsigned array)) )
);
insert into user values (1,'Matt','Lee','{"credit_scores":[360,353,351]}');
멀티 밸류 인덱스
활용을 위해 아래 함수들을 이용해서 검색해야 인덱스
활용 가능
MEMBER OF()
JSON_CONTAINS()
JSON_OVERLAPS()
select * from user
where 360 member of (credit_info->'$.credit_scores');
explain
select * from user
where 360 member of (credit_info->'$.credit_scores');
클러스터링
: 테이블의 레코드를 비슷한 것(프라이머리 키 기준)
들 끼리 묶어서 저장
클러스터링 인덱스
: 프라이머리 키 값이 비슷한
레코드끼리 묶어서 저장
하는 것
클러스터링 인덱스
의 리프 노드
에는 레코드의 모든 칼럼
이 같이 저장돼 있음
프라이머리 키
가 없는 InnoDB 테이블의 경우 아래 우선순위로 대체 칼럼 선택
클러스터링 키
not null
옵션의 유니크 인덱스
중 첫 번째 인덱스증가되는 칼럼
을 내부적으로 추가한 후 선택InnoDB의 모든 세컨더리 인덱스
는 해당 레코드가 저장된 주소가 아니라 프라이머리 키 값
을 저장하도록 구현
구분 | 내용 |
---|---|
장점 | - 프라이머리 키로 검색시 성능 매우 빠름 - 테이블의 모든 세컨더리 인덱스가 프라이머리 키를 가지고 있어 인덱스만으로 처리될 수 있음 |
단점 | - 테이블의 모든 세컨더리 인덱스가 클러스터링 키를 가져 키 값의 크기가 클 경우 전체적으로 인덱스 크기 커짐 - 세컨더리 인덱스로 검색시 프라이머리 키로 다시 한번 검색해야 하므로 처리 성능 느림 - insert할 때 프라이머리 키에 의해 레코드 저장 위치 결정돼 처리 성능 느림 -프라이머리 키 변경시 레코드 delete 하고 insert하는 작업 필요해 처리 성능 느림 |
클러스터링 테이블
의 경우 모든 세컨더리 인덱스
가 프라이머리 키
를 포함하므로 프라이머리 키
의 크기
가 커지면 세컨더리 인덱스도 자동
으로 크기가 커짐
프라이머리 키
는 대부분 검색에서 빈번하게 사용되므로 크기가 크더라
도 업무적으로 해당 레코드를 대표할 수 있다면 그 칼럼을 프라이머리 키로 설정
프라이머리 키를 생성하지 않으면 내부적으로 일련번호
칼럼을 추가하는데, 이는 사용자가 사용불가
하므로 똑같은 auto_increment
칼럼을 생성하고 프라이머리 키로 사용하는 것이 더 낫다.
세컨더리 인덱스
도 필요하고 프라이머리 키의 크기도 길다
면 auto_increment
칼럼을 추가하고 이를 프라이머리 키로 설정
=> 인조 식별자
: 프라이머리 키를 대체하기 위해 인위적으로 추가
된 프라이머리 키
유니크 인덱스
: Null 저장 가능
읽어야 할 레코드 건수가 같다면
성능상의 차이는 미미
일반 인덱스는 체인지 버퍼
를 이용해 버퍼링
이 가능하지만 유니크 인덱스
는 반드시 중복 체크
를 해야 하므로 버퍼링 하지 못해, 세컨더리 인덱스
보다 변경 작업이 더 느림
유니크 인덱스
도 일반 세컨더리 인덱스
와 같은 역할을 동일하게 수행하므로 중복으로 생성X
외래키 제약이 설정되면 자동으로 연관
되는 테이블의 칼럼에 인덱스 생성
외래키 관리
의 중요한 특징
변경
이 발생하는 경우에만 잠금 대기
발생최대한 잠금 대기 발생X
ex. child 테이블이 parent 테이블의 id를 참조하는 pid 칼럼 가지고 있음
자식 테이블의 외래 키 칼럼의 변경
은 부모 테이블의 확인이 필요한데, 이 상태에서 부모 테이블
의 해당 레코드가 쓰기 잠금
이 걸려있으면 해당 쓰기 잠금이 해제
될 때까지 대기
자식 테이블의 외래키(pid)
가 아닌 칼럼
의 변경은 외래키로 인한 잠금 확장 발생X
on delete cascade
때문에 child 테이블의 레코드에 대한 쓰기 잠금
이 해제될 때까지 parent 테이블은 대기해야 함