PostgreSQL | SQLite+MySQL経験者向け、30日で習得するPostgreSQL:設計とパフォーマンス - Day19 トランザクション

SQL PostgreSQL
スポンサーリンク

Day19 前半のゴール

「“同時に更新しても壊れない”仕組みをイメージで理解する」

ここからはトランザクション編です。
Day19 のテーマは PostgreSQL の心臓部、「MVCC(Multi Version Concurrency Control:マルチバージョン制御)」。

前半のゴールはこうです。
なぜ MVCC という仕組みが必要なのかを、直感レベルで理解する。
「同じ行に“バージョン”がいくつも存在する」というイメージを持てる。
トランザクションごとに「見えている世界が違う」ことを、簡単な例で説明できる。

まずは、難しい用語より「ストーリー」で掴みにいきます。


なぜMVCCが必要なのか

「同時アクセスで“ケンカしない”ためのルール」

データベースは、同時にたくさんのクライアントからアクセスされます。

あるユーザーが商品を購入して在庫を減らしている。
別のユーザーが同じ商品ページを開いて在庫数を見ている。
さらに別のバッチ処理が、売上集計のために大量に読み取っている。

もし「1つの行を誰かが触っている間、他の人は一切触れない」というルールにすると、
すぐにロックだらけになって、システム全体が詰まります。

逆に、「みんな好き勝手に読み書きしていい」とすると、
在庫がマイナスになったり、更新が消えたり、整合性が壊れます。

そこで PostgreSQL が採用しているのが、「MVCC」という考え方です。
ざっくり言うと、

同じ行の“過去バージョン”を残しておき、
トランザクションごとに「どのバージョンが見えるか」をコントロールすることで、
読み取りと書き込みがケンカしないようにする仕組み

です。


MVCCのざっくりイメージ

「1行に“時間軸で並んだコピー”があると思ってみる」

普通に考えると、「テーブルの1行」は1つだけ、と思いがちです。
でも PostgreSQL の内部では、「同じ行のバージョン」が時間軸でいくつも並んでいるイメージになります。

ユーザーID 1 の行を例にします。

最初の状態:
id = 1, name = ‘Taro’, balance = 1000

誰かがトランザクションAで balance を 1000 → 800 に更新したとします。

UPDATE が走った瞬間、内部的にはこうなります。

古いバージョン:id = 1, name = ‘Taro’, balance = 1000
新しいバージョン:id = 1, name = ‘Taro’, balance = 800

「上書き」ではなく、「新しい行を追加して、古い行も残しておく」というのがポイントです。
そして、「どのトランザクションから見たときに、どのバージョンが見えるか」を MVCC が決めています。


具体例1:同時に読む人と書く人がいる場合

「書き込み中でも“安定した読み取り”ができる理由」

タイムラインを使ってイメージしてみましょう。

時刻 t1:
トランザクションAが開始する(ユーザーA)。
SELECT * FROM accounts WHERE id = 1;
結果:balance = 1000

時刻 t2:
トランザクションBが開始する(ユーザーB)。
UPDATE accounts SET balance = 800 WHERE id = 1;
まだ COMMIT していない。

時刻 t3:
トランザクションAがもう一度同じ行を読む。
SELECT * FROM accounts WHERE id = 1;

ここで、トランザクションAは balance をいくつとして見るべきでしょうか。

MVCC では、「トランザクションAが開始した時点で“見えていた世界”を維持する」ように動きます。
つまり、A からはずっと balance = 1000 が見えます。

一方、トランザクションBの中では、更新後の balance = 800 が見えています。
A と B で「見えている世界」が違うわけです。

このおかげで、

読み取り側(A)は、書き込み中のBに邪魔されず、安定した結果を見続けられる。
書き込み側(B)は、新しいバージョンを作るだけで、読み取りを止めなくて済む。

という状態が実現できます。


具体例2:COMMITされた後に新しく来た人

「新しく来たトランザクションは“最新バージョン”を見る」

さきほどの続きです。

時刻 t4:
トランザクションBが COMMIT する。
これで balance = 800 のバージョンが「正式な最新」として確定します。

時刻 t5:
トランザクションCが新しく開始する。
SELECT * FROM accounts WHERE id = 1;

このとき、トランザクションCは balance = 800 を見ます。
なぜなら、「Cが開始した時点では、すでにBの更新がコミット済み」だからです。

整理すると、

トランザクションA(t1開始)
→ 一生 balance = 1000 の世界を見続ける。

トランザクションB(t2開始)
→ 自分の中では balance = 800 を見て、COMMITで確定させる。

トランザクションC(t5開始)
→ 最初から balance = 800 の世界を見てスタートする。

この「開始したタイミングで“どのバージョンまで見えるか”が決まる」という考え方が、
MVCC の超重要ポイントです。


「スナップショット」という考え方

「トランザクションは“世界の写真”を1枚持っている」

今の話を、もう少し抽象化してみます。

トランザクションが開始した瞬間に、その時点のデータベースの状態を「スナップショット」として切り取る。
そのトランザクションは、基本的にそのスナップショットに基づいてデータを見る。

さっきの例で言うと、

トランザクションAのスナップショット
→ balance = 1000 の世界。

トランザクションCのスナップショット
→ balance = 800 の世界。

という感じです。

この「スナップショットを持っている」というイメージを持つと、

なぜ途中で他人が更新しても、自分の SELECT の結果がブレないのか。
なぜトランザクションを長く開きっぱなしにすると、古いバージョンが溜まり続けてしまうのか。

といった話にもつながっていきます(後半で触れます)。


MVCCとロックの関係

「“全部ロック”ではなく“バージョン+必要最小限のロック”」

「バージョンを増やすなら、ロックいらないの?」という疑問も出てくると思います。

答えは、「ロックも使うけど、読み取りのために行ロックを強制しないようにしている」です。

更新系(UPDATE / DELETE / INSERT)は、当然ながら競合を避けるためにロックを使います。
ただし、MVCC のおかげで「読み取りのために更新をブロックする」必要が減ります。

読み取りは「自分のスナップショットに合うバージョン」を見ればいいので、
更新中の最新バージョンを無理に見に行かなくていい、という発想です。

これが、PostgreSQL が「読み取りが多いシステムでも比較的スムーズに動く」理由のひとつです。


ざっくりまとめると「こういう世界」

1行は1つではなく、「時間軸に沿って複数バージョンが並んでいる」と考える。
トランザクションが開始した瞬間に、「どのバージョンまで見えるか」が決まる(スナップショット)。
読み取りはそのスナップショットに基づいて行われるので、途中で他人が更新しても結果がブレない。
更新は新しいバージョンを追加する形で行われ、古いバージョンはすぐには消されない(後半でVACUUMの話につながる)。

この「マルチバージョン+スナップショット」のイメージが、MVCC のコアです。


Day19 前半のまとめ

PostgreSQL の MVCC は、「同じ行を“上書き”するのではなく、“新しいバージョンを追加し、古いバージョンも残す”ことで、トランザクションごとに見える世界を変える」仕組みであり、トランザクション開始時点の状態を“スナップショット”として固定することで、「読み取り中に他のトランザクションが更新しても、自分の SELECT の結果がブレない」状態を実現している。
その結果、読み取りは基本的に更新をブロックせずに進められ、更新側は新しいバージョンを作るだけで済むため、「同時に読む人と書く人がいてもケンカしない」世界が作られている――この「1行に複数バージョン」「トランザクションごとに違うスナップショット」というイメージを持つことが、Day19 前半の着地点になる。

タイトルとURLをコピーしました