8
4

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のSlow Queryを改善した話:たった1行の変更でパフォーマンスが劇的に改善したケーススタディ

8
Posted at

PostgreSQLのSlow Queryを改善した話:たった1行の変更でパフォーマンスが劇的に改善したケーススタディ

はじめに

最近担当したEC系プロジェクトで、よくある機能を実装していました。

  • 「次の注文(Next)」
  • 「前の注文(Previous)」

いわゆる詳細画面での前後ナビゲーションです。

一見シンプルですが、重要な制約がありました:

全件データをメモリにロードせずに実現すること

そのため、Offset Paginationではなく、**Keyset Pagination(カーソルベース)**を採用しました。


初期実装

ソート条件は以下の2つ:

  • paid_at(支払い日時)
  • id(同一時刻のタイブレーク)

Laravel Eloquentでの「次の注文」の実装はこんな感じでした:

->where(function (Builder $q) use ($order) {
    $q->where('paid_at', '>', $order->paid_at)
        ->orWhere(function (Builder $q1) use ($order) {
            $q1->where('paid_at', $order->paid_at)
                ->where('id', '>', $order->id);
        });
})
->orderBy('paid_at')
->orderBy('id')

ロジックとしては非常に自然です:

paid_at が大きいもの、
もしくは同じ場合は id が大きいものを取得する


発生した問題

データが数百万件規模に増えたタイミングで:

  • Slow Queryが発生
  • Query Planが以下のように悪化:
    • Bitmap Heap Scan
    • 最悪の場合 Seq Scan

一方で、DBには以下のインデックスがありました:

(paid_at, id)

つまり、インデックスは正しく張られているのに遅い状態です。


根本原因

原因はシンプルで、ORです。

PostgreSQLのQuery PlannerはORを含む条件に対して:

  • 条件を分岐として扱う
  • 複数のスキャンを組み合わせる
  • Composite Indexをうまく活用できない

結果として、意図したIndex Scanにならず、パフォーマンスが低下します。


解決策

ここで使ったのが、PostgreSQLの**Tuple Comparison(行値比較)**です。

修正後

->whereRaw('(paid_at, id) > (?, ?)', [$order->paid_at, $order->id])

「前の注文」は逆方向:

->whereRaw('(paid_at, id) < (?, ?)', [$order->paid_at, $order->id])

なぜこれで速くなるのか?

Composite Index (paid_at, id) は、内部的に以下の順序でソートされています:

  1. paid_at
  2. 同じ場合は id

これは辞書順(lexicographical order)です。

(paid_at, id) > (x, y)

という条件を書くことで、PostgreSQLは:

  • インデックス上の (x, y) の位置に直接ジャンプ
  • そこから順方向にスキャン(Index Scan)

つまり、ORのような分岐処理が不要になります。


結果

この変更により:

  • Query Planは Index Scan に改善
  • Slow Queryが解消
  • レスポンスが安定

さらに、副次的なメリットとして:

コードがシンプルに

Before:

  • ネストしたクロージャ
  • 可読性が低い

After:

->whereRaw('(paid_at, id) > (?, ?)', [...])

かなりスッキリしました。


学び

1. Keyset PaginationではORを避ける

特に:

  • 複数カラムでソートしている場合
  • Composite Indexを使っている場合

2. ORMを過信しない

ORMは便利ですが:

最適なSQLを常に生成してくれるわけではない

必要な場面では、Raw SQLを使う判断も重要です。


3. Tuple Comparisonは強力

PostgreSQLでは以下のような書き方が可能です:

(col1, col2) > (val1, val2)

ユースケース:

  • Keyset Pagination
  • 複数カラムの比較

まとめ

たった1行の変更で:

  • パフォーマンス改善
  • Slow Query解消
  • コードの簡潔化

が実現できました。

このケースから言えるのは:

パフォーマンス改善は、大きなリファクタではなく「DBの仕組み理解」で解決できることが多い

8
4
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
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?