From 92d72e1ad725faa3fc5675e3ac8278a03b02919f Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Tue, 3 Jun 2025 17:58:19 +0000
Subject: [PATCH v5 1/1] Normalize variable fetch sizes in a FETCH command

Prior to this patch, every FETCH call would generate a unique queryId.
This led to significant bloat in pg_stat_statements, as repeatedly calling
a specific cursor would result in a new queryId each time. For example,
FETCH 1 c1; and FETCH 2 c1; would produce different queryIds.

This patch improves the situation by normalizing the fetch size, so semantically
similar statements generate the same queryId. As a result, statements like the below,
which differ syntactically but have the same effect, will now share a single queryId:

FETCH FROM c1;
FETCH NEXT c1;
FETCH 1 c1;

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0tA6LbHCg2qSS%2BKuM850BZC_%2BZgHV7Ug6BXw22TNyF%2BMA%40mail.gmail.com
---
 .../pg_stat_statements/expected/cursors.out   | 173 +++++++++++++++++-
 .../expected/level_tracking.out               |   4 +-
 .../pg_stat_statements/expected/utility.out   |   2 +-
 contrib/pg_stat_statements/sql/cursors.sql    |  51 ++++++
 src/backend/parser/gram.y                     |  50 ++++-
 src/include/nodes/parsenodes.h                |  28 ++-
 src/tools/pgindent/typedefs.list              |   1 +
 7 files changed, 294 insertions(+), 15 deletions(-)

diff --git a/contrib/pg_stat_statements/expected/cursors.out b/contrib/pg_stat_statements/expected/cursors.out
index 0fc4b2c098d0..afe5b7b52d5b 100644
--- a/contrib/pg_stat_statements/expected/cursors.out
+++ b/contrib/pg_stat_statements/expected/cursors.out
@@ -57,8 +57,8 @@ SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
      1 |    0 | COMMIT
      1 |    0 | DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT $1
      1 |    0 | DECLARE cursor_stats_2 CURSOR WITH HOLD FOR SELECT $1
-     1 |    1 | FETCH 1 IN cursor_stats_1
-     1 |    1 | FETCH 1 IN cursor_stats_2
+     1 |    1 | FETCH $1 IN cursor_stats_1
+     1 |    1 | FETCH $1 IN cursor_stats_2
      1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
 (9 rows)
 
@@ -68,3 +68,172 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t;
  t
 (1 row)
 
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Normalization of FETCH statements
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+BEGIN;
+DECLARE pgss_cursor CURSOR FOR SELECT FROM generate_series(1, 50);
+-- implicit directions
+FETCH pgss_cursor;
+--
+(1 row)
+
+FETCH 1 pgss_cursor;
+--
+(1 row)
+
+FETCH 2 pgss_cursor;
+--
+(2 rows)
+
+FETCH 3 pgss_cursor;
+--
+(3 rows)
+
+FETCH -1 pgss_cursor;
+--
+(1 row)
+
+-- explicit NEXT
+FETCH NEXT pgss_cursor;
+--
+(1 row)
+
+-- explicit PRIOR
+FETCH PRIOR pgss_cursor;
+--
+(1 row)
+
+-- explicit FIRST
+FETCH FIRST pgss_cursor;
+--
+(1 row)
+
+-- explicit LAST
+FETCH LAST pgss_cursor;
+--
+(1 row)
+
+-- explicit ABSOLUTE
+FETCH ABSOLUTE 1 pgss_cursor;
+--
+(1 row)
+
+FETCH ABSOLUTE 2 pgss_cursor;
+--
+(1 row)
+
+FETCH ABSOLUTE 3 pgss_cursor;
+--
+(1 row)
+
+FETCH ABSOLUTE -1 pgss_cursor;
+--
+(1 row)
+
+-- explicit RELATIVE
+FETCH RELATIVE 1 pgss_cursor;
+--
+(0 rows)
+
+FETCH RELATIVE 2 pgss_cursor;
+--
+(0 rows)
+
+FETCH RELATIVE 3 pgss_cursor;
+--
+(0 rows)
+
+FETCH RELATIVE -1 pgss_cursor;
+--
+(1 row)
+
+-- explicit FORWARD
+FETCH ALL pgss_cursor;
+--
+(0 rows)
+
+-- explicit FORWARD ALL
+FETCH FORWARD ALL pgss_cursor;
+--
+(0 rows)
+
+-- explicit BACKWARD ALL
+FETCH BACKWARD ALL pgss_cursor;
+--
+(50 rows)
+
+-- explicit FETCH FORWARD
+FETCH FORWARD pgss_cursor;
+--
+(1 row)
+
+FETCH FORWARD 1 pgss_cursor;
+--
+(1 row)
+
+FETCH FORWARD 2 pgss_cursor;
+--
+(2 rows)
+
+FETCH FORWARD 3 pgss_cursor;
+--
+(3 rows)
+
+FETCH FORWARD -1 pgss_cursor;
+--
+(1 row)
+
+-- explicit FETCH BACKWARD
+FETCH BACKWARD pgss_cursor;
+--
+(1 row)
+
+FETCH BACKWARD 1 pgss_cursor;
+--
+(1 row)
+
+FETCH BACKWARD 2 pgss_cursor;
+--
+(2 rows)
+
+FETCH BACKWARD 3 pgss_cursor;
+--
+(1 row)
+
+FETCH BACKWARD -1 pgss_cursor;
+--
+(1 row)
+
+COMMIT;
+SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls |                               query                                
+-------+--------------------------------------------------------------------
+     1 | BEGIN
+     1 | COMMIT
+     1 | DECLARE pgss_cursor CURSOR FOR SELECT FROM generate_series($1, $2)
+     4 | FETCH ABSOLUTE $1 pgss_cursor
+     1 | FETCH ALL pgss_cursor
+     1 | FETCH BACKWARD ALL pgss_cursor
+     5 | FETCH BACKWARD pgss_cursor
+     1 | FETCH FIRST pgss_cursor
+     1 | FETCH FORWARD ALL pgss_cursor
+     5 | FETCH FORWARD pgss_cursor
+     1 | FETCH LAST pgss_cursor
+     1 | FETCH NEXT pgss_cursor
+     1 | FETCH PRIOR pgss_cursor
+     4 | FETCH RELATIVE $1 pgss_cursor
+     5 | FETCH pgss_cursor
+     1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(16 rows)
+
diff --git a/contrib/pg_stat_statements/expected/level_tracking.out b/contrib/pg_stat_statements/expected/level_tracking.out
index 75e785e1719e..ce4bd3a823d8 100644
--- a/contrib/pg_stat_statements/expected/level_tracking.out
+++ b/contrib/pg_stat_statements/expected/level_tracking.out
@@ -1142,7 +1142,7 @@ SELECT toplevel, calls, query FROM pg_stat_statements
  t        |     1 | CLOSE foocur
  t        |     1 | COMMIT
  t        |     1 | DECLARE FOOCUR CURSOR FOR SELECT * from stats_track_tab
- t        |     1 | FETCH FORWARD 1 FROM foocur
+ t        |     1 | FETCH FORWARD $1 FROM foocur
  f        |     1 | SELECT * from stats_track_tab
  t        |     1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
 (7 rows)
@@ -1172,7 +1172,7 @@ SELECT toplevel, calls, query FROM pg_stat_statements
  t        |     1 | CLOSE foocur
  t        |     1 | COMMIT
  t        |     1 | DECLARE FOOCUR CURSOR FOR SELECT * FROM stats_track_tab
- t        |     1 | FETCH FORWARD 1 FROM foocur
+ t        |     1 | FETCH FORWARD $1 FROM foocur
  t        |     1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
 (6 rows)
 
diff --git a/contrib/pg_stat_statements/expected/utility.out b/contrib/pg_stat_statements/expected/utility.out
index aa4f0f7e6280..9a413505f952 100644
--- a/contrib/pg_stat_statements/expected/utility.out
+++ b/contrib/pg_stat_statements/expected/utility.out
@@ -702,7 +702,7 @@ SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
      1 |   13 | CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas
      1 |   10 | CREATE TABLE pgss_ctas AS SELECT a, $1 b FROM generate_series($2, $3) a
      1 |    0 | DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv
-     1 |    5 | FETCH FORWARD 5 pgss_cursor
+     1 |    5 | FETCH FORWARD $1 pgss_cursor
      1 |    7 | FETCH FORWARD ALL pgss_cursor
      1 |    1 | FETCH NEXT pgss_cursor
      1 |   13 | REFRESH MATERIALIZED VIEW pgss_matv
diff --git a/contrib/pg_stat_statements/sql/cursors.sql b/contrib/pg_stat_statements/sql/cursors.sql
index 61738ac470e8..68ae91882293 100644
--- a/contrib/pg_stat_statements/sql/cursors.sql
+++ b/contrib/pg_stat_statements/sql/cursors.sql
@@ -28,3 +28,54 @@ COMMIT;
 
 SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
 SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Normalization of FETCH statements
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+BEGIN;
+DECLARE pgss_cursor CURSOR FOR SELECT FROM generate_series(1, 50);
+-- implicit directions
+FETCH pgss_cursor;
+FETCH 1 pgss_cursor;
+FETCH 2 pgss_cursor;
+FETCH 3 pgss_cursor;
+FETCH -1 pgss_cursor;
+-- explicit NEXT
+FETCH NEXT pgss_cursor;
+-- explicit PRIOR
+FETCH PRIOR pgss_cursor;
+-- explicit FIRST
+FETCH FIRST pgss_cursor;
+-- explicit LAST
+FETCH LAST pgss_cursor;
+-- explicit ABSOLUTE
+FETCH ABSOLUTE 1 pgss_cursor;
+FETCH ABSOLUTE 2 pgss_cursor;
+FETCH ABSOLUTE 3 pgss_cursor;
+FETCH ABSOLUTE -1 pgss_cursor;
+-- explicit RELATIVE
+FETCH RELATIVE 1 pgss_cursor;
+FETCH RELATIVE 2 pgss_cursor;
+FETCH RELATIVE 3 pgss_cursor;
+FETCH RELATIVE -1 pgss_cursor;
+-- explicit FORWARD
+FETCH ALL pgss_cursor;
+-- explicit FORWARD ALL
+FETCH FORWARD ALL pgss_cursor;
+-- explicit BACKWARD ALL
+FETCH BACKWARD ALL pgss_cursor;
+-- explicit FETCH FORWARD
+FETCH FORWARD pgss_cursor;
+FETCH FORWARD 1 pgss_cursor;
+FETCH FORWARD 2 pgss_cursor;
+FETCH FORWARD 3 pgss_cursor;
+FETCH FORWARD -1 pgss_cursor;
+-- explicit FETCH BACKWARD
+FETCH BACKWARD pgss_cursor;
+FETCH BACKWARD 1 pgss_cursor;
+FETCH BACKWARD 2 pgss_cursor;
+FETCH BACKWARD 3 pgss_cursor;
+FETCH BACKWARD -1 pgss_cursor;
+COMMIT;
+SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0b5652071d11..0dfcdbfb3100 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -7479,6 +7479,8 @@ fetch_args:	cursor_name
 					n->portalname = $1;
 					n->direction = FETCH_FORWARD;
 					n->howMany = 1;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_NONE;
 					$$ = (Node *) n;
 				}
 			| from_in cursor_name
@@ -7488,6 +7490,19 @@ fetch_args:	cursor_name
 					n->portalname = $2;
 					n->direction = FETCH_FORWARD;
 					n->howMany = 1;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_NONE;
+					$$ = (Node *) n;
+				}
+			| SignedIconst opt_from_in cursor_name
+				{
+					FetchStmt *n = makeNode(FetchStmt);
+
+					n->portalname = $3;
+					n->direction = FETCH_FORWARD;
+					n->howMany = $1;
+					n->location = @1;
+					n->direction_keyword = FETCH_KEYWORD_NONE;
 					$$ = (Node *) n;
 				}
 			| NEXT opt_from_in cursor_name
@@ -7497,6 +7512,8 @@ fetch_args:	cursor_name
 					n->portalname = $3;
 					n->direction = FETCH_FORWARD;
 					n->howMany = 1;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_NEXT;
 					$$ = (Node *) n;
 				}
 			| PRIOR opt_from_in cursor_name
@@ -7506,6 +7523,8 @@ fetch_args:	cursor_name
 					n->portalname = $3;
 					n->direction = FETCH_BACKWARD;
 					n->howMany = 1;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_PRIOR;
 					$$ = (Node *) n;
 				}
 			| FIRST_P opt_from_in cursor_name
@@ -7515,6 +7534,8 @@ fetch_args:	cursor_name
 					n->portalname = $3;
 					n->direction = FETCH_ABSOLUTE;
 					n->howMany = 1;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_FIRST;
 					$$ = (Node *) n;
 				}
 			| LAST_P opt_from_in cursor_name
@@ -7524,6 +7545,8 @@ fetch_args:	cursor_name
 					n->portalname = $3;
 					n->direction = FETCH_ABSOLUTE;
 					n->howMany = -1;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_LAST;
 					$$ = (Node *) n;
 				}
 			| ABSOLUTE_P SignedIconst opt_from_in cursor_name
@@ -7533,6 +7556,8 @@ fetch_args:	cursor_name
 					n->portalname = $4;
 					n->direction = FETCH_ABSOLUTE;
 					n->howMany = $2;
+					n->location = @2;
+					n->direction_keyword = FETCH_KEYWORD_ABSOLUTE;
 					$$ = (Node *) n;
 				}
 			| RELATIVE_P SignedIconst opt_from_in cursor_name
@@ -7542,15 +7567,8 @@ fetch_args:	cursor_name
 					n->portalname = $4;
 					n->direction = FETCH_RELATIVE;
 					n->howMany = $2;
-					$$ = (Node *) n;
-				}
-			| SignedIconst opt_from_in cursor_name
-				{
-					FetchStmt *n = makeNode(FetchStmt);
-
-					n->portalname = $3;
-					n->direction = FETCH_FORWARD;
-					n->howMany = $1;
+					n->location = @2;
+					n->direction_keyword = FETCH_KEYWORD_RELATIVE;
 					$$ = (Node *) n;
 				}
 			| ALL opt_from_in cursor_name
@@ -7560,6 +7578,8 @@ fetch_args:	cursor_name
 					n->portalname = $3;
 					n->direction = FETCH_FORWARD;
 					n->howMany = FETCH_ALL;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_ALL;
 					$$ = (Node *) n;
 				}
 			| FORWARD opt_from_in cursor_name
@@ -7569,6 +7589,8 @@ fetch_args:	cursor_name
 					n->portalname = $3;
 					n->direction = FETCH_FORWARD;
 					n->howMany = 1;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_FORWARD;
 					$$ = (Node *) n;
 				}
 			| FORWARD SignedIconst opt_from_in cursor_name
@@ -7578,6 +7600,8 @@ fetch_args:	cursor_name
 					n->portalname = $4;
 					n->direction = FETCH_FORWARD;
 					n->howMany = $2;
+					n->location = @2;
+					n->direction_keyword = FETCH_KEYWORD_FORWARD;
 					$$ = (Node *) n;
 				}
 			| FORWARD ALL opt_from_in cursor_name
@@ -7587,6 +7611,8 @@ fetch_args:	cursor_name
 					n->portalname = $4;
 					n->direction = FETCH_FORWARD;
 					n->howMany = FETCH_ALL;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_FORWARD_ALL;
 					$$ = (Node *) n;
 				}
 			| BACKWARD opt_from_in cursor_name
@@ -7596,6 +7622,8 @@ fetch_args:	cursor_name
 					n->portalname = $3;
 					n->direction = FETCH_BACKWARD;
 					n->howMany = 1;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_BACKWARD;
 					$$ = (Node *) n;
 				}
 			| BACKWARD SignedIconst opt_from_in cursor_name
@@ -7605,6 +7633,8 @@ fetch_args:	cursor_name
 					n->portalname = $4;
 					n->direction = FETCH_BACKWARD;
 					n->howMany = $2;
+					n->location = @2;
+					n->direction_keyword = FETCH_KEYWORD_BACKWARD;
 					$$ = (Node *) n;
 				}
 			| BACKWARD ALL opt_from_in cursor_name
@@ -7614,6 +7644,8 @@ fetch_args:	cursor_name
 					n->portalname = $4;
 					n->direction = FETCH_BACKWARD;
 					n->howMany = FETCH_ALL;
+					n->location = -1;
+					n->direction_keyword = FETCH_KEYWORD_BACKWARD_ALL;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index dd00ab420b8a..2f464ab23b8e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3422,15 +3422,41 @@ typedef enum FetchDirection
 	FETCH_RELATIVE,
 } FetchDirection;
 
+typedef enum FetchDirectionKeywords
+{
+	FETCH_KEYWORD_NONE,
+	FETCH_KEYWORD_NEXT,
+	FETCH_KEYWORD_PRIOR,
+	FETCH_KEYWORD_FIRST,
+	FETCH_KEYWORD_LAST,
+	FETCH_KEYWORD_ABSOLUTE,
+	FETCH_KEYWORD_RELATIVE,
+	FETCH_KEYWORD_ALL,
+	FETCH_KEYWORD_FORWARD,
+	FETCH_KEYWORD_FORWARD_ALL,
+	FETCH_KEYWORD_BACKWARD,
+	FETCH_KEYWORD_BACKWARD_ALL,
+} FetchDirectionKeywords;
+
 #define FETCH_ALL	LONG_MAX
 
 typedef struct FetchStmt
 {
 	NodeTag		type;
 	FetchDirection direction;	/* see above */
-	long		howMany;		/* number of rows, or position argument */
+	/* number of rows, or position argument */
+	long		howMany pg_node_attr(query_jumble_ignore);
 	char	   *portalname;		/* name of portal (cursor) */
 	bool		ismove;			/* true if MOVE */
+
+	/*
+	 * This field is set in gram.y when a direction_keyword (e.g., FETCH
+	 * FORWARD) is used, to distinguish it from a numeric variant (e.g., FETCH
+	 * 1) for the purpose of query jumbling.
+	 */
+	FetchDirectionKeywords direction_keyword;
+
+	ParseLoc	location pg_node_attr(query_jumble_location);
 } FetchStmt;
 
 /* ----------------------
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a8346cda633a..8b15ac5c307e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4305,3 +4305,4 @@ zic_t
 ExplainExtensionOption
 ExplainOptionHandler
 overexplain_options
+FetchDirectionKeywords
-- 
2.39.5 (Apple Git-154)

