SQLServer - TECH PLAY - TECH PLAY

TECH PLAY

SQLServer

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

G-gen の今村です。オンプレミスの PostgreSQL から Cloud SQL for PostgreSQL への移行において、 postgresql.conf の設定をどのように扱うべきか、マネージドサービスの仕様に基づくパラメータの分類と代替手法を解説します。 概要 データベースフラグの概要 データベースフラグとは フラグ設定時の注意点 設定値の確認と更新 パラメータの確認 パラメータの更新 Google が管理するパラメータ 前提と注意点 ネットワークと接続管理 ログ管理 ハードウェア依存の設定 ユーザーが管理するパラメータ 前提と注意点 パフォーマンスチューニングフラグ データベース内での代替設定 概要 Cloud SQL for PostgreSQL をデータベースとして採用する場合や、オンプレミスの PostgreSQL から Cloud SQL for PostgreSQL への移行を検討する際、データベース管理者が直面するのが postgresql.conf で定義するパラメータの扱いです。 Cloud SQL はフルマネージドサービスであるため、OS やインフラストラクチャの運用から解放される半面、すべてのパラメータを自由に設定できるわけではありません。また、設定できる項目とそうでない項目は、Cloud SQL の仕様によってあらかじめ決まっています。 当記事では、オンプレミス版(オープンソース版)の PostgreSQL でよく使用されるパラメータを例に挙げて、それらが Cloud SQL 版では Google が管理するパラメータ (サービスが管理するためユーザー側で設定が不可のパラメータ)と ユーザーが管理するパラメータ のどちらに分類されるかを解説します。あわせて、設定がサポートされていないパラメータの代替手法についても紹介します。 Cloud SQL の基本的な知識については、以下の記事を参照してください。 blog.g-gen.co.jp データベースフラグの概要 データベースフラグとは オンプレミス環境では、PostgreSQL のシステム全体の設定は主に postgresql.conf ファイルで管理します。しかし、Cloud SQL ではマネージドサービスの性質上、このファイルを直接編集できません。 代わりに、Cloud SQL では データベースフラグ を使用してパラメータを設定します。データベースフラグは MySQL や SQL Server でも同様にサポートされていますが、当記事では PostgreSQL を例に解説します。 参考 : データベース フラグを構成する フラグ設定時の注意点 データベースフラグを構成する際、以下の2点に注意する必要があります。 1つ目は、サポートされる値や範囲の違いです。各フラグについて、Cloud SQL でサポートされる値や範囲が、対応する PostgreSQL のパラメータやオプションと異なる場合があります。 2つ目は、再起動の発生です。すでに起動しているデータベースインスタンスに対してフラグを設定、変更、または削除すると、インスタンスの再起動が必要になる場合があります。稼働中のシステムに変更を加える際は、ダウンタイムに留意してください。 参考 : データベース フラグを構成する - サポートされているフラグ 設定値の確認と更新 パラメータの確認 Google Cloud コンソールから、現在インスタンスに設定されているデータベースフラグの一覧を確認できます。該当インスタンスの概要ページを開き、データベースフラグのセクションを確認します。 参考 : データベース フラグを構成する - インスタンスに設定されているデータベース フラグを確認する 設定されているデータベースフラグの例 また現在の設定値は、 psql クライアントなどでインスタンスにログインし、以下の SQL 文を実行することでも確認可能です。 SELECT name, setting FROM pg_settings; 参考 : データベース フラグを構成する - データベース フラグの現在の値を表示する パラメータの更新 Google Cloud コンソールや gcloud コマンドを使用して変更を行います。 システム全体に影響を与えるパラメータの多くは、このデータベースフラグを通じて設定が可能です。 Cloud SQL インスタンスを編集 フラグとパラメータの編集 gcloud コマンドでは、以下のようにフラグ名と値を対応させて実行します。 gcloud sql instances patch INSTANCE_NAME \ --database-flags = FLAG1 =VALUE1, FLAG2 =VALUE2 参考 : データベース フラグを構成する - データベース フラグを設定する Google が管理するパラメータ 前提と注意点 当セクションで紹介する「Google が管理するパラメータ」は、Cloud SQL では Google が完全に管理しており、ユーザー側で設定できないものです。これらはデータベースフラグとしてサポートされていません。 なお、当セクションで紹介するパラメータは、よく用いられる設定のごく一部です。実際には、システム要件と公式ドキュメントを照らし合わせ、事前に十分なパラメータ設計を行ってください。 ネットワークと接続管理 オンプレミスでは必須となる listen_addresses や port の設定は、Google Cloud では不要です。 Cloud SQL では、PostgreSQL の標準ポート( 5432 )が固定で使用されます。アクセス制御は pg_hba.conf を編集するのではなく、VPC ネットワークピアリングや承認済みネットワークなど、Google Cloud のネットワーク機能を使用して管理します。 参考 : Cloud SQL への接続方法を選択する 参考 : 接続の問題をデバッグする - 開いているローカルポート インスタンスの接続情報 VPC についての詳細は、以下の記事を参照してください。 blog.g-gen.co.jp blog.g-gen.co.jp ログ管理 log_destination 、 logging_collector 、 log_file_mode などのログファイルの出力先やローテーションに関する設定もマネージドサービスで代替可能です。 Cloud SQL のログは自動的に Cloud Logging に統合されます。ログの検索、監視などはデータベース側で行うのではなく、Google Cloud のオブザーバビリティ機能を使用して行います。 参考 : インスタンスのログを表示する Cloud Logging についての詳細は、以下の記事を参照してください。 blog.g-gen.co.jp ハードウェア依存の設定 dynamic_shared_memory_type などの OS やハードウェア基盤に強く依存するパラメータは設定できません。これらは Cloud SQL の基盤側で自動的に最適化されるため、ユーザーが意識する必要はありません。 参考 : マシンシリーズを選択する ユーザーが管理するパラメータ 前提と注意点 当セクションで紹介する「ユーザーが管理するパラメータ」は、Cloud SQL に移行した後でも、引き続きユーザー側でチューニングや設定を行う必要があるパラメータです。 当セクションで紹介するパラメータは例示であり、ごく一部です。実際には、システム要件を考慮し、どのパラメータに対してフラグや代替手段を用いた設定が必要になるのかを、公式ドキュメントと照らし合わせて十分に精査してください。 パフォーマンスチューニングフラグ max_connections 、 shared_buffers 、 maintenance_work_mem など、データベースのパフォーマンスに直結する重要なパラメータの多くが、データベースフラグとしてサポートされています。 なお、一部のフラグ( max_connections や max_worker_processes など)は、インスタンスのメモリサイズに応じて上限値やデフォルト値が自動的にスケーリングする仕様になっています。オンプレミスの設定値をそのまま移行するのではなく、自動設定されるデフォルト値を確認し、マネージドサービスへ設定を委譲できるかを評価してください。 参考 : データベース フラグを構成する - サポートされているフラグ データベース内での代替設定 データベースフラグのリストに存在しない場合でも、 ALTER DATABASE などの SQL コマンドを用いてデータベース内で設定できるパラメータがあります。 例えば、タイムゾーン( timezone )、日付の表示形式( datestyle )、ロケール書式( lc_monetary や lc_numeric など)は、インスタンス全体のフラグとして設定できなくても、特定のデータベースやユーザーに対して個別に適用できます。 マルチテナント環境などで、データベースごとに異なる言語設定や検索設定( default_text_search_config )を適用したい場合に有効な手法です。 参考 : データベース フラグを構成する - トラブルシューティング ALTER DATABASE の実行例 今村 壱生 (記事一覧) クラウドソリューション部 ソリューションアーキテクト課 2026年3月にG-genへ入社。約7年間 Web 広告運用やウェブ解析に携わり、その後は社内 SE として開発業務に従事。広告運用の現場感と技術的な視点、その双方を併せ持つ経験をベースに、現在は Google Cloud のスキルアップに注力。データ活用とクラウド技術を融合させ、お客様のビジネス成長を支えるエンジニアを目指している。 Follow
はじめに こんにちは、SRE部カート決済SREブロックの伊藤( @_itito_ )です。普段はZOZOTOWNのカート決済機能のリプレイス・運用・保守に携わっています。また、データベース(以下DB)領域でのテックリードを務めており、DBREとしてDB周りの運用・保守・構築に関わっています。 ZOZOTOWNでは以前からリプレイスを進めており、SQL Serverを中心としていたDB構成も、マイクロサービス化に伴ってAurora MySQLなどへ分割されてきました。 かつてはビジネスロジックの多くを、SQL Serverのストアドプロシージャ(以下、ストアド)が担っていました。データの近くで処理するため高速であり、ロジックをDBに集約できる利点があったためです。 一方で、「ストアドをスケールさせづらい」「ストアドのテストが書きづらい」といったような課題も抱えていました(参考: ZOZOTOWNリプレイス2020 )。 そのためリプレイスを進める中で多くのストアドが剥がされ、ロジックはアプリケーション側へ移ってきました。ただし、すべてを移し終えたわけではありません。残っているストアドもあり、改修は時折発生します。 そして、わずかな変更でも実行計画は変わります。結果として性能が大きく劣化する場合もあります。 本記事では、ストアドの変更をPull Requestの時点で検知する実行計画チェックCIを構築した取り組みを紹介します。GitHub ActionsとArgo Workflowsで本番DBへ一切接続せず推定の実行計画を取得し、Claude Code Actionによるレビューで性能懸念の有無を判定する構成としました。 目次 はじめに 目次 背景と課題 ストアド改修による性能劣化 従来のレビュー運用の限界 仕組みに求めた要件 CIの全体像 処理の流れ 各コンポーネントの実装 実行計画の取得 推定の実行計画を取得する CI用の一時名を付与する 実行計画のチェック ルールベース解析 変更前後の比較 LLMレビュー 判定をCIの成否へつなぐ マージをブロックする仕組み ワークフローの構成 dbre-review-passed ラベルによる救済 ラベルの抜け道をふさぐ 分析結果例 まとめと展望 背景と課題 ストアド改修による性能劣化 きっかけは、ストアドの改修リリース後に発生したDB負荷高騰でした。改修によってWHERE句で参照するカラムが変わり、それまで効いていたインデックスが使われなくなっていました。 問題が表面化したのは、リリースから時間が経ってからでした。通常のアクセス量であれば処理しきれていたためです。負荷の大きいイベントを迎えたところで、大量の読み込みによってDBが耐えられなくなりました。 ただし、より本質的な問題は別にありました。改修内容が数行の軽微なものだったため、リリース前後の性能検証が省略されていた点です。 対策として、リリース後の性能劣化をDBのパフォーマンスチェックやアラートで検知する仕組みも別途用意しました。ただし、リリース後の検知では影響が出てしまう可能性があります。理想はリリース前にも検知できることでした。 従来のレビュー運用の限界 ストアドの性能検証には、実行計画の確認が必要です。従来は、次のような観点での確認を開発者に委ねていました。 推定行数が極端に多い処理がないか テーブルのScanが発生していないか Index Seekでも絞り込める条件になっているか 実際の行数と予測の行数に大きな乖離がないか これらは実行計画を読める人でなければ判断できません。しかも、確認の実施やDBREへの相談も、開発者の判断次第でした。 つまり「改修が軽微かどうか」を、性能への影響を評価する前に人が判断していたのです。軽微な変更ほど検証は省略されやすくなります。この構造そのものが課題でした。 仕組みに求めた要件 再発防止策を検討する中で、仕組みに求める要件を4つ整理しました。 強制力があること … 人の判断で検証を省略できないよう、Pull Requestの単位で自動実行する 本番DBに影響を与えないこと … 検証のために本番DBへ接続したり負荷をかけたりしない 本番相当のデータが入った環境で確認できること … データがほとんどない開発用のDBでは、実行計画がその規模に合わせて最適化されてしまい、正しい情報が取れない 開発者の手間を増やさないこと … 既存のPull Requestフローに組み込み、追加の操作を求めない 要件2と要件3は、そのままでは両立しません。本番のデータ量に近い環境が必要ですが、本番DBは使えないためです。 そこでSTG環境を利用することとしました。ZOZOTOWNでは本番環境へのリリース前にSTG環境での動作確認を必須としており、STG環境へのリリースは stg ブランチへのマージで行う運用です。つまり、ストアドの改修は必ず stg ブランチ向けのPull Requestを通ります。 さらにSTG環境は、負荷試験の実行環境としても使っています。そのため本番規模のデータ量を保つようにしており、実行計画の確認先としても適していました。 stg ブランチ向けのPull Requestでストアドが変更されたときに、STG環境で推定の実行計画を自動取得して解析するCIを構築しました。開発者が必ず通る経路にチェックを置けるうえ、4つの要件をすべて満たせます。 CIの全体像 処理の流れ 構築したCIは、GitHub ActionsとArgo Workflows(Amazon EKS上で稼働)の2つで役割を分担しています。両者のデータの受け渡しはすべてAmazon S3を経由します。全体像は次の図のとおりです。 処理の流れは次のとおりです。 stg ブランチ向けのPull Requestが作成され、GitHub Actionsが起動する 追加・変更されたストアドの定義ファイルをすべて検出する ストアド名をCI用の一時名にリネームしたSQLファイルを作成する SQLと実行先のDB情報をまとめたJSONファイルをS3へアップロードする argo submit --wait でArgo Workflowsを起動する Argo WorkflowsがJSONを読み込み、ストアドごとに CREATE PROCEDURE → SHOWPLAN_XML で実行計画の取得 → DROP PROCEDURE を繰り返す 実行計画XMLと実行ステータスをS3へアップロードする GitHub Actionsが結果を収集し、ルールベース解析と変更前後の比較を実施する 解析結果をPull Requestへコメントする Claude Code Actionがレビュー結果と判定( block / pass )を追記する 手順4でDB情報を渡しているのは、対象となるDBが1つではないためです。ZOZOTOWNには複数のDBが存在し、ストアドの定義ファイルの配置場所に応じて実行先が変わります。そのため、どのストアドをどのDBで確認するかをJSONに含めてArgo Workflowsへ渡しています。 GitHub ActionsとArgo Workflowsで処理を分けたのは、ネットワーク構成が理由です。対象のDBはオンプレミス環境のプライベートなネットワーク内にあり、GitHub Actionsのランナーからは直接届きません。一方、既存のAmazon EKSクラスタからはDBへ到達できます。そこでDBへの接続はArgo Workflowsに任せ、GitHub Actionsは変更検出・解析・コメントだけを担う構成としました。 AWSへの接続には、CI/CDで一般的なOIDCによるIAM Roleの引き受けを利用しています。長期のアクセスキーを保持せずに接続でき、信頼ポリシーでは対象リポジトリのPull Requestイベントのみに絞っています。 両者の受け渡しにS3を選んだのは、実行計画XMLがサイズの大きなファイルになるためです。Argo WorkflowsのパラメータやGitHub Actionsのoutputで渡すには不向きでした。 各コンポーネントの実装 新たに用意したインフラリソースは、次の3種類です。 連携基盤 … GitHub Actions用のIAM Role(OIDC)、受け渡し用のS3バケット、Argo Workflows用のIRSAロール 実行計画の取得 … STG DBで実行計画を取得するArgo WorkflowTemplate LLMレビュー … モデル呼び出し用のApplication Inference Profile モデルの呼び出しにApplication Inference Profileを使っているのは、コスト配分タグを付与するためです。デフォルトの推論プロファイルでは、どのアプリケーションがコストを発生させたのかを追跡できません。 インフラのほかに、GitHub Actions側で動く処理をPythonのツールとして実装しました。責務ごとにモジュールを分割しています。 モジュール 役割 変更検出 git diff でPull Request内の変更されたストアド定義を抽出 定義パーサ ストアド定義の文字コードのデコード、CREATE名の置換 CI名の生成 CI用の一時名を生成(識別子128文字の上限に対応) ワークフロー実行 Argo Workflowsの呼び出しとS3経由の入出力 実行計画の解析 SHOWPLAN_XMLのルールベース解析 実行計画の比較 変更前後の実行計画の比較 コメント生成 Pull Requestコメント(Markdown)の生成 実行計画の取得 ここからは、処理の流れの手順6にあたる部分を説明します。STG環境のDBに一時的にストアドを作成し、推定の実行計画を取得する処理です。 実際にDBへストアドを作成するため、既存のストアドを壊さないための安全策も必要でした。取得の方法と、そのために重ねた安全策の順に説明します。 推定の実行計画を取得する 本CIが取得するのは、 SHOWPLAN_XML による 推定の実行計画 です。 SET SHOWPLAN_XML ON を有効にすると、以降のステートメントはコンパイルだけが行われ、実行計画が返ります。ストアドを EXEC しても、中のクエリは実行されません。 learn.microsoft.com 実行されないという性質には、もう1つ利点があります。 ストアドのパラメータを渡さなくても実行計画を取得できる 点です。 実際にストアドを実行する場合、必須のパラメータを省略すると「プロシージャまたは関数 'X' にはパラメータ '@p' が必要ですが、指定されていません」というエラーになります。しかし SET SHOWPLAN_XML ON の状態ではステートメントが実行されないため、この検査が働きません。そのため引数なしの EXEC だけで実行計画が返ります。 本CIはこの挙動を利用し、パラメータを一切渡していません。ストアドごとに引数を用意する必要がなく、CIとして自動化しやすくなります。 ただし精度は落ちます。パラメータの値が不明なため、オプティマイザは統計情報のヒストグラムではなく、密度ベクターによる平均値から行数を見積もります。データの偏りが大きい列では、本番の実行時と計画の形状が変わる場合もあります。 一方で、どのインデックスが使えるかという構造的な判断は、パラメータの値に左右されません。冒頭の事例のように参照するカラムが変わってインデックスが効かなくなるケースは、変更前後の実行計画を比べればスキャンの出現として現れます。精度が問題になるのは、値の偏りによって選ばれる計画が変わるような場合です。 取得の処理そのものは単純です。Argo WorkflowsからはsqlcmdをインストールしたPodを起動し、次のようなSQLを実行しています。 SET NOCOUNT ON ; GO SET SHOWPLAN_XML ON ; GO EXEC ${CI_PROCEDURE_NAME}; -- CI用にリネームして作成したストアド GO SET SHOWPLAN_XML OFF; GO CI用の一時名を付与する 作成するストアドには _CI_PR<Pull Request番号>_add|before|after_<ストアド名> という一時名を付与します。新規追加は add 、既存の変更は変更前が before 、変更後が after です。 _CI_PR で始まる名前は通常のストアドでは使わない形式のため、既存のストアドと衝突しません。 SQL Serverの識別子は最大128文字です。プレフィックスの付与で上限を超える場合は、元の名前を切り詰めてハッシュを付与し、一意性と可読性を両立させました。 def build_ci_name (proc_name: str , pr_number: int , variant: str ) -> str : suffix = SUFFIX_BY_VARIANT[variant] prefix = f "_CI_PR{pr_number}_{suffix}_" candidate = f "{prefix}{proc_name}" if len (candidate) <= MAX_IDENTIFIER_LEN: return candidate # 超過時: 元名を切り詰め + 8桁ハッシュで一意化 digest = hashlib.sha1(proc_name.encode( "utf-8" )).hexdigest()[: 8 ] budget = MAX_IDENTIFIER_LEN - len (prefix) - 1 - len (digest) if budget < 1 : return f "{prefix}{digest}" [:MAX_IDENTIFIER_LEN] return f "{prefix}{proc_name[:budget]}_{digest}" この関数が返すのはスキーマを含まない名前です。Argo Workflowsへ渡す際に [<スキーマ>].[<CI用の名前>] の形へ修飾しています。 さらにArgo Workflows側でも、渡されたSQLにCI用の名前が含まれているかを検証しています。万が一既存のストアド名でCREATEしようとした場合に、チェックを失敗させるためです。 # The caller supplies the fully-qualified CI procedure name and a # definition that already CREATEs under that name. Validate the form. if ! printf ' %s ' " $CI_PROCEDURE_NAME " | grep -qE ' ^\[[^]]+\]\.\[[^]]+\]$ '; then printf ' ERROR: ci_procedure_name must be in form [schema].[name], got: %s\n ' " $CI_PROCEDURE_NAME " | write_fail exit 0 fi # Sanity check: the definition must reference the CI name, so we never # accidentally CREATE under the original (production) procedure name. if ! grep -qF " $CI_PROCEDURE_NAME " /tmp/ci_def.sql ; then { printf ' ERROR: supplied definition does not reference %s; caller must rename to the CI name before submitting.\n ' " ${CI_PROCEDURE_NAME} " printf ' ---- supplied definition (head) ----\n ' head -c 2000 /tmp/ci_def.sql } | write_fail exit 0 fi GitHub Actions側で一時名に置換する処理が正しく動くことを前提にせず、DBに触れる直前でもう一度検証しています。 一時ストアドがSTG環境に残り続けないよう、 trap でエラー時にも必ず DROP PROCEDURE が実行されるようにしています。処理の冒頭でも同じ削除処理を呼び、前回の実行が異常終了して残っていた場合に備えました。 実行計画のチェック ここまでで実行計画XMLが手に入りました。続いて、処理の流れの手順8から手順10にあたる部分を説明します。 チェックは次の順に実行します。 ルールベース解析 … 決められた観点を機械的に抽出し、Pull Requestへ一覧をコメントする 変更前後の比較 … 変更によって悪化した点を抽出する LLMレビュー … 1と2の結果および実行計画XMLを読み、性能懸念の有無を判定する 最終的な判定を担うのはLLMレビューです。前段の2つは、LLMが実行計画を読み解くためのヒントを用意する簡易チェックという位置づけであり、検出結果そのものでマージをブロックしません。実行計画の解釈はLLMに委ねる方針としたため、ルールベース解析は簡易なものに留めています。 ヒントの用意をLLMに任せず、Python側で先に実行しているのには2つの理由があります。1つは、コストの増加率のような数値の算出や比較を機械的に処理することで、LLMへ渡す値が実行のたびに揺れるのを避けられるためです。もう1つは、LLMレビューより前にPull Requestへコメントまで済ませておくことで、LLMレビューが失敗した場合でも最低限のフィードバックが残るためです。 ルールベース解析 取得したSHOWPLAN_XMLを解析し、性能上の懸念を検出します。検出する観点は次の4つです。 観点 検出内容 Missing Index 推奨インデックスの未作成。Impactが50%以上なら high Scan / Lookup系 Table Scan / Clustered Index Scan / Index Scan / Key Lookup / RID Lookup 高コスト演算子 推定サブツリーコストが10以上の Sort / Hash Match / Nested Loops / Table Spool Warnings 暗黙の型変換、結合述語なし、tempdbへのスピル、統計情報のない列 たとえば暗黙の型変換(PlanAffectingConvert)は high として扱っています。冒頭の事例と同様に、インデックスが効かずスキャンへ変わる結果を招くためです。 変更前後の比較 既存ストアドの変更の場合は、変更前と変更後の両方の実行計画を取得して比較します。Pull Requestのベースブランチ( stg )上の定義と、Pull Requestの定義をそれぞれSTG環境に作成する形です。 悪化の判定基準は次の2つです。 総推定コストの増加率が20%以上 変更後にのみ出現した懸念(Missing Index / Warning / 高コスト演算子 / スキャン) 懸念の同一性は、ステートメント全文ではなくカテゴリとタイトルの組で判定しています。変更によってSQLの文言がわずかに変わっても、同じ懸念を「新規」と誤検知しないためです。 LLMレビュー ルールベース解析は決められた観点しか見られません。そこで、観点の外まで踏み込んだ判定をAmazon Bedrock経由のClaudeに任せています。実装には claude-code-action を使用しています。 github.com LLMには解析結果( findings.json )と実行計画XMLの両方を渡し、 XMLを一次ソースとして優先的に読ませる プロンプトとしました。ルールベースの抽出結果は照合用の参考情報として扱わせています。 - name : LLM review id : claude_review uses : anthropics/claude-code-action@a92e7c70a4da9793dc164451d829089dc057a464 # v1.0.159 with : github_token : ${{ secrets.GITHUB_TOKEN }} use_bedrock : "true" use_sticky_comment : "true" settings : | { "env" : { "ANTHROPIC_DEFAULT_SONNET_MODEL" : "${{ secrets.BEDROCK_CLAUDE_SONNET_INFERENCE_PROFILE_ARN }}" } } claude_args : | --max-turns 20 --model sonnet --json-schema '{"type":"object","properties":{"verdict":{"type":"string","enum":["block","pass"]},"review":{"type":"string"}},"required":["verdict","review"]}' prompt : | このPRで変更されたストアドプロシージャの推定実行計画チェック結果を、性能観点でレビューしてください。 参照ファイル(${{ env.OUTPUT_DIR }}/ 配下): - findings.json … ストアドごとのルールベース解析結果(Missing Index / Scan / 高コスト演算子 / 警告 / before-after比較) - plans/<proc>.after.xml , plans/<proc>.before.xml … 取得した SHOWPLAN_XML(存在する場合) やること : 1. まず findings.json を読み、各ストアドの status / change_type / proc_name を把握する。 2. `review` フィールドの冒頭に、対象ストアドの一覧を以下の形式で出力する(findings.json の内容をもとに作成): - 各ストアドの proc_name / change_type(追加 or 変更)/ status(実行計画取得済み / スキップ / 取得失敗) 3. status=analyzed のストアドについて、plans/ ディレクトリを確認し、<proc>.after.xml(および modified の場合は <proc>.before.xml)が存在すれば、 **XML を一次ソースとして優先的に読み込み**、以下の観点で直接分析する: - オペレータ構成(Seek vs Scan、Hash/Merge/Nested Loops の選択、Sort の有無) - EstimatedTotalSubtreeCost の配分と高コストノード - MissingIndex 要素の Impact 値とカバー列 - Warnings(PlanAffectingConvert / NoJoinPredicate / SpillToTempDb) - before→after で計画形状・コストが悪化していないか(コスト値も明記する) findings.json の findings はルールベースの抽出結果として参照し、XML から読み取れる追加の懸念と照合する。 4. status=plan_failed(Argo ワークフローでの実行計画取得に失敗したもの)は findings.json の notes にエラー詳細が含まれる。 エラー内容(構文エラー・テーブル不在・権限不足 等)を読み取り、原因と対処法を開発者向けに日本語で説明する。 5. status=skipped(実行計画を取得できなかったもの)は XML が存在しないため、変更されたストアド定義の CREATE 文を読み、 SELECT * / NOLOCK の濫用 / 暗黙の型変換を招く比較 / インデックス非対応のWHERE / カーソル使用 等の静的な懸念があれば指摘する。 6. 改善提案(インデックス追加、述語の見直し等)を簡潔にまとめ、日本語でレビュー内容を Markdown 形式で作成し `review` フィールドに設定する。 7. 以下の基準で判定し、結果("block" または "pass" )を `verdict` フィールドに設定する。 - block : 変更後に severity=high の新規発生・悪化がある、または重大な静的懸念がある - pass : それ以外(status=plan_failed のみの場合も pass とする) 注意 : - これは推定実行計画に基づく参考情報。パラメータを渡さずに取得しているため、本番と計画形状が異なりうる旨を `review` の冒頭(対象一覧の直後)に記載する。 - 懸念が無ければ「重大な性能懸念は検出されませんでした」と簡潔に述べる。 判定をCIの成否へつなぐ 先ほどのステップ定義では、 claude_args に --json-schema を指定していました。このオプションを渡すと、最終的な出力が指定したJSON Schemaに沿う形へ強制されます。今回は verdict ( block / pass )と review (レビュー本文)の2つを必須項目として定義しました。 結果は steps.<id>.outputs.structured_output から取得できます。次のように verdict を読み取り、 block であればジョブを失敗させています。 - name : Check LLM verdict if : vars.SP_PLAN_CHECK_LLM_VERDICT_ENABLED == 'true' env : STRUCTURED_OUTPUT : ${{ steps.claude_review.outputs.structured_output }} run : | VERDICT=$(echo "$STRUCTURED_OUTPUT" | jq -r '.verdict // "pass"' ) echo "LLM verdict: ${VERDICT}" if [ "$VERDICT" = "block" ] ; then echo "::error::LLMレビューにより重大な性能懸念が検出されました。レビューコメントを確認してください。" exit 1 fi 構造化出力を強制しているため、自然言語の応答をパースする必要がありません。LLMの判定をそのままCIの成否へ接続できます。 なお、判定によるブロックはリポジトリ変数 SP_PLAN_CHECK_LLM_VERDICT_ENABLED で切り替えられるようにしました。テスト運用の段階では無効にしておき、誤検知の傾向を確認してから有効化する想定です。 マージをブロックする仕組み ここからは、チェック結果をマージの可否へ接続する仕組みを説明します。ただし 現時点では、この章で説明する設定をまだ有効にしていません 。テスト運用として、結果をPull Requestへコメントするだけの状態で誤検知の傾向を見ています。 ワークフローの構成 ここまで説明したチェックは、1本のGitHub Actionsワークフローで実行しています。 pre-check / plan-check / llm-review の3ジョブで構成しています。 stg ブランチのブランチ保護ルールで plan-check と llm-review を必須ステータスチェックに指定すると、チェックNG時にマージがブロックされます。 加えて、このワークフローを補助する目的で簡単なワークフローを2本追加しています。どちらも後述する dbre-review-passed ラベルの扱いを制御するものです。 dbre-review-passed ラベルによる救済 推定の実行計画によるチェックは万能ではありません。NGと判定されたが実際には問題ないケースや、CI自体が失敗するケースもあります。 そこで、DBREが結果を確認して問題ないと判断した場合に dbre-review-passed ラベルを付与すると、マージできる仕組みにしました。ラベルが付与されている場合は pre-check ジョブが後続をスキップします。 - name : Check dbre-review-passed label id : check_label run : | LABELS=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/labels" --jq '[.[].name] | @json' ) if echo "$LABELS" | jq -e 'contains(["dbre-review-passed"])' > /dev/ null ; then echo "dbre-review-passed ラベルが付与されているため、後続チェックをスキップして成功終了します。" echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" fi GitHubの仕様上、ジョブレベルのスキップはSuccess扱いになります。そのため必須ステータスチェックを設定したままでも、ラベルによる救済フローが機能します。 ラベルの抜け道をふさぐ ラベルによる救済は、そのままでは抜け道になります。開発者が自分でラベルを付与すればチェックを回避できてしまいます。 ここでGitHubの仕様が制約になりました。 特定のラベルについて、付与できるユーザーを制限する仕組みが存在しません。 ラベルの付与はTriage以上のロールに許可された操作であり、ラベルの種類によって権限を分けることはできません。 docs.github.com そこで、付与を防ぐのではなく、付与された後に取り消す方針としました。ラベルの付与をトリガーとしてワークフローが起動し、条件を満たさない場合は自動で削除します。 ラベル付与後に新しいコミットがpushされた場合、ラベルを自動削除して再チェックを強制する DBREではないユーザー(特定の権限がないユーザー)がラベルを付与した場合、ラベルを自動削除する 分析結果例 以下が実装完了後の分析結果例です。Pull Requestのコメントとして出力されます。 各ストアドの分析結果が羅列された後、最後にマージしても問題ないかの判定が表示されます。 上図は問題なかった時の例です。NGの場合は次のようにCIがエラーになります。 前述のブランチ保護ルールを設定していれば、マージがブロックされます。 まとめと展望 本記事では、ストアドの改修による性能劣化をPull Requestの時点で検知するCIを構築した取り組みを紹介しました。 GitHub ActionsとArgo Workflowsで役割を分担し、S3を介して連携することで、本番DBへ接続せず推定の実行計画を自動取得する構成としています。取得した実行計画はルールベース解析で整理し、その結果と実行計画XMLをLLMレビューが読んで判定する形にしました。 現在はテスト運用の段階であり、マージをブロックする設定はまだ有効化していません。誤検知の傾向を見ながら、ルールベース解析の閾値とLLMレビューのプロンプトの判定条件を調整し、順次適用を進めていく予定です。 一方で、この仕組みでカバーできない観点もあります。取得しているのは推定の実行計画であり、クエリを実際に実行していないため、従来のレビューで確認していた「実際の行数と推定行数の乖離」は判断できません。統計情報の陳腐化に起因するような劣化は、引き続きリリース後のパフォーマンスチェックで捉える必要があります。PR時点のチェックとリリース後の監視は、どちらかで置き換えられるものではなく、互いを補完するものだと捉えています。 今回のターゲットはストアドのみで、アプリケーション側で組み立てるクエリは対象外です。ストアドに絞ったのは、負荷高騰の原因がストアドの改修だったことに加え、定義ファイルから変更内容を特定しやすいためです。ただし、アプリケーションから直接実行されるクエリも、変更差分から抽出して実行計画を取得することは理論上可能です。同じ仕組みを広げていければ、DBへの変更全体がチェックの対象になります。そうした形を目指して、対象範囲を広げていきたいと考えています。 「人が検証の必要性を判断する」運用は、判断が正しくなければ機能しません。判断そのものを仕組みに委ねることで、はじめて再発防止と呼べる状態になると考えています。DBの信頼性を高めるための取り組みを、引き続き進めていきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに この記事では、私がデータ移行・バッチ処理のプロジェクトで遭遇した「Foreign Data Wrapper(以下、FDW)を介したクエリ実行時におけるパフォーマンス低下(タイムアウト)とデータ欠落」という問題について、解決に至るまでの過程を共有します。

動画

該当するコンテンツが見つかりませんでした

書籍