こまぶろ

技術のこととか仕事のこととか。

AWS Certified Solutions Architect - Associate (SAA-C02) を自宅受験(ピアソンVue)して合格した

掲題の通り、AWS Certified Solutions Architect - Associate (SAA-C02) に合格しました。ピアソンVueでの自宅受験が可能になっていたので、自宅で受験しました。

今記事では、これから受験しようかなと思っている人向けに、自分がどんな勉強をしてどうだったのかと、自宅受験がどうだったのかを書きます。

勉強を始める前の状況

AWSは業務で利用していて、IAM、EC2、RDS、S3、Lambda...などといったメインどころのサービスがどんな感じかというのはわかっている状態でした。ただ、管理者ではなく開発者という立場であり、また新規でアーキテクチャ設計をするような場面は少なかったので、知らないサービスもたくさんあるし、主なサービスについても詳細までわかっているというのからは程遠かったです。GCPやAzureといったAWS以外のパブリッククラウドについてはマトモに触ったことはありませんでした。

AWSの各種サービスを理解する前提となる一般的な知識については、普通にソフトウェア開発を仕事で数年やってきて得てきたものが役立ちましたねという程度で、(IT未経験だったり、Web系未経験だったりの人よりは流石に有利でしょうけど)Webの会社で仕事をしている人たちの中で自分が特別何か有利な立場だったというのは(AWSを業務で使っている以外だと)なかった気がします。

受験までにやったこと

これから受験する人の参考に、受験までにやったことを書いておきます。

その前に、以下の勉強をして何点取れたのか

得点の範囲は100~1000点で、合格ラインが720点のところ、873点でした。

オーバーキルはオーバーキルですが、完璧ではないという感じですね。本番65問中で「違うかも」と思ったのが19問、「これわかんねえ」と思ったのが10問弱あったでしょうか。とはいえ、完全に知識不足でわからないという感じだったのは5問なかったくらいで、あとは「問題文・選択肢がもっと丁寧に書いてあればわかるのに〜」という感じだったので、もっと勉強すれば安心して合格できたかというと微妙です。ちなみに、問題文・選択肢を日本語で一読してよくわからず、英語の方で確認したことで「ああ、そういうことね」と回答を選ぶことができた、という問題も数問ありました。

どういうふうに勉強をしていったか

受験を決めた翌日に公式の模擬試験(無料)を解き、まずはゴールとの距離を測ろうとしました。模擬試験は本番よりも難度が低いという話だったのですが、ここで45%しか取れなかったため、「こりゃあかんわ」と感じ、真面目に勉強することを決意しました。

基本は、Udemyの講義をまず(倍速で)1周してベースの知識を抑えて、あとはひたすら問題を解いていきました。公式ドキュメントやホワイトペーパーなどは問題の解説を読んでいてもう少し情報が欲しかったときにのみ参照する程度で、またハンズオンも特にやりませんでした(Udemyの講義にはハンズオンパートがあり、それを見て満足していた)。

受験を決めてから本番までの約1ヶ月半のうち、最初の10日ほどで講義を1周、その後25日ほどかけて問題集(全12回分)を解き、残りの期間で問題集の見直し(時間の都合で全部はできず、半分のみ)をしました。

懸念していたのは、解いている問題集の傾向が本番の傾向とは乖離していて本番で歯が立たないという展開だったので、問題集を2種類(どちらもUdemy)買ってリスクを抑えることにしました。情報処理技術者試験などのように本番の過去問が相当量手に入る試験なら方針は違っただろうなと思います。

途中から始めてよかったなと思うのは、Miroにマインドマップ形式でとにかく情報を書き込んでいったことです。動画講義を見るだけ&選択式の問題を解くだけだと、知識を整理する機会がないのですが、Miroに書いていくことで整理ができました。また、出来上がったMiroは直前に見直す際にも非常に役立ちました。

f:id:ky_yk_d:20220319205007p:plain
Miro に書いていったもの(※ズームしても中身は見えません)

使った教材

ちょうど受験を決めたタイミングでUdemyがセール中だったので、勢いで講義のものを1本、問題集を2種類(各6回分で、合計12回分)購入しました。それ以外には、書籍を1冊購入しましたが、これはざっと1周読んだ程度でした。それぞれの教材について個別にコメントします。

講義:「【世界で40万人が受講】AWS 認定ソリューションアーキテクト アソシエイト SAA-C02 対応 2022 最新版」

www.udemy.com

メインにした教材。Udemyだと他に日本人の講師オリジナルのものがあり、そちらの方が人気っぽかったのですが、サンプル動画の印象がこちらの方がよかったのでこちらを選択しました。

全部で25.5時間で、視聴しはじめたときはまだ20時間分くらいまでしか公開されていなかったのですが、受験当日までに24.5/25.5時間まで公開されていたので、困ることはありませんでした(ちなみに、3月末に全部公開される予定とのことです)。

オリジナルは英語なのですが、こちらはその日本語版ということで、スライドも講義もハンズオンも全て日本語になっています。講義パートとハンズオンパートが交互に繰り返されながら進んでいく形で、説明もわかりやすく、非常に良い内容だったと思います。

使い方としては、前述のように最初に1周してしまって特に2周目というのは取り組まなかったのですが、試験当日は試験が午後だったので、理解・記憶が曖昧な部分を見直して午前を過ごしました。

問題集A:「【世界で3万人が受講】演習テスト: AWS 認定 ソリューションアーキテクトアソシエイト」

www.udemy.com

前記の講義と同じシリーズの問題集。日本語化を担当している方は講義の方とは違うのですが、オリジナルの方は同じ講師の監修ということで講義内容と問題の内容にリンクがあるように感じました。

解いた日 問題番号 正答率
2/13 1 61%
2/15 2 67%
2/20 3 66%
3/5 4 72%
3/6 5 83%
3/12 6 73%

問題集B:「【2022年版】AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問)」

www.udemy.com

日本では一番人気があるようだった講義コースのシリーズの問題集。前述のように問題集が1種類では不安だったので購入しました。高難度とうたっており、特に後半は難しい問題で構成されていて、以下のように点数は伸びませんでした。講師の人も「この問題集で合格点とれなくても受かっている人はいる」と書かれていたので、こんなもんなのだと思われます。

解いた日 問題番号 正答率
2/13 1 75%
2/14 2 70%
2/18 3 78%
2/23 4 67%
3/5 5 67%
3/6 6 64%

書籍:「AWS認定資格試験テキスト AWS認定ソリューションアーキテクト - アソシエイト 改訂第2版」

受験が終わった後も参照できるように1冊ぐらいは持っておいてもよいだろうということで購入しました。1周はざっと読んで、最後の問題集も解きましたが、書籍だけで合格できるという類のものではない(この本にもそう書いてある)ですし、あくまで補う意味でのものでした。

Udemy の講義では言及されておらず、この本では言及されている項目というのもいくつかあったので、無意味ではありませんでしたが、なくてもよかったかなとは思います。

自宅受験どう?

自宅受験の手順・注意点については、既にたくさんレポート記事もあるので、自分も参考にさせてもらい、大きなトラブルなく予約〜受験することができました。以下に参考にした記事をいくつか貼ります。

blog.usize-tech.com

dev.classmethod.jp

事前に記事などを確認しておいたおかげで助かった点を挙げておくと、以下があります。

  • 書籍が手に届く範囲にあるのはNGだと早めにわかったこと。自室(本まみれ)での受験を早々に諦め、他の家族の使っている部屋を当日は使えるように前もって準備できた。
  • (ガイドラインでも避けろと言われているのですが)会社貸与PCは使ってはいけないということ。当初は「いやいや会社PCでも大丈夫でしょ〜」と甘く考えていたが、本当にトラブルが発生することを自分の少し前に受験しようとした知人も実証してくれていたので、ちゃんと私物PCで受験した。

自宅受験は非常に便利なのですが、自分は長時間じっとしていられず、体を動かしたくなってしまうので、余計な動きでカンニングの疑いを持たれないかという不安もあってやりづらい部分がありました(笑)

受験を終えての感想

というわけで、AWS Certified Solutions Architect - Associate (SAA-C02) を無事取得できました。受験しようと思ったのは、新しいシステムの構築や既にAWSで動いているシステムの改善のためにどのようにAWSのサービスを使っていけばいいのかを知っておきたかったからで、その目的は一定程度果たすことができたのではないかと思います(もちろん、これからどれだけ活かせるかが大事なのですが)。

とはいえ、試験勉強が終始楽しかったかというとそれは違います。最初に講義を視聴していたときは、新しい知識がどんどん得られている感覚があり非常に楽しかったのですが、問題を解き始めてからは「こんなん覚えなくても公式ドキュメント見れば書いてあるやん」などとブツブツ言いながら勉強していました(笑) 途中からは「落ちても絶対再受験しないわ」と決意していました。

認定資格には3年の有効期限がありますし、他のアソシエイト資格や、プロフェッショナル資格、専門知識資格などまだたくさんあり、それぞれに勉強するとそれなりに得るものはありそうだなという思いはあるものの、正直もう受験はいいかなぁというのが今の思いです。試験対策向けの教材には実務にも役立つものもあるというのがわかったので、そういう付き合い方をしていければなと思っています。

MySQL 8.0.20 以前でサブクエリの結果をINで受けるDELETE文を書いたらテーブルスキャンになった話:MySQL 8.0.21 で有効になった最適化の検証

この記事はなに?

MySQL 8.0.20 以前でサブクエリの結果をINで受けるDELETE文(後述)を書いたところ、意図に反して削除対象のテーブルが全行スキャンされてしまいました。この挙動が、MySQL 8.0.21 で有効になった最適化によって解消するという情報を得た*1ので、検証してみた結果を書きます。

題材は以下のようになっているテーブルです。親 parents テーブルの主キーを参照している子 children テーブルがあり、親テーブルには主キーとは別にインデックスが効くテーブルがある場面です。

上記のようなテーブルに対して、以下のようなDELETE文を発行したいとします。親テーブルの主キーでないカラム(インデックスを持つ)が特定の値(下の例では3)であるようなレコード、に紐づく子テーブルのレコードを削除する、というDELETEですね。今回は、このときの実行計画とロックの取り方を検証します。

ちなみに、以下のDELETE文ではサブクエリの結果を使って IN で比較していますが、ここが = 比較の場合は挙動がだいぶ変わってきます(バージョンの新旧、最適化の有無に関わらず type: range の比較的平和な実行計画になります)。そのケースについては本記事のスコープ外です。

DELETE FROM children WHERE parent_id IN
(SELECT parent_id FROM  parents WHERE parent_idx_col = 3);

使用する MySQL のバージョンは、以下の通りです。

  • MySQL 8.0.20
  • MySQL 8.0.21

まとめ

そこそこ長いので、まとめを先に書いておきます。

  • DELETE(UPDATE)にINで受けるサブクエリを使ったときの動きが MySQL 8.0.21 で変わったらしいので見てみたよ
  • MySQL 8.0.20 だとDELETE対象のテーブルが先に評価されて全行スキャン、サブクエリは相関サブクエリ扱いになったよ
    • パフォーマンスが悪いよ & ロックもめっちゃかかるよ
  • MySQL 8.0.21 だとサブクエリが先に評価されて、その結果を使ってDELETE対象のテーブルをインデックスで検索しにいけるよ
    • パフォーマンスがいいよ & ロックもだいぶ減っているよ
    • semijoin の最適化が効いている結果みたいだよ
  • MySQL 8.0.20 以前を使っているなら更新系 SQLにINで受けるサブクエリを書くときは注意!
    • JOIN を使った書き方にするなどしましょう

目次

実験用のテーブルのDDL

親テーブル

CREATE TABLE parents (
  `parent_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `parent_idx_col` int(10) unsigned NOT NULL,
  PRIMARY KEY (`parent_id`),
  UNIQUE KEY `uk_parent_idx_col` (`parent_idx_col`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

子テーブル

CREATE TABLE children (
  `child_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `parent_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`child_id`),
  CONSTRAINT `fk_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `parents` (`parent_id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

データ

parents

parent_id parent_idx_col
1 1
2 2
3 3
4 4
5 5

children

child_id parent_id
1 1
2 1
3 2
4 2
5 3
6 3
7 4
8 4
9 5
10 5

実験の流れ

以下の設定にしておきます。

SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;

トランザクションを貼ってから以下のDELETE文の(実行計画も取得)実行します。

EXPLAIN DELETE FROM children WHERE parent_id IN
(SELECT parent_id FROM  parents WHERE parent_idx_col = 3);
DELETE FROM children WHERE parent_id IN
(SELECT parent_id FROM  parents WHERE parent_idx_col = 3);

トランザクションを commit せずに、以下でロックの状態を確認します。

SHOW ENGINE INNODB STATUS;

この流れを、MySQLのバージョンを変えて実施します。

MySQL 8.0.20 の場合

まず、変更前の MySQL 8.0.20 で試します。

実行計画

いかにも遅そうな実行計画になりました。

  • 子テーブルが先に評価され、 type: ALL(テーブルスキャン)
  • 親テーブルは DEPENDENT SUBQUERY (相関サブクエリ)扱い
mysql> EXPLAIN DELETE FROM children WHERE parent_id IN
    -> (SELECT parent_id FROM  parents WHERE parent_idx_col = 3);
+----+--------------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+
| id | select_type        | table    | partitions | type  | possible_keys             | key               | key_len | ref   | rows | filtered | Extra       |
+----+--------------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+
|  1 | DELETE             | children | NULL       | ALL   | NULL                      | NULL              | NULL    | NULL  |   10 |   100.00 | Using where |
|  2 | DEPENDENT SUBQUERY | parents  | NULL       | const | PRIMARY,uk_parent_idx_col | uk_parent_idx_col | 4       | const |    1 |   100.00 | Using index |
+----+--------------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+

SHOW ENGINE INNODB STATUS;

children テーブルは全行+ 上限行への排他のネクストキーロックがかかりました。

parent テーブルは検索対象の行だけが共有ロック対象になっています。

  • 12行のロック
  • children テーブルに IX ロック
    • 主キーのインデックスにネクストキーロックがかかる
    • 11行
      • supremum (上限を意味する仮想的な行)
      • 10行(=レコード行数)
        • 00000001:1
        • 0000000a : 10(=末尾のレコードのID)
  • parents テーブルに IS ロック
    • UNIQUEキーのインデックスにレコードロック locks rec but not gap がかかる
    • 1行
      • ID = 3 :検索対象のレコード

抜粋

トランザクションのロック全体

---TRANSACTION 2107, ACTIVE 79 sec
4 lock struct(s), heap size 1136, 12 row lock(s), undo log entries 2
MySQL thread id 9, OS thread handle 140137874208512, query id 50 localhost root starting

子テーブルへのロック

TABLE LOCK table `tmp`.`children` trx id 2107 lock mode IX
RECORD LOCKS space id 3 page no 4 n bits 80 index PRIMARY of table `tmp`.`children` trx id 2107 lock_mode X

親テーブルへのロック

TABLE LOCK table `tmp`.`parents` trx id 2107 lock mode IS
RECORD LOCKS space id 2 page no 5 n bits 72 index uk_parent_idx_col of table `tmp`.`parents` trx id 2107 lock mode S locks rec but not gap

全体

=====================================
2022-01-10 01:02:18 0x7f7464363700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 11 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 20 srv_active, 0 srv_shutdown, 2655 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 31
OS WAIT ARRAY INFO: signal count 24
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 2112
Purge done for trx's n:o < 2112 undo n:o < 0 state: running but idle
History list length 15
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421612870740520, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421612870739672, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 2107, ACTIVE 79 sec
4 lock struct(s), heap size 1136, 12 row lock(s), undo log entries 2
MySQL thread id 9, OS thread handle 140137874208512, query id 50 localhost root starting
show engine innodb status
TABLE LOCK table `tmp`.`parents` trx id 2107 lock mode IS
RECORD LOCKS space id 2 page no 5 n bits 72 index uk_parent_idx_col of table `tmp`.`parents` trx id 2107 lock mode S locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 00000003; asc     ;;
 1: len 4; hex 00000003; asc     ;;

TABLE LOCK table `tmp`.`children` trx id 2107 lock mode IX
RECORD LOCKS space id 3 page no 4 n bits 80 index PRIMARY of table `tmp`.`children` trx id 2107 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 000000000829; asc      );;
 2: len 7; hex 810000010f0110; asc        ;;
 3: len 4; hex 00000001; asc     ;;

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000002; asc     ;;
 1: len 6; hex 00000000082a; asc      *;;
 2: len 7; hex 820000010f0110; asc        ;;
 3: len 4; hex 00000001; asc     ;;

Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000003; asc     ;;
 1: len 6; hex 00000000082f; asc      /;;
 2: len 7; hex 81000000850110; asc        ;;
 3: len 4; hex 00000002; asc     ;;

Record lock, heap no 5 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000004; asc     ;;
 1: len 6; hex 000000000830; asc      0;;
 2: len 7; hex 82000000860110; asc        ;;
 3: len 4; hex 00000002; asc     ;;

Record lock, heap no 6 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000005; asc     ;;
 1: len 6; hex 00000000083b; asc      ;;;
 2: len 7; hex 010000011c0151; asc       Q;;
 3: len 4; hex 00000003; asc     ;;

Record lock, heap no 7 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000006; asc     ;;
 1: len 6; hex 00000000083b; asc      ;;;
 2: len 7; hex 010000011c017b; asc       {;;
 3: len 4; hex 00000003; asc     ;;

Record lock, heap no 8 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000007; asc     ;;
 1: len 6; hex 000000000833; asc      3;;
 2: len 7; hex 81000000870110; asc        ;;
 3: len 4; hex 00000004; asc     ;;

Record lock, heap no 9 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000008; asc     ;;
 1: len 6; hex 000000000834; asc      4;;
 2: len 7; hex 82000000880110; asc        ;;
 3: len 4; hex 00000004; asc     ;;

Record lock, heap no 10 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000009; asc     ;;
 1: len 6; hex 000000000835; asc      5;;
 2: len 7; hex 81000000880110; asc        ;;
 3: len 4; hex 00000005; asc     ;;

Record lock, heap no 11 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 0000000a; asc     ;;
 1: len 6; hex 000000000836; asc      6;;
 2: len 7; hex 82000001100110; asc        ;;
 3: len 4; hex 00000005; asc     ;;

--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
 ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
852 OS file reads, 641 OS file writes, 357 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 2.27 writes/s, 1.64 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 3 buffer(s)
0.00 hash searches/s, 2.45 non-hash searches/s
---
LOG
---
Log sequence number          28068318
Log buffer assigned up to    28068318
Log buffer completed up to   28068318
Log written up to            28068318
Log flushed up to            28068318
Added dirty pages up to      28068318
Pages flushed up to          28068318
Last checkpoint at           28068318
157 log i/o's done, 0.42 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 427323
Buffer pool size   8192
Free buffers       7194
Database pages     994
Old database pages 381
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 829, created 165, written 362
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 994, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1, Main thread ID=140137389229824 , state=sleeping
Number of rows inserted 15, updated 0, deleted 2, read 28
0.00 inserts/s, 0.00 updates/s, 0.18 deletes/s, 1.00 reads/s
Number of system rows inserted 37, updated 356, deleted 0, read 4702
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

MySQL 8.0.21 の場合

次に、MySQL 8.0.21 で試してみます。

実行計画

実行計画が変わっていることがわかります。サブクエリ部分が相関サブクエリでなくなり、 children テーブルもインデックスを使った参照に変わっています。

mysql> explain DELETE FROM children WHERE parent_id IN
    -> (SELECT parent_id FROM  parents WHERE parent_idx_col = 3);
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+
| id | select_type | table    | partitions | type  | possible_keys             | key               | key_len | ref   | rows | filtered | Extra       |
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | parents  | NULL       | const | PRIMARY,uk_parent_idx_col | uk_parent_idx_col | 4       | const |    1 |   100.00 | Using index |
|  1 | DELETE      | children | NULL       | ref   | fk_parent_id              | fk_parent_id      | 4       | const |    2 |   100.00 | NULL        |
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+

SHOW ENGINE INNODB STATUS;

ロックの取り方も変わっています。children テーブルでロック対象になっているレコードも減っています。

  • 6行のロック
  • children テーブルに IX ロック
    • 5行
      • 2行:外部キーのインデックス fk_parent_id にネクストキーロック
        • 00000003 :parent_id = 3
          • 00000005:child_id = 5
          • 00000006:child_id = 6
        • 検索該当(削除対象)レコード
      • 2行:主キーのインデックスにレコードロック locks rec but not gap
        • 00000005: child_id = 5
        • 00000006 : child_id = 6
      • 1行:外部キーのインデックス fk_parent_id にギャップロック locks gap before rec
        • 00000004 : parent_id = 4
          • 00000007:parent_id = 7
  • parents テーブルに IS ロック
    • UNIQUEキーのインデックス uk_parent_idx_col にレコードロック locks rec but not gap
    • 1行
      • 00000003:parent_idx_col = 3
        • 00000003 : parent_id = 3

抜粋

トランザクションのロック全体

---TRANSACTION 2108, ACTIVE 20 sec
6 lock struct(s), heap size 1136, 6 row lock(s), undo log entries 2
MySQL thread id 18, OS thread handle 140669508515584, query id 116 localhost root starting

子テーブルへのロック

TABLE LOCK table `tmp`.`children` trx id 2108 lock mode IX
RECORD LOCKS space id 3 page no 5 n bits 80 index fk_parent_id of table `tmp`.`children` trx id 2108 lock_mode X
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 4; hex 00000003; asc     ;;
 1: len 4; hex 00000005; asc     ;;

Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 4; hex 00000003; asc     ;;
 1: len 4; hex 00000006; asc     ;;

RECORD LOCKS space id 3 page no 4 n bits 80 index PRIMARY of table `tmp`.`children` trx id 2108 lock_mode X locks rec but not gap
Record lock, heap no 6 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000005; asc     ;;
 1: len 6; hex 00000000083c; asc      <;;
 2: len 7; hex 020000011e0151; asc       Q;;
 3: len 4; hex 00000003; asc     ;;

Record lock, heap no 7 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000006; asc     ;;
 1: len 6; hex 00000000083c; asc      <;;
 2: len 7; hex 020000011e017b; asc       {;;
 3: len 4; hex 00000003; asc     ;;

RECORD LOCKS space id 3 page no 5 n bits 80 index fk_parent_id of table `tmp`.`children` trx id 2108 lock_mode X locks gap before rec
Record lock, heap no 8 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 00000004; asc     ;;
 1: len 4; hex 00000007; asc     ;;

親テーブルへのロック

TABLE LOCK table `tmp`.`parents` trx id 2108 lock mode IS
RECORD LOCKS space id 2 page no 5 n bits 72 index uk_parent_idx_col of table `tmp`.`parents` trx id 2108 lock mode S locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 00000003; asc     ;;
 1: len 4; hex 00000003; asc     ;;

全体

=====================================
2022-01-10 01:05:07 0x7ff02c15f700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 18 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 23 srv_active, 0 srv_shutdown, 4092 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 18
OS WAIT ARRAY INFO: signal count 17
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 2113
Purge done for trx's n:o < 2113 undo n:o < 0 state: running but idle
History list length 2
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 422144539163200, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422144539162344, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 2108, ACTIVE 20 sec
6 lock struct(s), heap size 1136, 6 row lock(s), undo log entries 2
MySQL thread id 18, OS thread handle 140669508515584, query id 116 localhost root starting
SHOW ENGINE INNODB STATUS
TABLE LOCK table `tmp`.`parents` trx id 2108 lock mode IS
RECORD LOCKS space id 2 page no 5 n bits 72 index uk_parent_idx_col of table `tmp`.`parents` trx id 2108 lock mode S locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 00000003; asc     ;;
 1: len 4; hex 00000003; asc     ;;

TABLE LOCK table `tmp`.`children` trx id 2108 lock mode IX
RECORD LOCKS space id 3 page no 5 n bits 80 index fk_parent_id of table `tmp`.`children` trx id 2108 lock_mode X
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 4; hex 00000003; asc     ;;
 1: len 4; hex 00000005; asc     ;;

Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 4; hex 00000003; asc     ;;
 1: len 4; hex 00000006; asc     ;;

RECORD LOCKS space id 3 page no 4 n bits 80 index PRIMARY of table `tmp`.`children` trx id 2108 lock_mode X locks rec but not gap
Record lock, heap no 6 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000005; asc     ;;
 1: len 6; hex 00000000083c; asc      <;;
 2: len 7; hex 020000011e0151; asc       Q;;
 3: len 4; hex 00000003; asc     ;;

Record lock, heap no 7 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000006; asc     ;;
 1: len 6; hex 00000000083c; asc      <;;
 2: len 7; hex 020000011e017b; asc       {;;
 3: len 4; hex 00000003; asc     ;;

RECORD LOCKS space id 3 page no 5 n bits 80 index fk_parent_id of table `tmp`.`children` trx id 2108 lock_mode X locks gap before rec
Record lock, heap no 8 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 00000004; asc     ;;
 1: len 4; hex 00000007; asc     ;;

--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
 ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
984 OS file reads, 734 OS file writes, 409 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 1.39 writes/s, 1.00 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 3 buffer(s)
0.00 hash searches/s, 1.89 non-hash searches/s
---
LOG
---
Log sequence number          28221617
Log buffer assigned up to    28221617
Log buffer completed up to   28221617
Log written up to            28221617
Log flushed up to            28221617
Added dirty pages up to      28221617
Pages flushed up to          28221617
Last checkpoint at           28221617
179 log i/o's done, 0.26 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137363456
Dictionary memory allocated 453831
Buffer pool size   8192
Free buffers       7059
Database pages     1128
Old database pages 429
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 961, created 167, written 413
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1128, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1, Main thread ID=140669059286784 , state=sleeping
Number of rows inserted 15, updated 0, deleted 2, read 20
0.00 inserts/s, 0.00 updates/s, 0.11 deletes/s, 0.17 reads/s
Number of system rows inserted 38, updated 356, deleted 0, read 5052
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

MySQL 8.0.21 でオプティマイザスイッチを変えてみる

ここまでで、 MySQL 8.0.20 と 8.0.21 とで動きが変わることは確認できました。最後に、8.0.21 でどのような最適化が効いてこの結果になっているのかを調べてみます。

A single-table UPDATE or DELETE statement that uses a subquery having a [NOT] IN or [NOT] EXISTS predicate can now in many cases make use of a semijoin transformation or subquery materialization.

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.21 (2020-07-13, General Availability)

という記載があったので、オプティマイザスイッチを変えて実験してみました。

対象の設定は以下の2つです。

  • subquery materialization
  • semijoin transformation

subquery materialization

まず、subquery materialization をオフにしてみます。

dev.mysql.com

SET SESSION optimizer_switch='materialization=off';

実行計画を取ります。

mysql> EXPLAIN DELETE FROM children WHERE parent_id IN
    -> (SELECT parent_id FROM  parents WHERE parent_idx_col = 3);
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+
| id | select_type | table    | partitions | type  | possible_keys             | key               | key_len | ref   | rows | filtered | Extra       |
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | parents  | NULL       | const | PRIMARY,uk_parent_idx_col | uk_parent_idx_col | 4       | const |    1 |   100.00 | Using index |
|  1 | DELETE      | children | NULL       | ref   | fk_parent_id              | fk_parent_id      | 4       | const |    2 |   100.00 | NULL        |
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+

オプティマイザスイッチを変える前と変わりませんでした。以下の記事では、こちらのオプティマイザスイッチを変更することで動きが変わっているので、サブクエリの形などの違いによってどちらの最適化が効くのかが違うようです。

qiita.com

semijoin transformation

materialization を on に戻して、今度はsemijoin transformationをオフにしてみます。

dev.mysql.com

SET SESSION optimizer_switch='semijoin=off';
mysql> EXPLAIN DELETE FROM children WHERE parent_id IN
    -> (SELECT parent_id FROM  parents WHERE parent_idx_col = 3);
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+
| id | select_type | table    | partitions | type  | possible_keys             | key               | key_len | ref   | rows | filtered | Extra       |
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+
|  1 | DELETE      | children | NULL       | ALL   | NULL                      | NULL              | NULL    | NULL  |    8 |   100.00 | Using where |
|  2 | SUBQUERY    | parents  | NULL       | const | PRIMARY,uk_parent_idx_col | uk_parent_idx_col | 4       | const |    1 |   100.00 | Using index |
+----+-------------+----------+------------+-------+---------------------------+-------------------+---------+-------+------+----------+-------------+

8.0.20 の場合に似た実行計画になることが確認できました。MySQL 8.0.20 のものと比較すると、

  • select_typeDEPENDENT SUBQUERYからSUBQUERY なっている
  • rowsが 10 → 8 に変わっている

という違いはありますが、実行して SHOW ENGINE INNODB STATUS をとってみると、ロックのかかり方は(MySQL 8.0.20 と)変わらないように見えます。

ロックの詳細については以下。

=====================================
2022-01-10 07:49:33 0x7ff02c15f700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 12 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 25 srv_active, 0 srv_shutdown, 20947 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 24
OS WAIT ARRAY INFO: signal count 22
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 2119
Purge done for trx's n:o < 2119 undo n:o < 0 state: running but idle
History list length 2
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 422144539163200, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422144539162344, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 2114, ACTIVE 18 sec
4 lock struct(s), heap size 1136, 12 row lock(s), undo log entries 2
MySQL thread id 18, OS thread handle 140669508515584, query id 134 localhost root starting
show engine innodb status
TABLE LOCK table `tmp`.`parents` trx id 2114 lock mode IS
RECORD LOCKS space id 2 page no 5 n bits 72 index uk_parent_idx_col of table `tmp`.`parents` trx id 2114 lock mode S locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 00000003; asc     ;;
 1: len 4; hex 00000003; asc     ;;

TABLE LOCK table `tmp`.`children` trx id 2114 lock mode IX
RECORD LOCKS space id 3 page no 4 n bits 80 index PRIMARY of table `tmp`.`children` trx id 2114 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 00000000082a; asc      *;;
 2: len 7; hex 82000001110110; asc        ;;
 3: len 4; hex 00000001; asc     ;;

Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000002; asc     ;;
 1: len 6; hex 00000000082b; asc      +;;
 2: len 7; hex 810000010e0110; asc        ;;
 3: len 4; hex 00000001; asc     ;;

Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000003; asc     ;;
 1: len 6; hex 000000000830; asc      0;;
 2: len 7; hex 82000000860110; asc        ;;
 3: len 4; hex 00000002; asc     ;;

Record lock, heap no 5 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000004; asc     ;;
 1: len 6; hex 000000000831; asc      1;;
 2: len 7; hex 81000000860110; asc        ;;
 3: len 4; hex 00000002; asc     ;;

Record lock, heap no 6 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000005; asc     ;;
 1: len 6; hex 000000000842; asc      B;;
 2: len 7; hex 010000011d0151; asc       Q;;
 3: len 4; hex 00000003; asc     ;;

Record lock, heap no 7 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000006; asc     ;;
 1: len 6; hex 000000000842; asc      B;;
 2: len 7; hex 010000011d017b; asc       {;;
 3: len 4; hex 00000003; asc     ;;

Record lock, heap no 8 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000007; asc     ;;
 1: len 6; hex 000000000834; asc      4;;
 2: len 7; hex 82000000880110; asc        ;;
 3: len 4; hex 00000004; asc     ;;

Record lock, heap no 9 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000008; asc     ;;
 1: len 6; hex 000000000835; asc      5;;
 2: len 7; hex 81000000880110; asc        ;;
 3: len 4; hex 00000004; asc     ;;

Record lock, heap no 10 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000009; asc     ;;
 1: len 6; hex 000000000836; asc      6;;
 2: len 7; hex 82000001120110; asc        ;;
 3: len 4; hex 00000005; asc     ;;

Record lock, heap no 11 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 0000000a; asc     ;;
 1: len 6; hex 000000000837; asc      7;;
 2: len 7; hex 810000010f0110; asc        ;;
 3: len 4; hex 00000005; asc     ;;

--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
 ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
984 OS file reads, 784 OS file writes, 445 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 0 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 3 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
---
LOG
---
Log sequence number          28226956
Log buffer assigned up to    28226956
Log buffer completed up to   28226956
Log written up to            28226956
Log flushed up to            28226956
Added dirty pages up to      28226956
Pages flushed up to          28226956
Last checkpoint at           28226956
191 log i/o's done, 0.00 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137363456
Dictionary memory allocated 453831
Buffer pool size   8192
Free buffers       7056
Database pages     1131
Old database pages 429
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 961, created 170, written 439
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1131, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1, Main thread ID=140669059286784 , state=sleeping
Number of rows inserted 15, updated 0, deleted 4, read 49
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
Number of system rows inserted 38, updated 356, deleted 0, read 5052
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

微妙な違いはありますが、設定変更前の MySQL 8.0.21 では semijoin の最適化が効いていたと言えそうです。semijoin についてのドキュメントの一番下をみると、以下のように記載されています。

In MySQL 8.0.21 and later, a semijoin transformation can also be applied to a single-table UPDATE or DELETE statement that uses a [NOT] IN or [NOT] EXISTS subquery predicate, provided that the statement does not use ORDER BY or LIMIT, and that semijoin transformations are allowed by an optimizer hint or by the optimizer_switch setting.

MySQL :: MySQL 8.0 Reference Manual :: 8.2.2.1 Optimizing IN and EXISTS Subquery Predicates with Semijoin Transformations

終わりに

MySQL 8.0.21 で新しい最適化が効くようになったよ!というお話でした。

なお、本記事の執筆にあたっては mysql-casual Slack の皆様(特に @yoku0825 さん @hmatsu47 さん)に教えていただいた内容を大いに参考にしました。ありがとうございました!

mysql-casual.connpass.com

参考

qiita.com

abicky.net

dev.mysql.com

dev.mysql.com

nippondanji.blogspot.com

yoku0825.blogspot.com

dev.classmethod.jp

*1:@hmatsu47 さんから情報を提供していただきました。

ソフトウェアの複雑さに立ち向かう1つの哲学 :『A Philosophy of Software Design』 を読んだ

あけましておめでとうございます、になるはずだったのですが、後から読んだ『Googleのソフトウェアエンジニアリング』の方を先に記事にしたので新年2本目の更新です。

ky-yk-d.hatenablog.com

さて、本題。最近のお気に入りポッドキャストであるe34.fmで激賞されていた『A Philosophy of Software Design』を読みました。初版は2018年に出ていて、今回は2021年に出た第2版を読みました。

スパゲッティコードを想起させる装丁

scrapbox.io

どんな本?

書籍のテーマはソフトウェアの複雑さです。複雑さとは、システムを理解したり変更したりするのを困難にさせるものだと定義されています。そして、その兆候は、単純な変更がたくさんのコードの修正を強いてしまうことや、認知負荷(『Team Topologies』でも出てきましたね)が高まってしまうことなどに表れますが、最も深刻なのが「未知の未知」の発生だとされています。未知の未知は、バグの混入に容易に繋がってしまう一方で、定義上その存在に気づくことができないのでとても厄介です。

書籍では、このような複雑さを減らすためのさまざまな設計原則を紹介しています。このように書くと、設計原則のカタログのように見えてしまいそうですが、論述の展開は体系的で、さすが研究者の書いた本だという印象があります。

印象に残った箇所

Deep Modules と Shallow Modules

この本について語るときにはしばしば言及される部分ですが、モジュールをDeep/Shallowという軸で語るのは新鮮でした。Deepなモジュールの方がいいというのが本書の主張です。この区分は第4章で出てくるのですが、後続の章でも繰り返し言及される(「こうするとモジュールがDeepになるよ」などの形で)枠組みです。

インタフェース 機能(Functionality)
Deep 少ない 多い
Shallow 多い 少ない

※インタフェースが「多い」というのは、メソッドの多さや、あるメソッドの引数が多いことなどを指します。

General-purpose と Special-purpose

第6章くらいから出てくる軸で、「目下の必要を満たすよりも少しばかり(somewhat)一般的な目的に使えるように設計しましょう」というのが主張です。

この軸に関連して書かれていたこととして、ある処理(コード)がgeneral-purposeなのかspecial-purposeなのかを考えて、それらが混ざらないようにしましょうというアドバイスや、パフォーマンスに関する章(第20章)でのクリティカルパスからspecial-purposeな処理を取り除くとよいですよ、というアドバイスは印象に残りました。

コメントについて

「コメントを必要とするようなコードにしない」というのがよく言われることですが、この本では「そうはいってもコメントじゃないとできないことはあるでしょ」という立場からどういうコメントを書くのがいいのかを論じています。

個人的には有益だと感ずるアドバイスが多かったコメントについての論述でしたが、中でも印象に残っているのは、「コードよりも詳細を説明するコメントか、コードよりも抽象的なことを説明するコメントか」という区別です。コードの繰り返しに過ぎないようなコメントを書かないというのはよく言われることですが、詳細度という尺度をおいて、どっちの方向に寄せたコメントを書いているのかを意識するというのはとても役立ちそうな気がしました。

また、コメントを後で書くのではなく、設計の手段として最初に書きましょうという話がされていたのも印象に残りました。後から書こうとすると、コメントが実装の繰り返しになってしまったり、そもそもコメント書かなかったりということが起きがちですが、先にコメントを書くことでそれは設計の活動の一部となって楽しくなるし、抽象を考えることで設計の改善にもつながると言われています。

「インクリメントは機能ではなく抽象であるべき」

第19章では、オブジェクト指向(特に継承)、アジャイル開発、ユニットテスト、テスト駆動開発、デザインパターン、getter/setter といったトピックについて論じています。

よく聞く話も多かったのですが、アジャイル開発について論じているところで「the increments of development should be abstractions, not features」という記述があったのは印象的でした。インクリメンタルな開発をするときに、どのように設計を考えるかというのは大きなトピックですが、このような形でのアドバイスは初めてみました。続きの文章では、「抽象の必要に気づいたら、時間をかけて少しずつ作るのではなく、一気に設計しろ」と言っていて、あくまで「必要に気づいたら」ベースではありますが、開発対象の単位として抽象を主役として取り上げようという方向性は納得度はあるものでした。

全体的なスタンスについて

以上、印象に残った箇所をいくつか取り上げて紹介しました。個別のアドバイスについては「なるほどそういう考え方ができるのか」と思うところも多かったのですが、全体としては「今まで慣れ親しんできたのと結構違うスタイルだな」という印象も強く残りました。

大クラス主義?

上記のような印象を特に感じたのは、小さいクラスをたくさん作ることに全体的に否定的である点です。この書籍の中では、たくさんの小さいクラスを設けることは利用者にとってはインタフェースの多さ、ひいてはshallowなモジュールに繋がってしまいやすいものとされています。また、大きなクラス(メソッド)の方がファイルを行ったり来たりせずに一覧できるのでみやすい、という点にも言及しており、分割に対しては謙抑的なスタンスをとっています。

もちろん、「一つになっていた方が読みやすいんだから分割なんてするなよ」という雑な議論はしておらず、具体的な事例も交えつつ「こういうケースだとこういうデメリットがあるので分割しない方がいい」という丁寧な議論をしているのですが、自分は個人的には、責務の小さなクラスを多く作って組み合わせることで機能を実現する文化に親しんできて、分割前提でものを考えることが多かったので、(反発するわけではないですが)引っかかりながら読みました(それでこそ読む価値があるとも言えます)。

このような議論を展開するときには、著者はしばしばJavaの標準クラスライブラリを取り上げて批判します。Javaの標準クラスライブラリの一部に設計的にイケてないところがあるという話はしばしば耳に入ってくるので、言語の文化の話でもないのかもしれないですが、Ruby の特徴として「大クラス主義」というのが挙げられることがあるようで、文化(あるいは言語としての設計思想)の違いもあるのかなと思ったりしました(妄想)。

大クラス主義 クラス設計において一つのクラスにさまざまな機能を盛り込む方針。 Ruby の Array は、配列、リスト、タプル、集合、スタック(LIFO)、キュー(FIFO)などの機能を兼ね備えており、大クラス主義的と言える。

Ruby用語集 (Ruby 3.3 リファレンスマニュアル)

いや、これは多クラス主義でしょう。大クラス主義は結果的に少クラス主義につながります。

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/14664

私個人の印象ですが、小クラス主義で直交性が高いのは指数関数的な複雑さの原因になりやすいと思ってます。逆に大クラス主義でメソッドが多いのは線形の複雑さですね。私の好みはいうまでもないでしょう。

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/14675

ちなみに「大クラス主義」は海外では聞かない概念のようです。

なお、Java に近いコミュニティだと、『現場で役立つシステム設計の原則』の増田さんが以下のように「小クラス主義」と「大クラス主義」に言及していました。これは業務アプリケーションのバックエンドの設計の文脈で、書籍で出てくるUnixや言語の標準ライブラリの設計とはだいぶ前提としているものが違いそうなので、この文脈で「大クラス主義」「小クラス主義」という用語を用いることがどれだけ適切なのかは不明ではあるのですが。

なお、 id:snoozer-05 に「この大クラス主義ってのがRubyistじゃない自分にはよくわからんのですが」と聞いたところ、「『研鑽Rubyプログラミング』を読むといいんじゃないか」と言われました。確かに、関連する話が書かれているようなので、後で読んでみようと思います。

第 2 章「役に立つ独自クラスを設計する」では、いつ独自クラスを定義すべきか、 独自クラスへの SOLID 原則の適用、クラス設計でクラスを大きくすることとクラスの数を多くすることのトレードオフを扱います。 (『研鑽Rubyプログラミング β版』まえがき)

研鑽Rubyプログラミング β版www.lambdanote.com

とまぁ、妄想を書きましたが、真面目な(そして実践的な)話に戻ると、この書籍の主張している Deep なモジュールの優位性、そしてそのようなモジュールを実現するために過度のクラス分割を避けるという方針は理解できるものです。あるモジュールがあまりにたくさんのクラスをインタフェースとして外部に公開すると、クライアントの認知負荷は増大します。そのため、モジュールの設計者としては、最小のインタフェースで(少しばかりgeneral purposeな)機能を提供することを考えるのは重要だと思います。

一方で、あるモジュールのインタフェースを固定した上で、そこの内部で(外部に公開されない)クラスやメソッドの分割をどれだけするのかという点については、この節の冒頭に貼ったツイートで自分が書いているように、条件付きでサブタスクの分割を認めていて、納得がいくものです。たとえば、意味のあるまとまりを持ったコードを private メソッドに切り出すことは外部のインタフェースには影響を与えませんが、public メソッドの本体を読もうとしている人にとっては(ライブラリのクラスやモジュール内の他のクラスと同様に)所与のインタフェース、抽象を提供することになり、効果的である場合も多いでしょう(何も考えずに private メソッドに切り出すなよ、はその通り)。

『A Philosophy of Software Design』74ページより

第9章末尾(関数の長さについて)と第12章末尾(コメントについて)の2箇所にわたって、Uncle Bobこと Robert Martin (の、『Clean Code』における主張)が批判されていたのは印象的でした。プロレスですね。

MITアプローチ?

もう一つ、「おっ」と思った点として、以下の一節があります。

it is important for a module to have a simple interface than a simple implementation. (p.61)

(実装もインタフェースもシンプルであるに越したことはないという前提の上で)「シンプルな実装よりも、シンプルなインタフェースを」という主張で、いわゆるMITアプローチの立場を表明しているように読めました。

MITアプローチとNew Jersey アプローチについては、ajitofm 6: Worse is Betterの後半(40分くらいから)で id:t-wada さんが言及していたので、知っている人も多そうです。Richard Gabrielのエッセイ「The Rise of Worse is Better」で「MITアプローチ」のデザイン哲学の一つとして挙げられている「簡潔性」についてのスタンスは、まさにこれです。

Simplicity-the design must be simple, both in implementation and interface. It is more important for the interface to be simple than the implementation.

The Rise of ``Worse is Better''

日本語訳: The Rise of "Worse is Better"

上記のエッセイの中で、MITアプローチは「MIT/Stanford 方式の設計」とも言われていますが、まさにこの書籍の著者はスタンフォード大学の教授なので、(非常に雑な結びつけではありますが)一定のなるほど感がありました。

この書籍のスタンスをMITアプローチ(あるいは大クラス主義)の一形態だと断ずることには実益はないのですが、この書籍以外にもさまざまな立場があり、一部対立するように見える部分がある(例えば、MITアプローチと対置されているNew Jerseyアプローチでは、「実装のシンプルさの方がインタフェースのシンプルさよりも大事だ」と主張しています)ことがわかると、一歩距離をとって書籍を読むことができると思います。この意味で、書籍のタイトルが「A Philosophy」と不定冠詞になっているのは良いなと思いました(『A Pattern Language』の A にこだわるアレグザンダーオタクの感想)。

追記

改めてWorse is Better 関係の記事やツイートを読み直していて気づいたのですが、 Worse is Better についての @rui314 さんの記事で、Stanford在学中に "The Rise of Worse is Better" のオリジナルを読まされたとおっしゃっていて、MIT/Stanfordアプローチ といっても現代にまでどこまで引き継がれているかは相当怪しいということが改めてわかりました。

もともとの「悪いほうが良い」エッセイの最初のバージョンは1989年に書かれた。その中では「良いデザイン」はMITやStanfordのスタイルとして言及されている。面白いことに僕はこのデザイン原則のエッセイをStanfordのコンピュータサイエンスの授業で読まされた。どうやらStanfordの授業もこの30年間ですっかり「悪い方向」に変わってしまったようだ。その授業で僕がクラスの掲示板に投稿した文章を多少修正の上で翻訳したのがこのエッセイである。

note.com

おわりに

以上、『A Philosophy of Software Design』を読んだよ、というお話でした。

なお、日本語翻訳については、柴田芳樹さんが以前ブログの中で、

私が知る限り、残念ながら、この本は日本語へは翻訳されないようです。

『A Philosophy of Software Design』:柴田 芳樹 (Yoshiki Shibata):SSブログ

と述べられていて、「期待できないのかな?」と思っていたのですが、ドイツ語翻訳(2nd Editionベース)は既に出ているので、もしかしたら出るかもしれません。とはいえ、本文170ページ程度の小さな書籍で、英文もわかりやすかったと思いますので、「英語は苦手だけど読めるようになりたいな・・・」と思っている方にはおすすめです。

関連記事等

著者のGoogleでの講演


www.youtube.com

e34.fm のホストの deeeet さんのツイートと記事

deeeet.com

その他ブログなど

https://twitter.com/yohamta/status/1478381746962186240

blog.stanaka.org

budougumi0617.github.io

syfm.hatenablog.com

tech.innovator.jp.net

文化からツールまでを扱ったタイトルに違わぬ大著『Googleのソフトウェアエンジニアリング』を読んだ

昨年11月末に発売された『Googleのソフトウェアエンジニアリング』を読みました。

細かい内容についての感想はTwitterの方に放流しているので、ブログでは簡単に。

全体の構成

書籍全体の構成は、以下のようになっています。 分量としては、「第4部 ツール」が最も大きな部分を占めています。 第2部から第4部について少しずつ感想を書きます。

  • 第1部 主題(p.1 ~ p.30)
    • 1章 ソフトウェアエンジニアリングとは何か
  • 第2部 文化(p.31 ~ p.164)
    • 2章 チームでうまく仕事をするには
    • 3章 知識共有
    • 4章 公正のためのエンジニアリング
    • 5章 チームリーダー入門
    • 6章 スケールするリーダー
    • 7章 エンジニアリング生産性の計測
  • 第3部 プロセス(p.165 ~ p.374)
  • 第4部 ツール(p.375 ~ p.632)
    • 16章 バージョンコントロールとブランチ管理
    • 17章 Code Search
    • 18章 ビルドシステムとビルド哲学
    • 19章 GoogleのコードレビューツールCritique
    • 20章 静的解析
    • 21章 依存関係管理
    • 22章 大規模変更
    • 23章 継続的インテグレーション
    • 24章 継続的デリバリー
    • 25章 サービスとしてのコンピュート
  • 第5部 結論(p.633 ~ p.636)
    • あとがき

「第2部 文化」について

有名な HRT(謙虚、尊敬、信頼) の原則の話から始まり、同僚とのコラボレーションやリーダーシップ、マネジメントの話をしています。

第2章、第5章の執筆担当者はBrian Fitzpatrickで、この方は『Team Geek』の共著者でもあります。内容的にも、HRTに関する部分や、リーダーシップに関する部分などは、『Team Geek』の改訂版とも言える内容になっています。

エンジニア組織の文化の話は自分は関心のある領域で、どの章も面白く読んだのですが、中でも印象的だったのは第4章の「公正のためのエンジニアリング」です。分量としては多くないのですが、Google が(過去の失敗も踏まえつつ)どのようにダイバーシティなどの課題に取り組んでいるのかなどが書かれていて、面白く読みました。日本の企業では Google ほどこの論点についての課題感は強くなりづらいと思いますが、この書籍で丸々一章がこのテーマのために割かれたのは意味のあることだと思います。

「第3部 プロセス」について

スタイルガイド、コードレビュー、ドキュメンテーション、テスト、廃止について取り上げています。Google のテストについては、『テストから見えてくるグーグルのソフトウェア開発』(未読です)という本があるほか、 Google Testing Blog でもアウトプットを見ることができますが、本書ではかなり紙幅を割いて説明がされています。

testing.googleblog.com

第3部で説明されているプロセスは、第4部で説明されるGoogleのツールと結びついている側面が相当あるのですが、第3部は細かいツールの話をしないようにして記述されているので、Google で働いていない人にとっても参考になる内容が多く含まれていると感じました。

「第4部 ツール」について

第4部では、Google のエンジニアリング(特に第3部で説明されているプロセス)を支えるツール(基本的には社内ツールですが、一部オープンソース版のあるものもあります)が説明されています。正直、自分の仕事に直接役立てようと思って読むのは難しいことが多かったですが、どういう考えに基づいてそれらのツールが設計・運用されているかというのは面白く読みました。

ひとつだけ取り上げると、タスクベースのビルドツールとアーティファクトベースのビルドツールの話は面白かったです。Maven や Rake といった従来のビルドツールは、大多数がタスクベースで、タスクベースであるがための課題を抱えており、それらの課題をどのようにBlaze(そのオープンソース版がBazel)などのアーティファクトベースのビルドツールが解決していくのか、ということが分かりやすく説明されています。Bazel を使ってみたくなりました。

bazel.build

おわりに

Googleのソフトウェアエンジニアリング』、全部で600頁超の大著ですが、細かく章立てされているので、少しずつ読み進めるのには適した書籍だと思います。また、章ごとに執筆担当者の違う書籍にありがちな全体のつながりの悪さはそこまでなく、部という単位ではかなりまとまりを感じました。

かなり幅広いテーマについて一定程度の深さで話をしている書籍ですので、これからは広く参照されていくことになりそうだなと思いました。自分も一回では到底消化しきれていないので、これから折に触れて手に取ろうと思います。

f:id:ky_yk_d:20220103182106j:plain

Technical leadership and glue work / Being Glue を読んで

Tanya Reilly による「Technical leadership and glue work」という講演(および、その文字起こしによるブログ「Being Glue」)があります。(以下、講演に特に言及する場合を除き、「記事」として言及します)。


www.youtube.com

noidea.dog

Manager ではなく Individual Contributor としてのキャリア

筆者の Tanya Reilly は、元GoogleのStaff Systems Engineerで、現在はSquarespaceでPrincipal Software Engineerを務めています。これらの職名からもわかるように、彼女はマネージャーではなくエンジニアとして昇進を続けながら活躍してきた人です。

エンジニアリングマネージャーについての書籍は、翻訳が出ている『エンジニアのためのマネジメントキャリアパス ―テックリードからCTOまでマネジメントスキル向上ガイド』(原題:The Manager's Path)や『フェイスブック流 最強の上司』(原題:The Making of a Manager)の他にも、『An Elegant Puzzle: Systems of Engineering Management (English Edition)』などの書籍がありますが、IC(Individual Contributor)の上位職のための書籍としては『Staff Engineer: Leadership beyond the management track (English Edition)』があるくらいで、まだ整備されていないという状況があります(これは、ICの上位職の責務が企業により非常に異なっているという事情もあるようです)。

Tanya Reilly は、このICの上位職のための書籍を目下準備中(発売予定は2022年10月。Oreillyのサブスクで Early Release 版の第1章まで読めます)なのですが、冒頭の記事も近接した問題系についてのものです( Tanya Reilly が書籍で挙げているサンプルのキャリアラダーでは、この記事で言及される Senior Engineer の上で Staff Enginner と Manager とに分岐するのですが)。

learning.oreilly.com

(2024/3/5追記、すでに『The Staff Engineer's Path』として発売されています。)

Glue Work とその危険性

この記事の主題である「Glue Work」とは、他のメンバーがブロックされているのを助けたり、設計ドキュメントをレビューしたり、新しいメンバーのオンボーディングをしたりという、一見すると「技術的」ではないと思われるような仕事です。著者によると、これらの仕事は「技術的なリーダーシップ」の一部であり、シニア開発者には求められるものである一方で、シニアになる前にこれらの仕事をしすぎることはキャリアをダメにしてしまう危険性があるとされています。なぜなら、これらの仕事をしすぎると、コーディングなどのより「技術的」な仕事に時間を割くことができず、「シニア」への昇進で求められるような「技術的」スキルを磨く機会を失ってしまうからです。

記事の中では、1人の女性エンジニア(ここでわざわざ「女性」というのには意味があり、記事の後半で女性が男性よりも多くこの種の昇進に結びつかない仕事を頼まれる/する傾向があるという話が紹介されています)のストーリーを用いて説明がなされますが、この話を「自分のことだ!」と思う人は少なくないようです。自分も、割合この種の仕事をしがち(好んでやっているし、自分だけがしているわけでもないので不満はないのですが)なので、読んでいて「あーわかる〜」となる部分が多くありました。

自分は何をすべきで、何をすべきじゃないんだろう

記事の紹介はここまでで、ここからは自分の話なのですが、自分は結構な割合の労働時間を Glue Work に時間を割いている時期が今まで多かったです。コーディングが嫌いなわけではないし、技術に興味がないわけでもないのですが、どちらかというとコーディング以外の形でプロジェクトやチームに貢献するという機会の方を選択してきたように思います。

そうしてきたのは(後ろ向きな理由もなかったわけではないですが)、それが一番自分がその時点で貢献できるからであったり、それが一番面白いと思ったからであったりするのですが、改めて立ち止まってみると、自分のキャリアにとっては好ましくない部分もあったのかなと思いました(よかったと思う部分も多々あります)。たとえば、会社の身の回りのエンジニアに比べて、手が動くまでが遅いというのは、エンジニアとしての自分の大きな欠点だなと思っているところなのですが、 これが改善されてきていないのは自分のこれまでの選択の産物に他ならないと思います。

プログラミング経験ゼロからこの業界に入ってそろそろ5年になりますが、ちょうど自分が業界に入ったあとに「エンジニアリングマネージャー」というトピックが日本でもポピュラーになり、「将来はEMとかそっち方面かなー」とずっと思ってきたので、コーディングなど「技術的」な仕事に多くの時間を割くということをそこまで重んじてこなかったという側面もあります(現場の開発者としての経験はEMにも重要だと考えて、現場に近い仕事を転職の時に選ぶことはしましたが)。

Glue Work についての記事は、自分のキャリアについて特定の方向の示唆を与えるものでもないのですが、改めて自分のキャリアというものを考えてみたときに、どういう仕事を日々選択してやるのかというところも重要になってくるな、というのを改めて思わされました。自分のキャリアの観点とは別に、チームの状況という観点からも、これまでと同じ動き方をしていくことがベストではないのではないかというのが最近思い始めていることなので、改めて自分の働き方、動き方について、チームのメンバーやマネージャー、あるいは社外の人たちと話し合ってみたいなと思いました。

e34.fm