Code quality
Prism checks every statement as you type — syntax, unknown columns, aggregation mistakes, and the BigQuery cost traps a dry run won't tell you about. Problems get a squiggle, a hover explanation, and usually a one-keystroke fix.
SELECT * FROM `ace-analytics.warehouse.fact_mot_test` WHERE completed_date >= '2024-01-01' LIMIT 10
sql-best-practices(select-star-large-table)View Problem (Alt+F8) · Quick Fix… (Ctrl+.)
Reading a squiggle
| Underline | Severity | Means |
|---|---|---|
| Red | Error | BigQuery will reject this |
| Orange | Warning | Runs, but costs more or does something you probably didn’t mean |
| Blue | Info | Worth knowing; no action required |
| None | Hint | Shown only in the quick-fix menu when the cursor is on the code |
Hover an underline for the message; every message ends with the rule id in parentheses. ⌥F8 (AltF8 on Windows and Linux) jumps to the next problem. Hint-level rules (the suggest-* family, COUNT(DISTINCT), clustering order, large table scan) draw nothing; the lightbulb in the gutter is the only sign they fired.
Two sources feed the same underlines. Prism’s static analysis runs on every keystroke with no round-trip. The BigQuery dry run runs when you pause; its errors, like a missing required partition filter, also land in the status bar:
Fix with AI hands the error and the query to Prism AI; Explain asks it to explain the message. A syntax error shows the dry-run wording, Syntax error: Unexpected identifier "SELEC" at [1:1], with No quick fixes available.
Quick fixes
Put the cursor on the underlined code and press ⌘. (Ctrl. on Windows and Linux), or click the lightbulb. Enter applies the highlighted fix.
WITH tests AS ( SELECT make, test_result FROM `ace-analytics.warehouse.fact_mot_test` ), vehicles AS ( SELECT make, model FROM `ace-analytics.warehouse.dim_vehicle` ) SELECT make, test_result FROM tests JOIN vehicles USING (make)
The menu always ends with two entries: Disable ’…’ rule, which switches that rule off in settings (not offered for parse errors or rules your organization locks), and Fix with Prism AI, which sends the diagnostics and their suggested fixes to the agent.
Fixes you’ll meet most:
| Rule | Fix inserted |
|---|---|
| SELECT * on Large Table | Expand SELECT * to explicit columns |
| Missing Partition Filter / Missing Cluster Filter | Add column filter — a WHERE condition on the partition or cluster column |
| Missing LIMIT on Large Table | Add LIMIT 1000 (or |> LIMIT 1000 in pipe syntax) |
| Wildcard Table Scan Without Filter | Add _TABLE_SUFFIX filter — today only, last 7 days, last 30 days |
| Large Table Scan | Add TABLESAMPLE SYSTEM (1 PERCENT) — see TABLESAMPLE |
| Undefined Column / Table Alias / CTE | Did you mean ’…’? — nearest name, STRUCT fields marked |
| Ambiguous Column Reference | Qualify with ‘alias.column’ |
| Non-Aggregated Column | Add ‘column’ to GROUP BY · Use GROUP BY ALL |
| Aggregate in WHERE | Change WHERE to HAVING · Move aggregate condition to existing HAVING |
| NULL Comparison with Equals | Replace with IS NULL / IS DISTINCT FROM |
| COUNT(DISTINCT) Performance | Replace with APPROX_COUNT_DISTINCT |
| Cross Join Detected | Use explicit CROSS JOIN · Move condition to ON clause |
| Unused CTE | Remove unused CTE ‘name’ |
| Suggest QUALIFY / LIKE ANY / GROUP BY ALL / UNION BY NAME | Convert to QUALIFY clause · Replace with LIKE ANY · Use GROUP BY ALL · Use UNION BY NAME |
The same menu also lists refactorings that don’t need a diagnostic: expand t.*, extract a subquery to a CTE, inline a CTE, convert CROSS JOIN to INNER JOIN, flip JOIN sides, CASE → COALESCE / NULLIF, convert pipe syntax to standard SQL. Keyword casing and backtick cleanup live on the editor toolbar instead.
Suggest QUALIFY
The subquery + WHERE rn = 1 idiom has a shorter form. The rule fires for ROW_NUMBER(), RANK() and DENSE_RANK():
SELECT * FROM ( SELECT vehicle_id, test_result, ROW_NUMBER() OVER (PARTITION BY vehicle_id ORDER BY completed_date DESC) AS rn FROM `ace-analytics.warehouse.fact_mot_test` ) WHERE rn = 1
SELECT vehicle_id, test_result FROM `ace-analytics.warehouse.fact_mot_test` QUALIFY ROW_NUMBER() OVER (PARTITION BY vehicle_id ORDER BY completed_date DESC) = 1
Suggest LIKE ANY
WHERE make LIKE 'FORD%' OR make LIKE 'VAUXHALL%' OR make LIKE 'MINI%' becomes WHERE make LIKE ANY ('FORD%', 'VAUXHALL%', 'MINI%'). NOT LIKE … AND NOT LIKE … becomes NOT LIKE ANY.
Tuning thresholds and switching rules off
Settings → Code Quality (also listed under Settings). Two sections: Cost Optimization holds the thresholds, Diagnostic Rules the on/off switch for every rule.
The size choices are 100 MB / 1 GB / 10 GB (10 GB / 100 GB / 1 TB for the scan threshold) and Never warn. Sizes are decimal (1 GB = 10⁹ bytes) and compare against the table’s stored size, so a filtered query on a big table still counts. Missing-LIMIT only fires on unclustered tables; CROSS JOIN is not reported for CROSS JOIN UNNEST(...).
The Diagnostic Rules section lists the rules below with a switch each, Enable All / Disable All per group, and an Enable All Rules button with a N rules disabled badge. Severities are fixed. In an organization, an admin can lock rules on (they show an Org policy badge), raise a rule’s severity, and set thresholds; the stricter of yours and the org’s applies.
Every rule, by category
All rules are on by default. The id in brackets is what the hover shows.
Performance
| Rule | Severity | Catches |
|---|---|---|
SELECT * on Large Table select-star-large-table | Warning | SELECT * on a table above the threshold |
Missing Partition Filter missing-partition-filter | Warning | Partitioned table with no filter on the partition column |
Missing Cluster Filter missing-cluster-filter | Warning | Clustered table with no filter on a cluster column |
Missing LIMIT on Large Table missing-limit-large-table | Info | No LIMIT on a large unclustered table |
ORDER BY Without LIMIT order-by-without-limit | Warning | Sorting a large table with no LIMIT, the “Resources exceeded” case |
Wildcard Table Scan Without Filter wildcard-table-scan-without-filter | Warning | table_* with no _TABLE_SUFFIX filter |
Large Table Scan large-table-scan | Hint | Table above the scan threshold, no filter, no sampling |
Join with OR Condition join-with-or-condition | Warning | ON a = b OR c = d |
Cross Join Detected cross-join-detected | Info | Explicit or comma cross join |
COUNT(DISTINCT) Performance count-distinct-performance | Hint | COUNT(DISTINCT x) where an approximation would do |
Clustering Order Suboptimal clustering-order-suboptimal | Hint | WHERE conditions out of clustering-key order |
Code Quality
| Rule | Severity | Catches |
|---|---|---|
Unused CTE unused-cte | Warning | A CTE nothing references |
Duplicate CTE duplicate-cte | Error | Two CTEs with one name |
Duplicate Column Alias duplicate-column-alias | Warning | The same alias twice in a SELECT list |
Suggest QUALIFY Clause suggest-qualify | Hint | Subquery wrapper around a window function |
Suggest GROUP BY ALL suggest-group-by-all | Hint | A GROUP BY that just repeats the non-aggregated columns |
Suggest UNION BY NAME suggest-union-by-name | Hint | UNION arms with the same columns in a different order |
Suggest LIKE ANY suggest-like-any | Hint | Chained LIKE … OR LIKE … on one column |
DRAW line Without ORDER BY ggsql-line-without-order-by | Info | Line chart with no explicit ordering |
ggsql Geom Not Yet Rendered ggsql-geom-phase-not-supported | Info | histogram, smooth, boxplot, density — parsed, not drawn |
Unknown LABEL Key ggsql-label-key-not-rendered | Info | LABEL key other than title, subtitle, caption |
Correctness
| Rule | Severity | Catches |
|---|---|---|
Non-Aggregated Column non-aggregated-column | Error | Column neither grouped nor aggregated |
Aggregate in WHERE aggregate-in-where | Error | WHERE SUM(x) > 0 |
Nested Aggregate nested-aggregate | Error | SUM(COUNT(x)) |
HAVING Without GROUP BY having-without-group-by | Warning | |
SELECT * with GROUP BY select-star-with-group-by | Warning | |
Window Function in WHERE window-function-in-where | Error | |
QUALIFY Without Window Function qualify-without-window-function | Error | |
Non-Boolean WHERE non-boolean-where | Error | WHERE user_id |
NULL Comparison with Equals null-comparison-with-equals | Warning | = NULL, <> NULL |
Missing Join ON missing-join-on | Error | JOIN with no ON |
Missing Join Condition missing-join-condition | Error | JOIN whose condition doesn’t relate the tables |
ORDER BY Not in SELECT order-by-not-in-select | Error | With DISTINCT |
ORDER BY Invalid Position order-by-invalid-position | Error | ORDER BY 0 |
ORDER BY Position Out of Range order-by-position-out-of-range | Error | ORDER BY 5 with four columns |
CASE Type Inconsistency case-type-inconsistency | Warning | Branches of different types |
Conflicting Column Alias conflicting-column-alias | Warning | Alias that shadows a column |
Multi-level aggregation family, 8 rules multi-level-aggregation-* | Error | The forms BigQuery rejects: too deep, empty arguments, GROUPING(), HAVING MAX/MIN, COLLATE keys, ORDER BY / LIMIT / IGNORE NULLS modifiers, PIVOT, privacy clauses |
DRAW Without VISUALIZE ggsql-draw-without-visualize | Error | |
Unknown Geom Type ggsql-unknown-geom | Error | |
Unknown Aesthetic ggsql-unknown-aesthetic | Warning | |
Unresolved Column in VISUALIZE ggsql-unresolved-mapping-column | Error |
Semantic
| Rule | Severity | Catches |
|---|---|---|
Undefined Column undefined-column | Error | Column not in any table in scope |
Undefined Table Alias undefined-table-alias | Error | x.col where x isn’t declared |
Undefined CTE undefined-cte | Error | |
Column Without FROM column-without-from | Error | |
Ambiguous Column Reference ambiguous-column-reference | Warning | Unqualified name present in two joined tables |
Ambiguous Star Expansion ambiguous-star-expansion | Warning | Bare * over a JOIN |
Duplicate Column duplicate-column | Warning | Two result columns with one name |
Duplicate Table Alias duplicate-table-alias | Error | |
USING Column Not Found using-column-not-found | Error | |
Scalar Subquery Multiple Columns scalar-subquery-multiple-columns | Error | |
IN Subquery Multiple Columns in-subquery-multiple-columns | Error | |
String in Numeric Comparison string-in-numeric-comparison | Warning | odometer > '1000' |
Invalid Hierarchical Name invalid-hierarchical-name | Error | Not project.dataset.table |
Parser
All errors: Parse Error parse-error, ON After Comma Cross-Join comma-join-unnest-on-ambiguous, and Struct Path In Graph KEY Columns property-graph-key-struct-path.
What the checks need
- Schema loaded for the tables in FROM; column rules and size thresholds are silent otherwise
- Table sizes come from the schema tree; a table you’ve never expanded has none until the dry run fills it in
- dbt and Dataform files are checked after Jinja / SQLX preprocessing, so
ref()and{{ … }}don’t count as syntax errors