このドキュメントは、疑問を持っていた点をAIエージェントと対話しながら整理したメモです。InnoDB のインデックスとページ構造、ロックとMVCC、セッションとコネクションプールまでMySQL の核心概念を一度にまとめます。
1. ランダムアクセス(Random Access)#
1-1. 意味#
ランダムアクセスとは、格納されたデータの位置に関係なく、任意の位置のデータに直接アクセスできる方式を意味します。
例えば、配列で a[100] に直接アクセスしたり、DBでインデックスを使って特定のキー値を素早く見つけたりすることがランダムアクセスの代表的な例です。
1-2. データベースにおいてなぜ重要か#
DBMSは大量のデータを格納・検索します。このとき、すべてのデータを最初から最後まで順次読み取るのは非効率です。 そのためDBMSはインデックスを使って**「どのあたりに目的の値があるか」**を素早く見つけ、その位置に直接移動します。
つまり、インデックスはランダムアクセスを効率的に可能にするための核心ツールです。
2. InnoDBで主キー(PK)が物理的な格納位置を決めるということの意味#
2-1. クラスタード インデックス(Clustered Index)#
InnoDBではテーブルデータが主キー順に格納されます。 つまり、主キー自体が単なる識別子であるだけでなく、クラスタードインデックスのリーフページでの配置基準になります。
これはよく以下のように表現されます。
- InnoDB テーブル = 主キー基準のクラスタードインデックス
- リーフページ = 実際の行(row)データを格納
2-2. どういう意味か#
例えば、PKが以下のようだとします。
id = 1, 2, 3, 4, 5InnoDBはこのデータをおおむね id 順に並べた状態でページに格納します。
ここで言う「物理的な格納位置」とはファイル内のオフセットが永久に固定されるという意味よりも、B-treeのリーフページがPK順で構成されるという意味に近いです。
つまり:
- PKが小さいほど前のページ
- PKが大きいほど後ろのページ
に配置される傾向があります。
2-3. なぜ重要か#
主キー基準でデータが並べられているため:
- PK照会が非常に高速
- PK範囲照会が非常に有利
- セカンダリインデックスがPKを参照するため、PKのサイズがインデックス全体のサイズに影響を与える
3. 「頻繁に変わる値をPKにすると性能が悪くなる」の意味#
3-1. 理由#
InnoDBはデータをPK順に格納するため、PK値が変わるとその行の格納位置が変わる可能性があります。
つまり、UPDATE pk = ... は内部的に実質:
- 既存レコードを削除
- 新しいPK位置に再挿入
に近いコストを引き起こす可能性があります。
3-2. 副作用#
- ページ再配置コストの発生
- ページ分割(page split)の可能性が増加
- セカンダリインデックスも一緒に整備が必要
- ディスクI/Oの増加
- ロック競合の可能性が増加
3-3. 実務の観点#
そのためPKは通常以下の特性を持つ値が良いです。
- 短く
- 固定的で
- 絶対に変わらず
- 可能であれば単調増加(auto increment等)
4. 主キーが実際に変わる場合の例#
通常PKはあまり変わらないのが正しいです。 しかし間違ったモデリングをするとPKの変更が必要になることがあります。
例#
4-1. 自然キーをPKにした場合#
例:
- メールアドレス
- 住民番号
- 社員番号
- ユーザー名(username)
これらの値は「変わらないだろう」と思いますが、実際のサービスでは変わることがあります。
例:
- メール変更
- ユーザー名変更ポリシーの許可
- 外部システム統合によるID規則の変更
4-2. システム統合#
AシステムとBシステムを統合する際にPK体系が衝突するとPKの再整備が必要になる場合があります。
4-3. 業務ルールの変更#
以前は商品コードをPKとして使っていたが、商品コードのルール自体が変わる場合です。
5. B+木の探索計算量とディスクアクセス#
5-1. 探索計算量#
B+木は高さを h とすると探索コストがおおよそ O(h) です。
5-2. なぜディスクアクセス回数と結びつくか#
B+木の各ノードは通常**1ページ(page)**単位で格納されます。 したがってルート → 内部ノード → リーフノードと下っていくとき、ノードを1つ読むたびにページを1つ読むことになります。
つまり:
- 木の高さ = 最悪の場合に読む必要のあるページ数
- ページがバッファプールにない場合 = ディスクI/Oが発生
5-3. 一度にリーフまで行けない理由#
1ページの中には木全体は入っていません。 1ページには一部のキーと子ページのアドレスだけが入っています。
そのため検索過程は:
- ルートページを読む
- その中でどの子ページに行くかを決定
- 子ページを読む
- 再び次の子を決定
- リーフページに到達
という形になります。
6. MySQLで「アクセス」は誰が行うか#
MySQLでディスクアクセスはユーザーが直接行うのではなく、ストレージエンジンとOSが協力して実行します。
フローを単純化すると以下のようになります。
- クライアントがSQLを送信
- MySQLサーバーがパース・最適化
- ストレージエンジン(InnoDB等)が必要なページをリクエスト
- バッファプールになければOSを通じてディスクから読み込む
- ページをメモリに読み込んで使用
つまり、実際のSQLはMySQLが処理しますが、物理的な読み書きリクエストは結局OSとディスクサブシステムが担当します。
7. B+木のページの中には何が入っているか#
7-1. 内部ノード#
内部ノードには概ね以下があります。
- 複数のキー値
- 各キー範囲に対応する子ページのポインタ
例えば:
- key < 10 の場合はpage A
- 10 <= key < 20 の場合はpage B
- key >= 20 の場合はpage C
7-2. リーフノード#
リーフノードには実際の検索対象があります。
- InnoDBのクラスタードインデックスリーフ:実際の row データ
- セカンダリインデックスリーフ:インデックスキー + PK値
7-3. ページ間の移動#
現在のページでキー値を比較した後、 その結果に合った**次のページ番号(page id)**を見つけてそのページを読みます。
つまり、「別のページに行く」とは結局ページ番号を使ってそのページをメモリ/ディスクから読み取ることです。
8. バッファ管理ポリシー:STEAL / NO-STEAL、FORCE / NO-FORCE#
トランザクション中に修正されたページ(ダーティページ)をいつディスクに書き込むかを基準に分けた代表的なポリシーです。
ただしこれらの用語はデータベースの教科書でよく使われる分類であり、MySQL公式ドキュメントがInnoDBをこの4つの用語で直接規定しているわけではありません。 InnoDBのバッファプールflush、redo、チェックポイントの動作を理解するための概念的なフレームとして見るのが安全です。
8-1. STEAL#
コミット前でもダーティページをディスクに書き込める
意味#
バッファ空間が不足すると、まだコミットされていないトランザクションが修正したページもディスクに書き出せます。
長所#
- バッファ管理が柔軟
- メモリ使用効率が良い
短所#
- コミット前のデータがディスクに記録される可能性があるため、障害時にUNDOが必要
8-2. NO-STEAL#
コミット前のダーティページをディスクに書き込まない
長所#
- 障害時のUNDO負担が軽減
短所#
- バッファへの圧迫が大きい
- メモリ管理が難しく性能上不利になり得る
8-3. FORCE#
コミット時に該当トランザクションの修正内容をディスクページまで強制的に記録
長所#
- 障害後のREDO必要性が低減
短所#
- コミットのたびにディスク書き込み負担が大きい
- 性能低下の可能性が大きい
8-4. NO-FORCE#
コミット時にログのみ保証し、データページは後で書いても良い
長所#
- コミットが速い
- 現代的なWAL系DBMSで一般的な方向
短所#
- 障害時にREDOが必要
9. REDOとUNDO#
9-1. REDO#
すでにコミットされた変更を再び適用して復旧すること
例:
- コミットは完了したがデータページへの反映前に障害が発生
- ログを見て変更内容を再適用
つまり、**耐久性(Durability)**を確保するための復旧
9-2. UNDO#
コミットされていない変更を元に戻すこと
例:
- トランザクションが途中で失敗
- すでに一部の変更が反映されていれば元の状態に復元する必要がある
つまり、**原子性(Atomicity)**を確保するための復旧
9-3. 違いの整理#
- REDO:「やるべきだったことをもう一度行う」
- UNDO:「やってはいけないことを取り消す」
10. InnoDBのUNDOログとREDOログの性格#
10-1. UNDOログ#
InnoDBのUNDOは論理的な性格が強いです。
特に:
- UPDATE前の値を復元
- DELETEの取り消し
- INSERTの取り消し(挿入された行を削除)
などの目的で使用されます。
MVCCでも過去のバージョンを再構成する際に使用されます。 ただしINSERTのUNDOはMVCC用の過去バージョン提供には直接使用されず、主にロールバック時に「挿入された行を削除するための情報」として使われます。
10-2. REDOログ#
REDOは一般的に低レベルの変更復旧情報の性格が強いです。
つまり、SQLを再実行するというよりも、データファイルにまだ反映されていない変更を復旧時点で再適用できるように記録します。
11. 「INSERT前にはデータがないのにUNDOログがなぜ必要か?」#
非常に重要なポイントです。
11-1. 核心#
INSERT前には「以前の値」がないため、UPDATEのようにbefore imageを保存するわけではありません。 しかしロールバックするためには**「この行を削除すべきだ」**という情報は必要です。
つまり、INSERTのUNDOは通常:
- 以前の値を復元する用途というよりは
- 「このINSERTを取り消すためにどのレコードを削除すべきか」を示す情報
です。
11-2. MVCCとの関係#
INSERTされた新しい行はもともと過去のバージョンがないため、 他のトランザクションの一貫した読み取りではまったく見えなければそれで十分です。
そのためINSERTのUNDOはMVCCで過去バージョンを作る役割よりも、 主にロールバック/復旧用の意味が大きいです。
12. 物理的ログ vs 論理的ログ#
12-1. 物理的ログ#
「ページのどこがどのように変わったか」を記録
長所#
- 再適用が明確
- REDOに適している
短所#
- ログ量が増える可能性がある
- ページ構造に依存
- 同時実行性・柔軟性の面で不利になり得る
12-2. 論理的ログ#
「どの操作を行ったか」を記録
例:
- Aの残高から100差し引く
- Bの残高に100増やす
長所#
- より抽象的
- 一部の状況では同時実行性と柔軟性の面で有利
短所#
- 再実行時にコンテキストが必要になる場合がある
13. REPEATABLE READ、READ COMMITTED、MVCCの理解#
13-1. REPEATABLE READの核心#
InnoDBの REPEATABLE READ では、**通常の SELECT(consistent read)は概ねトランザクション内の最初のconsistent readが作った読み取りスナップショット(read view)**を基準に照会します。
そのため同じトランザクション内では、後から他のトランザクションがコミットしたデータでも、最初のスナップショットになければ見えない場合があります。
つまり:
- トランザクション2が開始した後
- トランザクション1がコミットしても
- トランザクション2の通常のSELECTは依然として以前のスナップショット基準で読む
これが 「トランザクション2は自身のトランザクションID以前の値しか読まないと思っていたが、なぜトランザクション1が記録したデータは読めないのか?」 に対する核心的な理由です。
正確な理解#
重要なのは単純に「トランザクションIDがより小さいか」ではなく、 トランザクション2が生成したRead Viewでそのトランザクションが可視かどうかです。
また SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE、DELETE のようなロックを使うパスは常に同じルールでsnapshotを読むのではなく最新状態とロックルールを合わせて使用するため、通常の SELECT と区別して理解する必要があります。
13-2. READ COMMITTEDの核心#
各SELECT文が実行されるたびに新しいRead Viewを作ります。
そのため一般的に:
- 一度照会したときに見えなかった値が
- 後で他のトランザクションがコミットした後に再照会すると
- 見える場合があります。
ただし実際の実験では以下に注意する必要があります。
- 最初のSELECTと2番目のSELECTが本当にそれぞれ独立したconsistent readだったか
- ロック読み取り(
SELECT ... FOR UPDATE)だったか - アプリケーション/フレームワークがトランザクション境界をどのように設定したか
つまり、理論上READ COMMITTEDは「文単位の最新コミット反映」ですが、 実験状況のクエリの種類とトランザクションの流れによって観察結果の解釈を慎重に行う必要があります。
14. ロック(lock)の種類とレコードロック#
14-1. レコードロック(Record Lock)#
インデックスレコードを1つロックします。
つまり「行全体」を抽象的にロックするのではなく、 実際にはインデックスエントリ基準でロックがかかると理解する必要があります。
14-2. いつ使用されるか#
UPDATEDELETESELECT ... FOR UPDATESELECT ... FOR SHARE(LOCK IN SHARE MODEと同等の構文)
などで使用されます。
15. INSERT時のロックはどのように動作するか#
ユーザーが特に多く気になる部分です。
15-1. 「存在しないデータにどのように排他ロックをかけるか?」#
正確には存在しない行自体をロックするのではなく、 その行が入るインデックス区間/位置と挿入されるレコードに対して適切なロックメカニズムを使用します。
関連概念:
- insert intention lock
- gap lock
- next-key lock
- record lock
15-2. Insert Intention Lock#
挿入しようとするインデックス区間に対して 「私はここにinsertしようとしている」という意図を示すロックです。
複数のトランザクションが同じgapにinsertしようとしても、 実際の同じ位置/同じキーの衝突でなければある程度並行可能性を持ちます。
15-3. Gap Lock#
存在するインデックスレコード間の「間隔」をロックします。
主な目的:
- ファントム読み取りの防止
- 範囲内での新しいレコード挿入の防止
15-4. Next-Key Lock#
Record Lock + Gap Lock
つまり:
- あるインデックスレコードと
- その前のgap
を一緒にロックします。
16. InnoDBはPKインデックスにのみロックをかけるか?#
いいえ。 InnoDBはインデックス基準でロックを管理し、これはPK(クラスタードインデックス)だけでなく**セカンダリインデックス(secondary index)**にも適用されます。
つまり:
- どのインデックスを通じて探索するか
- どのような条件で照会/修正するか
によってロックがかかるインデックスが変わる可能性があります。
ただしInnoDBでは実際のレコードはPKインデックスのリーフにあるため、 セカンダリインデックスを通じてアクセスしてもPK側の確認が伴う場合があります。
17. Uniqueインデックス vs 一般セカンダリインデックス#
17-1. よくある説明#
「ユニークインデックスは1件だけ読めばよいが、ユニークでないインデックスはもう1件余分に読む必要があるため遅い」
この説明は完全に間違いとは言えないですが、過度に単純化された説明です。
17-2. 正確には#
ユニークインデックスは条件が一致すると最大1件のみ存在することが保証されているため、 オプティマイザは探索結果を素早く確定できます。
一方、一般インデックスは同じキーが複数ある場合があり
- 追加レコードの存在可能性の確認
- 複数エントリの走査
などが必要になる場合があります。
しかし性能差は通常非常に微細であり、 実際のボトルネックは通常以下の方が大きいです。
- カバリングインデックスの有無
- ランダムI/Oの有無
- ページキャッシュのヒット率
- 返却カラム数
- 読まなければならない実際のrow数
18. 特定のメールの存在確認クエリの比較#
対象テーブル:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE
);比較対象:
SELECT * FROM users WHERE email = :user_email;
SELECT email FROM users WHERE email = :user_email;
SELECT count(*) FROM users WHERE email = :user_email;18-1. SELECT *#
最も非効率になる可能性が大きいです。
理由#
emailインデックスで位置は見つけても*のため全rowを読む必要がある- つまりインデックスだけでは終わらない
- 通常カバリングインデックスではない
実行計画の観点:
- インデックス探索後
- 実際のclustered index rowへのアクセスが必要
18-2. SELECT email#
このクエリは email インデックスだけで結果を返せます。
長所#
- カバリングインデックスが可能
- テーブルrowの全体を読まなくて済む場合がある
- ネットワーク返却データも少ない
18-3. SELECT count(*)#
存在確認だけしたい場合は動作自体は正しいです。
ただしこの例は email が UNIQUE なので差が小さい場合がありますが、存在確認だけ必要なら通常 EXISTS や SELECT 1 ... LIMIT 1 のように**「あるかどうか」を直接表現する形**をより多く推奨します。
18-4. 存在確認だけ必要な場合に最も推奨される形#
SELECT EXISTS(
SELECT 1
FROM users
WHERE email = :user_email
) AS email_exists;理由#
- 存在確認が目的であることが明確
- 最初の行を見つけたらそれ以上見る必要がない
- 読む人にも意図が明確
19. SELECT * が非効率な理由をオプティマイザと実行計画の観点で見ると#
19-1. カバリングインデックス不可#
SELECT * はすべてのカラムが必要なため
email ユニークインデックスだけではクエリを終えられません。
つまり:
emailインデックスで条件一致エントリを見つける- そのエントリが指す実際のrowを再度読む
19-2. 結果送信量の増加#
存在確認だけ必要なのに:
- id
- username
すべてを返すと不要なI/Oとネットワークコストが生じます。
19-3. 実務観点の結論#
**「必要なカラムだけ照会する」**という原則が非常に重要です。
20. Unique制約とインデックス#
20-1. UNIQUEをかけるとインデックスができるか?#
一般的にそうです。 MySQLはUNIQUE制約のためにユニークインデックスを作成します。
20-2. そのカラムでWHERE検索すると速くなるか?#
はい、通常速くなります。
例:
SELECT * FROM users WHERE email = 'a@b.com';ここで email にUNIQUEインデックスがあれば、
MySQLはインデックスを使って素早く探索できます。
20-3. UNIQUEでもNULLは?#
MySQLでは UNIQUE インデックスでも NULL 可能なカラムであれば複数の NULL 値を許容します。
つまり、「UNIQUE だから NULL も1つしかダメ」と理解すると誤りです。
21. JOINの構文2種類#
SELECT * FROM employees e JOIN salaries s ON e.emp_no = s.emp_no;
SELECT * FROM employees e, salaries s WHERE e.emp_no = s.emp_no;違い#
機能的には同じinner joinの結果を出します。
推奨方法#
最初のように JOIN ... ON を使うのが良いです。
理由#
- 可読性が良い
- JOINの条件が明確
- 複雑なクエリでのミスを減らす
22. Undoログと Undoページの違い#
Undoログ#
トランザクションの変更を元に戻すための論理的な情報
Undoページ#
そのUndoログが実際に格納される物理的なページ単位
つまり:
- Undoログ = 内容
- Undoページ = その内容を格納する空間
23. トランザクションはDB ACIDを保証するだけで、Javaコードの原子性を保証しない#
この文章は核心的に正しいです。
意味#
DBトランザクションはデータベース内部の変更に対してACIDを保証しますが、 アプリケーションコード全体が自動的に原子化されるわけではありません。
Springの @Transactional も基本的に現在の実行スレッドにバインドされたトランザクション境界を扱うものであり、メソッド内で新たに作ったスレッドにまで自動的に伝播するモデルではありません。
例:
- DB照会
- Java条件分岐
- なければ生成
- 保存
この全体がアプリケーションレベルの競合条件(race condition)にさらされる可能性があります。
synchronized で防ぐことがなぜ限界があるか#
- アプリケーションインスタンスが複数ある場合、JVM内部のlockは無力
- DBレベルの競合状態はDBの制約/ロックで解決する必要がある
- 代表的な解決策:
- UNIQUE制約
- 適切なトランザクションとロック
- upsertパターン
24. メディア復旧(Media Recovery)#
メディア復旧とはディスク障害、ファイル破損、記録媒体の障害などによって失われたデータを バックアップとログを利用して復元する作業です。
つまり:
- 単純なトランザクションのロールバック/クラッシュ復旧より大きな障害範囲
- バックアップ + アーカイブログ + redoの適用などの手順が重要
25. PKサイズが大きくなるほどインデックスサイズが大きくなる理由#
InnoDBではセカンダリインデックスのリーフエントリには単に「セカンダリインデックス値」だけでなく 該当rowを見つけるためのPK値も一緒に格納されます。
例:
- PK =
INT(4 bytes) - セカンダリインデックスの各エントリがPK 4 bytesを含む
ところがPKが:
- 長い文字列
- 複合キー
- UUID文字列形式
であれば、すべてのセカンダリインデックスエントリがその分大きくなります。
影響#
- インデックスサイズの増加
- ページあたりに格納できるエントリ数の減少
- B+木の高さが増加する可能性
- キャッシュ効率の低下
したがってPKは短いほど有利です。
26. MySQLスレッドとOSカーネルスレッドの関係#
MySQLサーバーは内部的に:
- クライアント接続ごとに紐づくリクエスト処理用スレッド
- バックグラウンド作業用スレッド
を使用します。
基本的なconnection thread modelではクライアント接続ごとに専用のスレッドが付き、これらのスレッドはOSがスケジューリングする実行単位の上で動作します。
同期がなぜ必要か#
複数のスレッドが同時に:
- バッファプール
- ロックテーブル
- インデックス構造
- ログバッファ
のような共有リソースにアクセスすると衝突が発生する可能性があります。
そのため:
- mutex
- semaphore
- latch
- rw-lock
のような同期ツールが使用されます。
つまりOSはスレッド実行と基本的な同期プリミティブ機能を提供し、 MySQLはこれを使ってDB内部の共有リソースを保護します。
27. B-treeインデックスの「固有番号」と実際のキー値#
この部分は表現上の混乱がありました。 実務/教科書で一般的に重要なのは以下です。
- 実際のキー値:インデックスを構成するカラム値
- 行の識別情報:
- InnoDBセカンダリインデックスであればPK値
- MyISAMであればデータファイルのoffsetのような位置情報
つまり、通常の学習で重要な区分は 「インデックスキー」vs「そのキーが指す実際のレコードの識別子」です。
28. MyISAM vs InnoDB#
28-1. MyISAMの特徴#
- トランザクション非サポート
- テーブルロックを使用
- read-mostly ワークロードに主に使用
- データとインデックスを別々に格納
28-2. MyISAMのテーブルロック#
MyISAMはrow lockではなくtable lock基準です。
長所#
- 実装がシンプル
- オーバーヘッドが少ない
短所#
- 同時書き込みに弱い
- 書き込み作業が多いとボトルネックになる
28-3. 高速な読み取り演算#
トランザクションとMVCCがなく、テーブルロック基準であるため、歴史的には読み取り比率が非常に高い環境で使われてきました。
28-4. InnoDBは遅いか?#
単純な読み取り構造だけ見るとMyISAMが有利に見えるかもしれませんが、 実際の現代サービス環境ではInnoDBの利点の方がはるかに大きいです。
特に:
- PK照会はInnoDBのクラスタードインデックスが非常に高速
- トランザクションサポート
- row-level locking
- crash recovery
- MVCC
そのため一般的なサービスではほぼInnoDBが標準的な選択です。
29. SpringでMyISAMを使うとトランザクションが使えないか?#
DBエンジンレベルでは事実上そうです。
Springの @Transactional はフレームワークレベルでトランザクション境界を設定してくれますが、
実際のDBがトランザクションをサポートしていなければACIDの保証を受けられません。
つまり:
- Springがアノテーションを付けても
- MyISAMの変更はCOMMIT/ROLLBACKベースの本物のトランザクションで保護されない
- したがってロールバックを呼び出してもMyISAMの変更は元に戻らない場合がある
したがってトランザクションが必要なアプリケーションであればInnoDBが必要です。
30. wait_timeout の global と session の違い#
global#
新たに作成されるセッションのデフォルト値
session#
現在接続された特定のセッションにのみ適用される値
つまり:
SET GLOBAL wait_timeout = ...→ 新しい接続から影響SET SESSION wait_timeout = ...→ 現在の接続にのみ影響
MySQLは接続スレッドが開始されるときにsession wait_timeout の値をglobal wait_timeout または interactive_timeout から初期化します。
31. MySQLのセッション(Session)とは何か#
セッションは**スキーマ単位ではなく、クライアントとサーバー間の1つの接続(connection)**を意味します。
つまり:
- WebサーバーのDB接続1つ = DBセッション1つ
このセッションは以下を持ちます。
- セッション変数
- トランザクション状態
- 現在のdefault schema
- temporary table等
32. Springサーバーに100人がリクエストを送るとセッションは1つか?#
いいえ。 通常はDBコネクションプール(HikariCP等)を使用します。
つまり:
- リクエスト100個が来ても
- DBセッションはコネクションプールのサイズ分だけ運用できる
- 各リクエストは必要に応じてプールから接続を1つ借りて使用
したがって「SpringサーバーとDBの間に単1つのセッションしかない」というのは誤りです。
33. インメモリDBMS vs 大きなページキャッシュを持つディスクベースのDBMS#
ユーザーが提起した質問は非常に本質的です。
「ページ全体をメモリにキャッシュするなら、ディスクベースのDBMSもインメモリDBMSと同じではないか?」
一見似ているように見えますが、構造的に異なります。
33-1. 単純なページキャッシュとインメモリDBの違い#
ディスクベースのDBMSは基本的に:
- ディスクページ構造
- バッファマネージャー
- ページのflush
- WALベースのrecovery
- ページフォーマットの維持
を前提として設計されています。
一方インメモリDBMSはもともと:
- ポインターベースのデータ構造
- キャッシュフレンドリーなレイアウト
- ディスクページ形式の最小化
- シリアライズ/デシリアライズの最小化
などを目標として設計されています。
33-2. シリアライズのオーバーヘッドの観点#
ディスクベースのDBMSはデータをディスクに永続化するために ページフォーマット、ログフォーマット、flushルールなどを管理する必要があります。
つまりメモリに載っていても:
- ディスクに合った構造の維持
- ページのdirty管理
- チェックポイント
- シリアライズ形式の考慮
のようなオーバーヘッドがあります。
インメモリDBMSはこのような従来のディスクページ中心の制約がはるかに少ないです。
33-3. データレイアウト維持のオーバーヘッド#
ディスクベースのDBMSは通常ページ単位の整合性を維持する必要があります。
例:
- スロットディレクトリ
- ページヘッダー
- 空きスペース管理
- page split
- page compaction
このようなレイアウト管理コストが大きいです。
一方インメモリDBMSはディスクページレイアウトにあまり縛られず CPU cache フレンドリーな構造を選ぶ自由がより大きいです。
つまり:
- 「すべてメモリに載せておく」という事実だけは同じ
- しかし内部のデータ構造と実行エンジンの最適化が異なります。
34. 選択度(Selectivity)/ 基数性(Cardinality)#
インデックスキー値の重複が多くなるほど:
- **基数性(cardinality)**は低くなり
- **選択度(selectivity)**も低くなります
なぜ重要か#
選択度が低いと特定の値で検索しても多くのrowが出る可能性が大きいため、 オプティマイザはインデックス使用の利点を低く評価する場合があります。
例:
- 性別カラム(
M、F)のインデックスは選択度が非常に低い - メールカラムはほぼ一意 → 選択度が非常に高い
35. データベースはどのようにデータを読み込むか#
全体的なフローを整理すると以下のようになります。
35-1. パース#
SQL文法の解析
35-2. 最適化#
オプティマイザが実行計画を選択
35-3. 実行#
- インデックス探索 or フルスキャン
- 必要なページをバッファプールで検索
- なければディスクから読み込む
35-4. 結果生成#
フィルタ、JOIN、ソート、集計を実行
35-5. 返却#
クライアントに結果を送信
核心は結局:
DBMSは「ページ」単位でデータを読み込み、バッファプール/インデックス/オプティマイザを利用してディスクI/Oを最小化する。
36. 最後に改めて整理する核心概念#
36-1. InnoDBで重要なこと#
- PK = クラスタードインデックス
- セカンダリインデックスのリーフにはPKが入る
- トランザクション/MVCC/UNDO/REDOサポート
- ロックはインデックス基準で動作
- REPEATABLE READでのスナップショット読み取りが重要
36-2. クエリ最適化観点の核心#
SELECT *を避ける- 必要なカラムだけ照会
- 存在確認は
EXISTSを検討 - カバリングインデックスの有無が重要
- PKは短く安定しているほど良い
36-3. アプリケーション観点の核心#
- DBトランザクションがJavaコード全体の原子性を保証するわけではない
- マルチインスタンス環境ではJVM
synchronizedだけでは不十分 - データの整合性はDBの制約条件とトランザクション設計で確保する必要がある
37. 学習ポイントのまとめチェックリスト#
以下の項目を自分で説明できれば、今回の対話の核心をよく理解したことになります。
- ランダムアクセスとは何かを説明できる。
- InnoDBでPKがなぜ物理的な格納順序に影響を与えるかを説明できる。
- PKが大きくなるとなぜセカンダリインデックスも大きくなるかを説明できる。
- B+木の高さとディスクI/O回数の関係を説明できる。
- STEAL / NO-STEAL、FORCE / NO-FORCE を教科書の概念として区別できる。
- REDOとUNDOの違いを説明できる。
- INSERTのUNDOがなぜ必要かを説明できる。
- REPEATABLE READでなぜ他のトランザクションのコミットが見えない場合があるかを説明できる。
- レコードロック、ギャップロック、ネクストキーロックを区別できる。
-
SELECT *、SELECT email、COUNT(*)、EXISTSの中で存在確認に何が適切かを説明できる。 - MyISAMとInnoDBの違いを説明できる。
- SpringのリクエストとDBセッション(コネクションプール)の関係を説明できる。
- インメモリDBとディスクベースDB+大きなキャッシュの違いを説明できる。
38. まとめ#
今回の対話は単純な文法よりも重要な、DBMSが内部的にどのように動作するかを理解する方向で進められました。 バックエンド開発者にとって特に重要な観点は以下の3つです。
- 性能:インデックス、ページ、ランダムI/O、カバリングインデックス
- 整合性:トランザクション、ロック、MVCC、UNIQUE制約
- 設計:PK選択、クエリ形式、アプリケーション-DBの役割分離
この3つを継続的に結びつけて学習するとMySQLをはるかに深く理解できます。
参考資料#
- MySQL 8.4 Reference Manual, Clustered and Secondary Indexes https://dev.mysql.com/doc/refman/8.4/en/innodb-index-types.html
- MySQL 8.4 Reference Manual, The Physical Structure of an InnoDB Index https://dev.mysql.com/doc/refman/8.4/en/innodb-physical-structure.html
- MySQL 8.4 Reference Manual, File Space Management https://dev.mysql.com/doc/refman/8.4/en/innodb-file-space.html
- MySQL 8.4 Reference Manual, Buffer Pool https://dev.mysql.com/doc/refman/8.4/en/innodb-buffer-pool.html
- MySQL 8.4 Reference Manual, Transaction Isolation Levels https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html
- MySQL 8.4 Reference Manual, Consistent Nonlocking Reads https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html
- MySQL 8.4 Reference Manual, Locking Reads https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html
- MySQL 8.4 Reference Manual, Locks Set by Different SQL Statements in InnoDB https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html
- MySQL 8.4 Reference Manual, Undo Logs https://dev.mysql.com/doc/refman/8.4/en/innodb-undo-logs.html
- MySQL 8.4 Reference Manual, Redo Log https://dev.mysql.com/doc/refman/8.4/en/innodb-redo-log.html
- MySQL 8.4 Reference Manual, Server System Variables (
wait_timeout) https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html - MySQL 8.4 Reference Manual, The MyISAM Storage Engine https://dev.mysql.com/doc/refman/8.4/en/myisam-storage-engine.html
- MySQL 8.4 Reference Manual, Connection Interfaces https://dev.mysql.com/doc/refman/8.4/en/connection-interfaces.html
- MySQL 8.4 Reference Manual, CREATE INDEX Statement https://dev.mysql.com/doc/refman/8.4/en/create-index.html
- Spring Framework Javadoc,
@Transactionalhttps://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html
