「本番環境でインデックスを貼ったらDBが死んだ」を避けるために。CONCURRENTLYを使いこなそう
こんにちは。今日はPostgreSQLの運用において、エンジニアなら誰もが一度は通る「ヒヤリハット」と、それを確実に回避する魔法のコマンドについて話そうと思う。
大規模なサービスを運営していると、必ずぶつかる壁がある。「検索が遅いからインデックスを追加したい。でも、テーブルは24時間365日書き込みが発生している……」という状況だ。
ここで何も考えずに `CREATE INDEX` を叩いてしまうと、テーブルに強力なロックがかかり、アプリ側の書き込みリクエストが全て待ち状態(ロック待ち)になって、サービスが数分間フリーズする。いわゆる「サービスダウン」だ。
そんな悪夢を見ないための必須テクニック、`CONCURRENTLY` オプションについて解説するよ。
通常のインデックス作成は「通行止め」を作る
まず、なぜ通常の `CREATE INDEX` が危険なのかを理解しておこう。
通常、インデックスを作成すると、PostgreSQLはテーブル全体を読み取ってインデックスを構築する。この作業中、一貫性を保つためにテーブルに対して「排他ロック」がかかる。
- 読み取り(SELECT): 待たされることはない。
- 書き込み(INSERT, UPDATE, DELETE): 完全にブロックされる。
数百万件あるテーブルでこれを行うと、インデックス構築が終わるまで書き込みが一切できなくなる。深夜のメンテナンス時間ならまだしも、昼間にやってしまったら目も当てられないよね。
CONCURRENTLYは「迂回路」を作りながら工事する
そこで登場するのが `CONCURRENTLY` だ。
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
このオプションを付けると、PostgreSQLはテーブルをロックせずにインデックスを構築してくれる。仕組みとしては、2回のスキャンを行ってインデックスの状態を少しずつ更新していくという、非常に賢いアプローチを取るんだ。
注意点:魔法ではない
ただし、この機能にはトレードオフがある。現場で使う前に、これだけは覚えておいてほしい。
1. 時間がかかる: 通常の作成よりも、CPUやI/Oを多く消費し、処理完了までの時間が長くなる。急いでいる時に使うものではないね。
2. トランザクションブロック内では使えない: `BEGIN; … COMMIT;` の中では実行できない。このコマンド自体が独自のトランザクションを持つからだ。
3. 失敗した時のケアが必要: もし何らかの原因(制約違反やタイムアウトなど)で構築が失敗すると、インデックスは「無効(INVALID)」な状態で残る。これを放置するとゴミになるから、`DROP INDEX` で消してやり直す必要がある。
実践:安全にインデックスを適用する手順
現場で安全に作業するなら、以下の流れを守るのが鉄板だ。
1. インデックス作成を実行
まずはバックグラウンドで実行する。
— ターミナルで実行、あるいはマイグレーションツールから発行
CREATE INDEX CONCURRENTLY idx_users_created_at ON users (created_at);
2. 失敗していないか確認
もし構築に失敗していたら、テーブルに「無効なインデックス」が残ってしまう。必ず `\d` やシステムカタログで確認しよう。
— PostgreSQLのコンソールで確認
\d users
もしインデックス名が表示されていても、`INVALID` となっていれば、それはゴミだ。すぐに `DROP INDEX CONCURRENTLY idx_users_created_at;` で削除して、原因(制約違反など)を特定してから再挑戦しよう。
最後に:エンジニアとしての心得
`CONCURRENTLY` は便利だけど、あくまで「リソースを食う」という事実は変わらない。
もしテーブルサイズがテラバイト級なら、構築中にサーバーの負荷が跳ね上がって、結果的にレスポンスが悪化する可能性だってある。本番環境で実行する前には、必ずステージング環境で構築にかかる時間を計測して、監視グラフを見ながら慎重に適用する。
「コマンドを知っている」ことと、「安全に運用できる」ことは別物だ。ぜひ、このオプションを使いこなして、深夜の緊急呼び出しを一つでも減らしてほしい。
それじゃ、また現場で会おう。
コメント