「読み込みは書き込みをブロックしない」— PostgreSQLのMVCCが僕たちに教えてくれること
現場でPostgreSQLを触っていると、たまにこんな質問を受けるんだ。「読み取り専用の重い集計クエリを走らせている間、なぜ更新系の処理が止まらないんですか?」ってね。
これ、実はPostgreSQLの心臓部にあるMVCC (Multi-Version Concurrency Control:多版型同時実行制御) という仕組みのおかげなんだ。今回は、この「縁の下の力持ち」がどうやって僕たちのアプリケーションのパフォーマンスを守っているのか、現場目線で紐解いていこう。
—
そもそも、なぜ「ロック」じゃないのか?
MySQLのInnoDBみたいに、行ロックを多用するDBに慣れていると、最初は違和感があるかもしれないね。
もし、読み取りのたびに厳密なロックをかけていたらどうなると思う?
「誰かがデータを読んでいる間、他の人は一切書き込めない」。こんな状態じゃ、今のモダンなWebサービスなんて一瞬でパンクしてしまうよね。
PostgreSQLは「読み取りと書き込みが互いをブロックしない」という設計思想を選んだ。これがMVCCの正体だ。
MVCCの魔法:データは「上書き」されない
MVCCの核心は、「更新しても古いデータは消さない」というルールにあるんだ。
PostgreSQLで`UPDATE`を実行したとき、実際には古いレコードに印をつけて、「これからはこっちの新しいレコードが正解ですよ」という新しい行を物理的に別の場所に書き込んでいるんだ。
これをエンジニア界隈では「タプル(行)のバージョン管理」と呼んでいる。
具体的な挙動を見てみよう
例えば、あるユーザーの残高(`balance`)を更新するトランザクションを考えてみるよ。
1. T1(トランザクションA) が開始される。
2. T1 が行を更新する。このとき、古い行はそのまま残り、新しい行が作成される。
3. T2(トランザクションB) が同じ行を読みに行く。
ここで魔法が起きる。PostgreSQLは、「今のトランザクション(T2)から見て、どのデータが有効か?」を判定するスナップショットという仕組みを使うんだ。
T2は「自分が開始された瞬間に有効だったデータ」だけを見に行くから、T1が裏で何をやろうが、T2は一貫性のある古いデータを読み取り続けられる。だから、読み取りは書き込みを待つ必要がないんだ。
—
実務で意識すべき「バキューム(VACUUM)」の話
ここまで聞くと「最高じゃん!」って思うかもしれないけど、一つだけ落とし穴がある。「不要になった古いデータがゴミとして溜まる」ことだ。
更新を繰り返すと、テーブルの中に「もう誰も参照していない古いバージョンの行」が山のように積み上がる。これが放置されると、ディスク容量を圧迫するし、スキャンする行数が増えてパフォーマンスがガタ落ちする。
ここで登場するのが `VACUUM` だ。
— 実際にはPostgreSQLがバックグラウンドで勝手にやってくれる(Autovacuum)
— けど、大量更新の直後は手動でやることもある
VACUUM ANALYZE users;
Autovacuumがうまく働かないと、いわゆる「テーブルの肥大化(Bloat)」という沼にハマる。現場で「最近クエリが遅いな?」と思ったら、まずはこのMVCCのゴミ掃除が追いついているかを疑うのが、ベテランへの第一歩だよ。
—
実践的なアドバイス
MVCCを理解していると、設計の視点も変わってくるはずだ。
- 長時間のトランザクションは避ける:
トランザクションをダラダラと開いたままにすると、スナップショットが古いまま固定され、システム全体で古いデータの掃除(VACUUM)ができなくなる。これが「トランザクションIDの周回問題」や「テーブル肥大化」の大きな原因になるんだ。
- 不要な更新を減らす:
値が変わっていないのに `UPDATE` を発行すると、MVCCの仕組み上、無駄に新しいバージョンが作られてしまう。`WHERE` 句で「実際に値が変わる時だけ更新する」工夫をするだけで、DBの負荷は劇的に軽くなるよ。
まとめ
PostgreSQLのMVCCは、「一貫性を保ちつつ、同時実行性を最大化する」ための、非常にエレガントな仕組みだ。
「読み取りと書き込みが喧嘩しない」というこの特性を深く理解しておけば、パフォーマンスチューニングの引き出しがグッと増えるはずだよ。もし同僚が「DBが重い」と嘆いていたら、まずは「もしかして、MVCCの掃除が間に合ってない?」と声をかけてみてほしい。
さて、今日はここまで。また現場で役立つ深い話をしよう。質問があったら、いつでもSlackに投げてくれよな!
コメント