--- 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();
