RDBの仕組み (Postgresqlの内部構造)
MySQLのアーキテクチャ
MySQL
のアーキテクチャは大きく2つに別れます。
-
接続からSQL実行までの共通部分
=> サーバー -
トランザクション管理やデータ永続化、インデックス管理などストレージエンジン固有部分
=> ストレージエンジン
ここでは、サーバーとストレージエンジンと呼ぶことにする。
サーバーは接続の受付
、ユーザ認証
や権限管理
、SQL文の構文解析
と最適化
が含まれている。
- 接続の受付
- ユーザ認証
- 権限管理
- SQL文の構文解析と最適化
ストレージエンジンの役割は大きく分けてトランザクション管理
、データフォーマットの定義
とデータ永続化
、インデックス管理
、ロック
と排他制御
がある。
- トランザクション管理
- データフォーマットの定義
- データ永続化
- インデックス管理
- ロック
- 排他制御
他にもストレージエンジンには固有の機能があり、MySQLなどで使われているInnoDB
には全文検索機能
や自動クラッシュリカバリ機能
などがある。
サーバー部分の詳細
多くのコア機能
と組み込み関数
がこの層で実装されている。
Connector
クライアントとMySQLの接続コネクションを管理する。
$ mysql> show processlist;
MySQL [production_recustomer_auth_db]> show processlist;
+------+-------+-----------+------+---------+------+----------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+------+-------+-----------+------+---------+------+----------+------------------+
| 3572 | admin | localhost | NULL | Query | 0 | starting | show processlist |
| 3575 | admin | localhost | NULL | Sleep | 121 | NULL | NULL |
+------+-------+-----------+------+---------+------+----------+------------------+
2 rows in set (0.01 sec)
Parser
SQLの字句解析と構文解析を行う。
-
字句解析
: 文字の並びを解析し、言語的に意味のある最小の単位(トークン)に分解する処理 -
構文解析
: 構文規則に基づいて、字句解析されたトークン同士の関係性を解析する処理
MySQLでは、ここで文法上の問題発見とアクセス先のテーブルの存在確認も行う。
Optimizer
実行に最適なクエリ計画を行う。
アクセス経路
、インデックスの有無
などのパラメーターから、最もコストの低いクエリを計画
する。
実行したいSQLの前にexplain
を付けると、クエリ計画を確認できる。
$ mysql>select * from user where id = 1;
$ mysql> explain select * from user where id = 1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | user | NULL | const | PRIMARY,id | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)
- partitions :
分割されたテーブルのうち、どれを検索対象にするか。
- possible_keys :
MySQL がこのテーブル内の行の検索に使用するために選択できるインデックス。
- key :
MySQL が実際に使用することを決定したキー (インデックス)。
- rows :
MySQL がクエリーを実行するために調査する必要があると考える行数。
Executor
エンジン(ストレージエンジン)
のインタフェースを利用して、コマンドを実行する。
ログシステム
SQL実行時の重要なログとして、以下の2種類が存在します。
- bin log
- redo log
bin log
はサーバー、redo log
はエンジン(InnoDBエンジン)
がデータベース更新時に出力するログです。
binlog
サーバーが出力するログであり、ログ内容としてはデータの変更内容を記録
しており、ログは可変的。
レコードの更新内容
or SQL文
を保持する。
謝って削除したデータを復元する際に利用する。
バックアップが存在する場合は、バックアップ ~ 指定時点のbinlog
を適用することで、指定時点のデータを復元することができる。(DBデータの復元)
マスタスレーブ構成など2つ以上のデータベースが存在する場合は、マスタからスレーブにbinlogを転送することでデータの同期を行う。(レプリケーション)
redo log
エンジンが出力するログ。
ディスクI/O
コストを削減するために設計されたログ。
メモリ上でレコードを更新し、redo log bufferに記録する。
その後、システムが空いている時にディスクに反映
させる。
redo logのサイズは固定
されており、サイズが足りなくなったらディスクに反映した上でログを上書きする。
DB自体に異常があった場合、メモリ上にあるredo logを使用することでデータの復旧を行うことができる。
MySQLの実行プロセス
- 更新対象レコードを取得
- レコードが
メモリ上に存在しない場合はディスク
から取得する - メモリ上でレコードを更新
-
redo log
を出力して、ステータスはprepared
-
binlog
を出力 -
redo log
のステータスをcommit
に変更する。
クラッシュによってMySQLが再起動した場合は、まずredo log
を確認する。
もしredo log
のステータスがcommit
であれば、redo logに記述された更新内容をコミット
する。
もしredo log
のステータスがprepared
であれば、binlog
の状態を確認する。
該当のbinlogが出力されていない
場合は、redo logの更新内容をロールバック
する。
binlogが出力されている
場合は、redo logの変更内容をコミット
する。
トランザクション分離レベルについて
ダーティーリード
あるトランザクションがコミットされていない未確定の変更情報の状態でも、別のトランザクションから変更内容を読み込めてしまう問題が発生する現象のこと。
ノンリピータブルリード - 反復不能読み取り
あるテーブルに対して読み取り(SELECT)
をした後に、別の人がそのテーブルに対して更新(UPDATE)によってデータを書き換えてしまうことで、次に読み取りした際に検索結果が異なってしまう問題が発生する現象のこと。
DBは複数の人が利用するので誰かがデータを読んだ後にデータが更新されることは当たり前ではあるものの、それによって不都合が起こる時がある。
ファントムリード
先ほどのノンリピータブルリードに似ている。
最初のテーブル読み取り(SELECT)の後から2回目の読み取り(SELECT)の間に、別の人が行の追加(INSERT)
を行うと、2回のSELECTで結果の行数が変わってしまうという問題が発生する現象のことです。
1回目の検索結果の行数を使った処理を行う時
に、2回目の検索結果の行数が違うことで問題
となってしまうことがある。
トランザクション分離レベル
上記3つの現象のどれが起きるかはトランザクション分離レベルによって決まる。
-
READ UNCOMMITED
: コミットされていない変更を他のトランザクションから参照できる設定。 -
READ COMMITED
: コミットされた変更を他のトランザクションから参照できる設定。 -
REPEATABLE READ
: 同じトランザクション内の一貫性読み取りはすべて、最初の読み取りによって確立されたスナップショットを読み取る。 -
SERIALIZABLE
: 一つ一つトランザクションを順番で実行する設定。
分離レベル | ダーティリード | ノンリピータブルリード | ファントムリード |
---|---|---|---|
READ-UNCOMMITTED | ○ | ○ | ○ |
READ-COMMITED | - | ○ | ○ |
REPEATABLE-READ | - | - | ○ |
SERIALIZABLE | - | - | - |
上記は「ロック」
という仕組みを使う。
ロックをかけるとその行は他の人のトランザクションからは読み書きができないようになります。
これによりデータの一貫性が保たれます。
トランザクションの分離
によって、データの不整合が発生する問題を解決することができますが、もちろんこれにはデメリットもある。
ロックをかけると、`その間は他のデータ処理が止まる`ので、データベースの動作が全体的に重くなってしまう。
一つ一つのロックは数ミリ秒ではあるものの、これが沢山発生すると大きな動作遅延に繋がってしまう。
MVCC - MultiVersion Concurrency Controll
RDBのisolation level(分離レベル)
がREAD COMMITTED
やREPEATBLE READ
なんかの時のために採用しているシステムのこと。
RDBでは同時に複数トランザクションが一つのテーブルを操作したり
する。
例えばあるトランザクションがテーブルAを参照中に、Aの内容を書き換えるような別のトランザクションが存在すると、取ってきたデータに不整合が生じる。
この不整合をどの程度許容するかが分離レベルである。
-
READ UNCOMMITTED
: COMMITされていないトランザクションAの変更をトランザクションBが参照できます -
READ COMMITTED
: COMMITされたトランザクションAの変更をトランザクションBが参照できます -
REPEATBLE READ
: COMMITされたトランザクションAの追加をトランザクションBが参照できます -
SERIALIZE
: 同時に最大1つまでのトランザクションしか存在しないのと等価です
READ COMMITTED
以降を実現するためにはSELECTで共有ロックを、UPDATEとINSERTで専有ロックを取得すれば良いが、パフォーマンスを考えるとなるべくロックの取得はしたくない。
そこで考え出されたのがMVCCであり、MVCC実装はRDBやストレージエンジンによって違うが、ここではMySQLのinnoDB
を考える。
MVCC実装
全てのテーブルの全てのレコードにはMySQLが自動的に以下の3つのカラム
を追加する。(ユーザからは見えません)
- DB_ROW_ID : その行のID
- DB_TRX_ID : 最後にそのレコードを追加・更新したトランザクションID
- DB_ROLL_PTR : そのレコードの過去の値を持つundo log recordへのポインタ
トランザクションIDが100
のトランザクションが、あるレコードを挿入するとDB_TRX_ID
に100が代入され、DB_ROLL_PTRはNULL
になる。
トランザクションIDが102のトランザクションでレコードの値を書き換えると,undo log recordに過去の値が保存され,DB_ROLL_PTRにそのrecordへのポインタが代入され,DB_TRX_IDに102が入ります.
もしこれらの変更がCOMMITされたとして、分離レベルがREPEATABLE READ
の場合、トランザクションIDが99のトランザクション
はこのレコードを参照できない。
トランザクションIDが101
のトランザクションはこのレコードの変更前の値をundo log record
から探して参照する。
トランザクションIDが103
のトランザクションは変更後の値を見ることができる。ざっくり「自分のトランザクションIDより未来の追加と変更は取得されない」と思っておけばそこまで困らない。