エンジニアのみなさん、お疲れ様です。
PostgreSQLを触っていると、インデックスを適切に貼っているのに「なぜかクエリが遅い」とか、「特定のレコードだけピンポイントで高速に取得したい」という場面に出くわすことがありますよね。
今日は、そんな悩みを抱えるエンジニアの秘密兵器、「TIDスキャン(TID Scan)」について深掘りしていこうと思います。これ、知っているのと知らないのとでは、いざという時のトラブルシューティングの引き出しの多さが全然違います。
—
TIDスキャンって何者?
普段、僕たちは`SELECT FROM users WHERE id = 123`のように、主キー(PK)を使って検索しますよね。この時、PostgresはB-treeインデックスを辿って、目的の行を探しに行きます。
でも、TIDスキャンはインデックスを介しません。
PostgreSQLの各行には、物理的な格納場所を示す「TID(Tuple Identifier)」というIDが付与されています。TIDは `(ブロック番号, オフセット番号)` という形式で管理されていて、いわば「データが物理的にどこに置いてあるか」という番地そのものです。
TIDスキャンは、この「番地」を直接指定して、ストレージから一撃でデータを引き抜く手法。インデックスを辿るオーバーヘッドすら存在しない、まさに最強のショートカットなんです。
—
実践:TIDスキャンを体験してみる
まずは、自分のテーブルでTIDがどうなっているか見てみましょう。システムカラムである `ctid` を指定すれば一目瞭然です。
— 普通にidで検索してみる
SELECT ctid, FROM users WHERE id = 123;
— 結果例: (0, 10) などが表示されるはずです
この `(0, 10)` が、そのデータが「0番目のページ(ブロック)の、10番目のスロット」にあることを示しています。
では、このCTIDを使って直接検索してみます。
— これがTIDスキャン!
SELECT FROM users WHERE ctid = ‘(0, 10)’;
どうでしょう? `EXPLAIN` をつけて実行計画を見てみてください。`Tid Scan` という文字が表示されたはずです。これが、インデックスすら使わずに最速でレコードを取り出す裏技です。
—
現場で使う時の「注意点」
ここまで聞くと「じゃあ、全部これで行けばいいじゃん!」と思うかもしれませんが、待ってください。ここに大きな罠があります。
1. ctidは「永久不変」ではない
これが一番の注意点です。PostgreSQLは `VACUUM` や `UPDATE` が発生すると、データの物理的な位置を動かすことがあります。つまり、さっきまで `(0, 10)` にあったデータが、更新後に `(0, 15)` に移動している、なんてことは日常茶飯事です。
なので、アプリケーションの永続的な参照先としてCTIDを保存するのは絶対にNGです。あくまで「一つのトランザクション内での一時的な参照」や、「複雑なクエリの最中の最適化」などに限定しましょう。
2. どんな時に役立つのか?
僕が現場で使うのは、主に以下のようなシーンです。
- 重複行の削除(デッドロック回避など):
「重複したデータがあるけど、ユニークキーがないから特定の1行だけを削除したい」という時、`DELETE FROM table WHERE ctid = ‘(x, y)’` とすれば、他の重複行に影響を与えずに確実にターゲットだけを消せます。
- 長時間実行されるバッチ処理での再開:
巨大なテーブルをループ処理する際、CTIDを一時的に保持しておいて、万が一のクラッシュ時に「ここから再開」という制御を行うケースがあります(もちろん、その間はVACUUMを走らせない工夫が必要ですが)。
—
まとめ
TIDスキャンは、PostgreSQLというエンジンの「裏口」を叩くような高度なテクニックです。常用するものではありませんが、「どうしても特定の1行を物理的に指し示したい」という時には、これ以上ないほど強力な味方になります。
データベースの内部構造を知っていると、クエリの実行計画を見た時の「解像度」がぐっと上がります。「なぜこのクエリは速いのか、遅いのか」を考える時、ぜひこの `ctid` の存在を思い出してみてください。
また現場で面白い挙動を見つけたら、共有しますね。それでは、いいコードを!
コメント