--- a/contrib/auto_explain/auto_explain.c +++ b/contrib/auto_explain/auto_explain.c @@ -25,6 +25,7 @@ #include "nodes/value.h" #include "parser/scansup.h" #include "utils/guc.h" +#include "utils/memutils.h" #include "utils/varlena.h" PG_MODULE_MAGIC_EXT( @@ -95,6 +96,14 @@ {NULL, 0, false} }; +/* State belonging to a query selected for auto_explain. */ +typedef struct AutoExplainQueryState +{ + int nesting_level; +} AutoExplainQueryState; + +static int auto_explain_queryDesc_id; + /* Current nesting depth of ExecutorRun calls */ static int nesting_level = 0; @@ -309,6 +318,8 @@ MarkGUCPrefixReserved("auto_explain"); + auto_explain_queryDesc_id = GetQueryDescExtensionId("auto_explain"); + /* Install hooks. */ prev_ExecutorStart = ExecutorStart_hook; ExecutorStart_hook = explain_ExecutorStart; @@ -346,6 +357,17 @@ if (auto_explain_enabled()) { + AutoExplainQueryState *qstate; + + /* + * A cursor can outlive the current top-level statement. Remember + * that we selected this query, independently of later sampling. + */ + qstate = MemoryContextAlloc(GetMemoryChunkContext(queryDesc), + sizeof(AutoExplainQueryState)); + qstate->nesting_level = nesting_level; + SetQueryDescExtensionState(queryDesc, auto_explain_queryDesc_id, qstate); + /* We're always interested in runtime */ queryDesc->query_instr_options |= INSTRUMENT_TIMER; @@ -420,7 +442,13 @@ static void explain_ExecutorEnd(QueryDesc *queryDesc) { - if (queryDesc->query_instr && auto_explain_enabled()) + AutoExplainQueryState *qstate = GetQueryDescExtensionState(queryDesc, + auto_explain_queryDesc_id); + + /* Instrumentation requested by another extension is not a sampling decision. */ + if (queryDesc->query_instr && qstate != NULL && + auto_explain_log_min_duration >= 0 && + (qstate->nesting_level == 0 || auto_explain_log_nested_statements)) { MemoryContext oldcxt; double msec; --- a/contrib/auto_explain/Makefile +++ b/contrib/auto_explain/Makefile @@ -6,7 +6,7 @@ auto_explain.o PGFILEDESC = "auto_explain - logging facility for execution plans" -EXTRA_INSTALL = contrib/pg_overexplain +EXTRA_INSTALL = contrib/pg_overexplain contrib/pg_stat_statements REGRESS = alter_reset extension_options TAP_TESTS = 1 --- a/contrib/auto_explain/meson.build +++ b/contrib/auto_explain/meson.build @@ -29,6 +29,7 @@ 'tap': { 'tests': [ 't/001_auto_explain.pl', + 't/002_sampling.pl', ], }, } --- /dev/null +++ b/contrib/auto_explain/t/002_sampling.pl @@ -0,0 +1,70 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('sampling'); +$node->init; +$node->append_conf('postgresql.conf', + "shared_preload_libraries = 'pg_stat_statements'"); +$node->append_conf('postgresql.conf', "compute_query_id = on"); +$node->append_conf('postgresql.conf', "pg_stat_statements.track = 'all'"); +$node->start; + +# The other extension also requests query instrumentation. Its presence must +# not make auto_explain log a cursor that auto_explain did not sample. +for my $end ('commit', 'close') +{ + for my $kind ('control', 'sampled', 'unsampled') + { + my $name = "${kind}_${end}"; + my $cursor = "pg19877_$name"; + my $initial = $kind eq 'unsampled' ? 0 : 1; + my $later = $kind eq 'sampled' ? 0 : 1; + my $sql = qq{ +LOAD 'auto_explain'; +SET client_min_messages = notice; +SET auto_explain.log_level = notice; +SET auto_explain.log_min_duration = 0; +SET auto_explain.log_analyze = off; +SET auto_explain.log_format = text; +SET auto_explain.log_nested_statements = off; +SET auto_explain.sample_rate = $initial; +BEGIN; +DECLARE $cursor CURSOR FOR SELECT 101 AS pg19877_value; +FETCH ALL FROM $cursor; +}; + + # A separate statement replaces the backend-global sampling decision. + if ($kind ne 'control') + { + $sql .= qq{ +SET auto_explain.sample_rate = $later; +SELECT 202 AS pg19877_resampler; +}; + } + $sql .= "CLOSE $cursor;\n" if $end eq 'close'; + $sql .= "COMMIT;\n"; + + my ($stdout, $stderr); + my $ret = $node->psql('postgres', $sql, + stdout => \$stdout, stderr => \$stderr); + is($ret, 0, "$name executes successfully"); + like($stdout, qr/^101$/m, "$name returns its cursor row"); + my $plan = qr/Query Text:\s*DECLARE\s+\Q$cursor\E\s+CURSOR\b/; + if ($kind eq 'unsampled') + { + unlike($stderr, $plan, "$name is not logged by auto_explain"); + } + else + { + like($stderr, $plan, "$name keeps original sampling decision"); + } + } +} + +$node->stop; +done_testing();