From af7c77c48f1b08f18fdbdac2daf0f1da77b25fe2 Mon Sep 17 00:00:00 2001 From: Michael Erickson Date: Wed, 29 Oct 2025 16:37:09 -0700 Subject: [PATCH 1/2] sql, tree: move two formatting helper functions into tree Move `formatStatementHideConstants` and `formatStatementSummary` from sql to tree so that we can call `tree.FormatStatementHideConstants` from the optbuilder package. Epic: None Release note: None --- pkg/sql/conn_executor.go | 6 ++--- pkg/sql/conn_executor_exec.go | 2 +- pkg/sql/exec_util.go | 29 ---------------------- pkg/sql/executor_statement_metrics_test.go | 2 +- pkg/sql/sem/tree/format.go | 29 ++++++++++++++++++++++ pkg/sql/statement.go | 4 +-- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pkg/sql/conn_executor.go b/pkg/sql/conn_executor.go index d190284d1b60..e298e1d5b2ee 100644 --- a/pkg/sql/conn_executor.go +++ b/pkg/sql/conn_executor.go @@ -4458,7 +4458,7 @@ func (ex *connExecutor) serialize() serverpb.Session { "serialization") continue } - sqlNoConstants := truncateSQL(formatStatementHideConstants(parsed.AST)) + sqlNoConstants := truncateSQL(tree.FormatStatementHideConstants(parsed.AST)) nPlaceholders := 0 if query.placeholders != nil { nPlaceholders = len(query.placeholders.Values) @@ -4483,7 +4483,7 @@ func (ex *connExecutor) serialize() serverpb.Session { ElapsedTime: elapsedTime, Sql: sql, SqlNoConstants: sqlNoConstants, - SqlSummary: formatStatementSummary(parsed.AST), + SqlSummary: tree.FormatStatementSummary(parsed.AST), Placeholders: placeholders, IsDistributed: query.isDistributed, Phase: (serverpb.ActiveQuery_Phase)(query.phase), @@ -4498,7 +4498,7 @@ func (ex *connExecutor) serialize() serverpb.Session { lastActiveQueryNoConstants := "" if ex.mu.LastActiveQuery != nil { lastActiveQuery = truncateSQL(ex.mu.LastActiveQuery.String()) - lastActiveQueryNoConstants = truncateSQL(formatStatementHideConstants(ex.mu.LastActiveQuery)) + lastActiveQueryNoConstants = truncateSQL(tree.FormatStatementHideConstants(ex.mu.LastActiveQuery)) } status := serverpb.Session_IDLE if len(activeQueries) > 0 { diff --git a/pkg/sql/conn_executor_exec.go b/pkg/sql/conn_executor_exec.go index f7a67cd7b0f0..e18c0a4247cd 100644 --- a/pkg/sql/conn_executor_exec.go +++ b/pkg/sql/conn_executor_exec.go @@ -4395,7 +4395,7 @@ func (ex *connExecutor) execWithProfiling( if prepared != nil { stmtNoConstants = prepared.StatementNoConstants } else { - stmtNoConstants = formatStatementHideConstants(ast) + stmtNoConstants = tree.FormatStatementHideConstants(ast) } labels := pprof.Labels( "appname", ex.sessionData().ApplicationName, diff --git a/pkg/sql/exec_util.go b/pkg/sql/exec_util.go index 581425374fbc..57bdfb49f47d 100644 --- a/pkg/sql/exec_util.go +++ b/pkg/sql/exec_util.go @@ -3481,35 +3481,6 @@ func HashForReporting(secret, appName string) string { return hex.EncodeToString(hash.Sum(nil)[:4]) } -// formatStatementHideConstants formats the statement using -// tree.FmtHideConstants. It does *not* anonymize the statement, since -// the result will still contain names and identifiers. -func formatStatementHideConstants(ast tree.Statement, optFlags ...tree.FmtFlags) string { - if ast == nil { - return "" - } - fmtFlags := tree.FmtHideConstants - for _, f := range optFlags { - fmtFlags |= f - } - return tree.AsStringWithFlags(ast, fmtFlags) -} - -// formatStatementSummary formats the statement using tree.FmtSummary -// and tree.FmtHideConstants. This returns a summarized version of the -// query. It does *not* anonymize the statement, since the result will -// still contain names and identifiers. -func formatStatementSummary(ast tree.Statement, optFlags ...tree.FmtFlags) string { - if ast == nil { - return "" - } - fmtFlags := tree.FmtSummary | tree.FmtHideConstants - for _, f := range optFlags { - fmtFlags |= f - } - return tree.AsStringWithFlags(ast, fmtFlags) -} - // DescsTxn is a convenient method for running a transaction on descriptors // when you have an ExecutorConfig. // diff --git a/pkg/sql/executor_statement_metrics_test.go b/pkg/sql/executor_statement_metrics_test.go index ed577ef7f99e..62600aa9712e 100644 --- a/pkg/sql/executor_statement_metrics_test.go +++ b/pkg/sql/executor_statement_metrics_test.go @@ -138,7 +138,7 @@ func makeTestQuery(i int) (*Statement, error) { } return &Statement{ - StmtNoConstants: formatStatementHideConstants(stmt.AST, tree.FmtCollapseLists|tree.FmtConstantsAsUnderscores), + StmtNoConstants: tree.FormatStatementHideConstants(stmt.AST, tree.FmtCollapseLists|tree.FmtConstantsAsUnderscores), Statement: stmt, }, nil } diff --git a/pkg/sql/sem/tree/format.go b/pkg/sql/sem/tree/format.go index 21963be48d67..39631bd5a685 100644 --- a/pkg/sql/sem/tree/format.go +++ b/pkg/sql/sem/tree/format.go @@ -874,6 +874,35 @@ func SerializeForDisplay(n NodeFormatter) string { return AsStringWithFlags(n, FmtParsable) } +// FormatStatementHideConstants formats the statement using FmtHideConstants. It +// does *not* anonymize the statement, since the result will still contain names +// and identifiers. +func FormatStatementHideConstants(ast Statement, optFlags ...FmtFlags) string { + if ast == nil { + return "" + } + fmtFlags := FmtHideConstants + for _, f := range optFlags { + fmtFlags |= f + } + return AsStringWithFlags(ast, fmtFlags) +} + +// FormatStatementSummary formats the statement using FmtSummary and +// FmtHideConstants. This returns a summarized version of the query. It does +// *not* anonymize the statement, since the result will still contain names and +// identifiers. +func FormatStatementSummary(ast Statement, optFlags ...FmtFlags) string { + if ast == nil { + return "" + } + fmtFlags := FmtSummary | FmtHideConstants + for _, f := range optFlags { + fmtFlags |= f + } + return AsStringWithFlags(ast, fmtFlags) +} + var fmtCtxPool = sync.Pool{ New: func() interface{} { return &FmtCtx{} diff --git a/pkg/sql/statement.go b/pkg/sql/statement.go index b958655e0137..2d5705b9be8f 100644 --- a/pkg/sql/statement.go +++ b/pkg/sql/statement.go @@ -54,8 +54,8 @@ func makeStatement( return Statement{ Statement: parserStmt, - StmtNoConstants: formatStatementHideConstants(parserStmt.AST, fmtFlags), - StmtSummary: formatStatementSummary(parserStmt.AST, fmtFlags), + StmtNoConstants: tree.FormatStatementHideConstants(parserStmt.AST, fmtFlags), + StmtSummary: tree.FormatStatementSummary(parserStmt.AST, fmtFlags), QueryID: queryID, QueryTags: tags, } From c5ea6a2d59cc7b63d3499c4c0a1d37e9d2fa33e8 Mon Sep 17 00:00:00 2001 From: Michael Erickson Date: Fri, 24 Oct 2025 22:41:03 -0700 Subject: [PATCH 2/2] sql: implement EXPLAIN (FINGERPRINT) statement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds support for EXPLAIN (FINGERPRINT), a new EXPLAIN variant that returns statement fingerprints. Statement fingerprints are normalized forms of SQL statements where constants are replaced with underscores, making them useful for query pattern analysis and monitoring. Key features: - Returns a single row with single string column containing statement fingerprint - Respects sql.stats.statement_fingerprint.format_mask cluster setting Implementation details: - Added ExplainFingerprint mode to AST with validation - Implemented fingerprint computation during optbuild phase - Added telemetry tracking for usage - Comprehensive test coverage including edge cases and prepared statements Examples: - EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 123 Returns: "SELECT * FROM t WHERE a = _" Informs: #153633 Release note (sql change): Added EXPLAIN (FINGERPRINT) statement that returns normalized statement fingerprints with constants replaced by underscores. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- pkg/sql/opt/exec/execbuilder/statement.go | 9 + pkg/sql/opt/exec/execbuilder/testdata/explain | 4 +- .../execbuilder/testdata/explain_fingerprint | 249 ++++++++++++++++++ .../execbuilder/tests/local/generated_test.go | 7 + pkg/sql/opt/ops/statement.opt | 3 + pkg/sql/opt/optbuilder/explain.go | 28 +- pkg/sql/sem/tree/explain.go | 27 +- pkg/sql/sqltelemetry/planning.go | 4 + 8 files changed, 318 insertions(+), 13 deletions(-) create mode 100644 pkg/sql/opt/exec/execbuilder/testdata/explain_fingerprint diff --git a/pkg/sql/opt/exec/execbuilder/statement.go b/pkg/sql/opt/exec/execbuilder/statement.go index ae3f83768aa6..9837caed04be 100644 --- a/pkg/sql/opt/exec/execbuilder/statement.go +++ b/pkg/sql/opt/exec/execbuilder/statement.go @@ -168,6 +168,15 @@ func (b *Builder) buildExplain( return b.buildExplainOpt(explainExpr) } + if explainExpr.Options.Mode == tree.ExplainFingerprint { + var ep execPlan + ep.root, err = b.factory.ConstructExplainOpt(explainExpr.Fingerprint, exec.ExplainEnvData{}) + if err != nil { + return execPlan{}, colOrdMap{}, err + } + return ep, b.outputColsFromList(explainExpr.ColList), nil + } + var ep execPlan ep.root, err = b.factory.ConstructExplain( &explainExpr.Options, diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain b/pkg/sql/opt/exec/execbuilder/testdata/explain index 0bea47759527..0748d62e0351 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/explain +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain @@ -2347,7 +2347,7 @@ quality of service: regular query T EXPLAIN (OPT, MEMO) SELECT * FROM tc JOIN t ON k=a ---- -memo (optimized, ~19KB, required=[presentation: info:14] [distribution: test]) +memo (optimized, ~20KB, required=[presentation: info:14] [distribution: test]) ├── G1: (explain G2 [presentation: a:1,b:2,k:8,v:9] [distribution: test]) │ └── [presentation: info:14] [distribution: test] │ ├── best: (explain G2="[presentation: a:1,b:2,k:8,v:9] [distribution: test]" [presentation: a:1,b:2,k:8,v:9] [distribution: test]) @@ -2428,7 +2428,7 @@ TABLE t ├── crdb_internal_origin_timestamp decimal [hidden] [system] └── PRIMARY INDEX t_pkey └── k int not null -memo (optimized, ~19KB, required=[presentation: info:14] [distribution: test]) +memo (optimized, ~20KB, required=[presentation: info:14] [distribution: test]) ├── G1: (explain G2 [presentation: a:1,b:2,k:8,v:9] [distribution: test]) │ └── [presentation: info:14] [distribution: test] │ ├── best: (explain G2="[presentation: a:1,b:2,k:8,v:9] [distribution: test]" [presentation: a:1,b:2,k:8,v:9] [distribution: test]) diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain_fingerprint b/pkg/sql/opt/exec/execbuilder/testdata/explain_fingerprint new file mode 100644 index 000000000000..74b5ff45116a --- /dev/null +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain_fingerprint @@ -0,0 +1,249 @@ +# LogicTest: local + +statement ok +CREATE TABLE t (a INT PRIMARY KEY, b STRING, c FLOAT) + +statement ok +CREATE TABLE u (x INT, y STRING, INDEX idx_y (y)) + +# Basic EXPLAIN (FINGERPRINT) test +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t +---- +SELECT * FROM t + +# Test with constants - they should be replaced with placeholders +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 123 +---- +SELECT * FROM t WHERE a = _ + +# Test with string constants +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE b = 'hello' +---- +SELECT * FROM t WHERE b = _ + +# Test with multiple constants +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 1 AND b = 'test' AND c = 3.14 +---- +SELECT * FROM t WHERE ((a = _) AND (b = _)) AND (c = _) + +# Test with JOIN +query T +EXPLAIN (FINGERPRINT) SELECT t.a, u.y FROM t JOIN u ON t.a = u.x +---- +SELECT t.a, u.y FROM t JOIN u ON t.a = u.x + +# Test with GROUP BY and aggregation +query T +EXPLAIN (FINGERPRINT) SELECT b, count(*) FROM t GROUP BY b +---- +SELECT b, count(*) FROM t GROUP BY b + +# Test with ORDER BY +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t ORDER BY a DESC +---- +SELECT * FROM t ORDER BY a DESC + +# Test with LIMIT +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t LIMIT 10 +---- +SELECT * FROM t LIMIT _ + +# Test with subquery +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a IN (SELECT x FROM u WHERE y = 'test') +---- +SELECT * FROM t WHERE a IN (SELECT x FROM u WHERE y = _) + +# Test with INSERT +query T +EXPLAIN (FINGERPRINT) INSERT INTO t VALUES (1, 'foo', 2.5) +---- +INSERT INTO t VALUES (_, __more__) + +# Test with UPDATE +query T +EXPLAIN (FINGERPRINT) UPDATE t SET b = 'updated' WHERE a = 100 +---- +UPDATE t SET b = _ WHERE a = _ + +# Test with DELETE +query T +EXPLAIN (FINGERPRINT) DELETE FROM t WHERE a > 50 +---- +DELETE FROM t WHERE a > _ + +# Test with index hint +query T +EXPLAIN (FINGERPRINT) SELECT * FROM u@idx_y WHERE y = 'search' +---- +SELECT * FROM u@idx_y WHERE y = _ + +# Test with UNION +query T +EXPLAIN (FINGERPRINT) SELECT a FROM t UNION SELECT x FROM u +---- +SELECT a FROM t UNION SELECT x FROM u + +# Test with CTE +query T +EXPLAIN (FINGERPRINT) WITH cte AS (SELECT * FROM t WHERE a < 10) SELECT * FROM cte +---- +WITH cte AS (SELECT * FROM t WHERE a < _) SELECT * FROM cte + +# Test prepared statement functionality +statement ok +PREPARE stmt1 AS SELECT * FROM t WHERE a = $1 + +query T +EXPLAIN (FINGERPRINT) EXECUTE stmt1 +---- +EXECUTE stmt1 + +statement ok +PREPARE stmt2 AS INSERT INTO t VALUES ($1, $2, $3) + +query T +EXPLAIN (FINGERPRINT) EXECUTE stmt2 +---- +EXECUTE stmt2 + +statement ok +PREPARE stmt3 AS SELECT t.a, u.y FROM t JOIN u ON t.a = u.x WHERE t.b = $1 + +query T +EXPLAIN (FINGERPRINT) EXECUTE stmt3 +---- +EXECUTE stmt3 + +# Test prepare of EXPLAIN (FINGERPRINT) +statement ok +PREPARE stmt4 AS EXPLAIN (FINGERPRINT) SELECT 1 + +query T +EXECUTE stmt4 +---- +SELECT _ + +# Test that invalid combinations are rejected +statement error EXPLAIN \(FINGERPRINT\) cannot be used with VERBOSE +EXPLAIN (FINGERPRINT, VERBOSE) SELECT * FROM t + +statement error EXPLAIN \(FINGERPRINT\) cannot be used with TYPES +EXPLAIN (FINGERPRINT, TYPES) SELECT * FROM t + +statement error pq: at or near "EOF": syntax error: the JSON flag can only be used with DISTSQL +EXPLAIN (FINGERPRINT, JSON) SELECT * FROM t + +# Test that regular EXPLAIN EXECUTE is still rejected while FINGERPRINT works +statement error EXPLAIN EXECUTE is not supported; use EXPLAIN ANALYZE +EXPLAIN EXECUTE stmt1 + +# But EXPLAIN (FINGERPRINT) EXECUTE should work (already tested above) + +# Test with more complex queries +query T +EXPLAIN (FINGERPRINT) +SELECT t.a, t.b, count(u.x) as cnt +FROM t +LEFT JOIN u ON t.a = u.x +WHERE t.c > 1.0 AND (t.b LIKE 'prefix%' OR t.b IS NULL) +GROUP BY t.a, t.b +HAVING count(u.x) > 5 +ORDER BY cnt DESC, t.a ASC +LIMIT 20 OFFSET 10 +---- +SELECT t.a, t.b, count(u.x) AS cnt FROM t LEFT JOIN u ON t.a = u.x WHERE (t.c > _) AND ((t.b LIKE _) OR (t.b IS NULL)) GROUP BY t.a, t.b HAVING count(u.x) > _ ORDER BY cnt DESC, t.a ASC LIMIT _ OFFSET _ + +# Test with window functions +query T +EXPLAIN (FINGERPRINT) +SELECT a, b, row_number() OVER (PARTITION BY b ORDER BY a) as rn +FROM t +---- +SELECT a, b, row_number() OVER (PARTITION BY b ORDER BY a) AS rn FROM t + +query T +EXPLAIN (FINGERPRINT) +SELECT + department_id, + employee_id, + salary, + avg(salary) OVER ( + PARTITION BY department_id + ORDER BY employee_id + ROWS BETWEEN 2 PRECEDING AND CURRENT ROW + ) AS moving_avg_salary, + round(avg(salary) OVER ( + PARTITION BY department_id + ), 2) AS dept_avg_salary, + 1.05 * salary AS adjusted_salary, + salary + 1000 AS bonus_salary +FROM employees +---- +SELECT department_id, employee_id, salary, avg(salary) OVER (PARTITION BY department_id ORDER BY employee_id ROWS BETWEEN _ PRECEDING AND CURRENT ROW) AS moving_avg_salary, round(avg(salary) OVER (PARTITION BY department_id), _) AS dept_avg_salary, _ * salary AS adjusted_salary, salary + _ AS bonus_salary FROM employees + + +# Test with CASE expression +query T +EXPLAIN (FINGERPRINT) +SELECT a, + CASE + WHEN a > 100 THEN 'high' + WHEN a > 50 THEN 'medium' + ELSE 'low' + END as category +FROM t +---- +SELECT a, CASE WHEN a > _ THEN _ WHEN a > _ THEN _ ELSE _ END AS category FROM t + +# Test that different constant values produce the same fingerprint +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 999 +---- +SELECT * FROM t WHERE a = _ + +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 111 +---- +SELECT * FROM t WHERE a = _ + +# Test with cluster setting sql.stats.statement_fingerprint.format_mask set to 0 +statement ok +SET CLUSTER SETTING sql.stats.statement_fingerprint.format_mask = 0 + +# With format_mask=0, long lists should not be collapsed to __more__ +query T +EXPLAIN (FINGERPRINT) INSERT INTO t VALUES (1, 'a', 1.1), (2, 'b', 2.2), (3, 'c', 3.3), (4, 'd', 4.4), (5, 'e', 5.5) +---- +INSERT INTO t VALUES (_, '_', __more1_10__), (__more1_10__) + +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) +---- +SELECT * FROM t WHERE a IN (_, _, __more1_10__) + +query T +EXPLAIN (FINGERPRINT) UPDATE t SET b = 'updated' WHERE a IN (10, 20, 30, 40, 50, 60, 70, 80, 90, 100) +---- +UPDATE t SET b = '_' WHERE a IN (_, _, __more1_10__) + +# Reset cluster setting to default +statement ok +RESET CLUSTER SETTING sql.stats.statement_fingerprint.format_mask + +# Verify default behavior is restored (long lists should be collapsed) +query T +EXPLAIN (FINGERPRINT) INSERT INTO t VALUES (1, 'a', 1.1), (2, 'b', 2.2), (3, 'c', 3.3), (4, 'd', 4.4), (5, 'e', 5.5) +---- +INSERT INTO t VALUES (_, __more__), (__more__) + +query T +EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) +---- +SELECT * FROM t WHERE a IN (_, __more__) diff --git a/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go b/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go index f6adc124d6e9..12f7f8b6cfcc 100644 --- a/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go +++ b/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go @@ -218,6 +218,13 @@ func TestExecBuild_explain_env( runExecBuildLogicTest(t, "explain_env") } +func TestExecBuild_explain_fingerprint( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runExecBuildLogicTest(t, "explain_fingerprint") +} + func TestExecBuild_explain_gist( t *testing.T, ) { diff --git a/pkg/sql/opt/ops/statement.opt b/pkg/sql/opt/ops/statement.opt index fa3d53dd1e3f..5c3edeed74d7 100644 --- a/pkg/sql/opt/ops/statement.opt +++ b/pkg/sql/opt/ops/statement.opt @@ -129,6 +129,9 @@ define ExplainPrivate { # StmtType stores the type of the statement return we are explaining. StmtType StatementReturnType + + # Fingerprint stores the statement fingerprint for EXPLAIN (FINGERPRINT). + Fingerprint string } # ShowTraceForSession returns the current session traces. diff --git a/pkg/sql/opt/optbuilder/explain.go b/pkg/sql/opt/optbuilder/explain.go index 84d27ffe0c1f..1e81a1982e16 100644 --- a/pkg/sql/opt/optbuilder/explain.go +++ b/pkg/sql/opt/optbuilder/explain.go @@ -18,12 +18,25 @@ import ( func (b *Builder) buildExplain(explain *tree.Explain, inScope *scope) (outScope *scope) { if _, ok := explain.Statement.(*tree.Execute); ok { - panic(pgerror.New( - pgcode.FeatureNotSupported, "EXPLAIN EXECUTE is not supported; use EXPLAIN ANALYZE", - )) + // EXPLAIN (FINGERPRINT) EXECUTE is supported, but other modes are not. + if explain.Mode != tree.ExplainFingerprint { + panic(pgerror.New( + pgcode.FeatureNotSupported, "EXPLAIN EXECUTE is not supported; use EXPLAIN ANALYZE", + )) + } } - stmtScope := b.buildStmtAtRoot(explain.Statement, nil /* desiredTypes */) + var stmtScope *scope + if explain.Mode == tree.ExplainFingerprint { + // We don't actually need to build the statement for EXPLAIN (FINGERPRINT), + // so don't. This allows someone to run EXPLAIN (FINGERPRINT) for statements + // they don't have permission to execute, for example. Instead, we create a + // dummy empty VALUES clause as input. + emptyValues := &tree.LiteralValuesClause{Rows: tree.RawRows{}} + stmtScope = b.buildLiteralValuesClause(emptyValues, nil /* desiredTypes */, inScope) + } else { + stmtScope = b.buildStmtAtRoot(explain.Statement, nil /* desiredTypes */) + } outScope = inScope.push() @@ -56,6 +69,9 @@ func (b *Builder) buildExplain(explain *tree.Explain, inScope *scope) (outScope case tree.ExplainGist: telemetry.Inc(sqltelemetry.ExplainGist) + case tree.ExplainFingerprint: + telemetry.Inc(sqltelemetry.ExplainFingerprint) + default: panic(errors.Errorf("EXPLAIN mode %s not supported", explain.Mode)) } @@ -68,6 +84,10 @@ func (b *Builder) buildExplain(explain *tree.Explain, inScope *scope) (outScope Props: stmtScope.makePhysicalProps(), StmtType: explain.Statement.StatementReturnType(), } + if explain.Mode == tree.ExplainFingerprint { + stmtFingerprintFmtMask := tree.FmtFlags(tree.QueryFormattingForFingerprintsMask.Get(&b.evalCtx.Settings.SV)) + private.Fingerprint = tree.FormatStatementHideConstants(explain.Statement, stmtFingerprintFmtMask) + } outScope.expr = b.factory.ConstructExplain(input, &private) return outScope } diff --git a/pkg/sql/sem/tree/explain.go b/pkg/sql/sem/tree/explain.go index d4dcaaa1ee92..11655aface72 100644 --- a/pkg/sql/sem/tree/explain.go +++ b/pkg/sql/sem/tree/explain.go @@ -70,17 +70,21 @@ const ( // ExplainGist generates a plan "gist". ExplainGist + // ExplainFingerprint returns the statement fingerprint. + ExplainFingerprint + numExplainModes = iota ) var explainModeStrings = [...]string{ - ExplainPlan: "PLAN", - ExplainDistSQL: "DISTSQL", - ExplainOpt: "OPT", - ExplainVec: "VEC", - ExplainDebug: "DEBUG", - ExplainDDL: "DDL", - ExplainGist: "GIST", + ExplainPlan: "PLAN", + ExplainDistSQL: "DISTSQL", + ExplainOpt: "OPT", + ExplainVec: "VEC", + ExplainDebug: "DEBUG", + ExplainDDL: "DDL", + ExplainGist: "GIST", + ExplainFingerprint: "FINGERPRINT", } var explainModeStringMap = func() map[string]ExplainMode { @@ -304,6 +308,15 @@ func MakeExplain(options []string, stmt Statement) (Statement, error) { }, nil } + if opts.Mode == ExplainFingerprint { + // FINGERPRINT mode doesn't support any flags + for f := ExplainFlag(1); f <= numExplainFlags; f++ { + if opts.Flags[f] { + return nil, pgerror.Newf(pgcode.Syntax, "EXPLAIN (FINGERPRINT) cannot be used with %s", f) + } + } + } + if opts.Mode == ExplainDebug { return nil, pgerror.Newf(pgcode.Syntax, "DEBUG flag can only be used with EXPLAIN ANALYZE") } diff --git a/pkg/sql/sqltelemetry/planning.go b/pkg/sql/sqltelemetry/planning.go index 8bb7ffdac882..b2d48a98e4b6 100644 --- a/pkg/sql/sqltelemetry/planning.go +++ b/pkg/sql/sqltelemetry/planning.go @@ -122,6 +122,10 @@ var ExplainOptVerboseUseCounter = telemetry.GetCounterOnce("sql.plan.explain-opt // EXPLAIN (GIST) is run. var ExplainGist = telemetry.GetCounterOnce("sql.plan.explain-gist") +// ExplainFingerprint is to be incremented whenever +// EXPLAIN (FINGERPRINT) is run. +var ExplainFingerprint = telemetry.GetCounterOnce("sql.plan.explain-fingerprint") + // CreateStatisticsUseCounter is to be incremented whenever a non-automatic // run of CREATE STATISTICS occurs. var CreateStatisticsUseCounter = telemetry.GetCounterOnce("sql.plan.stats.created")