query-test-tool is structured as a three-layer pipeline:
SQL string → Parser → AST → Scorers → Report
query-test-tool/
├── cmd/query-test-tool/ # CLI entry point
│ └── main.go # Flag parsing, I/O, text/JSON output
├── src/
│ ├── parser/ # SQL parsing and AST traversal
│ │ └── parser.go # Parse(), Walk(), Children()
│ └── scorer/ # Scoring engine
│ ├── scorer.go # ScoreQuery(), Report, types
│ ├── efficiency.go # EfficiencyScorer
│ ├── memory_compute.go # MemoryComputeScorer
│ └── cognitive.go # CognitiveScorer
└── docs/ # Documentation
The parser wraps pg_query_go v5, which embeds PostgreSQL's actual parser via cgo. This means every SQL construct that PostgreSQL accepts is parseable — no grammar maintenance required.
The Parse() function returns a *pg_query.ParseResult containing a protobuf-based AST. The Walk() function provides depth-first traversal with a callback that receives each node and its depth. Children() maps each node type to its child nodes, enabling generic traversal without type-switching at every call site.
The pg_query protobuf AST uses a Node wrapper with a oneof field for each node type. There is no built-in traversal. Our Children() function exhaustively maps the node types we care about to their child references, making Walk() generic.
Each scorer implements the Scorer interface:
type Scorer interface {
Score(tree *pg_query.ParseResult) DimensionScore
}Scorers are independent — they walk the AST separately and produce findings with no shared state. ScoreQuery() runs all three and combines the results into a Report.
Walks the AST looking for anti-patterns that prevent the query optimizer from using indexes or cause full table scans:
- SELECT *: Detected by checking
ResTarget.ValforColumnRefcontainingA_Star. - Missing predicates: Counts
RangeVarnodes inFromClause; flags if ≥2 without aWhereClause. - Correlated subqueries: Checks
SubLinknodes —EXISTSis always flagged,IN/ANY/ALLflagged whenTestexpris present. - Non-sargable predicates: Walks
WhereClauseforFuncCallnodes wrappingColumnRef. Handles PostgreSQL's schema-qualified names (e.g.,pg_catalog.btrimforTRIM()). - DISTINCT as dedup: Flags
DISTINCTwhen the query also containsJoinExpr.
Targets operations that require materializing intermediate result sets:
- Unbounded sort:
SortClausepresent withoutLimitCountorLimitOffset. - GROUP BY fan-out:
GroupClausepresent with aggregate functions in the target list. - Window functions:
FuncCallwith anOverclause. Extra penalty whenPARTITION BYis absent. - Cartesian products:
JoinExprwith noQualsand noUsingClause, or multipleRangeVarin FROM without WHERE.
Adapts cyclomatic complexity to SQL readability:
- Subquery nesting:
SubLinkandRangeSubselectnodes, with penalty multiplied by nesting depth. - Joins: Each
JoinExpradds a flat penalty. - Boolean nesting:
BoolExprnodes at depth > 0 within the WHERE/HAVING clause tree. - CTEs: Each
CommonTableExpradds a flat penalty. - CASE expressions:
CaseExprnodes in the target list. - Set operations:
UNION/INTERSECT/EXCEPTdetected viaSelectStmt.Op.
The CLI handles three input modes (flag, file, stdin), two output formats (text, JSON), and a verbose mode that expands individual findings. It's a thin wrapper around scorer.ScoreQuery().
Embedded weights: Weights are compiled into the binary via //go:embed. No config files at runtime. Run make build/full to regenerate weights from PostgreSQL, or make build to use existing weights.
Independent scorers: Each dimension walks the AST independently. This is slightly less efficient than a single-pass walk but makes each scorer testable in isolation and trivial to add or remove.
Empirically calibrated weights: All weights are derived from paired EXPLAIN ANALYZE comparisons (antipattern query vs control query). The calibration tool supports both random schema generation and custom schema import (-schema-file) so weights reflect your actual workload.
Paired comparison over regression: Instead of OLS regression on absolute costs, weights are derived by directly comparing antipattern queries against control queries on the same schema. This isolates the cost impact of each antipattern from confounding factors.
Custom schema import: The -schema-file flag allows importing business-specific DDL into the calibration pipeline. The imported schema goes through the same mutation and query generation as random schemas, ensuring weights are tuned to real-world structures.
PostgreSQL-only: We chose depth over breadth. pg_query_go gives us complete PostgreSQL grammar support. MySQL support via vitess/sqlparser is achievable with a dialect abstraction layer but was deferred to avoid premature generalization.