AlloyDB for PostgreSQL データベースのクエリ パフォーマンスを最適化するには、AI を活用した分析情報を使用して SQL クエリをチューニングします。ai.tune_sql 関数は、大規模言語モデル(LLM)を活用してクエリ実行プランを分析することで、複雑なボトルネックを特定し、最適化のために実行可能な推奨事項を提供します。
ai.tune_sql 関数を呼び出すと、クエリが分析され、次の領域でクエリの分析情報と推奨事項が提供されます。
- クエリの概要: クエリのパフォーマンスのボトルネックを分析します。
- クエリの書き換え: 元のクエリを同等でより効率的なクエリに書き換える最適化手法を提案します。
- インデックスの推奨事項: 式のインデックス、一般化逆インデックス(GIN)インデックス、Scalable Nearest Neighbors(ScaNN)インデックスなどのインデックス タイプを推奨します。
- 統計情報の収集に関する推奨事項: クエリ実行プランに基づいて、テーブル
ANALYZEと複数の列の統計情報の収集を推奨します。 - Grand Unified Configuration(GUC)フラグのチューニング: 実行プランと、メモリやバッファの使用量やサイズなどのシステム情報を分析して、新しい GUC 値を推奨します。
- 分析とバキューム: 分析とバキュームの履歴、ライブタプルとデッドタプルの数に基づいて、テーブルで
ANALYZEとVACUUMを実行することをおすすめします。
制限事項
SQL クエリのチューニングには次の制限があります。
- 準備済みステートメントとパラメータ化されたステートメント: 準備済みステートメントとパラメータ化されたステートメントの場合、
ai.tune_sqlはクエリの書き換えに関する推奨事項を提供しませんが、他のチューニングに関する推奨事項は提供します。パラメータ プレースホルダ(Query Insights からコピーされた$1や$2、pg_stat_statementsなど)を含むクエリのクエリ書き換えの推奨事項を受け取るには、ai.tune_sqlを実行する前に、プレースホルダを代表的なリテラル値に置き換えます。 explain_analyzeがTRUEの場合の読み取り専用SELECTステートメントのみ:explain_analyzeがTRUEに設定されている場合、単一の読み取り専用SELECTステートメントのみがサポートされます。explain_analyzeをTRUEに設定してai.tune_sqlを他のステートメント(INSERT、UPDATE、DELETE、SELECT INTO、SELECT FOR UPDATEなど)で実行すると、エラー メッセージが返されます。
始める前に
以下があることを確認します。
- PostgreSQL 18 以降を実行している AlloyDB インスタンス。詳細については、クラスタとそのプライマリ インスタンスを作成するをご覧ください。
- インスタンスに接続された
psqlクライアント、または AlloyDB Studio へのアクセス。 - Vertex AI の統合が有効。
google_ml_integration拡張機能のバージョン 1.6.2 以降がインストールされ、google_db_advisor拡張機能とhypopg拡張機能が有効になっている。ai.tune_sqlがインデックスの推奨事項の評価と生成に使用するhypopg拡張機能を有効にするには、CREATE EXTENSION IF NOT EXISTS hypopg;を実行します。
SQL チューニングを有効にする
LLM を登録し、必要なデータベース フラグを有効にします。
インスタンス リージョンにグローバル モデル エンドポイントまたはリージョン モデル エンドポイントを登録して、
ai.tune_sql関数用の Gemini モデル エンドポイントを作成します。インスタンス リージョンでモデルが使用できない場合(
locations/globalのgemini-3.5-flashなど)にグローバル モデル エンドポイントを登録するには、次のステートメントを実行します。CALL google_ml.create_model( model_id => 'gemini-3.5-flash-global', model_request_url => 'https://aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/global/publishers/google/models/gemini-3.5-flash:generateContent', model_qualified_name => 'gemini-3.5-flash', model_provider => 'google', model_type => 'llm', model_auth_type => 'alloydb_service_agent_iam');PROJECT_IDは、モデルが使用可能なプロジェクトの ID に置き換えます。インスタンス リージョンにリージョン モデル エンドポイント(
locations/REGION_IDのgemini-2.5-proなど)を登録するには、次のステートメントを実行します。CALL google_ml.create_model( model_id => 'gemini-2.5-pro', model_request_url => 'https://REGION_ID-aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/REGION_ID/publishers/google/models/gemini-2.5-pro:generateContent', model_qualified_name => 'gemini-2.5-pro', model_provider => 'google', model_type => 'llm', model_auth_type => 'alloydb_service_agent_iam');次のように置き換えます。
REGION_ID: AlloyDB インスタンスのリージョン(us-central1など)。PROJECT_ID: モデルが使用可能なプロジェクトの ID。
現在のデータベース セッションで SQL クエリのチューニングの推奨事項のモデル ID を設定します。前の手順で登録した
model_id('gemini-3.5-flash-global'や'gemini-2.5-pro'など)を指定します。SET google_db_advisor.llm_model_id = 'gemini-3.5-flash-global';google_db_advisor.enable_ai_tune_sqlフラグを設定して、現在のデータベース セッションの SQL クエリのチューニングを有効にします。SET google_db_advisor.enable_ai_tune_sql = on;SETは現在のデータベース セッションにのみ適用されるため、両方のフラグにALTER DATABASE DATABASE_NAME SET FLAG_NAME = VALUE;を実行するか、インスタンスのデータベース フラグを構成することで、セッション間でこれらの設定を保持できます。
SQL クエリを手動でチューニングする
SQL クエリを手動でチューニングするには、SELECT ステートメントを使用して ai.tune_sql 関数を実行します。
psqlクライアントを AlloyDB インスタンスに接続するか、AlloyDB Studio を開きます。SETを使用してgoogle_db_advisor.llm_model_idとgoogle_db_advisor.enable_ai_tune_sqlを構成した場合は、ai.tune_sqlを呼び出す前に、同じデータベース セッションでこれらのSETステートメントを実行します。SQL クエリをチューニングするには、
ai.tune_sql関数を実行します。この関数は、クエリに関連するスキーマを取得し、クエリとともに LLM に送信してパフォーマンス分析を行います。SELECT ai.tune_sql(query => 'SQL_QUERY', explain_analyze => TRUE);SQL_QUERYは、分析する SQL クエリに置き換えます。デフォルトでは、実行パラメータ(
explain_analyze)はTRUEに設定されています。TRUEに設定すると、関数はEXPLAIN ANALYZEを実行してクエリを実行し、実行プランをキャプチャします。このアプローチにより、より正確で有用なクエリ チューニングのアドバイスが提供されます。explain_analyzeをFALSEに設定すると、関数はEXPLAINのみを実行して、クエリを実行せずにプランを生成します。
パフォーマンスの推奨事項を含むクエリプランを表示する
出力には、最初に標準のクエリプランが表示され、その後に AI 生成の詳細な推奨事項が表示されます。推奨事項には、クエリの概要、推奨される書き換え、統計情報の収集、GUC フラグのチューニング、バキューム処理に関する推奨事項が含まれます。ai.tune_sql 関数で推奨事項が生成されるまでに 2 ~ 4 分かかることがあります。
次の例は、アンチ結合クエリの ai.tune_sql 出力を示しています。
SELECT ai.tune_sql(
'SELECT *
FROM store_sales
WHERE ss_customer_sk NOT IN (
SELECT sr_customer_sk
FROM store_returns
WHERE sr_item_sk = ss_item_sk)');
tune_sql
--------------------------------------------------
NOTE
--------------------------------------------------
Advices generated with the assistance of an LLM. Independent verification of these advices is required due to the potential for inaccuracies.
--------------------------------------------------
ORIGINAL QUERY PLAN
--------------------------------------------------
Seq Scan on store_sales (cost=0.00..111292742.52 rows=1440384 width=115) (actual time=0.092..103093.207 rows=1306312.00 loops=1)
Filter: (NOT (ANY (ss_customer_sk = (SubPlan 1).col1)))
Rows Removed by Filter: 1574198
Buffers: shared hit=72987539
SubPlan 1
-> Index Scan using idx_sr_item_sk on store_returns (cost=0.42..76.74 rows=18 width=4) (actual time=0.005..0.034 rows=19.38 loops=2880510)
Index Cond: (sr_item_sk = store_sales.ss_item_sk)
Index Searches: 2880510
Buffers: shared hit=72933670
Planning Time: 0.257 ms
--------------------------------------------------
QUERY SUMMARY
--------------------------------------------------
The original query plan shows a Sequential Scan on store_sales with a correlated SubPlan for the NOT IN condition. For each of the 1.44 million rows in store_sales, it executes an Index Scan on store_returns, resulting in over 2.88 million index searches. This nested loop execution is the primary bottleneck, taking over 103 seconds. Converting the correlated NOT IN subquery to an anti-join (NOT EXISTS) will allow the optimizer to use a Hash Anti Join, processing both tables in bulk and significantly reducing execution time.
--------------------------------------------------
AI SUGGESTED QUERY 1
--------------------------------------------------
SELECT * FROM store_sales WHERE NOT EXISTS (SELECT 1 FROM store_returns WHERE store_returns.sr_item_sk = store_sales.ss_item_sk AND (store_returns.sr_customer_sk = store_sales.ss_customer_sk OR store_returns.sr_customer_sk IS NULL OR store_sales.ss_customer_sk IS NULL));
Reason: The original query uses a NOT IN subquery which is executed as a correlated subquery (SubPlan), leading to millions of index scans. Rewriting to NOT EXISTS allows the optimizer to use a Hash Anti Join, which is much more efficient for large datasets. The OR IS NULL conditions ensure strict semantic equivalence with NOT IN when dealing with NULL values.
--------------------------------------------------
REWRITTEN QUERY PLAN
--------------------------------------------------
Gather (cost=8541.96..139088.40 rows=154194 width=115)
Workers Planned: 2
-> Parallel Hash Anti Join (cost=7541.96..122669.00 rows=64248 width=115)
Hash Cond: (store_sales.ss_item_sk = store_returns.sr_item_sk)
Join Filter: ((store_returns.sr_customer_sk = store_sales.ss_customer_sk) OR (store_returns.sr_customer_sk IS NULL) OR (store_sales.ss_customer_sk IS NULL))
-> Parallel Seq Scan on store_sales (cost=0.00..65872.20 rows=1200320 width=115)
-> Parallel Hash (cost=6044.76..6044.76 rows=119776 width=8)
-> Parallel Seq Scan on store_returns (cost=0.00..6044.76 rows=119776 width=8)
--------------------------------------------------
AI SUGGESTED INDEX 1
--------------------------------------------------
CREATE INDEX idx_sr_item_customer ON store_returns (sr_item_sk, sr_customer_sk);
Reason: A composite index on (sr_item_sk, sr_customer_sk) allows index-only scans for the subquery or early aggregation, reducing table access and speeding up the anti-join condition evaluation.
Note: Indexes are suggested solely based on the given query, but they don't consider the overall system workload.
The suggested indexes improve the performance of the original query.
--------------------------------------------------
GUC FLAGS TUNING
--------------------------------------------------
SET work_mem = '64MB'; The current work_mem of 4MB is too small for a Hash Anti Join on tables with hundreds of thousands of rows. Increasing it to 64MB allows the hash table for store_returns to fit entirely in memory, preventing costly disk spills during the join.
Execution Time: 103167.475 ms
(50 rows)