MySQLのクエリプランナーミスを回避するインデックスヒントの追加

rails/solid_queue

Solid Queueの並行実行制御において、ブロックされたジョブの解放クエリにインデックスヒントを追加することで、MySQLのクエリプランナーによる不適切な実行計画選択を防ぎ、不要な行ロックを回避できるようになりました。これにより、並行実行制限を設定したジョブが期待通りの並列度で処理される問題が解決されます。

背景

並行実行制限を設定したジョブで、期待される並列度よりも少ないワーカーしか動作しない問題が報告されていました。#694では、並行度5に設定したジョブが最初の5件処理後、1ワーカーのみが処理を継続する現象が確認されています。

この問題は、ブロックされた実行を取得するクエリが引き起こす競合状態に起因していました。以下のクエリは、concurrency_key列のインデックスにより、単一行ではなく同じconcurrency_keyを持つ行の範囲全体をロックします:

SELECT `solid_queue_blocked_executions`.*
FROM `solid_queue_blocked_executions`
WHERE `solid_queue_blocked_executions`.`concurrency_key` = 'DummyJob/DummyJob'
ORDER BY `solid_queue_blocked_executions`.`priority` ASC, `solid_queue_blocked_executions`.`job_id` ASC
LIMIT 1 FOR UPDATE SKIP LOCKED;

2つのワーカーがこのクエリを同時に実行すると、2番目のワーカーはレコードを取得できず、ブロックされたジョブがないと判断して処理を終了します。セマフォ値は正しく4を保持しているにもかかわらず、10分ごとのオーケストレーターによる解放まで、ブロックされたジョブが取得されない状態が続きます。

技術的な変更

BlockedExecution.release_oneメソッドに、複合インデックスindex_solid_queue_blocked_executions_for_releaseを使用するヒントが追加されました。

変更前:

def release_one(concurrency_key)
  transaction do
    if execution = ordered.where(concurrency_key: concurrency_key).limit(1).non_blocking_lock.first
      execution.release
    end
  end
end

変更後:

def release_one(concurrency_key)
  transaction do
    if execution = ordered.where(concurrency_key: concurrency_key).limit(1)
        .use_index(:index_solid_queue_blocked_executions_for_release)
        .non_blocking_lock.first
      execution.release
    end
  end
end

新たに追加されたuse_indexメソッドは、SolidQueue::Recordクラスメソッドとして実装され、Railsのoptimizer_hintsを利用してインデックスヒントを生成します:

def use_index(*indexes)
  optimizer_hints "INDEX(#{quoted_table_name} #{indexes.join(', ')})"
end

このメソッドは、MySQL 8のオプティマイザーヒント形式/*+ INDEX(...) */でSQLコメントを生成します。MySQLのクエリプランナーはこのコメントをヒントとして解釈し、SQLiteとPostgreSQLは通常のコメントとして無視するため、データベース間の互換性が保たれます。

設計判断

インデックスヒントをSQLコメントとして実装する方式が採用されました。

MySQL固有のUSE INDEX構文ではなく、オプティマイザーヒントコメント/*+ ... */を使用することで、他のデータベースエンジンとの互換性を維持しています。SQLiteとPostgreSQLはこのコメントを無視するため、データベースごとに異なるクエリを生成する必要がありません。

optimizer_hintsは既にActive Recordに存在するAPIであり、新たな抽象化を導入することなく、Railsのクエリビルダーの標準的な拡張ポイントを活用した判断といえます。use_indexメソッドをSolidQueue::Recordに実装することで、Solid Queue全体で再利用可能なインターフェースを提供しています。

まとめ

本PRは、MySQLのクエリプランナーが不適切なインデックスを選択する問題に対し、明示的なインデックスヒントを追加することで解決しています。SQLコメント形式のヒントを使用することで、MySQL環境での問題を解決しながら、他のデータベースエンジンとの互換性を保持する設計です。並行実行制御における競合状態の根本原因に対処し、期待通りの並列度でジョブ処理が行われるようになります。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の3部構成が明確です。リード文、背景、技術的な変更、設計判断、まとめの各要素が過不足なく含まれており、理想的な構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)とGitHubのPR/Issueリンク記法([#番号](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「クエリプランナー」「インデックスヒント」「行ロック」といった専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されています。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは、提供されたDiffの内容を正確に反映しています。変更前後のコード引用、ファイルパスともに正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「optimizer_hints」「クエリプランナーミス」などの技術用語が、PR Descriptionの内容と一致しており、文脈上も正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

インデックスヒントがなぜ必要だったのか、SQLコメント形式のヒントがどのように機能するのかについて、技術的に正確かつ論理的に説明されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(問題の背景、解決策、実装方法)は、PRのDescription、Diff、関連Issue番号によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#718)と関連Issue番号(#694)が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「MySQLのクエリプランナーミスを回避するインデックスヒントの追加」は、PRのタイトル「Add index hint for releasing blocked executions」の内容を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれない外部知識(バージョンのサポート状況、リリース日程など)の追加はなく、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事内の時間表現はPR情報と矛盾しておらず、事実関係を正確に伝えています。