MySQL 8.0 におけるJSON型のpartial update、およびそれに対するInnoDBの最適化について

こんにちわ。せじまです。

InnoDB に関する話をします。今回は大半が MySQL Server BlogWorkLog のまとめ記事ですので、ゆるふわと言っていいでしょう。

JSON型の partial update に対する最適化というと、 binlog や replication にも関連するのですが、それについては今回は触れません。

はじめに

今回の内容は、MySQL Server Blog を熟読されている方であれば、すでにご存知のことが多いでしょう。ただ、「結局のところ、JSON型の partial update は、 InnoDB Adaptive Flushing においても有効なのか?」というところが疑問に残った方もいらっしゃるかもしれません。私はそう感じたので InnoDB のソースコード読んだのですが

  • MySQL 8.0.12、 WL#11328 で、差分だけ mlog_write_string() で書く修正が入ったみたいなので、更新内容が100byte以下のときは、 InnoDB Adaptive Flushing の観点に置いても最適化できているのでは。

といったように見えました。以上です。(ここだけ気になる方は、 InnoDB Adaptive Flushing に関するところまで読み飛ばしていただいても良いでしょう)

では、そこに至るまでの解説を始めます。

はじめにまとめ

  • MySQL8.0 のJSON型の partial updateは、かなりの最適化がなされている。 MySQL8.0 で JSON型を使う場合は、 partial update を活用することが強く推奨される。
  • JSON 内の key と value を如何に更新するかによって、 partial update の振る舞いは変わる。

  • valueのlengthが変わらない、あるいはvalueのlengthが短くなる場合においては、 partial update は有効である。

  • また、更新の差分が 100byte以下の場合は、 redo log および undo log の更新も最適化され、 InnoDB Adaptive Flushing についても、かなりの最適化が見込める
  • ただしこの100byteはLOBのoffsetなども含まれるので「アプリケーションからの更新が100byteまで最適化される」という意味ではない

  • 一方、 value の length が長くなる update 、あるいは存在しない key を追加した場合、LOBの再構築が実行される。

  • LOBの再構築が実行される場合、MVCCの都合上(Transactionをrollback可能にするために)、 *.ibd の内で、更新前のLOBと更新後のLOBを同時に保持する必要が発生する。

  • 更新前のLOBと更新後のLOBを同時に保持する必要が発生するため、 int のような固定長
    columnしか定義されていない table と比較し、 *.ibd 内のフラグメントはかなり進行すると考えられる。ただ、これについては MySQL5.7 以前から変わっていないことである。

Large object(LOB)

MySQL8.0 の InnoDB において、 JSON も BLOB も TEXT も、型としてはすべて等しく large object(LOB) となります。 MySQL 8.0 で JSON の partial update に対する最適化が入ったというのは、LOBに対する最適化が入ったからなのですが、 BLOB や TEXT には、差分更新するための syntax がありません。一方、 JSON には JSON_SET() や JSON_REPLACE() など、差分更新のための機能が入ってます。そういうこともあって、現在、 partial update への最適化が実現できているのは、 JSON 型だけなのでしょう。

MySQL 8.0 の InnoDB において、 LOB に対する最適化は、次の3つがあります。

では、順を追って見ていきましょう。

LOB の format 変更

ここから MySQL 8.0: InnoDB Introduces LOB Index For Faster Updates の抜粋になりますので、素直に、元の記事の図へのURLを張らせていただきます。

MySQL5.7 以前の large object は、一つの page に収まらないくらいサイズが大きかった場合、 Linked list として扱われていました。よって、一部分だけ変更することが難しかったようです。

Old Format of Uncompressed LOB

MySQL8.0 以降の large object は、一つの page に収まらないくらいサイズが大きかった場合、 Linked list ではなく、 index によって管理されるようになりました。

New Format of Uncompressed LOB

これにより、一部の page だけ更新することが容易になったようです。

undo log や MVCC の最適化

詳しくは MySQL 8.0: MVCC of Large Objects in InnoDB を参照していただくのが良いのですが、図を含めて抜粋させていただきます。

MySQL5.7以前では、 複数の page で構成される large object は、次のように full copy しないと更新できなかったようです。 undo log には page そのものではなく、 page のポインタが格納されていて、 rollback するときはポインタを切り替えれば良い、というわけですね。

Before UPDATE operation

After UPDATE operation

一方、 MySQL8.0 以降は、複数の page で構成される large object は、 full copy するのではなく、一部の page だけ差分更新できるようになったようです。

Before Patial Update operation

After Partial Update operation

ただ、value の length が長くなる update 、あるいは存在しない key を追加した場合、LOBの再構築が実行されます。LOBの再構築が実行される場合、MVCCの都合上(Transactionをrollback可能にするために)、 *.ibd の内で、更新前のLOBと更新後のLOBを同時に保持する必要が発生するので、MySQL 8.0 においても、 LOB の full copy は不可避なようです。

ここまで見ただけでも、かなり大掛かりな修正だと感じられますね。

100byte以下の partial update に対する最適化

いよいよ、MySQL 8.0: Optimizing Small Partial Update of LOB in InnoDB に話は移ります。 undo log へのさらなる最適化です。

通常、InnoDBにおいて、 LOB は巨大すぎるので、undo log に値は複製されません。更新前の page と更新後の page を *.ibd 内に用意して、 undo log はそのポインタを保持するだけです。よって、MySQL5.7では、更新前と更新後で、LOBを構成するすべてのpageの full copy が必要でした。
MySQL8.0 では、更新前と更新後で、更新対象の page のみ、 full copy が必要になるはずでした。

しかし、MySQL8.0 では、WL#11328 で、ソースコード中に lob::ref_t::LOB_SMALL_CHANGE_THRESHOLD という定数が定義されました(現状、値は100になっています。)

https://github.com/mysql/mysql-server/commit/4524b5f8da014b2d40932cc3c7095c3ac53dd282#diff-a47c6b584ad2bc5db86dc5a109ae21d1R185

MySQL8.0 では、LOBに対する(現状、JSON型のみに対する)100byte以下の更新は、undo log に update vector を保存するようになりました。これにより、差分更新の情報を undo log 内に保持できるようになったため、LOBやpageのfull copyが不要になったそうです(ただしこの100byteには、LOBのoffsetなども含まれます。実際にユーザが使えるのは数十byteでしょう)。
LOB_SMALL_CHANGE_THRESHOLD は、recordごとのThresholdではなく、LOBごとのThresholdだそうです。よって、一つのレコードに複数のLOBを定義することは、partial update のための最適化として、適切といえるのではないでしょうか。

では、 InnoDB Adaptive Flushing に対してはどうなのか

undo log や LOB の format については、 MySQL Server Blog で詳細な解説がありました。(今回は詳細に触れませんが)binlog についても、 binlog_row_value_options=PARTIAL_JSON というオプションが追加されました。では、 redo log にはどのような最適化の恩恵があるのでしょうか。

めんどくさいのでソースコードを読んでたら、わかりました。
MySQL8.0.12、 WL#11328 の commit で、 mtr に関する修正入っていました。

https://github.com/mysql/mysql-server/commit/4524b5f8da014b2d40932cc3c7095c3ac53dd282

ここでいう mtr が何かといいますと、”MySQL mtr” でググってヒットする “MySQL Test Framework(MTR)” ではありません。
InnoDB のソースコードの中では mtr(mini-transaction) と呼ばれるものがあり、これが redo log に書き出されています。詳しくは General idea of redo logなどでも解説されており、 ソースコード的には このあたり です。

結論から言うと、差分だけ mlog_write_string() で書いてるみたいなので、更新内容が100byte以下のとき、redo log の消費量を削減する最適化ができていると思われます。

更新内容が、先程出てきた ref_t::LOB_SMALL_CHANGE_THRESHOLD 以下だった場合、

https://github.com/mysql/mysql-server/blob/4524b5f8da014b2d40932cc3c7095c3ac53dd282/storage/innobase/lob/lob0update.cc#L106-L108

lob::update() で呼び出されるのは、 lob::replace() ではなく、 lob::replace_inline() に なります。

https://github.com/mysql/mysql-server/blob/4524b5f8da014b2d40932cc3c7095c3ac53dd282/storage/innobase/lob/lob0update.cc#L142-L147

lob::replace_inline() では、 lob::first_page_t::replace_inline() あるいは lob::data_page_t::replace_inline() が呼び出されるのですが
https://github.com/mysql/mysql-server/blob/4524b5f8da014b2d40932cc3c7095c3ac53dd282/storage/innobase/lob/lob0update.cc#L474
https://github.com/mysql/mysql-server/blob/4524b5f8da014b2d40932cc3c7095c3ac53dd282/storage/innobase/lob/lob0update.cc#L532-L546

何れにせよ、充分に小さい差分更新であれば、 mlog_write_string() で書き出されるのは LOB の full copy ではなく、差分だけになるわけです。

https://github.com/mysql/mysql-server/blob/4524b5f8da014b2d40932cc3c7095c3ac53dd282/storage/innobase/lob/lob0first.cc#L35-L46

https://github.com/mysql/mysql-server/blob/4524b5f8da014b2d40932cc3c7095c3ac53dd282/storage/innobase/lob/lob0pages.cc#L34-L48

おわりに

MySQL 8.0 では、 JSON 型の partial update を最適化するために、 InnoDB の LOB の format や、それに伴う undo log の修正まで入っていました。また、 MySQL Server Blog を読むだけでも、かなりの部分、理解できるようになりました。 InnoDB の開発チームは、このあたり、けっこう注力しているのだろうなぁと感じさせられます。

開発者はこれらに対するフィードバックに関心があるようで、例えば、MySQL 8.0: Optimizing Small Partial Update of LOB in InnoDB には、lob::ref_t::LOB_SMALL_CHANGE_THRESHOLD に対して次のような記述があります。

Note that this threshold is for one LOB. One record can have multiple LOBs and many of them can be partially updated at the same time. This threshold was a design decision that I had to make. It might not be an ideal value, but hopefully we can fine tune it later on, if such a need arises. If you have any suggestions from the field on how to optimally tune this value then I’m very interested to in your feedback.

何か良い方法を思いつかれた方は、 feature request を出されても良いかもしれません。

次回も、 MySQL に関する話をしたいと思います。

References