To optimize AlloyDB for PostgreSQL database query performance, you can tune your SQL
queries by using AI-driven insights. By analyzing your query execution plans,
the ai.tune_sql function—powered by a large language model
(LLM)—identifies complex bottlenecks and provides recommendations that you
can act on for optimization.
When you invoke the ai.tune_sql function, it analyzes your query and provides
query insights and recommendations in the following areas:
- Query summary: analyzes query performance bottlenecks.
- Query rewrite: suggests optimization techniques to rewrite the original query into an equivalent, more efficient query.
- Index recommendations: recommends index types such as indexes on expressions, Generalized Inverted Index (GIN) indexes, and Scalable Nearest Neighbors (ScaNN) indexes.
- Statistics collection recommendations: recommends table
ANALYZEand statistics collection on multiple columns based on the query execution plan. - Grand Unified Configuration (GUC) flags tuning: recommends new GUC values by analyzing the execution plan and system information such as memory and buffer usage and size.
- Analyze and vacuum: recommends running
ANALYZEandVACUUMon tables based on the analyze and vacuum history and the live and dead tuple counts.
Limitations
SQL query tuning has the following limitations:
- Prepared and parameterized statements: for prepared and parameterized
statements,
ai.tune_sqldoesn't provide query rewrite recommendations, although it still provides other tuning recommendations. To receive query rewrite recommendations for a query that contains parameter placeholders, such as$1or$2copied from Query Insights orpg_stat_statements, replace the placeholders with representative literal values before runningai.tune_sql. - Read-only
SELECTstatements only whenexplain_analyzeisTRUE: only single, read-onlySELECTstatements are supported whenexplain_analyzeis set toTRUE. Runningai.tune_sqlwithexplain_analyzeset toTRUEon other statements—such asINSERT,UPDATE,DELETE,SELECT INTO, orSELECT FOR UPDATE—returns an error message.
Before you begin
Verify that you have the following:
- An AlloyDB instance running PostgreSQL 18 or later. For more information, see Create a cluster and its primary instance.
- A
psqlclient connected to your instance, or access to AlloyDB Studio. - Vertex AI integration enabled.
- Version 1.6.2 or later of the
google_ml_integrationextension installed, and thegoogle_db_advisorandhypopgextensions enabled. To enable thehypopgextension, whichai.tune_sqluses to evaluate and generate index recommendations, runCREATE EXTENSION IF NOT EXISTS hypopg;.
Enable SQL tuning
Register an LLM and enable the required database flags.
Create a Gemini model endpoint for the
ai.tune_sqlfunction by registering either a global model endpoint or a regional model endpoint in your instance region:To register a global model endpoint when the model isn't available in your instance region—for example,
gemini-3.5-flashonlocations/global—run the following statement: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');Replace
PROJECT_IDwith the ID of the project where the model is available.To register a regional model endpoint in your instance region—for example,
gemini-2.5-proonlocations/REGION_ID—run the following statement: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');Replace the following:
REGION_ID: the region of your AlloyDB instance—for example,us-central1.PROJECT_ID: the ID of the project where the model is available.
Set the model ID for SQL query tuning recommendations in your current database session. Specify the
model_idthat you registered in the previous step, such as'gemini-3.5-flash-global'or'gemini-2.5-pro':SET google_db_advisor.llm_model_id = 'gemini-3.5-flash-global';Turn on SQL query tuning for your current database session by setting the
google_db_advisor.enable_ai_tune_sqlflag:SET google_db_advisor.enable_ai_tune_sql = on;Because
SETapplies only to the current database session, you can persist these settings across sessions by runningALTER DATABASE DATABASE_NAME SET FLAG_NAME = VALUE;for both flags, or by configuring the instance's database flags.
Manually tune SQL queries
To manually tune SQL queries, run the ai.tune_sql function by using a SELECT
statement.
Connect a
psqlclient to an AlloyDB instance or open AlloyDB Studio.If you used
SETto configuregoogle_db_advisor.llm_model_idandgoogle_db_advisor.enable_ai_tune_sql, run thoseSETstatements in the same database session before callingai.tune_sql.To tune SQL queries, run the
ai.tune_sqlfunction. This function retrieves the query-related schema and sends it, along with the query, to the LLM for performance analysis:SELECT ai.tune_sql(query => 'SQL_QUERY', explain_analyze => TRUE);Replace
SQL_QUERYwith the SQL query that you want to analyze.By default, the execution parameter (
explain_analyze) is set toTRUE. When set toTRUE, the function runsEXPLAIN ANALYZEto execute the query and capture the execution plan. This approach provides more accurate and useful query tuning advice. If you setexplain_analyzetoFALSE, the function only runsEXPLAINto generate a plan without executing the query.
View query plans with performance recommendations
The output first shows the standard query plan, followed by the detailed
AI-generated recommendations. The recommendations include a query summary,
suggested rewrites, and recommendations for statistics collection, GUC flag
tuning, and vacuuming. The ai.tune_sql function can take two to four minutes
to generate recommendations.
The following example shows the ai.tune_sql output for an anti-join query:
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)