0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PostgreSQL のインデックス、どれくらい効くのか実測してみた

0
Posted at

注意: この記事は特定のスキーマ・データ量・クエリパターンでの検証結果をまとめたものです。インデックスの効果はテーブル設計やワークロードによって大きく変わるため、あくまで一例として参考にしてください。実際のプロダクトでは自身の環境で計測することを推奨します。


この記事でやること

「インデックスを貼れば SELECT は速くなる」というのはよく知られた話ですが、自分が未経験だった頃は、ただ貼ればいいというのであれば、全カラムにindexを貼ればいいのでは?と考えたりしたことがあります。

ですが、インデックスを貼ると同時にデメリットももちろん存在します。
具体的に言えばINSERT / UPDATE が遅くなるコストと、ストレージ容量の増加です。

ただ、一概にデメリットがあると言えど、

  • どのくらい速くなるのか?
  • どのくらい遅くなるのか?
  • インデックスはストレージをどれだけ食うのか?

このあたりはいつも曖昧なままインデックスを貼ってしまう事が多いので、
本記事ではこれらを同じデータ・同じクエリで計測して、インデックスを貼る/貼らないの判断材料を整理することを目的とします。

検証環境

項目
DB PostgreSQL 18 (Docker)
ORM Prisma v6.19
計測回数 15回 (ウォームアップ3回)
バッチサイズ 1,000件/回

スキーマ (抜粋)

model BenchmarkRecord {
  id        Int      @id @default(autoincrement())
  tenantId  Int
  status    Int      // 0 or 1 (低選択性)
  score     Int
  email     String   // ユニークに近い (高選択性)
  payload   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

比較する 3 シナリオ

シナリオ 内容
baseline 主キー (id) のみ。インデックスなし
selective_indexes 選択性が高い列だけにインデックス (email, tenantId + status)
over_indexed 上記に加え、低選択性の列にもインデックス (status, score, createdAt)

計測するクエリ

クエリ 内容 想定
pointReadByEmail email で 1件取得 高選択性 READ
selectiveReadByTenantStatus tenantId + status で COUNT 複合キー READ
lowSelectivityReadByStatus status だけで COUNT 低選択性 READ
selectiveUpdateByTenantStatus tenantId + status で UPDATE インデックス列の更新
createMany 1,000件バッチ INSERT 書き込みコスト

結果

20,000 行

クエリ baseline selective over_indexed
pointReadByEmail 0.665ms 0.212ms (3.1x) 0.247ms (2.7x)
selectiveReadByTenantStatus 0.894ms 0.196ms (4.6x) 0.217ms (4.1x)
lowSelectivityReadByStatus 1.012ms 0.647ms (1.6x) 0.649ms (1.6x)
selectiveUpdateByTenantStatus 2.917ms 1.688ms (1.7x) 2.327ms (1.3x)
createMany 15.091ms 14.466ms (1.0x) 16.183ms (0.93x)

100,000 行

クエリ baseline selective over_indexed
pointReadByEmail 2.654ms 0.216ms (12.3x) 0.267ms (9.9x)
selectiveReadByTenantStatus 4.410ms 0.360ms (12.3x) 0.367ms (12.0x)
lowSelectivityReadByStatus 5.066ms 2.812ms (1.8x) 3.266ms (1.6x)
selectiveUpdateByTenantStatus 8.186ms 2.473ms (3.3x) 5.174ms (1.6x)
createMany 15.869ms 15.662ms (1.0x) 18.650ms (0.85x)

当たり前ですが、データ量が増えるほどインデックスの効果が大きくなるのが顕著です。email の pointRead は 20K 行で 3.1 倍、100K 行で 12.3 倍と、データ量に応じて差が開いています。

ちょっと意外だったのはselectiveとover_indexedでpointReadByEmailの読み取りが悪化していることです。計測回数による誤差の範囲かもしれませんが、インデックスを増やせば READが速くなるとは限らない可能性はありそうです。


インデックスのストレージコスト

レイテンシだけでなく、インデックスがどれだけディスクとメモリを消費するかも見ておきます。

計測前の私の勝手なイメージですが、テーブルが単純に1つ増えるので2倍近く(1.5~1.7?)に膨らむんじゃないかと予想していました。

100,000 行での BenchmarkRecord のサイズ

シナリオ テーブル本体 インデックス合計 合計 Index / Table
baseline 21.4 MB 2.2 MB 23.6 MB 10.1%
selective 22.7 MB 14.2 MB 36.9 MB 62.6%
over_indexed 21.5 MB 15.8 MB 37.3 MB 73.7%

baseline でもPKインデックス分の 2.2 MB が存在しますが、selective ではテーブル本体の 62% 、over_indexed では 74% にまでインデックスが膨らんでいます。
大体予想通りでしたね。

個別インデックスのサイズ (100K 行)

インデックス 対象列 サイズ
idx_br_email email (文字列) 11.2 MB
idx_br_score score (整数) 1.3 MB
idx_br_status status (整数, 低選択性) 984.0 kB
idx_br_tenant_status tenantId + status (複合) 904.0 kB
idx_br_created_at createdAt (タイムスタンプ) 816.0 kB

文字列カラム (email) のインデックスは整数カラムの 約10倍 のサイズです。インデックスはメモリ (shared_buffers) にキャッシュされるため、不要なインデックスは他のデータのキャッシュを圧迫します。


インデックスを貼る判断基準

以上を踏まえると、シンプルな判断式は以下の通りです。

インデックスを維持する条件:
  読み取り改善量 (ms) × 読み取り頻度 > 書き込み劣化量 (ms) × 書き込み頻度

はい、そうですね。当たり前の計算式になってしまいますね。

たとえば 100K 行で selective_indexes の場合:

  • pointReadByEmail の改善: 2.654 - 0.216 = 2.438ms/回
  • createMany の劣化: 15.662 - 15.869 = ほぼ 0ms/回 (誤差範囲)

この場合、read が 1回でもあれば元が取れます。

一方、over_indexed の lowSelectivityReadByStatus:

  • 改善: 5.066 - 3.266 = 1.8ms/回
  • createMany の劣化: 18.650 - 15.869 = 2.78ms/回

write 1回あたりの劣化が read 1回あたりの改善を上回っています。read/write 比率が 2:1 以上でないとこのインデックスは割に合いません。さらに status の選択性が低いことを考えると、このインデックスは不要と判断できます。


まとめ

今回の検証で、インデックスの READ 改善効果と WRITE のペナルティを具体的な数字で確認してみました。

個人的には、READ の改善幅が想像以上に大きかったのが印象的でした。100K 行で 12 倍というのは、貼らない理由を探す方が難しいレベルです。
一方で INSERT / UPDATE のペナルティは、selective であれば正直あまり気にならない程度でした。
それよりも、インデックスを増やしたときのストレージの膨らみ方の方が気になりました。over_indexed ではテーブル本体の 74% に相当するサイズがインデックスに使われており、これがキャッシュ効率やディスク消費に効いてくると考えると、WRITE の数ms よりもこちらの方が実運用上のコストとしては大きいかもしれないです。


おまけ: PostgreSQL 16 vs 18 の比較

同じスクリプト・同じスキーマで PostgreSQL 16 でも計測していたので、100K 行 / selective_indexes の結果を並べてみます。

クエリ PG16 PG18 変化
pointReadByEmail 0.385ms 0.216ms 44% 速い
selectiveReadByTenantStatus 0.498ms 0.360ms 28% 速い
lowSelectivityReadByStatus 4.149ms 2.812ms 32% 速い
selectiveUpdateByTenantStatus 3.493ms 2.473ms 29% 速い
createMany 20.851ms 15.662ms 25% 速い

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?