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

「え、まだ `WHERE id = 123` で消耗してるの?」

現場で若手エンジニアの書いたクエリを見ていて、ふと思うことがあります。もちろん、プライマリキーを使った検索はデータベースの基本中の基本。インデックスが効いていれば十分高速です。

でもね、もし君が「あとコンマ数ミリ秒を削り出したい」「超巨大なテーブルから、特定のレコードだけをピンポイントで取り出す必要がある」という修羅場に立たされているなら、今日話すTIDスキャン(TID Scan)という奥の手を覚えておいて損はありません。

—

TIDスキャンって何者?

PostgreSQLには、内部的に各行を識別するための物理的なアドレスが存在します。それが `ctid` です。

簡単に言うと、`ctid` は `(ページ番号, ページ内のスロット番号)` という形式で管理されている、いわば「そのデータが物理ディスクのどこに埋まっているか」を示す住所のようなもの。

通常、インデックススキャンは「B-treeを辿って → ページを探して → レコードを見つける」というステップを踏みますが、TIDスキャンは違います。「そこにいるのは分かっているから、直接そこへ行け」という命令です。これがTIDスキャンの正体。PostgreSQLにおける最速のアクセスパスです。

どうやって使うの?

使い方は拍子抜けするほど簡単です。`ctid` カラムを直接指定するだけ。

— テーブルの中身を見てみる
SELECT ctid, FROM users LIMIT 5;

— 特定の物理位置を直接指定して取得
SELECT FROM users WHERE ctid = ‘(10, 1)’;

これだけ。これを通すと、実行計画(`EXPLAIN`)には見慣れない `Tid Scan` という文字が現れます。

ただし、「劇薬」であることを忘れないで

ここまで聞くと「じゃあ全部これでいいじゃん!」と思うかもしれませんが、ここで先輩からの忠告です。TIDスキャンは諸刃の剣どころか、使い方を間違えると大事故につながります。

1. `ctid` は「永遠」ではない

これが最大の注意点です。PostgreSQLのVACUUMやUPDATE処理によって、データが別のページに移動(HOTアップデートの失敗など)すると、`ctid` は平気で変わります。
つまり、「昨日保存した `ctid` を今日のクエリで使う」なんてことは絶対にNGです。

2. トランザクション内での利用が鉄則

TIDスキャンが輝くのは、あくまで「同一トランザクション内、あるいはバッチ処理の直後のステップ」のような、データの位置関係が保証されている短いスパンです。
例えば、「一度フルスキャンして、特定の条件に合致したレコードの `ctid` リストを保持し、その後の処理で高速に再アクセスする」といった使い方が、実務では最も一般的です。

実践的な使いどころ:大量データの更新・削除

僕がよくやるのは、数千万行あるテーブルに対する「特定の行だけを消したい」という処理です。

— 1. まずターゲットの ctid だけを抽出して一時テーブルへ
CREATE TEMP TABLE target_ctids AS
SELECT ctid FROM orders WHERE status = ‘invalid’ FOR UPDATE;

— 2. 直接 ctid で指定してバッチ処理を行う
DELETE FROM orders WHERE ctid IN (SELECT ctid FROM target_ctids);

このように、一度特定してしまえば、あとは物理アドレス直叩きで処理できるので、インデックスのオーバーヘッドを気にせず爆速で処理が完了します。

—

最後に:銀の弾丸はない

TIDスキャンは、PostgreSQLの深い部分に触れる強力な武器です。でも、初心者が闇雲に使うようなものではありません。「なぜインデックスが効かないのか?」「なぜこのアクセスパスが必要なのか?」を理解した上で使うからこそ、意味があります。

データベースのチューニングは、こうした「仕組みの裏側」を知ることで、一段階上のレベルに行けます。もし君のプロジェクトで、数百万件のレコードを相手にクエリが遅くて泣きそうになっているなら、一度 `EXPLAIN` を叩いて、物理層のデータ配置に思いを馳せてみてください。

さて、今日の現場のトラブルシューティングはここまで。また面白いクエリの書き方を思いついたら共有しますね。頑張って!

コメント

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