테스트 배경

지난 글 (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% 더 성능이 하락한 것 확인