【実務・中級編】 TIDスキャン – PostgreSQL

エンジニアのみなさん、お疲れ様です。

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` の存在を思い出してみてください。

また現場で面白い挙動を見つけたら、共有しますね。それでは、いいコードを!

コメント

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