Documentation
¶
Index ¶
- Constants
- func CostKey(sql string) (string, error)
- func Fingerprint(sql string) (string, error)
- func Normalize(sql string) (string, error)
- func Prettify(sql string) (string, error)
- func WalkNodes(n *pg_query.Node, visit func(*pg_query.Node))
- type Cluster
- type DynamicKeyObservation
- type ParamAttribution
- type Query
- type SortProfile
- type SortVariant
- type TableRef
Constants ¶
const CostKeyPrefix = "qsort1:"
CostKeyPrefix marks a sort-insensitive cost key. Deliberately not "sha1:" so joining a cost key against a fingerprint column returns empty rather than a false match. Own version digit, independent of GroupingVersion.
const GroupingVersion = 1
GroupingVersion identifies the clustering and fingerprinting rules. Bump it in any change that moves a Fingerprint value or moves a query between clusters.
Variables ¶
This section is empty.
Functions ¶
func CostKey ¶ added in v0.3.2
CostKey groups sort permutations of one statement under a single key for cost attribution. Attribution only: never EXPLAIN it, never use it as a Fingerprint — grouped members differ in sort order, which decides index column order and direction.
Same reshaped tree and fingerprint as Fingerprint, with statement-level ORDER BY dropped, so the key is never finer than Fingerprint. Aggregate and window ORDER BY survive (forEachChild does not reach them); LIMIT/ OFFSET are kept (top-N heapsort vs full sort is a real plan difference). The deparsed text is a hashing input only — its $N positions no longer line up with the fine canonical, so it is never returned or planned.
func Fingerprint ¶
func Prettify ¶ added in v0.4.0
Prettify re-indents a statement for display: line breaks at clause boundaries, indentation by nesting depth. It changes WHITESPACE ONLY. Every token is copied verbatim from the input, so the result parses to the same tree and fingerprints to the same value as what went in; TestPrettifyPreservesFingerprint pins that over the captured corpus.
It is a display transform, never a normalization. The canonical single-line form stays the fingerprint input and the storage format; this is what a human reads.
Built on the PostgreSQL scanner rather than on pattern matching over the text, because only the real lexer knows that the FROM in `WHERE note = 'sent FROM x'` is four letters inside a string constant and not a clause. The same goes for dollar quoting, comments and quoted identifiers containing keywords; a regex formatter corrupts all four, silently.
Types ¶
type Cluster ¶
type Cluster struct {
Fingerprint string `json:"fingerprint"`
Canonical string `json:"canonical"`
Members []Query `json:"members"`
TotalCalls int64 `json:"total_calls"`
TotalExecTimeMs float64 `json:"total_exec_time_ms,omitempty"`
MeanExecTimeMs float64 `json:"mean_exec_time_ms,omitempty"`
Rows int64 `json:"rows,omitempty"`
// Summed over the members, and nil unless EVERY member carried them: a partial
// sum understates the spill, which is the direction that makes "raise work_mem"
// advice wrong.
TempBlksRead *int64 `json:"temp_blks_read,omitempty"`
TempBlksWritten *int64 `json:"temp_blks_written,omitempty"`
Params []ParamAttribution `json:"params,omitempty"`
Owners map[string]string `json:"owners,omitempty"`
RegresqlMeta map[string]string `json:"regresql_meta,omitempty"`
DynamicTagKeys []DynamicKeyObservation `json:"dynamic_tag_keys,omitempty"`
}
func Group ¶
Group clusters queries by canonical fingerprint. Queries that fail to parse become singleton clusters with empty Fingerprint. Output is sorted by descending TotalExecTimeMs (when any timing is present), otherwise by descending TotalCalls, with Fingerprint as the tiebreaker. Each cluster's Members are sorted by QueryID, then Raw, and Canonical is the normalized form of the first member after that sort.
Fingerprinting already uses all cores; callers should not fan Group out across their own worker pool.
type DynamicKeyObservation ¶ added in v0.3.0
type ParamAttribution ¶
type ParamAttribution struct {
Position int `json:"position"`
Kind string `json:"kind,omitempty"` // "" (column) | "limit" | "offset"
Shape string `json:"shape,omitempty"` // "" (scalar) | "array"
Schema string `json:"schema,omitempty"`
Table string `json:"table,omitempty"`
Column string `json:"column,omitempty"`
// exact | expression | heuristic | none
Confidence string `json:"confidence"`
Note string `json:"note,omitempty"`
}
type Query ¶
type Query struct {
Raw string `json:"raw"`
QueryID int64 `json:"queryid,omitempty"`
Calls int64 `json:"calls,omitempty"`
TotalExecTimeMs float64 `json:"total_exec_time_ms,omitempty"`
// Ignored by Group; a cluster's mean is derived from total/calls.
MeanExecTimeMs float64 `json:"mean_exec_time_ms,omitempty"`
StddevExecTimeMs float64 `json:"stddev_exec_time_ms,omitempty"`
Rows int64 `json:"rows,omitempty"`
// Temp blocks read and written by this statement: the sorts and hashes that
// spilled out of work_mem. Pointers because a caller that did not collect them
// (an older pg_stat_statements read, a fixture) means unknown, and 0 would say
// the statement spilled nothing.
TempBlksRead *int64 `json:"temp_blks_read,omitempty"`
TempBlksWritten *int64 `json:"temp_blks_written,omitempty"`
}
type SortProfile ¶ added in v0.3.2
type SortProfile struct {
CostKey string `json:"cost_key"`
DistinctSorts int `json:"distinct_sorts"`
Variants []SortVariant `json:"variants"`
TotalCalls int64 `json:"total_calls"`
TotalExecTimeMs float64 `json:"total_exec_time_ms,omitempty"`
MeanExecTimeMs float64 `json:"mean_exec_time_ms,omitempty"`
Rows int64 `json:"rows,omitempty"`
}
func SortProfiles ¶ added in v0.3.2
func SortProfiles(clusters []Cluster) []SortProfile
SortProfiles groups clusters by CostKey and reports the distinct orderings in each
type SortVariant ¶ added in v0.3.2
type SortVariant struct {
Fingerprint string `json:"fingerprint"`
SortClauses []string `json:"sort_clauses,omitempty"`
Calls int64 `json:"calls,omitempty"`
TotalExecTimeMs float64 `json:"total_exec_time_ms,omitempty"`
MeanExecTimeMs float64 `json:"mean_exec_time_ms,omitempty"`
Rows int64 `json:"rows,omitempty"`
}
type TableRef ¶ added in v0.4.0
TableRef is one relation a statement references.
Schema is empty when the SQL did not qualify the name, which is the common case and is NOT resolvable here: pg_stat_statements carries no search_path, so `FROM orders` in a multi-tenant database may mean a different relation per session. Callers must treat an unqualified name as a name to match, not as an identity.
func Tables ¶ added in v0.4.0
Tables reports the relations a query references, deduplicated and ordered (schema, name). It is a read-only walk over the parse tree: it does not reshape, normalize or fingerprint, so it cannot move a Fingerprint value and needs no GroupingVersion bump.
Every RangeVar in the tree counts, which is what makes the DML targets come out right: `INSERT INTO orders`, `UPDATE orders`, `DELETE FROM orders` and `MERGE INTO orders` hang the target off the statement's own Relation field, not off a FROM list, and a walker written around FROM clauses reports zero tables for all four.
Scoped to DML (SELECT/INSERT/UPDATE/DELETE/MERGE/VALUES), which is what pg_stat_statements corpora carry. Utility statements are best-effort and draw no line between a relation used and one defined or dropped: `CREATE TABLE t` reports t, while `DROP TABLE t` reports nothing at all (DropStmt names its target as a string list, not a RangeVar).
CTE names are excluded when unqualified: `WITH recent AS (...) SELECT * FROM recent` references one real table, not two. The exclusion is by name across the whole input rather than per lexical scope, so a real table sharing a name with a CTE declared in any other scope (or, for multi-statement input, in any other statement) is dropped too. That is the deliberate direction to be wrong in: a missing cross-link is a gap, an invented one is a false claim about what a query touches.
An empty result is legitimate and does NOT mean the query was unreadable: `SELECT $1`, `VALUES ($1)` and a blank string all reference no relation. Callers that need "could we read this at all" must use the error, not the length.