From 4739a6961f426d3eb32dd0367651a14b5f51cb3d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Dec 2025 05:12:06 +0000 Subject: [PATCH 1/2] pg_stat_statements: Add rows_scanned column Add a new rows_scanned column to pg_stat_statements that tracks the total number of rows scanned by scan nodes (SeqScan, IndexScan, IndexOnlyScan, BitmapHeapScan, etc.) before filter conditions are applied. This metric is collected by walking the plan tree and summing up ntuples + nfiltered1 for all scan nodes. This information is valuable for identifying queries that scan many rows but return few, which often indicates missing indexes or suboptimal query plans. Combined with the existing rows column, users can calculate the filtering efficiency of their queries. The new column appears after rows in the view, so existing queries that select specific columns by name will continue to work. Bump extension version to 1.14. --- contrib/pg_stat_statements/Makefile | 1 + contrib/pg_stat_statements/meson.build | 1 + .../pg_stat_statements--1.13--1.14.sql | 79 +++++++++++++ .../pg_stat_statements/pg_stat_statements.c | 107 +++++++++++++++++- .../pg_stat_statements.control | 2 +- 5 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.13--1.14.sql diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile index fe0478ac55266..ee41ee0311e70 100644 --- a/contrib/pg_stat_statements/Makefile +++ b/contrib/pg_stat_statements/Makefile @@ -7,6 +7,7 @@ OBJS = \ EXTENSION = pg_stat_statements DATA = pg_stat_statements--1.4.sql \ + pg_stat_statements--1.13--1.14.sql \ pg_stat_statements--1.12--1.13.sql \ pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \ pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \ diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build index 7b8bfbb1de78c..378d363eef830 100644 --- a/contrib/pg_stat_statements/meson.build +++ b/contrib/pg_stat_statements/meson.build @@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements install_data( 'pg_stat_statements.control', 'pg_stat_statements--1.4.sql', + 'pg_stat_statements--1.13--1.14.sql', 'pg_stat_statements--1.12--1.13.sql', 'pg_stat_statements--1.11--1.12.sql', 'pg_stat_statements--1.10--1.11.sql', diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.13--1.14.sql b/contrib/pg_stat_statements/pg_stat_statements--1.13--1.14.sql new file mode 100644 index 0000000000000..8dc7b5db4bf66 --- /dev/null +++ b/contrib/pg_stat_statements/pg_stat_statements--1.13--1.14.sql @@ -0,0 +1,79 @@ +/* contrib/pg_stat_statements/pg_stat_statements--1.13--1.14.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.14'" to load this file. \quit + +/* First we have to remove them from the extension */ +ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements; +ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean); + +/* Then we can drop them */ +DROP VIEW pg_stat_statements; +DROP FUNCTION pg_stat_statements(boolean); + +/* Now redefine */ +CREATE FUNCTION pg_stat_statements(IN showtext boolean, + OUT userid oid, + OUT dbid oid, + OUT toplevel bool, + OUT queryid bigint, + OUT query text, + OUT plans int8, + OUT total_plan_time float8, + OUT min_plan_time float8, + OUT max_plan_time float8, + OUT mean_plan_time float8, + OUT stddev_plan_time float8, + OUT calls int8, + OUT total_exec_time float8, + OUT min_exec_time float8, + OUT max_exec_time float8, + OUT mean_exec_time float8, + OUT stddev_exec_time float8, + OUT rows int8, + OUT rows_scanned int8, + OUT shared_blks_hit int8, + OUT shared_blks_read int8, + OUT shared_blks_dirtied int8, + OUT shared_blks_written int8, + OUT local_blks_hit int8, + OUT local_blks_read int8, + OUT local_blks_dirtied int8, + OUT local_blks_written int8, + OUT temp_blks_read int8, + OUT temp_blks_written int8, + OUT shared_blk_read_time float8, + OUT shared_blk_write_time float8, + OUT local_blk_read_time float8, + OUT local_blk_write_time float8, + OUT temp_blk_read_time float8, + OUT temp_blk_write_time float8, + OUT wal_records int8, + OUT wal_fpi int8, + OUT wal_bytes numeric, + OUT wal_buffers_full int8, + OUT jit_functions int8, + OUT jit_generation_time float8, + OUT jit_inlining_count int8, + OUT jit_inlining_time float8, + OUT jit_optimization_count int8, + OUT jit_optimization_time float8, + OUT jit_emission_count int8, + OUT jit_emission_time float8, + OUT jit_deform_count int8, + OUT jit_deform_time float8, + OUT parallel_workers_to_launch int8, + OUT parallel_workers_launched int8, + OUT generic_plan_calls int8, + OUT custom_plan_calls int8, + OUT stats_since timestamp with time zone, + OUT minmax_stats_since timestamp with time zone +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_stat_statements_1_14' +LANGUAGE C STRICT VOLATILE PARALLEL SAFE; + +CREATE VIEW pg_stat_statements AS + SELECT * FROM pg_stat_statements(true); + +GRANT SELECT ON pg_stat_statements TO PUBLIC; diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c index 39208f80b5bb7..e3f8e84ddcfc5 100644 --- a/contrib/pg_stat_statements/pg_stat_statements.c +++ b/contrib/pg_stat_statements/pg_stat_statements.c @@ -56,6 +56,7 @@ #include "jit/jit.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "nodes/queryjumble.h" #include "optimizer/planner.h" #include "parser/analyze.h" @@ -116,6 +117,7 @@ typedef enum pgssVersion PGSS_V1_11, PGSS_V1_12, PGSS_V1_13, + PGSS_V1_14, } pgssVersion; typedef enum pgssStoreKind @@ -166,6 +168,7 @@ typedef struct Counters double sum_var_time[PGSS_NUMKIND]; /* sum of variances in * planning/execution time in msec */ int64 rows; /* total # of retrieved or affected rows */ + int64 rows_scanned; /* total # of rows scanned by scan nodes */ int64 shared_blks_hit; /* # of shared buffer hits */ int64 shared_blks_read; /* # of shared disk blocks read */ int64 shared_blks_dirtied; /* # of shared disk blocks dirtied */ @@ -327,6 +330,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_10); PG_FUNCTION_INFO_V1(pg_stat_statements_1_11); PG_FUNCTION_INFO_V1(pg_stat_statements_1_12); PG_FUNCTION_INFO_V1(pg_stat_statements_1_13); +PG_FUNCTION_INFO_V1(pg_stat_statements_1_14); PG_FUNCTION_INFO_V1(pg_stat_statements); PG_FUNCTION_INFO_V1(pg_stat_statements_info); @@ -355,6 +359,7 @@ static void pgss_store(const char *query, int64 queryId, int query_location, int query_len, pgssStoreKind kind, double total_time, uint64 rows, + int64 rows_scanned, const BufferUsage *bufusage, const WalUsage *walusage, const struct JitInstrumentation *jitusage, @@ -878,6 +883,7 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate) PGSS_INVALID, 0, 0, + 0, NULL, NULL, NULL, @@ -960,6 +966,7 @@ pgss_planner(Query *parse, PGSS_PLAN, INSTR_TIME_GET_MILLISEC(duration), 0, + 0, &bufusage, &walusage, NULL, @@ -1071,6 +1078,74 @@ pgss_ExecutorFinish(QueryDesc *queryDesc) PG_END_TRY(); } +/* + * Helper structure for collecting scan statistics + */ +typedef struct ScanStats +{ + int64 rows_scanned; /* total rows scanned (before filtering) */ +} ScanStats; + +/* + * Determine if a node is a scan node that reads from storage. + * For scan nodes, we want to count tuples before filter conditions are applied. + */ +static inline bool +IsScanNode(PlanState *planstate) +{ + switch (nodeTag(planstate)) + { + case T_SeqScanState: + case T_SampleScanState: + case T_IndexScanState: + case T_IndexOnlyScanState: + case T_BitmapHeapScanState: + case T_TidScanState: + case T_TidRangeScanState: + case T_ForeignScanState: + case T_CustomScanState: + return true; + default: + return false; + } +} + +/* + * Walker function to collect scan statistics from all nodes. + * For scan nodes, rows_scanned = tuples output + tuples filtered. + */ +static bool +pgss_collect_scan_stats_walker(PlanState *planstate, void *context) +{ + ScanStats *stats = (ScanStats *) context; + + if (planstate->instrument && IsScanNode(planstate)) + { + /* + * For scan nodes, rows_scanned is the number of tuples produced plus + * the number of tuples filtered out by the scan's filter condition. + * This represents the total number of tuples read from storage. + */ + double scanned = planstate->instrument->ntuples + + planstate->instrument->nfiltered1; + + stats->rows_scanned += (int64) scanned; + } + + return planstate_tree_walker(planstate, pgss_collect_scan_stats_walker, context); +} + +/* + * Collect scan statistics from the entire plan tree. + */ +static void +pgss_collect_scan_stats(PlanState *planstate, ScanStats *stats) +{ + memset(stats, 0, sizeof(ScanStats)); + if (planstate) + pgss_collect_scan_stats_walker(planstate, stats); +} + /* * ExecutorEnd hook: store results if needed */ @@ -1082,12 +1157,20 @@ pgss_ExecutorEnd(QueryDesc *queryDesc) if (queryId != INT64CONST(0) && queryDesc->totaltime && pgss_enabled(nesting_level)) { + ScanStats scan_stats; + /* * Make sure stats accumulation is done. (Note: it's okay if several * levels of hook all do this.) */ InstrEndLoop(queryDesc->totaltime); + /* + * Collect scan statistics from the plan tree. This must be done + * before standard_ExecutorEnd which will destroy the planstate. + */ + pgss_collect_scan_stats(queryDesc->planstate, &scan_stats); + pgss_store(queryDesc->sourceText, queryId, queryDesc->plannedstmt->stmt_location, @@ -1095,6 +1178,7 @@ pgss_ExecutorEnd(QueryDesc *queryDesc) PGSS_EXEC, queryDesc->totaltime->total * 1000.0, /* convert to msec */ queryDesc->estate->es_total_processed, + scan_stats.rows_scanned, &queryDesc->totaltime->bufusage, &queryDesc->totaltime->walusage, queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL, @@ -1229,6 +1313,7 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, PGSS_EXEC, INSTR_TIME_GET_MILLISEC(duration), rows, + 0, /* rows_scanned not available for utility statements */ &bufusage, &walusage, NULL, @@ -1293,6 +1378,7 @@ pgss_store(const char *query, int64 queryId, int query_location, int query_len, pgssStoreKind kind, double total_time, uint64 rows, + int64 rows_scanned, const BufferUsage *bufusage, const WalUsage *walusage, const struct JitInstrumentation *jitusage, @@ -1460,6 +1546,7 @@ pgss_store(const char *query, int64 queryId, } } entry->counters.rows += rows; + entry->counters.rows_scanned += rows_scanned; entry->counters.shared_blks_hit += bufusage->shared_blks_hit; entry->counters.shared_blks_read += bufusage->shared_blks_read; entry->counters.shared_blks_dirtied += bufusage->shared_blks_dirtied; @@ -1581,7 +1668,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS) #define PG_STAT_STATEMENTS_COLS_V1_11 49 #define PG_STAT_STATEMENTS_COLS_V1_12 52 #define PG_STAT_STATEMENTS_COLS_V1_13 54 -#define PG_STAT_STATEMENTS_COLS 54 /* maximum of above */ +#define PG_STAT_STATEMENTS_COLS_V1_14 55 +#define PG_STAT_STATEMENTS_COLS 55 /* maximum of above */ /* * Retrieve statement statistics. @@ -1593,6 +1681,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS) * expected API version is identified by embedding it in the C name of the * function. Unfortunately we weren't bright enough to do that for 1.1. */ +Datum +pg_stat_statements_1_14(PG_FUNCTION_ARGS) +{ + bool showtext = PG_GETARG_BOOL(0); + + pg_stat_statements_internal(fcinfo, PGSS_V1_14, showtext); + + return (Datum) 0; +} + Datum pg_stat_statements_1_13(PG_FUNCTION_ARGS) { @@ -1765,6 +1863,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, if (api_version != PGSS_V1_13) elog(ERROR, "incorrect number of output arguments"); break; + case PG_STAT_STATEMENTS_COLS_V1_14: + if (api_version != PGSS_V1_14) + elog(ERROR, "incorrect number of output arguments"); + break; default: elog(ERROR, "incorrect number of output arguments"); } @@ -1948,6 +2050,8 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, } } values[i++] = Int64GetDatumFast(tmp.rows); + if (api_version >= PGSS_V1_14) + values[i++] = Int64GetDatumFast(tmp.rows_scanned); values[i++] = Int64GetDatumFast(tmp.shared_blks_hit); values[i++] = Int64GetDatumFast(tmp.shared_blks_read); if (api_version >= PGSS_V1_1) @@ -2038,6 +2142,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 : api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 : api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 : + api_version == PGSS_V1_14 ? PG_STAT_STATEMENTS_COLS_V1_14 : -1 /* fail if you forget to update this assert */ )); tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control index 2eee0ceffa894..61ae41efc1472 100644 --- a/contrib/pg_stat_statements/pg_stat_statements.control +++ b/contrib/pg_stat_statements/pg_stat_statements.control @@ -1,5 +1,5 @@ # pg_stat_statements extension comment = 'track planning and execution statistics of all SQL statements executed' -default_version = '1.13' +default_version = '1.14' module_pathname = '$libdir/pg_stat_statements' relocatable = true From 0e9d6f81c912c174d53e2cff97c0dacf7d03757c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Dec 2025 05:17:42 +0000 Subject: [PATCH 2/2] pg_stat_statements: Add rows_filtered column Add a rows_filtered column to pg_stat_statements to track rows removed by scan/join/other filter conditions. This metric helps identify queries that may benefit from better indexing. The implementation: - Enables per-node instrumentation with INSTRUMENT_ALL before ExecutorStart - Walks the plan tree in ExecutorEnd to sum nfiltered1 (scanqual/joinqual) and nfiltered2 (other quals) from all nodes - Reads both ntuples and tuplecount to capture complete tuple counts from both completed and current execution cycles - Includes the new column in the SQL function and view for version 1.15 --- contrib/pg_stat_statements/Makefile | 1 + contrib/pg_stat_statements/meson.build | 1 + .../pg_stat_statements--1.14--1.15.sql | 80 ++++++++++++++++++ .../pg_stat_statements/pg_stat_statements.c | 84 +++++++++++++++++-- .../pg_stat_statements.control | 2 +- 5 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.14--1.15.sql diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile index ee41ee0311e70..b62b0bf7ae6aa 100644 --- a/contrib/pg_stat_statements/Makefile +++ b/contrib/pg_stat_statements/Makefile @@ -7,6 +7,7 @@ OBJS = \ EXTENSION = pg_stat_statements DATA = pg_stat_statements--1.4.sql \ + pg_stat_statements--1.14--1.15.sql \ pg_stat_statements--1.13--1.14.sql \ pg_stat_statements--1.12--1.13.sql \ pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \ diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build index 378d363eef830..3f80ecae6291a 100644 --- a/contrib/pg_stat_statements/meson.build +++ b/contrib/pg_stat_statements/meson.build @@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements install_data( 'pg_stat_statements.control', 'pg_stat_statements--1.4.sql', + 'pg_stat_statements--1.14--1.15.sql', 'pg_stat_statements--1.13--1.14.sql', 'pg_stat_statements--1.12--1.13.sql', 'pg_stat_statements--1.11--1.12.sql', diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.14--1.15.sql b/contrib/pg_stat_statements/pg_stat_statements--1.14--1.15.sql new file mode 100644 index 0000000000000..94acd39ee5008 --- /dev/null +++ b/contrib/pg_stat_statements/pg_stat_statements--1.14--1.15.sql @@ -0,0 +1,80 @@ +/* contrib/pg_stat_statements/pg_stat_statements--1.14--1.15.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.15'" to load this file. \quit + +/* First we have to remove them from the extension */ +ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements; +ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean); + +/* Then we can drop them */ +DROP VIEW pg_stat_statements; +DROP FUNCTION pg_stat_statements(boolean); + +/* Now redefine */ +CREATE FUNCTION pg_stat_statements(IN showtext boolean, + OUT userid oid, + OUT dbid oid, + OUT toplevel bool, + OUT queryid bigint, + OUT query text, + OUT plans int8, + OUT total_plan_time float8, + OUT min_plan_time float8, + OUT max_plan_time float8, + OUT mean_plan_time float8, + OUT stddev_plan_time float8, + OUT calls int8, + OUT total_exec_time float8, + OUT min_exec_time float8, + OUT max_exec_time float8, + OUT mean_exec_time float8, + OUT stddev_exec_time float8, + OUT rows int8, + OUT rows_scanned int8, + OUT rows_filtered int8, + OUT shared_blks_hit int8, + OUT shared_blks_read int8, + OUT shared_blks_dirtied int8, + OUT shared_blks_written int8, + OUT local_blks_hit int8, + OUT local_blks_read int8, + OUT local_blks_dirtied int8, + OUT local_blks_written int8, + OUT temp_blks_read int8, + OUT temp_blks_written int8, + OUT shared_blk_read_time float8, + OUT shared_blk_write_time float8, + OUT local_blk_read_time float8, + OUT local_blk_write_time float8, + OUT temp_blk_read_time float8, + OUT temp_blk_write_time float8, + OUT wal_records int8, + OUT wal_fpi int8, + OUT wal_bytes numeric, + OUT wal_buffers_full int8, + OUT jit_functions int8, + OUT jit_generation_time float8, + OUT jit_inlining_count int8, + OUT jit_inlining_time float8, + OUT jit_optimization_count int8, + OUT jit_optimization_time float8, + OUT jit_emission_count int8, + OUT jit_emission_time float8, + OUT jit_deform_count int8, + OUT jit_deform_time float8, + OUT parallel_workers_to_launch int8, + OUT parallel_workers_launched int8, + OUT generic_plan_calls int8, + OUT custom_plan_calls int8, + OUT stats_since timestamp with time zone, + OUT minmax_stats_since timestamp with time zone +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_stat_statements_1_15' +LANGUAGE C STRICT VOLATILE PARALLEL SAFE; + +CREATE VIEW pg_stat_statements AS + SELECT * FROM pg_stat_statements(true); + +GRANT SELECT ON pg_stat_statements TO PUBLIC; diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c index e3f8e84ddcfc5..a369f897c20d1 100644 --- a/contrib/pg_stat_statements/pg_stat_statements.c +++ b/contrib/pg_stat_statements/pg_stat_statements.c @@ -118,6 +118,7 @@ typedef enum pgssVersion PGSS_V1_12, PGSS_V1_13, PGSS_V1_14, + PGSS_V1_15, } pgssVersion; typedef enum pgssStoreKind @@ -169,6 +170,7 @@ typedef struct Counters * planning/execution time in msec */ int64 rows; /* total # of retrieved or affected rows */ int64 rows_scanned; /* total # of rows scanned by scan nodes */ + int64 rows_filtered; /* total # of rows filtered out by quals */ int64 shared_blks_hit; /* # of shared buffer hits */ int64 shared_blks_read; /* # of shared disk blocks read */ int64 shared_blks_dirtied; /* # of shared disk blocks dirtied */ @@ -331,6 +333,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_11); PG_FUNCTION_INFO_V1(pg_stat_statements_1_12); PG_FUNCTION_INFO_V1(pg_stat_statements_1_13); PG_FUNCTION_INFO_V1(pg_stat_statements_1_14); +PG_FUNCTION_INFO_V1(pg_stat_statements_1_15); PG_FUNCTION_INFO_V1(pg_stat_statements); PG_FUNCTION_INFO_V1(pg_stat_statements_info); @@ -360,6 +363,7 @@ static void pgss_store(const char *query, int64 queryId, pgssStoreKind kind, double total_time, uint64 rows, int64 rows_scanned, + int64 rows_filtered, const BufferUsage *bufusage, const WalUsage *walusage, const struct JitInstrumentation *jitusage, @@ -884,6 +888,7 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate) 0, 0, 0, + 0, NULL, NULL, NULL, @@ -967,6 +972,7 @@ pgss_planner(Query *parse, INSTR_TIME_GET_MILLISEC(duration), 0, 0, + 0, &bufusage, &walusage, NULL, @@ -1008,6 +1014,20 @@ pgss_planner(Query *parse, static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags) { + /* + * If we're going to track this query, ensure per-node instrumentation is + * enabled so we can collect rows_scanned and rows_filtered statistics. + * This must be done before calling standard_ExecutorStart, which is when + * the instrumentation structures are allocated for each plan node. + * + * We use INSTRUMENT_ROWS rather than INSTRUMENT_ALL to avoid the overhead + * of timing instrumentation (which requires system calls per node). + */ + if (pgss_enabled(nesting_level) && queryDesc->plannedstmt->queryId != INT64CONST(0)) + { + queryDesc->instrument_options |= INSTRUMENT_ROWS; + } + if (prev_ExecutorStart) prev_ExecutorStart(queryDesc, eflags); else @@ -1084,6 +1104,7 @@ pgss_ExecutorFinish(QueryDesc *queryDesc) typedef struct ScanStats { int64 rows_scanned; /* total rows scanned (before filtering) */ + int64 rows_filtered; /* total rows filtered by all nodes */ } ScanStats; /* @@ -1113,23 +1134,48 @@ IsScanNode(PlanState *planstate) /* * Walker function to collect scan statistics from all nodes. * For scan nodes, rows_scanned = tuples output + tuples filtered. + * For all nodes, rows_filtered = sum of nfiltered1 + nfiltered2. */ static bool pgss_collect_scan_stats_walker(PlanState *planstate, void *context) { ScanStats *stats = (ScanStats *) context; - if (planstate->instrument && IsScanNode(planstate)) + if (planstate->instrument) { + Instrumentation *instr = planstate->instrument; + /* - * For scan nodes, rows_scanned is the number of tuples produced plus - * the number of tuples filtered out by the scan's filter condition. - * This represents the total number of tuples read from storage. + * Get the total tuple count for this node. The tuples are tracked in + * two places: 'ntuples' holds the count from completed cycles, and + * 'tuplecount' holds the count from the current (possibly incomplete) + * cycle. We need both to get an accurate total. + * + * Note: InstrEndLoop() would normally move tuplecount to ntuples, but + * it returns early if 'running' is false (which happens after each + * InstrStopNode call), so we can't rely on it here. */ - double scanned = planstate->instrument->ntuples + - planstate->instrument->nfiltered1; + double node_tuples = instr->ntuples + instr->tuplecount; - stats->rows_scanned += (int64) scanned; + /* + * Collect rows_filtered from all nodes. nfiltered1 tracks tuples + * removed by scanqual or joinqual, nfiltered2 tracks tuples removed + * by "other" quals. + */ + stats->rows_filtered += (int64) (instr->nfiltered1 + instr->nfiltered2); + + if (IsScanNode(planstate)) + { + /* + * For scan nodes, rows_scanned is the number of tuples produced + * plus the number of tuples filtered out by the scan's filter + * condition. This represents the total number of tuples read + * from storage. + */ + double scanned = node_tuples + instr->nfiltered1; + + stats->rows_scanned += (int64) scanned; + } } return planstate_tree_walker(planstate, pgss_collect_scan_stats_walker, context); @@ -1179,6 +1225,7 @@ pgss_ExecutorEnd(QueryDesc *queryDesc) queryDesc->totaltime->total * 1000.0, /* convert to msec */ queryDesc->estate->es_total_processed, scan_stats.rows_scanned, + scan_stats.rows_filtered, &queryDesc->totaltime->bufusage, &queryDesc->totaltime->walusage, queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL, @@ -1314,6 +1361,7 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, INSTR_TIME_GET_MILLISEC(duration), rows, 0, /* rows_scanned not available for utility statements */ + 0, /* rows_filtered not available for utility statements */ &bufusage, &walusage, NULL, @@ -1379,6 +1427,7 @@ pgss_store(const char *query, int64 queryId, pgssStoreKind kind, double total_time, uint64 rows, int64 rows_scanned, + int64 rows_filtered, const BufferUsage *bufusage, const WalUsage *walusage, const struct JitInstrumentation *jitusage, @@ -1547,6 +1596,7 @@ pgss_store(const char *query, int64 queryId, } entry->counters.rows += rows; entry->counters.rows_scanned += rows_scanned; + entry->counters.rows_filtered += rows_filtered; entry->counters.shared_blks_hit += bufusage->shared_blks_hit; entry->counters.shared_blks_read += bufusage->shared_blks_read; entry->counters.shared_blks_dirtied += bufusage->shared_blks_dirtied; @@ -1669,7 +1719,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS) #define PG_STAT_STATEMENTS_COLS_V1_12 52 #define PG_STAT_STATEMENTS_COLS_V1_13 54 #define PG_STAT_STATEMENTS_COLS_V1_14 55 -#define PG_STAT_STATEMENTS_COLS 55 /* maximum of above */ +#define PG_STAT_STATEMENTS_COLS_V1_15 56 +#define PG_STAT_STATEMENTS_COLS 56 /* maximum of above */ /* * Retrieve statement statistics. @@ -1681,6 +1732,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS) * expected API version is identified by embedding it in the C name of the * function. Unfortunately we weren't bright enough to do that for 1.1. */ +Datum +pg_stat_statements_1_15(PG_FUNCTION_ARGS) +{ + bool showtext = PG_GETARG_BOOL(0); + + pg_stat_statements_internal(fcinfo, PGSS_V1_15, showtext); + + return (Datum) 0; +} + Datum pg_stat_statements_1_14(PG_FUNCTION_ARGS) { @@ -1867,6 +1928,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, if (api_version != PGSS_V1_14) elog(ERROR, "incorrect number of output arguments"); break; + case PG_STAT_STATEMENTS_COLS_V1_15: + if (api_version != PGSS_V1_15) + elog(ERROR, "incorrect number of output arguments"); + break; default: elog(ERROR, "incorrect number of output arguments"); } @@ -2052,6 +2117,8 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, values[i++] = Int64GetDatumFast(tmp.rows); if (api_version >= PGSS_V1_14) values[i++] = Int64GetDatumFast(tmp.rows_scanned); + if (api_version >= PGSS_V1_15) + values[i++] = Int64GetDatumFast(tmp.rows_filtered); values[i++] = Int64GetDatumFast(tmp.shared_blks_hit); values[i++] = Int64GetDatumFast(tmp.shared_blks_read); if (api_version >= PGSS_V1_1) @@ -2143,6 +2210,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 : api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 : api_version == PGSS_V1_14 ? PG_STAT_STATEMENTS_COLS_V1_14 : + api_version == PGSS_V1_15 ? PG_STAT_STATEMENTS_COLS_V1_15 : -1 /* fail if you forget to update this assert */ )); tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control index 61ae41efc1472..a3c920ba631b9 100644 --- a/contrib/pg_stat_statements/pg_stat_statements.control +++ b/contrib/pg_stat_statements/pg_stat_statements.control @@ -1,5 +1,5 @@ # pg_stat_statements extension comment = 'track planning and execution statistics of all SQL statements executed' -default_version = '1.14' +default_version = '1.15' module_pathname = '$libdir/pg_stat_statements' relocatable = true