diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index 58c85ded58717117bd835f69a4abedc5d93d2be1..178fdb9d4c0cb67f93ccf5ad26d76f47a7e8f61a 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -174,7 +174,7 @@ typedef struct pgssJumbleState
 
 /*---- Local variables ----*/
 
-/* Current nesting depth of ExecutorRun calls */
+/* Current nesting depth of ExecutorRun+ProcessUtility calls */
 static int	nested_level = 0;
 
 /* Saved hook values in case of unload */
@@ -773,7 +773,22 @@ pgss_ProcessUtility(Node *parsetree, const char *queryString,
 					ParamListInfo params, bool isTopLevel,
 					DestReceiver *dest, char *completionTag)
 {
-	if (pgss_track_utility && pgss_enabled())
+	/*
+	 * If it's an EXECUTE statement, we don't track it and don't increment
+	 * the nesting level.  This allows the cycles to be charged to the
+	 * underlying PREPARE instead (by the Executor hooks), which is much more
+	 * useful.
+	 *
+	 * We also don't track execution of PREPARE.  If we did, we would get one
+	 * hash table entry for the PREPARE (with hash calculated from the query
+	 * string), and then a different one with the same query string (but hash
+	 * calculated from the query tree) would be used to accumulate costs of
+	 * ensuing EXECUTEs.  This would be confusing, and inconsistent with other
+	 * cases where planning time is not included at all.
+	 */
+	if (pgss_track_utility && pgss_enabled() &&
+		!IsA(parsetree, ExecuteStmt) &&
+		!IsA(parsetree, PrepareStmt))
 	{
 		instr_time	start;
 		instr_time	duration;