mita2 database life

主にMySQLに関するメモです

MySQL Shell dumpInstance の分割ロジック その1

この記事は MySQL Advent Calendar 2023 の21日目の記事です。

dumpInstance

dumpInstance は論理バックアップを取得する mysqlsh のコマンドです。 テーブルを分割し、並列でSELECTし、高速にバックアップしてくれます。

このように、1つのテーブルが複数のファイル(chunk)に分割されて出力されます。

$ ls -al ~/dump | grep pkchar | head
-rw-r----- 1 s-mitani s-mitani 2155324 Sep 11 13:12 t@pkchar_tbl@0.tsv
-rw-r----- 1 s-mitani s-mitani      24 Sep 11 13:12 t@pkchar_tbl@0.tsv.idx
-rw-r----- 1 s-mitani s-mitani 2155324 Sep 11 13:12 t@pkchar_tbl@10.tsv
-rw-r----- 1 s-mitani s-mitani      24 Sep 11 13:12 t@pkchar_tbl@10.tsv.idx
-rw-r----- 1 s-mitani s-mitani 2155324 Sep 11 13:12 t@pkchar_tbl@11.tsv
-rw-r----- 1 s-mitani s-mitani      24 Sep 11 13:12 t@pkchar_tbl@11.tsv.idx

バックアップ中は以下ような各chunkに対応するレンジ(WHERE句) が指定されたクエリが流れています。 このレンジはどのように決定されているのでしょうか?

SELECT SQL_NO_CACHE `pk_char`,`col1` FROM `t`.`pkchar_tbl`
WHERE ((
`pk_char`>='000000-00001')
AND
(`pk_char`<='000000-10000')
) ORDER BY `pk_char`;

AUTO_INCREMENTのように主キーが連続している数値の場合は、簡単に分割できそうです。 しかし、文字型の場合はどうしているのでしょうか。dumpInstance のロジックを追いかけてみました。

chunk あたりの行数を求める

chunk のサイズは --bytesPerChunk オプションで与えられます。

$ mysqlsh -- util dumpInstance dump --threads 4 --bytesPerChunk 3M

ここから、一行あたりのサイズ (average_row_length) を用いて、chunk あたりの行数を求めます。 average_row_length は統計情報に由来します。

info.rows_per_chunk = m_dumper->m_options.bytes_per_chunk() / average_row_length;

dumper.cc#L1508-L1509

以降、SELECT結果が rows_per_chunk 行に近くなるように、SELECT文のWHERE句(レンジ)を決めていきます。

キーの型によってロジックが分かれる

レンジを求めるロジックはPKの型によって異なっていました。 数値型の場合は chunk_integer_column、数値型でなければ chunk_non_integer_column が適用されます。

    if (mysqlshdk::db::Type::Integer == type ||
        mysqlshdk::db::Type::UInteger == type ||
        mysqlshdk::db::Type::Decimal == type) {
      return chunk_integer_column(info, begin, end);
    } else {
      return chunk_non_integer_column(info, begin, end);
    }
  }

dumper.cc#L1449-L1456

キーが数値型以外の場合 (chunk_non_integer_column)

キーが数値型でない場合のロジックは非常にシンプルです。LIMIT を用いて、線形にレンジの区切りをSELECTしていきます。

説明を簡単にするために、以下のように、連続した数値が含まれる、文字型のPKを仮定します。

mysql> desc pkchar_tbl;
+---------+--------------+------+-----+---------+-------+
| Field   | Type         | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| pk_char | varchar(200) | NO   | PRI | NULL    |       |
| col1    | varchar(255) | YES  |     | NULL    |       |
+---------+--------------+------+-----+---------+-------+
2 rows in set (0.01 sec)
mysql> SELECT pk_char FROM pkchar_tbl;
000000-00001
000000-00002
000000-00003
...

最小値と最大値を求めます。

SELECT SQL_NO_CACHE `pk_char` FROM `t`.`pkchar_tbl` ORDER BY `pk_char` LIMIT 1
SELECT SQL_NO_CACHE `pk_char` FROM `t`.`pkchar_tbl` ORDER BY `pk_char` DESC LIMIT 1

rows_per_chunk を LIMIT 句に指定し、線形にチャンクの区切りをSELECTしていく。 rows_per_chunk = 10000 とすると、最初の区切りは以下のSELECTで求められます。

SELECT SQL_NO_CACHE `pk_char` FROM `t`.`pkchar_tbl`
WHERE (`pk_char`>='000000-00001') -- PKの最小値からスタート
ORDER BY `pk_char` LIMIT 10000,2

> 000000-10000
> 000000-10001

これで、 000000-00001 〜 000000-10000 が 最初の chunk の範囲だとわかります。 また、 000000-10001 がその次の chunk の開始だとわかります。以下、同様に chunk の区切りを求めていく。

SELECT SQL_NO_CACHE `pk_char` FROM `t`.`pkchar_tbl`
WHERE (`pk_char`>='000000-10001')
ORDER BY `pk_char` LIMIT 10000,2

> 000000-20000
> 000000-20001

上記の結果からこのようなSELECT文が生成され、バックアップが作成されます。

SELECT SQL_NO_CACHE `pk_char`,`col1` FROM `t`.`pkchar_tbl`
WHERE ((
`pk_char`>='000000-00001')
AND
(`pk_char`<='000000-10000')
) ORDER BY `pk_char`;

SELECT SQL_NO_CACHE `pk_char`,`col1` FROM `t`.`pkchar_tbl`
WHERE ((
`pk_char`>='000000-10001')
AND
(`pk_char`<='000000-20000')
) ORDER BY `pk_char`;

キーが数値型の場合 (chunk_integer_column)

ということで、キーが数値型以外のケースについて見てみました。 長くなってしまったので、キーが数値型の場合については、別のエントリーにします!

つづく。。。

MySQL Advent Calendar 2023

明日は、@ikuosaito1989 さんです!

qiita.com