테스트 배경
지난 글 (mysql instant , inplace DDL 디버깅) 에서 MySQL 8 버전에서는 inplace / instant add column 이 어떤식으로 수행되는지를 확인하고 비교해보았습니다
(add column 기준) inplace는 테이블을 다시 쓰고, instant ddl은 metadata, 정확히는 Data Dictinary 부분을 수정하기 때문에 inplace와 달리 DDL이 거의 즉시 완료됩니다.
그런데 만약 add column col1 varchar(10) default ‘hi’ 같은 컬럼 추가 DDL을 instant 방식으로 처리한다면 데이터를 어떻게 읽어오는걸까요 ?
DB에서 데이터를 읽을 땐 디스크에 저장된 page를 메모리에 올리고 page 내 필요한 record를 찾아가서 데이터를 읽게 되는데
inplace의 경우엔 DDL을 처리할 때 테이블과 page를 다시 rebuild 하니까 record에 default ‘hi’ 값이 자연스레 저장되겠지만
instant의 경우엔 rebuild를 통해 다시 쓰는 과정이 없습니다.
이번 글에서는 Instant add column으로 추가된 데이터를 MySQL이 어떻게 가져오는지,
그리고 MySQL 내부 처리 관련해서 Instant DDL 로 인해 달라진 부분이 있을텐데 이런 부분에서 성능 변화는 없을지를 확인해보겠습니다
테스트 결과
MySQL 8.0.32 버전 기준 instant (default value와 함께) 로 컬럼 추가한 테이블을 쿼리하는 경우, 테스트상으로는 아주 약간의 차이는 있었으나 유의미한 차이는 아니었다
아주 약간의 차이가 있었던 이유는 테스트 표본이 적어서일 수도 있으나- Instant add column으로 처리된 컬럼이 있는지 체크하고
- 데이터가 instant add column 이전에 들어온 것인지, 이후에 들어온것이지를 두번째 체크하고
- add column 이전에 들어온 데이터라면 record를 읽는 것 대신, data dictionary 로부터 default 값을 가져와서 처리하는 과정이 추가되었기 때문으로 추측된다
단, long query에 대해서는 성능 테스트를 하지 못했음 (ex. 1분 걸리는 쿼리가 1분 10초 걸린다던지 )
혹시나해서 찾아보니 비슷한 내용의 버그 리포트가 있다!
테스트 과정
테스트는 tb_inplace , tb_instant 각각의 테이블에 add column varchar(10) default ‘hihi’ 로 default 값을 가진 컬럼을 추가하고
데이터파일을 Hexdump 떠서 inplace / instant 로 page 내 record가 어떻게 저장되었는지를 확인했습니다
hexdump는 mysql의 datafile이 어떻게 생겼는지 조금이나마 볼 수 있는 유틸리티입니다.
이걸로 대충 데이터파일에 데이터가 어떻게 저장되는지 구경할 수 있습니다
hexdump로 inplace 일때와 instant 일 때 데이터 저장 방식에 있어서 차이가 있음을 확인하고,
그 후 gdb 로 instant add column이 반영된 테이블은 select 할 때 어떻게 처리되는지를 확인했습니다
inplace / instant 데이터 저장 방식 차이를 확인해보자
- 테이블을 생성한뒤
- 데이터를 몇건 넣어주고
- default 값을 가진 varchar() 컬럼을 각각 inplace / instant 로 추가해줍니다
- 그 후 데이터를 몇건 더 넣어줍니다
먼저 inplace 입니다
inplace 테스트
### 데이터 준비
create table inplace_test (a varchar(10) primary key);
insert into inplace_test(a) values('kimdubi1');
insert into inplace_test(a) values('kimdubi2');
insert into inplace_test(a) values('kimdubi3');
### hexdump 1차
mysql> select * from inplace_test;
+----------+
| a |
+----------+
| kimdubi1 |
| kimdubi2 |
| kimdubi3 |
+----------+
3 rows in set (0.00 sec)
00010060 02 00 1b 69 6e 66 69 6d 75 6d 00 04 00 0b 00 00 |...infimum......|
00010070 73 75 70 72 65 6d 75 6d 08 00 00 10 00 1b 6b 69 |supremum......ki|
00010080 6d 64 75 62 69 31 00 00 00 00 49 f3 81 00 00 00 |mdubi1....I.....|
00010090 c8 01 10 08 00 00 18 00 1b 6b 69 6d 64 75 62 69 |.........kimdubi|
000100a0 32 00 00 00 00 49 f4 82 00 00 00 c7 01 10 08 00 |2....I..........|
000100b0 00 20 ff bc 6b 69 6d 64 75 62 69 33 00 00 00 00 |. ..kimdubi3....|
000100c0 49 f8 81 00 00 00 ca 01 10 00 00 00 00 00 00 00 |I...............|
000100d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000100e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000100f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
### hexdump 2차
mysql> alter table inplace_test add column inplace_col1 varchar(10) default 'hihi',algorithm=inplace;
Query OK, 0 rows affected (0.10 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql>
mysql> select * from inplace_test;
+----------+--------------+
| a | inplace_col1 |
+----------+--------------+
| kimdubi1 | hihi |
| kimdubi2 | hihi |
| kimdubi3 | hihi |
+----------+--------------+
3 rows in set (0.00 sec)
00010060 02 00 1d 69 6e 66 69 6d 75 6d 00 04 00 0b 00 00 |...infimum......|
00010070 73 75 70 72 65 6d 75 6d 04 08 00 00 00 10 00 21 |supremum.......!|
00010080 6b 69 6d 64 75 62 69 31 00 00 00 00 49 f3 81 00 |kimdubi1....I...|
00010090 00 00 c8 01 10 68 69 68 69 04 08 00 00 00 18 00 |.....hihi.......|
000100a0 21 6b 69 6d 64 75 62 69 32 00 00 00 00 49 f4 82 |!kimdubi2....I..|
000100b0 00 00 00 c7 01 10 68 69 68 69 04 08 00 00 00 20 |......hihi..... |
000100c0 ff ae 6b 69 6d 64 75 62 69 33 00 00 00 00 49 f8 |..kimdubi3....I.|
000100d0 81 00 00 00 ca 01 10 68 69 68 69 00 00 00 00 00 |.......hihi.....|
000100e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000100f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
### hexdump 3차
mysql> insert into inplace_test(a) values('kimdubi4');
Query OK, 1 row affected (0.01 sec)
mysql> insert into inplace_test(a) values('kimdubi5');
Query OK, 1 row affected (0.03 sec)
mysql> insert into inplace_test(a) values('kimdubi6');
Query OK, 1 row affected (0.02 sec)
mysql> select * from inplace_test;
+----------+--------------+
| a | inplace_col1 |
+----------+--------------+
| kimdubi1 | hihi |
| kimdubi2 | hihi |
| kimdubi3 | hihi |
| kimdubi4 | hihi |
| kimdubi5 | hihi |
| kimdubi6 | hihi |
+----------+--------------+
6 rows in set (0.01 sec)
00010060 02 00 1d 69 6e 66 69 6d 75 6d 00 07 00 0b 00 00 |...infimum......|
00010070 73 75 70 72 65 6d 75 6d 04 08 00 00 00 10 00 21 |supremum.......!|
00010080 6b 69 6d 64 75 62 69 31 00 00 00 00 49 f3 81 00 |kimdubi1....I...|
00010090 00 00 c8 01 10 68 69 68 69 04 08 00 00 00 18 00 |.....hihi.......|
000100a0 21 6b 69 6d 64 75 62 69 32 00 00 00 00 49 f4 82 |!kimdubi2....I..|
000100b0 00 00 00 c7 01 10 68 69 68 69 04 08 00 00 00 20 |......hihi..... |
000100c0 00 21 6b 69 6d 64 75 62 69 33 00 00 00 00 49 f8 |.!kimdubi3....I.|
000100d0 81 00 00 00 ca 01 10 68 69 68 69 04 08 00 00 00 |.......hihi.....|
000100e0 28 00 21 6b 69 6d 64 75 62 69 34 00 00 00 00 4a |(.!kimdubi4....J|
000100f0 10 82 00 00 00 d1 01 10 68 69 68 69 04 08 00 00 |........hihi....|
00010100 00 30 00 21 6b 69 6d 64 75 62 69 35 00 00 00 00 |.0.!kimdubi5....|
00010110 4a 11 81 00 00 00 d3 01 10 68 69 68 69 04 08 00 |J........hihi...|
00010120 00 00 38 ff 4b 6b 69 6d 64 75 62 69 36 00 00 00 |..8.Kkimdubi6...|
00010130 00 4a 16 82 00 00 00 d3 01 10 68 69 68 69 00 00 |.J........hihi..|
00010140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00010150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00010160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
=> inplace의 경우 default ‘hihi’ 값이 컬럼 추가 이전의 데이터 이건, 이후의 데이터건 상관없이 다른 컬럼의 값들과 함께 레코드 형태로 물리적으로 같이 저장되는 것을 볼 수 있습니다
다음은 instant 입니다
instant
### 데이터 준비
create table instant_test (a varchar(10) primary key);
insert into instant_test(a) values('kimdubi1');
insert into instant_test(a) values('kimdubi2');
insert into instant_test(a) values('kimdubi3');
### hexdump 1차
mysql> select * from instant_test;
+----------+
| a |
+----------+
| kimdubi1 |
| kimdubi2 |
| kimdubi3 |
+----------+
3 rows in set (0.00 sec)
00010060 02 00 1b 69 6e 66 69 6d 75 6d 00 04 00 0b 00 00 |...infimum......|
00010070 73 75 70 72 65 6d 75 6d 08 00 00 10 00 1b 6b 69 |supremum......ki|
00010080 6d 64 75 62 69 31 00 00 00 00 4a 1f 82 00 00 00 |mdubi1....J.....|
00010090 d8 01 10 08 00 00 18 00 1b 6b 69 6d 64 75 62 69 |.........kimdubi|
000100a0 32 00 00 00 00 4a 20 81 00 00 00 da 01 10 08 00 |2....J .........|
000100b0 00 20 ff bc 6b 69 6d 64 75 62 69 33 00 00 00 00 |. ..kimdubi3....|
000100c0 4a 25 82 00 00 00 dc 01 10 00 00 00 00 00 00 00 |J%..............|
000100d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000100e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
### hexdump 2차
mysql> alter table instant_test add column instant_col1 varchar(10) default 'hihi',algorithm=instant;
Query OK, 0 rows affected (0.17 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> select * from instant_test;
+----------+--------------+
| a | instant_col1 |
+----------+--------------+
| kimdubi1 | hihi |
| kimdubi2 | hihi |
| kimdubi3 | hihi |
+----------+--------------+
3 rows in set (0.00 sec)
00010060 02 00 1b 69 6e 66 69 6d 75 6d 00 04 00 0b 00 00 |...infimum......|
00010070 73 75 70 72 65 6d 75 6d 08 00 00 10 00 1b 6b 69 |supremum......ki|
00010080 6d 64 75 62 69 31 00 00 00 00 4a 1f 82 00 00 00 |mdubi1....J.....|
00010090 d8 01 10 08 00 00 18 00 1b 6b 69 6d 64 75 62 69 |.........kimdubi|
000100a0 32 00 00 00 00 4a 20 81 00 00 00 da 01 10 08 00 |2....J .........|
000100b0 00 20 ff bc 6b 69 6d 64 75 62 69 33 00 00 00 00 |. ..kimdubi3....|
000100c0 4a 25 82 00 00 00 dc 01 10 00 00 00 00 00 00 00 |J%..............|
000100d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000100e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
### hexdump 3차
mysql> insert into instant_test(a) values('kimdubi4');
Query OK, 1 row affected (0.09 sec)
mysql> insert into instant_test(a) values('kimdubi5');
Query OK, 1 row affected (0.01 sec)
mysql> insert into instant_test(a) values('kimdubi6');
Query OK, 1 row affected (0.01 sec)
mysql> select * from instant_test;
+----------+--------------+
| a | instant_col1 |
+----------+--------------+
| kimdubi1 | hihi |
| kimdubi2 | hihi |
| kimdubi3 | hihi |
| kimdubi4 | hihi |
| kimdubi5 | hihi |
| kimdubi6 | hihi |
+----------+--------------+
6 rows in set (0.01 sec)
00010060 02 00 1b 69 6e 66 69 6d 75 6d 00 07 00 0b 00 00 |...infimum......|
00010070 73 75 70 72 65 6d 75 6d 08 00 00 10 00 1b 6b 69 |supremum......ki|
00010080 6d 64 75 62 69 31 00 00 00 00 4a 1f 82 00 00 00 |mdubi1....J.....|
00010090 d8 01 10 08 00 00 18 00 1b 6b 69 6d 64 75 62 69 |.........kimdubi|
000100a0 32 00 00 00 00 4a 20 81 00 00 00 da 01 10 08 00 |2....J .........|
000100b0 00 20 00 1e 6b 69 6d 64 75 62 69 33 00 00 00 00 |. ..kimdubi3....|
000100c0 4a 25 82 00 00 00 dc 01 10 04 08 00 01 40 00 28 |J%...........@.(|
000100d0 00 22 6b 69 6d 64 75 62 69 34 00 00 00 00 4a 2f |."kimdubi4....J/|
000100e0 81 00 00 00 e4 01 10 68 69 68 69 04 08 00 01 40 |.......hihi....@|
000100f0 00 30 00 22 6b 69 6d 64 75 62 69 35 00 00 00 00 |.0."kimdubi5....|
00010100 4a 30 82 00 00 00 e4 01 10 68 69 68 69 04 08 00 |J0.......hihi...|
00010110 01 40 00 38 ff 5a 6b 69 6d 64 75 62 69 36 00 00 |.@.8.Zkimdubi6..|
00010120 00 00 4a 35 81 00 00 00 e8 01 10 68 69 68 69 00 |..J5.......hihi.|
00010130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00010140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
=> Instant Add column의 경우 ,
default ‘hihi’ 값이 inplace와는 다르게 컬럼 추가 이전의 데이터는 datafile record에 저장되지 않고
컬럼 추가 이후에 들어온 데이터부터 datafile record에 저장되었습니다
정리하면
- inplace의 경우 default ‘hihi’ 값이 다른 컬럼의 값들과 함께 레코드 형태로 물리적으로 같이 저장되었음
- instant add column 이전에 생성된 데이터에 대해서는 default ‘hihi’ 값을 페이지 내 레코드에 저장되지 않아 찾을 수 없었습니다.
- instant add column 이후에 insert 된 데이터는 default ‘hihi’ 값이 기존과 마찬가지로 페이지 내 레코드에 잘 저장이 되었음
inplace / Instant 간 데이터 저장방식이 차이가 난다는 것을 확인했고
그중에서도 instant column 반영 전 데이터 / 반영 후 데이터 에서도 저장방식이 차이가 난다는 것을 확인했습니다
그렇다면 instant add column 이전, 이후의 데이터에 대해서 어떻게 처리하는지를 확인해보면 되겠습니다.
instant로 추가된 데이터를 어떻게 처리할까
1편과 마찬가지로 gdb 노가다를 통해 대략 아래와 같은 순서로 진행됨을 확인했습니다
rec_init_null_and_len_comp
if (!index->has_instant_cols())
*n_null = index->n_nullable;
else if (rec_get_instant_flag_new(rec) ==== > 여기서 걸리면 Instant add column 이후에 들어온 데이터
non_default_fields = rec_get_n_fields_instant
*n_null = index->get_n_nullable_before(non_default_fields);
get_rec_insert_state
else ==== > 여기서 걸리면 Instant add column 이전에 들어온 데이터
*n_null = index->n_instant_nullable;
non_default_fields = index->get_instant_fields();
get_rec_insert_state
row_sel_store_mysql_rec
for (i = 0; i < prebuilt->n_template; i++) ==== > 컬럼 개수만큼 순회하면서 innodb에서 가져온 데이터를 리턴할 수 있는 포맷으로 변경
row_sel_store_mysql_field
rec_get_nth_field_instant ====> instant ddl로 추가된 컬럼의 record 내 위치를 파악함
get_nth_default ==== > instant 컬럼으로 추가된 데이터의 default 값을 가져옴
row_sel_field_store_in_mysql_format_func
흐름을 간략하게 살펴보면,
rec_init_null_and_len_comp → rec_get_instant_flag_new 에서 해당 레코드에 instant column이 있는지를 확인합니다
그리고 get_rec_insert_state를 통해 instant add column이 수행된 시점에 따라 record의 종류를 나눕니다
이때 instant column은 크게 세종류(작게는 여섯개) 가 있는데 이를 구분하는데 쓰이는 함수입니다.
- Instant column이 없다
- Instant column이 추가된 이후에 저장된 레코드다 => 추가된 컬럼의 default 값이 기존과 동일하게 record에 저장되므로 다른 작업할 필요 없음
- Instant column이 추가되기 이전에 저장된 레코드다 => 추가된 컬럼의 default 값이 기존과 다르게 record가 아닌, 메타데이터에 저장되므로 별도의 작업이 필요함
그 후엔
row_sel_store_mysql_rec → row_sel_store_mysql_field 단계에서
innodb record 형태로 저장된 데이터를 mysql 엔진과 사용자가 읽고 이해할 수 있는 포맷으로 변경합니다.
특히 rec_get_nth_field_instant→ get_nth_default
이 부분이 이번에 궁금했던 default값이 있는 Instant add column 과 관련된 부분입니다.
이 단계에서는 instant + default 값이 있는 필드의 레코드상 위치를 파악하고, data dictionary에서 default 값을 가져오는 부분을 호출합니다
(사실 여기서 중요한 것은 record에서 열(column)의 위치를 bit를 확인하여 알아내는 부분, null인 컬럼의 위치를 bit를 확인하여 알아내는 부분 등 record의 bit를 확인하는 부분인데 이 부분이 해석이 불가능하여 포함하지 못했습니다 )
(gdb) b rec_init_null_and_len_comp
Breakpoint 1 at 0x4c81700: rec_init_null_and_len_comp. (2 locations)
(gdb) b rec_get_instant_flag_new
Breakpoint 2 at 0x4c50a48: rec_get_instant_flag_new. (4 locations)
(gdb) b rec_get_nth_field_instant
Breakpoint 3 at 0x4adaac8: rec_get_nth_field_instant. (11 locations)
(gdb) b row_sel_field_store_in_mysql_format_func
Breakpoint 4 at 0x4d1a594: file /mysql_source/mysql-8.0.32/storage/innobase/row/row0sel.cc, line 2507.
breakpoint 는 소박하게 위 단계만 찍어보았습니다.
- instant column 여부 확인
dict_index_t::has_instant_cols_or_row_versions (this=0xfffe7ca64058) at /mysql_source/mysql-8.0.32/storage/innobase/include/dict0mem.h:1357
1357 return (has_row_versions() || has_instant_cols());
1352 if (!is_clustered()) {
1353 ut_ad(!has_row_versions() && !has_instant_cols());
1354 return false;
1355 }
1356
1357 return (has_row_versions() || has_instant_cols());
1358 }
(gdb) p (has_row_versions() || has_instant_cols())
$6 = true
==> 테이블에 instant 컬럼이 있는지 확인합니다.
일단 instant column이 확인이 되었으니, 그 다음은 우리가 조회하는 record의 state를 확인하러 갑니다.
- record_state 확인
Breakpoint 1, rec_get_instant_flag_new (rec=0xffff2c5cc07e "kimdubi1") at /mysql_source/mysql-8.0.32/storage/innobase/rem/rec.h:623
(gdb) list
618
619 /** The following function tells if a new-style record is instant record.
620 @param[in] rec new-style record
621 @return true if it is instant affected */
622 static inline bool rec_get_instant_flag_new(const rec_t *rec) {
623 ulint info = rec_get_info_bits(rec, true);
624 return ((info & REC_INFO_INSTANT_FLAG) != 0);
625 }
627 /** The following function tells if a new-style temp record is instant record.
(gdb) n
624 return ((info & REC_INFO_INSTANT_FLAG) != 0);
(gdb) p (info & REC_INFO_INSTANT_FLAG)
$2 = 1
(gdb) p ((info & REC_INFO_INSTANT_FLAG) != 0)
$3 = true
===> 조회하려는 record에 instant_flag 가 설정되었습니다
get_rec_insert_state (index=0xfffe54a69d88, rec=0xffff2c5cc07e "kimdubi1", temp=false) at /mysql_source/mysql-8.0.32/storage/innobase/rem/rec.h:766
786 if (is_versioned) {
787 ut_a(is_valid_row_version(version));
788 if (version == 0) {
789 ut_ad(index->has_instant_cols());
790 rec_insert_state =
791 INSERTED_AFTER_UPGRADE_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION;
792 } else {
793 ut_ad(index->has_row_versions());
794 rec_insert_state = INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION;
795 }
796 } else if (is_instant) {
797 ut_ad(index->table->has_instant_cols());
798 rec_insert_state = INSERTED_AFTER_INSTANT_ADD_OLD_IMPLEMENTATION;
799 } else if (index->table->has_instant_cols()) {
800 rec_insert_state = INSERTED_BEFORE_INSTANT_ADD_OLD_IMPLEMENTATION;
(gdb) list
801 } else {
802 rec_insert_state = INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION;
803 }
806 return rec_insert_state;
(gdb) p rec_insert_state
$8 = INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION
(gdb) info locals
v_ptr = 0xffff2c5cc078 "\b"
is_versioned = false
version = 255 '\377'
is_instant = false
rec_insert_state = INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION
=> 조회하는 테이블과 record의 rec_insert_state 는 INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION 상태로
즉, instant add column이 수행되기 전에 insert 된 레코드로 상태가 확인됩니다.
위에서 instant add column의 hexdump 테스트에서 봤듯이
kimdubi1,kimdubi2,kimdubi3 의 값은 instant add column을 하기전에 추가된 레코드이므로 INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION 상태가 맞습니다.
그리고 이 상태값이 중요한 이유는 위에서 보았듯이
instant add column의 default 값은, 그전에 추가된 레코드에는 데이터파일에 반영되지 않고 그 이후에 추가된 레코드부터 데이터파일에 반영됩니다.
DB에서 이런 상태 차이를 나타낼 수 있는 것이 rec_insert_state로 추정됩니다
참고로 rec_insert_state는 아래와 같이 6종류가 있습니다
enum REC_INSERT_STATE {
/* Record was inserted before first instant add done in the earlier
implementation. */
INSERTED_BEFORE_INSTANT_ADD_OLD_IMPLEMENTATION,
/* Record was inserted after first instant add done in the earlier
implementation. */
INSERTED_AFTER_INSTANT_ADD_OLD_IMPLEMENTATION,
/* Record was inserted after upgrade but before first instant add done in the
new implementation. */
INSERTED_AFTER_UPGRADE_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION,
/* Record was inserted before first instant add/drop done in the new
implementation. */
INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION,
/* Record was inserted after first instant add/drop done in the new
implementation. */
INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION,
/* Record belongs to table with no verison no instant */
INSERTED_INTO_TABLE_WITH_NO_INSTANT_NO_VERSION,
NONE
};
- 쿼리 결과 데이터 변환 & instant ddl의 default 값 가져오는 부분
row_sel_store_mysql_field (mysql_rec=0xfffe54a651d0 "\377", prebuilt=0xfffe54b546d8, rec=0xffff2c5cc07e "kimdubi1", rec_index=0xfffe54a69d88,
prebuilt_index=0xfffe54a69d88, offsets=0xffff5812f760, field_no=0, templ=0xfffe54b6a830, sec_field_no=18446744073709551615,
lob_undo=0xfffe54b548f0, blob_heap=@0xfffe54b54828: 0x0) at /mysql_source/mysql-8.0.32/storage/innobase/row/row0sel.cc:2822
2822 if (len == UNIV_SQL_NULL) {
(gdb) info locals
_db_trace = {m_stack_frame = {
func = 0x6fadda5 "row_sel_store_mysql_rec(byte*, row_prebuilt_t*, const rec_t*, const dtuple_t*, bool, const dict_index_t*, const dict_index_t*, const ulint*, bool, lob::undo_vers_t*, mem_heap_t*&)", func_len = 23, file = 0x6facdb8 "/mysql_source/mysql-8.0.32/storage/innobase/row/row0sel.cc",
level = 2147483661, prev = 0xffff5812f188}}
__PRETTY_FUNCTION__ = "bool row_sel_store_mysql_field(byte*, row_prebuilt_t*, const rec_t*, const dict_index_t*, const dict_index_t*, const ulint*, ulint, const mysql_row_templ_t*, ulint, lob::undo_vers_t*, mem_heap_t*&)"
data = 0xffff2c5cc07e "kimdubi1"
len = 8
clust_field_no = 0
clust_templ_for_sec = false
=> a 컬럼 kimdubi1 레코드를 확인합니다
Breakpoint 3, rec_get_nth_field_instant (rec=0xffff43ffc07e "kimdubi1", offsets=0xffff783d1760, n=3, index=0xfffe7ca64058, len=0xffff783d1080)
at /mysql_source/mysql-8.0.32/storage/innobase/include/rem0rec.ic:1017
1017 if (index->has_row_versions()) {
(gdb)
1020 n = index->get_field_off_pos(n);
(gdb)
1024 ulint off = rec_get_nth_field_offs(nullptr, offsets, n, len);
(gdb)
1026 if (*len != UNIV_SQL_ADD_COL_DEFAULT && *len != UNIV_SQL_INSTANT_DROP_COL) {
(gdb)
1030 ut_a(index != nullptr);
(gdb)
1031 ut_ad(index->has_instant_cols_or_row_versions());
(gdb)
1033 if (*len == UNIV_SQL_INSTANT_DROP_COL) {
(gdb)
1038 return (index->get_nth_default(n, len));
(gdb)
1039 }
=> a 컬럼 kimdubi1 의 다른 컬럼 중 instant ddl 로 처리된 컬럼의 default 값을 구하기 위해 get_nth_default를 호출합니다.
dict_index_t::get_physical_field (this=0xfffe7ca64058, pos=3) at /mysql_source/mysql-8.0.32/storage/innobase/include/dict0mem.h:1492
1492 return get_field(fields_array[pos]);
(gdb) p *(get_field(fields_array[pos]))
$55 = {col = 0xfffe7ca7c3f8, name = {m_name = 0xfffe7ca7c53a "instant_col1"}, prefix_len = 0, fixed_len = 0, is_ascending = 1}
===> instant_col1 컬럼
rec_get_instant_offset (index=0xfffe7ca64058, n=3, offs=21) at /mysql_source/mysql-8.0.32/storage/innobase/rem/rec.h:495
495 index->get_nth_default(n, &length);
dict_index_t::get_nth_default (this=0xfffe7ca64058, nth=3, length=0xffff783d1058) at /mysql_source/mysql-8.0.32/storage/innobase/include/dict0mem.h:1586
1586 return (col->instant_default->value);
(gdb) p (col->instant_default->value)
$57 = (byte *) 0xfffe7ca7c5c0 "hihi"
(gdb) info locals
_db_trace = {m_stack_frame = {
func = 0x6fadda5 "row_sel_store_mysql_rec(byte*, row_prebuilt_t*, const rec_t*, const dtuple_t*, bool, const dict_index_t*, const dict_index_t*, const ulint*, bool, lob::undo_vers_t*, mem_heap_t*&)", func_len = 23, file = 0x6facdb8 "/mysql_source/mysql-8.0.32/storage/innobase/row/row0sel.cc",
level = 2147483661, prev = 0xffff5812f188}}
__PRETTY_FUNCTION__ = "bool row_sel_store_mysql_field(byte*, row_prebuilt_t*, const rec_t*, const dict_index_t*, const dict_index_t*, const ulint*, ulint, const mysql_row_templ_t*, ulint, lob::undo_vers_t*, mem_heap_t*&)"
data = 0xfffe54a76570 "hihi"
len = 4
clust_field_no = 0
clust_templ_for_sec = false
=> get_nth_default 에서는 default ‘hihi’가 지정된 instant_col1 컬럼의 값으로 hihi 를 잘 가져왔습니다.
이번엔 instant add column 이후에 추가된 kimdubi4를 조회해보겠습니다
instant add column 이전에 추가 된 kimdubi1 과는 다르게 처리되어야 정상입니다
- record_state 확인
rec_get_instant_flag_new (rec=0xffff3b5f4080 "kimdubi4") at /mysql_source/mysql-8.0.32/storage/innobase/rem/rec.h:624
624 return ((info & REC_INFO_INSTANT_FLAG) != 0);
(gdb) info locals
info = 0
(gdb) p (info & REC_INFO_INSTANT_FLAG)
$2 = 0
=> instant_flag가 꺼져있습니다
786 if (is_versioned) {
787 ut_a(is_valid_row_version(version));
(gdb) list
788 if (version == 0) {
789 ut_ad(index->has_instant_cols());
790 rec_insert_state =
791 INSERTED_AFTER_UPGRADE_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION;
792 } else {
793 ut_ad(index->has_row_versions());
794 rec_insert_state = INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION;
795 }
796 } else if (is_instant) {
797 ut_ad(index->table->has_instant_cols());
(gdb) list
798 rec_insert_state = INSERTED_AFTER_INSTANT_ADD_OLD_IMPLEMENTATION;
799 } else if (index->table->has_instant_cols()) {
800 rec_insert_state = INSERTED_BEFORE_INSTANT_ADD_OLD_IMPLEMENTATION;
801 } else {
802 rec_insert_state = INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION;
803 }
804
805 ut_ad(rec_insert_state != REC_INSERT_STATE::NONE);
806 return rec_insert_state;
807 }
(gdb) p rec_insert_state
$8 = INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION
(gdb) info locals
v_ptr = 0xffff8411407b "\001@"
is_versioned = true
version = 1 '\001'
is_instant = false
rec_insert_state = INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION
=> 기대했던 대로 이번엔 rec_insert_state = INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION 입니다
이름만 보면 record의 state가 instant add column 이후의 값으로 보입니다
- 쿼리 결과 데이터 변환 & instant ddl의 default 값 가져오는 부분
Breakpoint 3, rec_get_nth_field_instant (rec=0xffff84114081 "kimdubi4", offsets=0xffff783d1760, n=0, index=0xfffe7cd05438, len=0xffff783d1080)
at /mysql_source/mysql-8.0.32/storage/innobase/include/rem0rec.ic:1017
1017 if (index->has_row_versions()) {
(gdb)
1020 n = index->get_field_off_pos(n);
1015 const dict_index_t *index,
1016 ulint *len) {
1017 if (index->has_row_versions()) {
1018 /* Fields are stored in record in order of the version they are added in.
1019 Get the field number on physical record. */
1020 n = index->get_field_off_pos(n);
1021 }
1022
1023 /* nullptr for index as n is physical here */
1024 ulint off = rec_get_nth_field_offs(nullptr, offsets, n, len);
1025
1026 if (*len != UNIV_SQL_ADD_COL_DEFAULT && *len != UNIV_SQL_INSTANT_DROP_COL) {
1027 return (rec + off);
1028 }
(gdb) n
1024 ulint off = rec_get_nth_field_offs(nullptr, offsets, n, len);
(gdb)
1026 if (*len != UNIV_SQL_ADD_COL_DEFAULT && *len != UNIV_SQL_INSTANT_DROP_COL) {
(gdb)
1027 return (rec + off);
(gdb)
1039 }
=> rec_get_nth_field_instant 여기까지 왔지만,
앞서 본 INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION 케이스와는 다르게 무언가 record 의 offset 까지만 확인하는 것 같습니다.
1029
1030 ut_a(index != nullptr);
1031 ut_ad(index->has_instant_cols_or_row_versions());
1032
1033 if (*len == UNIV_SQL_INSTANT_DROP_COL) {
1034 *len = 0;
1035 return nullptr;
1036 }
1037
1038 return (index->get_nth_default(n, len));
1039 }
=> return (index->get_nth_default(n, len)); 즉 get_nth_default 까지 호출되지 않습니다
지금 까지를 정리하면!
Instant column이 추가되기 이전에 저장된 레코드다
- 추가된 컬럼의 default 값이 기존과 다르게 record가 아닌, 메타데이터에 저장되므로 이 값을 가져오기 위해 별도의 작업이 필요함
- rec_state 가 INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION 임을 확인하고,
- rec_get_nth_field_instant -> get_nth_default 를 통해 instant add column default 의 값을 가져온다
Instant column이 추가된 이후에 저장된 레코드다
- 추가된 컬럼의 default 값이 기존과 동일하게 record에 저장되므로 다른 작업할 필요 없음
- rec_state 가INSERTED_AFTER_INSTANT_ADD_NEW_IMPLEMENTATION 임을 확인하고,
- rec_get_nth_field_instant에서 instant add column 된 컬럼의 offset (레코드 내 해당 컬럼 위치) 까지만 확인함
이렇게 동작이 조금 다르고,
INSERTED_BEFORE_INSTANT_ADD_NEW_IMPLEMENTATION 에 대해서는 몇단계 추가적인 스텝이 있는 것으로 보입니다
그러면 이러한 부분들이 성능에는 영향이 없을까? 그것이 또 궁금해지는데요
그래서 성능 테스트도 해보았습니다
성능테스트
- 각각 tb_inplace, tb_instant 테이블에 데이터를 1000만건 넣어두고
- add column t1 varchar(10) default ‘hihi’ 컬럼을 각각 algorithm= inplace, instant 로 추가한다
- 그 후 tb_inplace, tb_instant 테이블을 select 하는 성능테스트를 한다.
테스트 결과 예상은 tb_inplace는 record state 관련, rec_get_nth_field_instant -> get_nth_default 이런 단계를 거치지 않을 것이고
tb_instant 는 모두 체크하는 단계가 있으니 tb_instant 가 조금 더 느릴 것이다
- 테스트 준비
### 테이블 스키마
CREATE TABLE `tb_inplace` (
`id` bigint NOT NULL AUTO_INCREMENT,
`group_id` int,
`index_value` varchar(17),
`col_a` varchar(10),
`col_b` varchar(10),
`col_c` varchar(10),
`col_d` varchar(10),
`col_e` varchar(10),
PRIMARY KEY (`id`),
KEY `IX_colA` (`index_value`)
);
### 데이터 세팅
mysqlslap --user=master -p --host=kimdubi-test.cluster-ckwx7ipq1exp.ap-northeast-2.rds.amazonaws.com --port=6025 --concurrency=50 --number-of-queries=10133611 --query="insert into tb_inplace (group_id, index_value, col_a, col_b, col_c, col_d, col_e ) values (round(rand()*10), unix_timestamp(now(6)), 'aaa','bbb','ccc','ddd','eee');"
### inplace로 default 컬럼 추가
alter table tb_inplace
add column t1 varchar(10) default 'hihi',
add column t2 varchar(10) default 'hihi',
add column t3 varchar(10) default 'hihi',
add column t4 varchar(10) default 'hihi',
add column t5 varchar(10) default 'hihi',
add column t6 varchar(10) default 'hihi',
add column t7 varchar(10) default 'hihi',
add column t8 varchar(10) default 'hihi',
add column t9 varchar(10) default 'hihi',
add column t10 varchar(10) default 'hihi',
add column t11 varchar(10) default 'hihi',
add column t12 varchar(10) default 'hihi',
add column t13 varchar(10) default 'hihi',
add column t14 varchar(10) default 'hihi',
add column t15 varchar(10) default 'hihi',
add column t16 varchar(10) default 'hihi',
add column t17 varchar(10) default 'hihi',
add column t18 varchar(10) default 'hihi',
add column t19 varchar(10) default 'hihi',
add column t20 varchar(10) default 'hihi',algorithm=inplace,lock=none;
### 테이블 스키마
CREATE TABLE `tb_instant` (
`id` bigint NOT NULL AUTO_INCREMENT,
`group_id` int,
`index_value` varchar(17),
`col_a` varchar(10),
`col_b` varchar(10),
`col_c` varchar(10),
`col_d` varchar(10),
`col_e` varchar(10),
PRIMARY KEY (`id`),
KEY `IX_colA` (`index_value`)
);
### 데이터 세팅
mysqlslap --user=master -p --host=kimdubi-test.cluster-ckwx7ipq1exp.ap-northeast-2.rds.amazonaws.com --port=6025 --concurrency=50 --number-of-queries=10133611 --query="insert into tb_instant (group_id, index_value, col_a, col_b, col_c, col_d, col_e ) values (round(rand()*10), unix_timestamp(now(6)), 'aaa','bbb','ccc','ddd','eee');"
### instant로 default 컬럼 추가
alter table tb_instant
add column t1 varchar(10) default 'hihi',
add column t2 varchar(10) default 'hihi',
add column t3 varchar(10) default 'hihi',
add column t4 varchar(10) default 'hihi',
add column t5 varchar(10) default 'hihi',
add column t6 varchar(10) default 'hihi',
add column t7 varchar(10) default 'hihi',
add column t8 varchar(10) default 'hihi',
add column t9 varchar(10) default 'hihi',
add column t10 varchar(10) default 'hihi',
add column t11 varchar(10) default 'hihi',
add column t12 varchar(10) default 'hihi',
add column t13 varchar(10) default 'hihi',
add column t14 varchar(10) default 'hihi',
add column t15 varchar(10) default 'hihi',
add column t16 varchar(10) default 'hihi',
add column t17 varchar(10) default 'hihi',
add column t18 varchar(10) default 'hihi',
add column t19 varchar(10) default 'hihi',
add column t20 varchar(10) default 'hihi',algorithm=instant;
### 테스트 커맨드
mysqlslap --user=master -pqhdks123 --host=kimdubi-test.cluster-ckwx7ipq1exp.ap-northeast-2.rds.amazonaws.com --port=6025 --concurrency=50 --iterations=1000 --query="SET @rand_id = FLOOR(1 + (10000000 * RAND())); SELECT * FROM mysqlslap.tb_instant WHERE id = @rand_id;" --delimiter=";" >> instant.txt
=> 동시에 50개의 쓰레드로 select * from tb_instant where id =? 쿼리를 50개 쓰레드 * 각 쓰레드당 기본10번 * 반복 1000번 으로 500,000 번 수행하고 평균 수행시간을 기록함
1회차(단위 초) 2회차 3회차 4회차 5회차 6회차 7회차 8회차 9회차 10회차 avg
inplace 0.244 0.232 0.225 0.224 0.224 0.223 0.219 0.221 0.222 0.227 0.2261
instant 0.244 0.231 0.229 0.255 0.247 0.232 0.256 0.248 0.240 0.227 0.2409
=> inplace일 때 평균 0.2261 초, instant 일때 평균 0.2409초로 instant 일때 6.55% 더 성능이 하락한 것 확인