1. 해시알고리즘

해시 알고리즘은 검색을 위한 인덱스와 테이블의 파티셔닝 용도로 사용된다. 해시 알고리즘이 사용될 때 해시 함수의 결과 값이 범위가 넓어야 충돌이 줄어들고 그만큼 검색 성능이 높아진다.(버켓이 많이 필요해서 공간의 낭비 발생) 해시 함수의 결과 값의 범위가 좁으면 입력 값은 다르지만 해시값이 같아져 충돌이 많이 발생하여 검색의 효율이 떨어진다.

테이블 파티셔닝 용도로 사용되는 경우에는 해시 함수가 필요한 파티션의 개수만큼만 만들어지도록 설계해야 하므로 해시 함수의 결과 값의 범위를 좁게 사용한다.

2. 해시 인덱스의 가용성 및 효율성

2-1. 해시 인덱스를 사용하는 쿼리 (작업 범위 제한 조건)

다음 패턴의 쿼리는 동등 비교 조건으로 값을 검색하고 있으므로 해시 인덱스를 사용할 수 있다.

SELECT * FROM TEST WHERE NAME = '해시';

SELECT * FROM TEST WHERE NAME <=> '해시';

SELECT * FROM TEST WHERE NAME IN ( '해시','알고리즘');

SELECT * FROM TEST WHERE NAME IS NULL;

SELECT * FROM TEST WHERE NAME IS NOT NULL;

* NULL-SAFE EQUAL 연산자 (<=>)

NULL-Safe Equal 연산자는 = 연산자와 같은 비교 연산을 실행하지만 양쪽의 연산자가 모두 NULL이면 1을 하나의 연산자만 NULL이면 0 을 리턴한다.

select 1=1, 1<=>1, null=null, null<=>null, 1=null, 1<=>null;

1=1
1<=>1
null=null
null<=>null
1=null
1<=>null
1
1
NULL
1
NULL
0

2-2. 해시 인덱스를 사용하지 못하는 쿼리

범위 비교나 부정형 비교는 해시 인덱스를 사용할 수 없다.

SELECT * FROM TEST WHERE NAME >= '해시';

SELECT * FROM TEST WHERE SAL BETWEEN 1500 AND 2000;

SELECT * FROM TEST WHERE NAME LIKE '해시%';

SELECT * FROM TEST WHERE NAME <> '해시';

다중 컬럼으로 생성된 해시 인덱스에서도 모든 컬럼이 동등 조건으로 비교되는 경우에만 인덱스를 사용할 수 있다

MySQL 8.0 테스트시에는 하나의 선행 컬럼만 쓰여도 해시 인덱스를 사용한다.

[TEST] MySQL 8.0
-- drop index ix_hashtest on employees;
create index ix_hashtest on employees (last_name, hire_date ) using hash ;

EXPLAIN
select *
from employees
where last_name='Aingworth'
-- and hire_date = '1993-05-21'
;
last_name='Aingworth'
id
select_type
table
partitions
type
possible_keys
key
key_len
ref
rows
filtered
Extra
1
SIMPLE
employees
NULL
ref
ix_hashtest
ix_hashtest
66
const
172
100
NULL
last_name ='Aingworth' and hire_date = '1993-05-21'
id
select_type
table
partitions
type
possible_keys
key
key_len
ref
rows
filtered
Extra
1
SIMPLE
employees
NULL
ref
ix_hiredate,ix_hashtest
ix_hashtest
69
const,const
1
100
NULL

선행컬럼이 = 조건으로 사용 안되어 있을 시에는 해시 인덱스를 사용 못한다.

drop index ix_hashtest on employees;
create index ix_hashtest on employees (last_name, birth_date ) using hash ;

EXPLAIN
select *
from employees
where birth_date = '1958-02-19'
;

*인덱스 생성 삭제 예시

create index ix_ascdesc on employees (emp_no desc, hire_date);
drop index ix_ascdesc on employees;

'MySQL > MySQL' 카테고리의 다른 글

MySQL 실행계획  (0) 2024.12.06
RealMySQL 예제 데이터  (0) 2024.12.06
INDEX 엑세스 조건  (0) 2024.12.06
INDEX의 구조(B-Tree)  (0) 2024.12.06
MySQL의 격리 수준  (0) 2024.12.06

1. 인덱스 엑세스 조건으로 사용할 수 없는 경우

- NOT-EQUAL로 비교된 경우(<>,NOT IN, NOT BETWEEN, IS NOT NULL)

- LIKE '%??' (앞부분이 아닌 뒷부분 일치)형태로 문자열 패턴이 비교된 경우

- 스터어드 함수나 다른 연사자로 인덱스 컬러이 변형된 후 비교된 경우

- NOT-DETERMINISTIC 속성의 스토어드 함수가 비교 조건에 사용된 경우

WHERE column = determinnistic_function()

- 데이터 타입이 서로 다른 비교(인덱스 컬럼의 타입을 변환해야 비교가 가능한 경우)

- 문자열 데이터 타입의 COLLATION이 다른 경우

WHERE utf8_bin_char_column = euckr_bin_char_column

2. IS NULL 조건 --> 인덱스 엑세스 조건

다른 일반적인 DBMS에서는 NULL값은 인덱스에 저장되지 않지만 MySQL에서는 NULL값도 인덱스로 관리된다.

WHERE column IS NULL

3. 인덱스 엑세스 조건(범위 결정 조건)으로 사용되는 경우

INDEX emp_ix (col1, col2, col3, col4, ... ,coln)

col1~col(n-1) 컬럼까지 Equal 형태("=" or "IN")로 비교

coli 컬럼에 대해 다음 연산자 중 하나로 비교

- Equal (=)

- 크다 작다 형태 (> , <)

- LIKE로 좌측 일치 패턴(LIKE 'SH%')

예제)

-- 범위결정조건 : col2까지

WHERE col1 = 1 AND col2 >10

-- 범위결정조건 : col3까지

WHERE col1 IN (1,2) AND col2 = 2 AND col3 <=10

-- 범위결정조건 : col3까지 , 체크조건 : col4

WHERE col1 = 1 AND col2 = 2 AND col3 IN (10,20,30) AND col4 <> 100

-- 범위결정조건 : col3까지 , 좌측 패턴 일치 LIKE 비교는 크다 또는 작다 비교와 등급으로 생각한다

WHERE col1 =1 AND col2 IN (1,3) AND col3 LIKE 'SH%'

'MySQL > MySQL' 카테고리의 다른 글

RealMySQL 예제 데이터  (0) 2024.12.06
해시알고리즘, 해시인덱스  (0) 2024.12.06
INDEX의 구조(B-Tree)  (0) 2024.12.06
MySQL의 격리 수준  (0) 2024.12.06
트랜잭션 격리 수준과 잠금  (0) 2024.12.06

1. 인덱스의 의미

컬럼(들)의 값과 해당 레코드가 저장된 주소를 같이 저장하여 인덱스를 만든다.

SotedList 자료구조로 데이터가 정렬된 구조로 저장하여 인덱스가 많은 테이블은 Insert, Update, Delete 문장의 처리가 느려진다.

인덱스는 데이터의 저장(INSERT, UPDATE, DELETE) 성능을 희생하고 그 대신 데이터의 읽기 속도를 높이는 기능이다.

1-1. 인덱스를 역할별로 구분

- Primary Key : 테이블에서 해당 레코드를 식별할 수 있는 기준값으로 Unique해야 하며 Not null 이어야 한다.

- Secondary Index : Primary Key를 제외한 나머지 모든 인덱스를 보조 인덱스로 분류한다. Unique Index는 대체키로 분류하기도 하고 보조 인덱스로 분류 하기도 한다.

1-2. 데이터 저장 방식(알고리즘)

- B(alanced)-Tree 알고리즘 : B-Tree 인덱스는 컬럼의 값을 변형하지 않고, 원래의 값을 이용해서 인덱싱

- Hash 인덱스 알고리즘 : 컬럼의 값으로 해시 값을 계산해서 인덱싱하는 알고리즘, 등호(=) 조건 일때만 사용 가능

- Fractal-Tree 알고리즘 : B-Tree의 단점을 보완하기 위해 고안된 알고리즘. 값을 변형하지 않고 인덱싱하며 데이터가 저장되거나 삭제될 때 처리 비용을 상당히 줄일 수 있게 설계된 것이 특징

1-3. 데이터 중복여부

- Unique Index : 항상 동등 조건으로 검색하고 1건의 레코드만 찾음

- Non-Unique Index : 같은 값이 1개 이상 존재 할 수 있는 인덱스

2. B-Tree Index

2-1. 구조 및 특징

최상위에 하나의 루트 노드가 존재하고 중간 노드를 브랜치 노드 젤 하위 노드를 리프 노드라 한다. 인덱스의 리프노드는 항상 실제 데이터 레코드를 찾아가기 위한 레코드의 주소 값을 가진다.

레코드 주소는 DBMS종류나 MySQL의 스토리지 엔진에 따라 의미가 달라진다. 오라클은 물리적인 레코드 주소(rowid)가 되지만 MyISAM 테이블에서는 내부적인 레코드의 아이디(번호)를 의미한다. 그리고 InnoDB 테이블은 Primary Key에 의해 클러스터링되기 때문에 Primary Key 값 자체가 주소 역할을 한다. MySQL 테이블의 인덱스는 인덱스컬럼값+주소값(InnoDB의 PK or MyISAM의 레코드 아이디값)이 인덱스 레코드로 구성된다.

데이터 파일에서 레코드는 특정 기준으로 정렬되지 않고 임의의 순서대로 저장된다. 하지만 InnoDB 테이블에서 레코드는 클러스터되어 디스크에 저장되므로 Primary Key 순서대로 정렬되어 저장된다. 오라클의 IOT(Index Organized Table)나 MS-SQL의 클러스터 테이블과 같은 구조를 말한다. 다른 DBMS에서는 클러스터링 기능이 선택사항이지만, InnoDB에서는 디폴트로 클러스터링 테이블이 생성된다. 클러스터링이란 비슷한 값을 최대한 모아서 저장하는 방식이다.

2-2. 인덱스키 추가 및 삭제

인덱스의 새로운 키 값이 B-Tree에 저장될 때는 저장될 키값을 이용해 저장될 위치를 검색하여 인덱스의 키값과 대상 레크드의 주소 정보를 B-Tree의 리프노드에 저장된다. 리프노드에 더이상 저장 할 수 없을 때는 리프 노드가 Split돼야 하는데, 이는 상위 브랜치 노드까지 처리의 범위가 넓어진다.(비용이 발생)

인덱스 추가로 인한 INSERT나 UPDATE문장에 끼치는 영향을 살펴보자. 일반적으로 테이블에 B-Tree 인덱스가 3개 있다면 테이블에 인덱스가 하나도 없을때 비용이 1이고, 3개인 경우에는 5.5(1.5*3+1) 정도의 비용이 든다고 예측할 수 있다. 이 비용의 대부분이 디스크로부터 인덱스 페이지를 읽고 쓰기를 해야 하기 때문에 시간이 오래 걸린다.

MyISAM 스토리지 엔진은 "delay-key-write" 파라미터 설정해 인덱스 키 추가 작업을 지연 처리할 수 있는데 이는 동시 작업 환경에서는 적합하지 않다. InnoDB 스토리지 엔진은 상황에 따라 인덱스 키 추가 작업을 지연시킬지 바로 처리할지 결정한다.

[Insert Buffer의 처리 방식]

1) 사용자 쿼리 실행

2) InnoDB 버퍼 풀에 새로운 키값을 추가해야 할페이지(B-Tree의 리프 노드)가 존재한다면 즉시 키 추가 작업 처리

3) 버퍼 풀에 B-Tree의 리프 노드가 없다면 인서트 버퍼에 추가할 키값과 레코드의 주소를 임시로 기록해 두고 작업 완료(사용자의 쿼리는 실행 완료됨)

4) 백그라운드 작업으로 인덱스 페이지를 읽을 때마다 인서트 버퍼에 머지해야 할 인덱스 키값이 있는지 확인한 후, 있다면 병합함(B-Tree에 인덱스 키와 레코드의 주소(PK)값을 저장)

5) 데이터베이스 서버 자원의 여우가 생기면 MySQL 서버의 인서트 버퍼 머지 스레드가 조금씩 인서트 버퍼에 임시 저장된 인덱스 키와 주소 값을 머지(B-Tree에 인덱스키와 주소값을 저장)시킴

인서트 버퍼는 MySQL 5.1이하에서는 INSERT로 인한 인덱스 키 추가 작업만 버퍼링 및 지연처리 함.

MySQL 5.5 이상의 버전에서는 INSERT뿐 아니라 DELETE등에 의한 인덱스 키의 추가 및 삭제 작업까지 버퍼링해서 지연 처리하여 Change Buffering이라 한다. 관련 설정 파라미터 "innodb_change_buffering" 설정 값을 이용해 키 추가 작업과 키 삭제 작업중 어느 것을 지연 처리할지 설정한다.

인덱스 키 삭제는 B-Tree의 리프 노드를 찾아서 삭제 마크를 하면 작업이 완료 된다. 인덱스 키 삭제로 인한 마킹 작업 또한 디스크 쓰기가 필요하므로 디스크 I/O 작업이다. MySQL 5.5이상 버전의 InnoDB 스토리지 엔진에서는 이 작업 또한 버퍼링되어 지연 처리할 수 있다. MyISAM 이나 Memory 스토리지 엔진의 테이블에서는 인서트 버퍼와 같은 기능이 없으므로 인덱스 키 삭제가 완료된 후 쿼리 실행이 완료된다.

인덱스 키 변경시 B-Tree의 키값 변경 작업은 먼저 키값을 삭제한 후, 다시 새로운 키값을 추가하는 형태로 처리된다.

인덱스를 검색하는 작업은 B-Tree의 루트 노드부터 시작해 브랜치 노드를 거쳐 최종 리프노드까지 이동하면서 비교 작업을 수행한다. 이 과정을 트리 탐색(Tree Traversal)이라고 한다.

B-Tree 인덱스를 이용한 검색은 = 조건이나(100%일치) 또는 값의 앞부분(Left_most part)만 일치하는 경우에 사용할 수 있다. 부등호(<>) 비교나 값의 뒷부분이 일치하는 경우에는 B-Tree 인덱스를 이용한 검색이 불가능하다.

또한, 인덱스 키값에 변형이 가해지면 B-Tree 검색을 사용할 수 없다. 변형된 값은 B-Tree 인덱스에 존재하는 값이 아니다.

InnoDB 스토리지 엔진에서 인덱스는 더 중요한 의미를 가진다. InnoDB 테이블에서 지원하는 레코드 잠금이나 넥스트 키 락(갭 락)이 검색을 수행한 인덱스를 잠근 후 테이블의 레코드를 잠그는 방식으로 구현되어 있다. 따라서 UPDATE나 DELETE문장이 실행될 때 테이블에 적절히 사용할 수 있는 인덱스가 없다면 불필요하게 많은 레코드를 잠근다.

2-3. 인덱스 키값의 크기

InnoDB 스토리지 엔진은 디스크에 데이터를 저장하는 가장 기본적인 단위를 페이지(Page) 또는 블록(Block)이라고 하며, 디스크의 모든 읽기 및 쓰기 작업의 최소 작업 단위가 된다. 또한 페이지는 버퍼풀에서 데이터를 버퍼링하는 기본 단위이다. 인덱스도 결국 페이지 단위로 관리된다.

InnoDB의 모든 페이지 크기는 16KB로 고정돼 있다(이를 변경하려면 소스 컴파일이 필요함) 인덱스의 키가 16바이트라 가정하고 자식노드 주소는 데략 6-12바이트로 평균적으로 12바이트로 구성된다고 가정하자. 하나의 인덱스 페이지(16KB)에 16*1024/(16+12) = 585개 저장 할 수 있다. 이 경우에 자식 노드를 585개 가질 수 있는 B-Tree가 된다.

키값의 크기가 두 배인 32바이트로 늘었다고 가정한다면 한 페이지에 인덱스 키를 16*1024/(32+12) = 372개 저장할 수 있다. SELECT 쿼리가 레코드 500개를 읽어야 한다면 전자는 인덱스 페이지 1개로 해결 될 수도 있지만 후자는 최소한 2번 이상의 페이지를 디스크로부터 읽어야 한다.

인덱스를 구성하는 키값의 크기가 커지면 디스크로부터 읽어야 하는 횟수가 늘어나고 그만큼 느려진다. 또한, 인덱스 키 값의 길이가 길어진다는 것은 인덱스의 크기가 커진다는 것을 의미한다. 인덱스를 캐시해 두는 InnoDB의 버퍼 풀이나 MyISAM의 키 캐시 영역은 크기가 제한적이기 때문에 하나의 레코드를 위한 인덱스 크기가 커지면 커질수록 메모리(버퍼 풀 이나 키 캐시)에 캐시해 둘 수 있는 레코드의 수는 줄어들어 메모리의 효율이 떨어지게 된다.

2-4. B-Tree 깊이

B-Tree의 깊이는 MySQL에서 값을 검색할 때 몇 번이나 랜덤하게 디스크를 읽어야 하는지와 직결되는 문제다. 인덱스 키값의 크기가 커지면 커질수록 하나의 인덱스 페이지가 담을 수 있는 인덱스 키값의 개수가 작아지고, 그 때문에 같은 레코드 건수라 하더라도 B-Tree의 깊이(Depth)가 깊어져서 디스크 읽기가 더 많이 필요하게 된다.

인덱스 키값의 크기는 가능하면 작게 만드는 것이 좋다 실제로는 아무리 대용량의 데이터베이스라도 B-Tree의 깊이가 4-5이상까지 깊어지는 경우는 거의 발생하지 않는다.

'MySQL > MySQL' 카테고리의 다른 글

해시알고리즘, 해시인덱스  (0) 2024.12.06
INDEX 엑세스 조건  (0) 2024.12.06
MySQL의 격리 수준  (0) 2024.12.06
트랜잭션 격리 수준과 잠금  (0) 2024.12.06
인덱스와 잠금  (0) 2024.12.06

 
DIRTY READ
NON-REPEATABLE READ
PHANTOM READ
READ UNCOMMITTED
O
O
O
READ COMMITTED
X
O
O
REPEATABLE READ
X
X
O
X(InnoDB)
SERIALIZABLE
X
X
X

MySQL에선 REPEATABLE READ 많이 사용?

REPEATABLE READ 와 READ COMMITTED 사용하는 경우 정리 필요....

1. READ UNCOMMITTED

어떤 트랜잭션이 처리한 작업이 완료되지 않았는데도 다른 트랙잭션에서 볼 수 있게 되는 현상인 Dirty read가 발생하여 정합성에 문제가 발생하여 사용안한다.

A : BEGIN TRANSACTION

A : insert into test (no, name) values(1170,'READ UNCOMMITTED');

B : BEGIN TRANSACTION

B : select * from test where no = 1170; --> 커밋하기도 전에 검색하여 사용

A: rollback; --> rollback 하여도 B에선 no=1170인 데이터 계속해서 사용하여 작업하여 정합성 문제 발생

2. READ COMMITTED

어떤 트랜잭션에서 데이터를 변경했더라도 commit된 데이터만 다른 트랜잭션에서 조회할 수 있다.

[NON-REPEATABLE READ 발생]


테이블명 : TEST

no
name
1000
READ COMMITTED
1170
READ UNCOMMITTED
B : BEGIN TRANSACTION

B : select * from test where name = 'NON'; --> 결과없음

A : BEGIN TRANSACTION

A : update set name ='NON' where no = 1000;

A : commit;

B : select * from test where name = 'NON'; --> 결과 1건

B가 하나의 트랜잭션 내에서 똑같은 SELECT문을 실행 했을 때 항상 같은 결과를 가져와야 한다는 REPEATABLE READ 정합성에 어긋난다.

하나의 트랜잭션에서 동일 데이터를 여러 번 읽고 변경하는 작업이 금전적인 처리와 연결될 때 문제 발생

다른 트랜잭션에서 입금과 출금 처리가 계속 진행되고 있을 때 다른 트랜잭션에서 오늘 입금된 금액의 총합을 조회할때 REPEATABLE READ가 보장되지 않아 실행될 때마다 다른 결과가 발생한다.

3. REPEATABLE READ

REPEATABLE READ는 MySQL의 InnoDB 스토리지 엔진에서 기본적으로 사용되는 격리 수준이다.

바이너리 로그를 가진 MySQL 장비에서는 최소 REPEATABLE READ 격리 수준 이상을 사용해야 한다.

NON-REPEATABLE-READ 부정합이 발생하지 않는다.

InnoDB 스토리지 엔진은 트랜잭션이 Rollback될 가능성에 대비해 변경전 레코드를 Undo 공간에 백업하고 실제 레코드 값을 변경한다. 이러한 변경 방식을 MVCC(Multi Version Concurrency Control)라 한다.

Undo 영역에 백업된 데이트를 이요해 동일 트랜잭션에서 동일한 결과를 보장한다.

InnodDB의 트랜잭션은 트랜잭션 번호를 가진다. 언두 영역에 백업된 모든 레코드에는 변경을 발생시킨 트랜잭션 번호가 포함되어 있다. MVCC를 보장하기 위해서 실행중이고 가장 오래된 트랜잭션번호보다 앞선 트랜잭션 번호를 언두 영역의 데이터는 삭제할 수 없다.

[REPEATABLE READ 예시]

 


테이블명 : employees

TRX-ID
emp_no
name
8
1000
Eli
9
1001
Sun

TRX-ID : 10 ---------------------------------------------------------------------------------------------------------------

Begin Transaction

select name from employees where emp_no = 1001;

결과 : Sun

TRX-ID : 15 ---------------------------------------------------------------------------------------------------------------

Begin Transaction

update set name = 'Blue' where emp_no = 1001;

변경전 데이터 undo 영역에 백업

TRX-ID
emp_no
name
9
1001
Sun

commit;

employees

TRX-ID
emp_no
name
8
1000
Eli
15
1001
Blue

TRX-ID : 10 ---------------------------------------------------------------------------------------------------------------

select name from employees where emp_no = 1001;

emp_no=1001의 데이터가 TRX-ID의 값이 10보다 큰 15이므로 변경 전 데이터를 Undo 영역에서 가져온다.

TRX-ID
emp_no
name
9
1001
Sun

결과 : Sun


한 사용자가 BEGIN으로 트랜잭션을 시작하고 장시간 동안 트랜잭션을 종료하지 않으면 UNDO영역에 백업된 데이터가 무한정 커질 수도 있다. 언두영역에 백업데이터가 많아지면 서버의 처리 성능이 떨어 질 수 있다.

장시간 실행되는 트랜잭션의 제한 방법?

REPEATABLE READ 격리 수준에서도 부정합이 발생한다

SELECT하는 쿼리에 쓰기 잠금을 거는 SELECT .. FOR UPDATE나 SELECT ... LOCK IN SHARE MODE로 조회되는 레코드는 언두 영역에 잠금을 걸 수 없다. 그래서 현재 레코드의 값을 가져오게 된다.

이러한 SELECT ..FOR UPDATE 쿼리는 작업중일때 다른 트랜잭션이 INSERT문후에 COMMIT을 한다면 결과값이 달라지는 PHANTOM READ가 발생한다.

[PHANTOM READ 예시]

 

[TRX-ID:12] BEGIN TRANSACTION

[TRX-ID:12] SELECT * FROM employees WHERE emp_no >=1000 FOR UPDATE; --> 결과: 2건

[TRX-ID:20] BEGIN TRANSACTION

[TRX-ID:20] INSERT INTO employees VALEUS (1111, 'Grick');

[TRX-ID:20] commit;

[TRX-ID:12] SELECT * FROM employees WHERE emp_no >=1000 FOR UPDATE; --> 결과: 3건(1건추가)

 

4. SERIALIZABLE

읽기 작업도 공유 잠금을 획득해야 하며, 동시에 다른 트랜잭션은 잠긴 레코드를 변경하지 못한다. 한 트랜잭션에서 사용중인 레코드를 다른 트랜잭션에서는 접근할 수 가 없기 때문에 동시성이 떨어진다.

이미 InnoDB 스토리지 엔진의 REPETABLE READ 격리수준에서 Phantom Read가 발생하지 않기에 사용할 필요성이 없다.

5. REPEATABLE READ VS READ COMMITTED 성능

성능차이가 거의 없다. 100GB 크기의 테이블에서는 READ COMMITTED가 7% 정도 높은 성능을 보였다.

바이너리 로그(STATEMENT 포맷을 사용)가 활성화된 MYSQL 서버에서는 READ-COMMITTED 격리 수준을 사용할 수 없다. READ-COMMITTED 격리 수준에서는 레코드기반의 바이너리 로그(Row based binary log)만 사용 가능하다

'MySQL > MySQL' 카테고리의 다른 글

INDEX 엑세스 조건  (0) 2024.12.06
INDEX의 구조(B-Tree)  (0) 2024.12.06
트랜잭션 격리 수준과 잠금  (0) 2024.12.06
인덱스와 잠금  (0) 2024.12.06
트랜잭션 & Autoincrement  (0) 2024.12.04

innodb_locks_unsafe_for_binlog=1 활성화 --> next key locking 사용 안함

innodb_locks_unsafe_for_binlog=0 비활성화 --> next key locking 사용

InnoDB에서 사용되는 대부분의 갭 락이나 넥스트키락을 제거하는 방법

버전
설정
MySQL 5.0
innodb_locks_unsafe_for_binlog=1 (넥스트키락 사용안함)
트랜잭션 격리 수준을 READ-COMMITED로 설정
MySQL 5.1 이상
바이너리 로그를 비활성화
트랜잭션 격리 수준을 READ-COMMITED로 설정
레코드 기반의 바이너리 로그 사용innodb_locks_unsafe_for_binlog=1 (넥스트키락 사용안함)
트랜잭션 격리 수준을 READ-COMMITED로 설정

바이너리 로그(STATEMENT 포맷을 사용)가 활성화된 MYSQL 서버에서는 READ-COMMITED 격리 수준을 사용할 수 없다. READ-COMMITED 격리 수준에서는 레코드기반의 바이너리 로그(Row based binary log)만 사용 가능하다.

레코드기반의 바이너리 로그(Row based binary log)를 사용하거나 바이너리 로그를 사용하지 않는 경우 Innodb의 갭 락이나 넥스트 키 락의 사용을 크게 줄일 수 있다.

이러한 설정에도 유니크키나 외래키에 대한 갭 락은 없어지지 않는다.

이 설정에서 Update문장을 처리하기 위해서 일치하는 레코드를 인덱스를 이용해 검색하여 조건에 일치하는 레코드에 대해 배타적 잠금을 걸지만 나머지 조건을 비교해서 일치하지 않는 레코드는 즉시 잠금을 해제한다.

'MySQL > MySQL' 카테고리의 다른 글

INDEX의 구조(B-Tree)  (0) 2024.12.06
MySQL의 격리 수준  (0) 2024.12.06
인덱스와 잠금  (0) 2024.12.06
트랜잭션 & Autoincrement  (0) 2024.12.04
InnoDB 아키텍처 요약  (0) 2024.12.04

InnoDB의 잠금은 레코드를 잠그는 방식이 아니라 인덱스를 잠그는 방식을 사용한다. 변경해야 할 레코드를 찾기 위해 검색한 조건에 만족하는 인덱스의 레코드를 모두 잠그는 방식이다.

예제를 통해 알아보자~!

show index from employees;
Table
Key_name
Column_name
Cardinality
Index_type
Visible
employees
PRIMARY
emp_no
299645
BTREE
YES
employees
ix_firstname
first_name
1266
BTREE
YES
employees
ix_hiredate
hire_date
4943
BTREE
YES

 

select count(*) cnt from employees
union all
select count(*) cnt from employees where first_name = 'Georgi'
union all 
select count(*) cnt from employees where first_name = 'Georgi' and last_name='Klassen'
;
CNT
300,024
253
1

 

mysql> Update emloyees SET hire_date=NOW()
       where first_name ='Georgi' and last_name = 'Klassen';

Mysql 인덱스 구조는 Primary Key 값이 PKV를 포함하고 있다.

  • Update 문장이 실행되면 1건의 레코드가 변경된다.
  • 인덱스는 ix_firstname 를 사용하고 first_name = 'Georgi'를 만족하는 253건의 레코드에 Lock을 걸고  Update되는 레코드는 1건이다.
  • Update를 위해서 특정 테이블에 적절한 인덱스가 존재하지 않는다면 각 클라이언트 간의 동시성이 떨어져서 한 세션에서 그 테이블에 Update 작업을 하고 있다면 다른 세션은 Update문을 실행하기 위해 레코드에 잠금이 풀릴 때까지 기다려야 한다.
  • 만약 테이블에 인덱스가 하나도 없다면 테이블을 FULL SCAN하면서 Update작업을 해야 하며 테이블의 모든 레코드를 잠그게 된다.
  • 이것은 MySQL 만의 방식이어서 MySQL의 InnoDB에서 인덱스 설계가 중요한 이유이다.

Primary Key 조회

select * from information_schema.KEY_COLUMN_USAGE where table_name = 'employees';

INDEX 조회

show index from employees;
SELECT TABLE_SCHEMA
     , TABLE_NAME
     , COLUMN_NAME
     , ORDINAL_POSITION
     , DATA_TYPE
     , COLUMN_KEY "COLUMN_KEY(PRI,UNI,MUL)"
 FROM information_schema.COLUMNS 
WHERE table_name = 'employees';
TABLE_SCHEMA
TABLE_NAME
COLUMN_NAME
ORDINAL_POSITION
COLUMN_KEY(PRI,UNI,MUL)
employees
employees
emp_no
1
PRI
employees
employees
birth_date
2
 
employees
employees
first_name
3
MUL
employees
employees
last_name
4
 
employees
employees
gender
5
 
employees
employees
hire_date
6
MUL

'MySQL > MySQL' 카테고리의 다른 글

MySQL의 격리 수준  (0) 2024.12.06
트랜잭션 격리 수준과 잠금  (0) 2024.12.06
트랜잭션 & Autoincrement  (0) 2024.12.04
InnoDB 아키텍처 요약  (0) 2024.12.04
sample script(emp, dept, salgrade)  (0) 2024.12.04

Lock은 동시성을 제어하기 위한 기능

Transaction은 데이터의 정합성을 보장하기 위한 기능

Lock은 여러 커넥션에서 동시에 동일한 자원(레코드나 테이블)을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 변경할 수 있게 해주는 역할을 한다. Lock이 없다면 하나의 데이터를 여러 커넥션에서 동시에 변경해버릴 수 있게 되어 해당 레코드의 값은 예측할 수 없는 상태가 된다.

격리 수준이라는 것은 하나의 트랙잭션 내에서 또는 여러 트랜잭션 간의 작업 내용을 어떻게 공유하고 차단할 것인지를 결정하는 레벨을 의미한다.

 

트랜잭션을 지원하지 않는 MyISAM과 트랜잭션을 지원하는 InnoDB

트랜잭션은 여러 개의 변경 작업을 수행하는 쿼리가 조합됐을 때만 의미 있는 개념이 아니다. 트랜잭션은 하나의 논리적인 작업 셋에 하나의 쿼리가 있든 두 개 이상의 쿼리가 있든 관계없이 논리적인 작업 셋 자체가 100% 적용되거나(COMMIT을 실행했을 때) 또는 아무것도 적용되자 않아야(ROLLBACK 또는 트랜잭션을 ROLLBACK시키는 오류가 발생했을 때)함을 보장해 주는 것이다.

 

네임 락

데이터베이스 객체의 이름을 변경하는 경우 획득하는 잠금

mysql> RENAME TABLE rank TO rank_bakcup;

mysql> RENAME TABLE rank_new TO rank;

 

 

Table_locks_immediate : 다른 잠금이 풀리기를 기다리지 않고 바로 잠금을 획득한 횟수

Table_locks_waited: 다른 잠금이 이미 해당 테이블을 사용하고 있어서 기다렸던 횟수

잠금 대기 쿼리 비율 = Table_locks_waited / (Table_locks_immediate + Table_locks_waited) * 100;

InnoDB스토리지 엔진의 경우는 레코드 단위의 잠금을 사용하기 때문에 위의 집계에 포함되지 않는다.

집계된 수치는 MyISAM이나 MEMORY 또는 MERGE 스토리지 엔진을 사용하는 테이블이 대상이 된다.

테이블 수준의 잠금 확인 및 해제

[session1]

use employees;

lock tables employees READ;

[session2]

use employees;

update employees set hire_date=now() where emp_no = 100001;

[session3]

use employees;

update employees set birth_date = now(), hire_date=now() where emp_no = 100001;

[session4]

show open tables from employees;

show processlist ;

 

kill 10;

show processlist ;

 

 

InnoDB 스토리지 엔진의 잠금 정보를 진단

show engine innodb status

=====================================
2020-12-08 23:41:32 0x7f68ab4b6700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 57 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 3 srv_active, 0 srv_shutdown, 8223 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 1
OS WAIT ARRAY INFO: signal count 1
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 11286
Purge done for trx's n:o < 11284 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421563432513776, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421563432517200, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421563432516344, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421563432515488, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421563432514632, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421563432512064, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421563432511208, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
932 OS file reads, 270 OS file writes, 87 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.07 writes/s, 0.07 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 1 merges
merged operations:
insert 0, delete mark 1, delete 0
discarded operations:
insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 4 buffer(s)
0.00 hash searches/s, 0.04 non-hash searches/s
---
LOG
---
Log sequence number 395045126
Log buffer assigned up to 395045126
Log buffer completed up to 395045126
Log written up to 395045126
Log flushed up to 395045126
Added dirty pages up to 395045126
Pages flushed up to 395045126
Last checkpoint at 395045126
30 log i/o's done, 0.02 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137363456
Dictionary memory allocated 432691
Buffer pool size 8192
Free buffers 7146
Database pages 1037
Old database pages 402
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 895, created 142, written 189
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1037, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1941, Main thread ID=140087946848000 , state=sleeping
Number of rows inserted 0, updated 2, deleted 0, read 204
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.04 reads/s
Number of system rows inserted 0, updated 318, deleted 0, read 7739
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

 

 

트랜잭션의 잠금 상태 조회

INFORMATION_SCHEMA.INNODB_TRX

InnoDB의 잠금 방식

현재 트랜잭션에서 변경하고자 하는 레코드에 대해 잠금을 획득하고 변경 작업을 처리하는 방식의 비관적 잠금

InnoDB의 잠금 종류

InnoDB 스토리지 엔진은 레코드 기반의 잠금 기능을 제공하며, 잠금 정보가 상당히 작은 공간으로 관리되기 때문에 레코드 락이 페이지 락으로 또는 테이블 락으로 레벨업되는 경우(락 에스컬레이션)는 없다.

레코드와 레코드 사이의 간격을 잠그는 갭(GAP)락이 존재

그림] InnoDB 잠금의 종류

 

레코드 락(Recode lock, Record only lock)

레코드 자체만을 잠근다 InnoDB 스토리지 엔진은 특징은 레코드 자체가 아니라 인덱스의 레코드를 잠근다.

만약 인덱스가 하나도 없는 테이블이라 하더라도 내부적으로 자동 생성된 클러스터 인덱스를 이용해 잠금을 설정한다. InnoDB에서는 대부분 보조 인덱스를 이용한 변경 작업은 넥스트 키락 또는 갭 락을 사용하지만, Primary Key 또는 Unique Index에 의한 변경 작업은 갭(Gap, 간격)에 대해서는 잠그지 않고 레코드 자체에만 락을 건다.

갭 락(Gap lock)

Gap lock은 레코드 자체가 아니라 레코드와 바로 인접한 레코드 사이의 간격만을 잠그는 것을 의미한다. Gap lock의 역할은 레코드와 레코드 사이의 간격에 새로운 레코드가 생성(INSERT)되는 것을 제어하는 것이다. 갭 락은 개념일 뿐이지 자체적으로 사용되지는 않고 넥스트 키 락의 일부로 사용된다.

넥스트 키 락(Next key lock)

레코드 락과 갭 락을 합쳐 놓은 형태의 잠금을 넥스크 키락 이라고 한다. STATEMENT 포맷의 바이너리 로그를 사용하는 MySQL 서버에서는 REPEATABLE READ 격리 수준을 사용해야 한다. 또한 innodb_locks_unsafe_for_ninlog 파라미터가 비활성화 되면(파라미터 값이 0으로 설정되면) 변경을 위해 검색하는 레코드에는 넥스트 키 락 방식으로 잠금이 걸린다. InnoDB의 갭 락이나 넥스트 키 락은 바이너리 로그에 기록되는 쿼리가 슬레이브에서 실행될 때 마스터에서 만들어 낸 결과와 동일한 결과를 만들어내도록 보장하는 것이 주 목적이다.

자동 증가 락(Auto Increment lock)

Auto Increment lock은 테이블 수준의 락을 제공한다. AUTO_INCREMENT락은 명시적으로 획득하고 해제하는 방법이 없다.

innodb_autoinc_lock_mode 파라미터

SHOW VARIABLES LIKE '%innodb_autoinc%'

innodb_autoinc_lock_mode =0

모든 INSERT문장은 자동 증가 락을 사용한다.

innodb_autoinc_lock_mode =1

MySQL서버가 INSERT되는 레코드 건수를 예측할 수 있을때는 훨씬 가볍고 빠른 래치(뮤텍스)를 이용해서 처리한다. 하지만 INSERT SELECT 와 같이 건수를 예측할 수 없을 때는 자동 증가 락을 사용한다.

대량 INSERT가 수행될 때는 InnoDB 스토리지 엔진은 한번에 여러 개의 자동 증가 값을 한번에 할당받아서 INSERT되는 레코드에 사용되어 할당받은 자동 증가 값이 남아서 폐기하므로 대량 INSERT 문장의 실행 이후에 INSERT되는 레코드의 자동증가 값은 연속되지 않고 누락된 값이 발생할 수 있다.

하나의 INSERT문장으로 삽입되는 레코드는 연속된 자동 증가 값을 가지게 된다. 그래서 이설정 모드를 연속모드(Consecutive Mode) 라고도 한다.

innodb_autoinc_lock_mode =2

항상 경량화된 래치(뮤텍스)를 사용한다. 하나의 INSERT문장으로 삽입되는 레코드도 연속된 자동 증가 값을 보장하지 않는다. 그래서 이 설정 모드를 인터리빙 모들(Interleaved mode)라고도 한다. 다른 모드와 달리 대량 INSERT 문장이 실행되는 중에도 다른 커넥션에서 INSERT를 수행할 수 있게 되므로 동시 처리 성능이 높아지게 된다. 이 설정에서 작동하는 자동 증가 기능은 유니크한 값이 생성된다는 것만 보장하며, 복제를 사용하는 경우에는 마스터와 슬레이브의 자동 증가 값이 달라질 가능성도 있기 때문에 주의해야 한다.

더 자세한 내용은 MySQL메뉴얼의 Configurable InnoDB Auto-Increment Locking 을 참조

 

 

 

* 참고도서 - RealMysql

'MySQL > MySQL' 카테고리의 다른 글

MySQL의 격리 수준  (0) 2024.12.06
트랜잭션 격리 수준과 잠금  (0) 2024.12.06
인덱스와 잠금  (0) 2024.12.06
InnoDB 아키텍처 요약  (0) 2024.12.04
sample script(emp, dept, salgrade)  (0) 2024.12.04

테이블구조

innodb의 모든 테이블은 PK를 기준으로 클러스터링되어 저장된다. 오라클의 IOT(index Organized Table)과 동일한 구조로 이다.

Buffer Pool

Innodb의 buffer Pool 크기를 설정하는 파라미터는 innodb_buffer_pool_size 이다.

Innodb의 buffer Pool 크기는 각 클라이언트 스레드가 사용할 메모리와 운영체제가 사용할 메모리를 제외하고 물리메모리의 50-80% 수준으로 권고된다.

Insert Buffer

Innodb는 Insert나 Update 작업으로 변경해야 할 인덱스 페이지가 버퍼 풀에 있으면 바로 변경하지만 디스크로부터 읽어와서 업데이트를 해야 한다면 임시공간에 저장해두고 바로 사용자에게 결과를 반환하는 형태로 성능을 향상시키는데 이때 사용하는 임시 메모리 공간을 Insert Buffer 라고 한다.

사용자에게 결과를 전달하기 전에 중복여부를 체크해야하는 Unique Index는 Insert Buffer를 사용할 수 없다.

인서트 버퍼에 임시로 저장되어 있는 인덱스 레코드조각은 이후 백그라운드 스레드에 의해 병합되는데 이 스레드를 인서트 버퍼 Merge thread 라고 한다. MySQL5.5부터는 Insert나 Delete로 인해 키를 추가하거나 삭제하는 작업에 대해서도 버퍼링이 된다.

Insert buffer는 Insert_change_buffering 설정 파라미터를 사용하여 사용을 제어한다

Redo로그 (로그 파일)

ACID를 보장하기 위해 변경된 내용을 순차적으로 디스크에 기록하는 로그 파일을 가지고 있다.

로그버퍼

사용량이 매우 많은 DBMS 서버의 경우에는 리두 로그의 기록 작업이 큰 문제가 되는데 이를 보완하기 위해 리두 로그를 버퍼링하게 되는데 이러한 공간을 로그 버퍼라고 한다.

로그 버퍼의 크기는 일반적으로 1-8MB 수준에서 설정하는 것이 적합한데, BLOB이나 TEXT와 같이 큰 데이터를 자주 변경하는 경우에는 더 크게 설정한다.

ACID

데이터베이스에서 트랜잭션의 무결성을 보장하기 위해 반드시 필요한 4가지 요소

A : Atomic, 트랙잭션은 원자성 작업이어야 한다.

C : Consistent, 일관성

I : Isolated, 격리성

D : Durable, 한번 저장된 데이터는 지속적으로 유지되어야 한다.

MVCC

레코드 레벨의 트랙잭션을 지원하는 DBMS가 제공하는 기능으로 잠금을 사용하지 않는 일관된 읽기를 제공한다.

InnoDB는 언두 로그를 이용해 이 기능을 구현한다.

멀티 버전이라 함은 하나의 레코드에 대해 여러 개의 버전이 동시에 관리된다는 의미다.

READ UNCOMMITTED

다른 트랜잭션에서 COMMIT 되지 않은 데이터들을 읽어올 수 있는 레벨이다

- COMMIT 되지 않은 신뢰할 수 없는 데이터를 읽어옴(dirty read)

- 한 트랜잭션에서 동일한 SELECT 쿼리의 결과가 다름(non-repeatable read)

- 이전의 SELECT 쿼리의 결과에 없던 row가 생김(phantom read)

READ COMMITTED

다른 트랜잭션에서 COMMIT 된 데이터만 읽어올 수 있는 level

READ COMMITTED 에서 각각의 SELECT 쿼리는 그때그때 최신의 snapshot을 구축하여 데이터를 읽는다

- 한 트랜잭션에서 동일한 SELECT 쿼리의 결과가 다름(non-repeatable read)

- 이전의 SELECT 쿼리의 결과에 없던 row가 생김(phantom read)

REPEATABLE READ

MySQL InnoDB의 기본 isolation level.(동시성과 안정성의 균형)

This is the default isolation level for InnoDB. Consistent reads within the same transaction read the snapshot established by the first read.

REPEATABLE READ 는 한 트랜잭션에서 처음 데이터를 읽어올 때 구축한 snapshot에서 모두 데이터를 읽어온다. 매번 SELECT 쿼리의 결과들이 항상 처음과 동일하고, phantom read도 발생하지 않는다.

 

*참고도서 : RealMySQL

'MySQL > MySQL' 카테고리의 다른 글

MySQL의 격리 수준  (0) 2024.12.06
트랜잭션 격리 수준과 잠금  (0) 2024.12.06
인덱스와 잠금  (0) 2024.12.06
트랜잭션 & Autoincrement  (0) 2024.12.04
sample script(emp, dept, salgrade)  (0) 2024.12.04

+ Recent posts