この記事は MySQL Advent Calendar 2020 の16日目の記事です。
MySQL Advent Calendar 2020 8日目、 lhfukamachi さんの foreign_key_checks
に関する記事を見て思い付いたものです。
システム変数のforeign_key_checks の話は上記の記事によくまとまっています。
この記事では、「じゃあ foreign_key_checks
はどういう挙動で外部キー制約をチェックしないような実装になっているのか」を説明します。興味がない方はここでタブを閉じてもらって大丈夫なんですがおいちょっと待てふかまち、君は閉じるな。
さて foreign_key_checks
がシステム変数(my.cnfや SET GLOBAL
で変更できるやつ)である以上、 sql/sys_vars.cc のどこかにある可能性が高いでしょう。
Ctrl + Fならブラウザでも検索できる時代です。簡単に見つかりました。
5121 static Sys_var_bit Sys_foreign_key_checks(
5122 "foreign_key_checks", "foreign_key_checks",
5123 HINT_UPDATEABLE SESSION_VAR(option_bits), NO_CMD_LINE,
5124 REVERSE(OPTION_NO_FOREIGN_KEY_CHECKS), DEFAULT(true), NO_MUTEX_GUARD,
5125 IN_BINLOG);
Sys_var_*
系の構造体は引数が多いので最初のうちは何が何やらですが、慣れれば脳死で読めるようになるので大丈夫です。
- https://github.com/mysql/mysql-server/blob/mysql-8.0.22/sql/sys_vars.h#L1659-L1666
ややこしいのはHINT_UPDATABLE SESSION_VAR
のマクロが展開されるとsys_var::HINT_UPDATEABLE + sys_var::SESSION, offsetof(System_variables, option_bits), sizeof(((System_variables *)0)->option_bits)
の3引数に化けることくらいでしょうか。昔、調べるのに難儀しましたが、事前に知っておけば次からは突っかからずに読めますね。 - https://github.com/mysql/mysql-server/blob/mysql-8.0.22/sql/sys_vars.h#L127
- https://github.com/mysql/mysql-server/blob/mysql-8.0.22/sql/sys_vars.h#L107-L109
大事なのは、「このシステム変数の本体が option_bits
であり、OPTION_NO_FOREIGN_KEY_CHECKS
の反転で表されるらしいこと」です。ついでですがこのオプションはバイナリログに記録されるので、外部キー制約があるテーブルに対して TRUNCATE
とかする時でも、「マスターで foreign_key_checks=0
していればレプリカでリプレイされる時にも foreign_key_checks=0
として振る舞」います。安心ですね。
OPTION_NO_FOREIGN_KEY_CHECKS
は2^26なので67108864らしいですがまあそれはどうでもいいですが、 sql/query_options.h にはその他にもいろいろなオプションが羅列されていて面白いです。 SELECT_HIGH_PRIORITY
とか OPTION_FOUND_ROWS
(!!) とか、 SELECT
文の修飾句は割とここらへんにいることが多いです。楽しいですね。
というわけで、 foreign_key_checks=0
は option_bitsに OPTION_NO_FOREIGN_KEY_CHECKS が立っている状態
、というのはわかりました。
となればあとは option_bits & OPTION_NO_FOREIGN_KEY_CHECKS
とかやっていそうなところを探せば実装に行きつけそうですね。調べてみると実際結構出てきます。
待つんだそこで満足げにブラウザを閉じようとしているふかまち。まだ世の中には面白いことがある。
思い出していただきたい、「外部キー制約はストレージエンジンの機能であって、MySQLサーバーのコア機能ではない」ことを。 option_bits
は基本的にサーバーコアが触るためのスレッド単位の変数なので、InnoDBはおそらくここをほとんど見ない(この記事書きながら調べたけど、NDBもそうで一度他のflagにここの値を写し取っている)
もう一度さっきの検索結果を見てみるのだ。驚くほど storage/innobase
のコードはないだろう? ほとんどがサーバーコアのコードだ。
GitHubの検索結果に出てこない理由は俺は知らないけれど、InnoDBとしての「外部キー制約をチェックしない」の起点はここだ。 innobase_trx_init
という如何にもトランザクションの開始時に呼ばれそうなところで、 trx->check_foreigns
というプロパティに詰めている。
………あれ? ってことはトランザクションの途中で SET SESSION foreign_key_checks = 0
とかやるとどうなるの?
mysql80 23> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql80 23> DELETE FROM parent;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`d2`.`child`, CONSTRAINT `child_ibfk_1` FOREIGN KEY (`id`) REFERENCES `parent` (`id`))
mysql80 23> SET SESSION foreign_key_checks = 0;
Query OK, 0 rows affected (0.00 sec)
mysql80 23> DELETE FROM parent;
Query OK, 2 rows affected (0.00 sec)
mysql80 23> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql80 23> SELECT * FROM parent;
+----+
| id |
+----+
| 1 |
| 2 |
+----+
2 rows in set (0.00 sec)
ちゃんと途中で変えても追随して動いた。これはどうも、Handlerの層からストレージエンジンの層に降りて来る時にInnoDB側でまだトランザクションを保持しているか確かめる(InnoDB側で保持していなかったら、サーバーコアの層に「ロールバックを知らせるエラー」を返すために) check_trx_exists
をしょっちゅう呼んでいて、その中でも innobase_trx_init
を呼んでいるからだ。
俺が最初に innobase_trx_init
の響きから期待したようなものはinnobase_trx_allocate
で、これも内部で innobase_trx_init
を呼んでいる。こっちがトランザクションの起点かな。
gdb
でアタッチしてとなりのコンソールから同じステートメントを流してみると、まあ大体合ってそう。
(gdb) b innobase_trx_allocate
+b innobase_trx_allocate
Breakpoint 1 at 0x21c8405: file /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc, line 2509.
(gdb) b check_trx_exists
+b check_trx_exists
Breakpoint 2 at 0x21d2c2b: check_trx_exists. (2 locations)
(gdb) c
+c
Continuing.
Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544 if (trx == nullptr) {
(gdb) c
+c
Continuing.
Breakpoint 1, innobase_trx_allocate (thd=thd@entry=0x68bbd40)
at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2509
2509 trx = trx_allocate_for_mysql();
(gdb) c
+c
Continuing.
Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544 if (trx == nullptr) {
(gdb) c
+c
Continuing.
Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544 if (trx == nullptr) {
(gdb) c
+c
Continuing.
Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544 if (trx == nullptr) {
(gdb) c
+c
Continuing.
Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544 if (trx == nullptr) {
..
あ、ちなみにここから先は row_ins_check_foreign_constraint
, row_ins_check_foreign_constraints
(複数形、ややこし…)あたりが trx->check_foreign == FALSE
の時は即座にreturnするってだけなのでセルフでお願いします。
それでは良い年末を!
0 件のコメント :
コメントを投稿