Skip to content

Commit c5ea6a2

Browse files
michae2claude
andcommitted
sql: implement EXPLAIN (FINGERPRINT) statement
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 <[email protected]>
1 parent af7c77c commit c5ea6a2

File tree

8 files changed

+318
-13
lines changed

8 files changed

+318
-13
lines changed

pkg/sql/opt/exec/execbuilder/statement.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ func (b *Builder) buildExplain(
168168
return b.buildExplainOpt(explainExpr)
169169
}
170170

171+
if explainExpr.Options.Mode == tree.ExplainFingerprint {
172+
var ep execPlan
173+
ep.root, err = b.factory.ConstructExplainOpt(explainExpr.Fingerprint, exec.ExplainEnvData{})
174+
if err != nil {
175+
return execPlan{}, colOrdMap{}, err
176+
}
177+
return ep, b.outputColsFromList(explainExpr.ColList), nil
178+
}
179+
171180
var ep execPlan
172181
ep.root, err = b.factory.ConstructExplain(
173182
&explainExpr.Options,

pkg/sql/opt/exec/execbuilder/testdata/explain

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2347,7 +2347,7 @@ quality of service: regular
23472347
query T
23482348
EXPLAIN (OPT, MEMO) SELECT * FROM tc JOIN t ON k=a
23492349
----
2350-
memo (optimized, ~19KB, required=[presentation: info:14] [distribution: test])
2350+
memo (optimized, ~20KB, required=[presentation: info:14] [distribution: test])
23512351
├── G1: (explain G2 [presentation: a:1,b:2,k:8,v:9] [distribution: test])
23522352
│ └── [presentation: info:14] [distribution: test]
23532353
│ ├── 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
24282428
├── crdb_internal_origin_timestamp decimal [hidden] [system]
24292429
└── PRIMARY INDEX t_pkey
24302430
└── k int not null
2431-
memo (optimized, ~19KB, required=[presentation: info:14] [distribution: test])
2431+
memo (optimized, ~20KB, required=[presentation: info:14] [distribution: test])
24322432
├── G1: (explain G2 [presentation: a:1,b:2,k:8,v:9] [distribution: test])
24332433
│ └── [presentation: info:14] [distribution: test]
24342434
│ ├── 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])
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
# LogicTest: local
2+
3+
statement ok
4+
CREATE TABLE t (a INT PRIMARY KEY, b STRING, c FLOAT)
5+
6+
statement ok
7+
CREATE TABLE u (x INT, y STRING, INDEX idx_y (y))
8+
9+
# Basic EXPLAIN (FINGERPRINT) test
10+
query T
11+
EXPLAIN (FINGERPRINT) SELECT * FROM t
12+
----
13+
SELECT * FROM t
14+
15+
# Test with constants - they should be replaced with placeholders
16+
query T
17+
EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 123
18+
----
19+
SELECT * FROM t WHERE a = _
20+
21+
# Test with string constants
22+
query T
23+
EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE b = 'hello'
24+
----
25+
SELECT * FROM t WHERE b = _
26+
27+
# Test with multiple constants
28+
query T
29+
EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 1 AND b = 'test' AND c = 3.14
30+
----
31+
SELECT * FROM t WHERE ((a = _) AND (b = _)) AND (c = _)
32+
33+
# Test with JOIN
34+
query T
35+
EXPLAIN (FINGERPRINT) SELECT t.a, u.y FROM t JOIN u ON t.a = u.x
36+
----
37+
SELECT t.a, u.y FROM t JOIN u ON t.a = u.x
38+
39+
# Test with GROUP BY and aggregation
40+
query T
41+
EXPLAIN (FINGERPRINT) SELECT b, count(*) FROM t GROUP BY b
42+
----
43+
SELECT b, count(*) FROM t GROUP BY b
44+
45+
# Test with ORDER BY
46+
query T
47+
EXPLAIN (FINGERPRINT) SELECT * FROM t ORDER BY a DESC
48+
----
49+
SELECT * FROM t ORDER BY a DESC
50+
51+
# Test with LIMIT
52+
query T
53+
EXPLAIN (FINGERPRINT) SELECT * FROM t LIMIT 10
54+
----
55+
SELECT * FROM t LIMIT _
56+
57+
# Test with subquery
58+
query T
59+
EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a IN (SELECT x FROM u WHERE y = 'test')
60+
----
61+
SELECT * FROM t WHERE a IN (SELECT x FROM u WHERE y = _)
62+
63+
# Test with INSERT
64+
query T
65+
EXPLAIN (FINGERPRINT) INSERT INTO t VALUES (1, 'foo', 2.5)
66+
----
67+
INSERT INTO t VALUES (_, __more__)
68+
69+
# Test with UPDATE
70+
query T
71+
EXPLAIN (FINGERPRINT) UPDATE t SET b = 'updated' WHERE a = 100
72+
----
73+
UPDATE t SET b = _ WHERE a = _
74+
75+
# Test with DELETE
76+
query T
77+
EXPLAIN (FINGERPRINT) DELETE FROM t WHERE a > 50
78+
----
79+
DELETE FROM t WHERE a > _
80+
81+
# Test with index hint
82+
query T
83+
EXPLAIN (FINGERPRINT) SELECT * FROM u@idx_y WHERE y = 'search'
84+
----
85+
SELECT * FROM u@idx_y WHERE y = _
86+
87+
# Test with UNION
88+
query T
89+
EXPLAIN (FINGERPRINT) SELECT a FROM t UNION SELECT x FROM u
90+
----
91+
SELECT a FROM t UNION SELECT x FROM u
92+
93+
# Test with CTE
94+
query T
95+
EXPLAIN (FINGERPRINT) WITH cte AS (SELECT * FROM t WHERE a < 10) SELECT * FROM cte
96+
----
97+
WITH cte AS (SELECT * FROM t WHERE a < _) SELECT * FROM cte
98+
99+
# Test prepared statement functionality
100+
statement ok
101+
PREPARE stmt1 AS SELECT * FROM t WHERE a = $1
102+
103+
query T
104+
EXPLAIN (FINGERPRINT) EXECUTE stmt1
105+
----
106+
EXECUTE stmt1
107+
108+
statement ok
109+
PREPARE stmt2 AS INSERT INTO t VALUES ($1, $2, $3)
110+
111+
query T
112+
EXPLAIN (FINGERPRINT) EXECUTE stmt2
113+
----
114+
EXECUTE stmt2
115+
116+
statement ok
117+
PREPARE stmt3 AS SELECT t.a, u.y FROM t JOIN u ON t.a = u.x WHERE t.b = $1
118+
119+
query T
120+
EXPLAIN (FINGERPRINT) EXECUTE stmt3
121+
----
122+
EXECUTE stmt3
123+
124+
# Test prepare of EXPLAIN (FINGERPRINT)
125+
statement ok
126+
PREPARE stmt4 AS EXPLAIN (FINGERPRINT) SELECT 1
127+
128+
query T
129+
EXECUTE stmt4
130+
----
131+
SELECT _
132+
133+
# Test that invalid combinations are rejected
134+
statement error EXPLAIN \(FINGERPRINT\) cannot be used with VERBOSE
135+
EXPLAIN (FINGERPRINT, VERBOSE) SELECT * FROM t
136+
137+
statement error EXPLAIN \(FINGERPRINT\) cannot be used with TYPES
138+
EXPLAIN (FINGERPRINT, TYPES) SELECT * FROM t
139+
140+
statement error pq: at or near "EOF": syntax error: the JSON flag can only be used with DISTSQL
141+
EXPLAIN (FINGERPRINT, JSON) SELECT * FROM t
142+
143+
# Test that regular EXPLAIN EXECUTE is still rejected while FINGERPRINT works
144+
statement error EXPLAIN EXECUTE is not supported; use EXPLAIN ANALYZE
145+
EXPLAIN EXECUTE stmt1
146+
147+
# But EXPLAIN (FINGERPRINT) EXECUTE should work (already tested above)
148+
149+
# Test with more complex queries
150+
query T
151+
EXPLAIN (FINGERPRINT)
152+
SELECT t.a, t.b, count(u.x) as cnt
153+
FROM t
154+
LEFT JOIN u ON t.a = u.x
155+
WHERE t.c > 1.0 AND (t.b LIKE 'prefix%' OR t.b IS NULL)
156+
GROUP BY t.a, t.b
157+
HAVING count(u.x) > 5
158+
ORDER BY cnt DESC, t.a ASC
159+
LIMIT 20 OFFSET 10
160+
----
161+
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 _
162+
163+
# Test with window functions
164+
query T
165+
EXPLAIN (FINGERPRINT)
166+
SELECT a, b, row_number() OVER (PARTITION BY b ORDER BY a) as rn
167+
FROM t
168+
----
169+
SELECT a, b, row_number() OVER (PARTITION BY b ORDER BY a) AS rn FROM t
170+
171+
query T
172+
EXPLAIN (FINGERPRINT)
173+
SELECT
174+
department_id,
175+
employee_id,
176+
salary,
177+
avg(salary) OVER (
178+
PARTITION BY department_id
179+
ORDER BY employee_id
180+
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
181+
) AS moving_avg_salary,
182+
round(avg(salary) OVER (
183+
PARTITION BY department_id
184+
), 2) AS dept_avg_salary,
185+
1.05 * salary AS adjusted_salary,
186+
salary + 1000 AS bonus_salary
187+
FROM employees
188+
----
189+
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
190+
191+
192+
# Test with CASE expression
193+
query T
194+
EXPLAIN (FINGERPRINT)
195+
SELECT a,
196+
CASE
197+
WHEN a > 100 THEN 'high'
198+
WHEN a > 50 THEN 'medium'
199+
ELSE 'low'
200+
END as category
201+
FROM t
202+
----
203+
SELECT a, CASE WHEN a > _ THEN _ WHEN a > _ THEN _ ELSE _ END AS category FROM t
204+
205+
# Test that different constant values produce the same fingerprint
206+
query T
207+
EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 999
208+
----
209+
SELECT * FROM t WHERE a = _
210+
211+
query T
212+
EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a = 111
213+
----
214+
SELECT * FROM t WHERE a = _
215+
216+
# Test with cluster setting sql.stats.statement_fingerprint.format_mask set to 0
217+
statement ok
218+
SET CLUSTER SETTING sql.stats.statement_fingerprint.format_mask = 0
219+
220+
# With format_mask=0, long lists should not be collapsed to __more__
221+
query T
222+
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)
223+
----
224+
INSERT INTO t VALUES (_, '_', __more1_10__), (__more1_10__)
225+
226+
query T
227+
EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
228+
----
229+
SELECT * FROM t WHERE a IN (_, _, __more1_10__)
230+
231+
query T
232+
EXPLAIN (FINGERPRINT) UPDATE t SET b = 'updated' WHERE a IN (10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
233+
----
234+
UPDATE t SET b = '_' WHERE a IN (_, _, __more1_10__)
235+
236+
# Reset cluster setting to default
237+
statement ok
238+
RESET CLUSTER SETTING sql.stats.statement_fingerprint.format_mask
239+
240+
# Verify default behavior is restored (long lists should be collapsed)
241+
query T
242+
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)
243+
----
244+
INSERT INTO t VALUES (_, __more__), (__more__)
245+
246+
query T
247+
EXPLAIN (FINGERPRINT) SELECT * FROM t WHERE a IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
248+
----
249+
SELECT * FROM t WHERE a IN (_, __more__)

pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/sql/opt/ops/statement.opt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ define ExplainPrivate {
129129

130130
# StmtType stores the type of the statement return we are explaining.
131131
StmtType StatementReturnType
132+
133+
# Fingerprint stores the statement fingerprint for EXPLAIN (FINGERPRINT).
134+
Fingerprint string
132135
}
133136

134137
# ShowTraceForSession returns the current session traces.

pkg/sql/opt/optbuilder/explain.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,25 @@ import (
1818

1919
func (b *Builder) buildExplain(explain *tree.Explain, inScope *scope) (outScope *scope) {
2020
if _, ok := explain.Statement.(*tree.Execute); ok {
21-
panic(pgerror.New(
22-
pgcode.FeatureNotSupported, "EXPLAIN EXECUTE is not supported; use EXPLAIN ANALYZE",
23-
))
21+
// EXPLAIN (FINGERPRINT) EXECUTE is supported, but other modes are not.
22+
if explain.Mode != tree.ExplainFingerprint {
23+
panic(pgerror.New(
24+
pgcode.FeatureNotSupported, "EXPLAIN EXECUTE is not supported; use EXPLAIN ANALYZE",
25+
))
26+
}
2427
}
2528

26-
stmtScope := b.buildStmtAtRoot(explain.Statement, nil /* desiredTypes */)
29+
var stmtScope *scope
30+
if explain.Mode == tree.ExplainFingerprint {
31+
// We don't actually need to build the statement for EXPLAIN (FINGERPRINT),
32+
// so don't. This allows someone to run EXPLAIN (FINGERPRINT) for statements
33+
// they don't have permission to execute, for example. Instead, we create a
34+
// dummy empty VALUES clause as input.
35+
emptyValues := &tree.LiteralValuesClause{Rows: tree.RawRows{}}
36+
stmtScope = b.buildLiteralValuesClause(emptyValues, nil /* desiredTypes */, inScope)
37+
} else {
38+
stmtScope = b.buildStmtAtRoot(explain.Statement, nil /* desiredTypes */)
39+
}
2740

2841
outScope = inScope.push()
2942

@@ -56,6 +69,9 @@ func (b *Builder) buildExplain(explain *tree.Explain, inScope *scope) (outScope
5669
case tree.ExplainGist:
5770
telemetry.Inc(sqltelemetry.ExplainGist)
5871

72+
case tree.ExplainFingerprint:
73+
telemetry.Inc(sqltelemetry.ExplainFingerprint)
74+
5975
default:
6076
panic(errors.Errorf("EXPLAIN mode %s not supported", explain.Mode))
6177
}
@@ -68,6 +84,10 @@ func (b *Builder) buildExplain(explain *tree.Explain, inScope *scope) (outScope
6884
Props: stmtScope.makePhysicalProps(),
6985
StmtType: explain.Statement.StatementReturnType(),
7086
}
87+
if explain.Mode == tree.ExplainFingerprint {
88+
stmtFingerprintFmtMask := tree.FmtFlags(tree.QueryFormattingForFingerprintsMask.Get(&b.evalCtx.Settings.SV))
89+
private.Fingerprint = tree.FormatStatementHideConstants(explain.Statement, stmtFingerprintFmtMask)
90+
}
7191
outScope.expr = b.factory.ConstructExplain(input, &private)
7292
return outScope
7393
}

0 commit comments

Comments
 (0)