Running EXPLAIN ANALYZE on functions
Last edited: 1/16/2026
Sometimes it can help to look at Postgres query plans inside a function. The problem is that running EXPLAIN ANALYZE on a function usually just shows a function scan or result node, which gives little insight into how the queries actually perform.
auto_explain is a pre-installed module that is able to log query plans for queries within functions.
auto_explain has a few settings that you still need to configure:
auto_explain.log_nested_statements: log the plans of queries within functionsauto_explain.log_analyze: capture theexplain analyzeresults instead ofexplainauto_explain.log_min_duration: if a query is expected to run for longer than the setting's threshold, log the plan
Changing these settings at a broad scale can lead to excessive logging. Instead, you can change the configs within a begin/rollback block with the set local command. This ensures the changes are isolated to the transaction, and any writes made during testing are undone.
1begin;23set local auto_explain.log_min_duration = '0'; -- log all query plans4set local auto_explain.log_analyze = true; -- use explain analyze5set local auto_explain.log_buffers = true; -- use explain (buffers)6set local auto_explain.log_nested_statements = true; -- log query plans in functions78select example_func(); ---<--ADD YOUR FUNCTION HERE910rollback;If needed, you can change these settings for specific roles, but we don't recommend configuring the value below 1s for extended periods, as it may degrade performance.
For instance, you could change the value for the authenticator role (powers the Data API).
1ALTER ROLE postgres SET auto_explain.log_min_duration = '.5s';After running your test, you should be able to find the plan in the Postgres logs. The auto_explain module always starts logs with the term "duration:", which can be used as a filter keyword.
You can also filter for the specific function in the log explorer with the below query:
1select2 cast(postgres_logs.timestamp as datetime) as timestamp,3 event_message as query_and_plan,4 parsed.user_name,5 parsed.context6from7 postgres_logs8 cross join unnest(metadata) as metadata9 cross join unnest(metadata.parsed) as parsed10where regexp_contains(event_message, 'duration:') and regexp_contains(context, '(?i)FUNCTION_NAME')11order by timestamp desc12limit 100;