perf: speed up SQL auto-categorization - #5924
Conversation
Signed-off-by: dirk.nilius <dirk.nilius@nc-group.net>
|
Nice find and fix. Confirmed significant speedups myself (160x on a 400 projection model). Looks like all of the categorizations are safe and / or safer (in the case of ORDER BY 2 being NON_BREAKING and now classified as undetermined). Only one additional easy win - the ORDER BY 2 fix won't kick in when ORDER BY sits after a UNION. Since you've already modified that area, worth a patch. Some other nits listed in-line |
… additions Signed-off-by: dirk.nilius <dirk.nilius@nc-group.net>
Hey @mday-io, Thanks for looking into this and your comments. They are all valid. I made appropriate changes. Especially the blind spot when Please take a second look. |
|
@nc-dirknilius hey the style test failed. May you run Make Style again please? |
Hey @mday-io, It failed on files not touched in this PR. This error was merged in from |
|
@nc-dirknilius ah, thanks. Will fix. |
Description
Motivation
SqlModel.is_breaking_changecurrently calls SQLGlot's general-purposediffimplementation to answer a much narrower question: whether a rendered query changed exclusively through addedSELECTprojections.SQLGlot's tree diff computes candidate matchings across both ASTs, introducing quadratic work in its matching phases.
SQLMesh performs this work synchronously for each model during auto-categorization. We encountered the resulting
amplification in a project with more than
600models: a broadly used macro change can require hundreds of models tobe categorized, repeatedly paying the superlinear cost for large rendered queries and causing
sqlmesh planto spendmore than an hour in categorization without completing; the plan was cancelled at that point.
The existing
_additive_projection_changefallback handles cases where SQLGlot emits spuriousMoveorUpdateedits, such as repeated cast types. However, the fallback runs only after the expensive tree diff has completed, so it does not address the performance problem.What changed
This replaces the general tree diff and its fallback with a specialized AST comparison:
SELECTprojection list as an ordered subsequence of the current list.CASTtypes.CTEs,EXISTSqueries,UNIONs, and other nested query structures.The comparison is linear in the traversed AST and projection lists rather than performing general candidate matching across the trees.
Safety and behavior
The specialized comparison preserves SQLMesh's conservative categorization rules:
FROM,WHERE,GROUP BY,ORDER BY,DISTINCT, or other query structure remain undetermined.UDTFs remain undetermined because they can change row cardinality.UDTFis accepted only when its nearestSubqueryancestor is contained within the added projection.GROUP BYorORDER BYuses ordinal references, because inserting a projection can shift the referenced output.None, retaining SQLMesh's conservative fallback behavior.There are no public API or configuration changes.
Performance
Measured on
mainate075200using Python3.13.13and SQLGlot30.8.0on macOS ARM64.Both ASTs were parsed before timing. Each benchmark copied the previous query and added one scalar projection. The measurements time only the existing
sqlglot.diffcall and the specialized comparison.sqlglot.diffThese are single-process wall-clock measurements intended to show the order-of-magnitude difference; parsing and rendering are excluded from both sides.
A representative breaking change on the same large model retained the conservative result while dropping comparison time from approximately
12.1seconds to20milliseconds.Test Plan
Added coverage for:
CASTtypes.UDTFs.UDTFs inside projection subqueries.UDTFs inside derived-table projections.CTEprojection additions.EXISTS.UNIONbranches.ORDER BYsafeguards.Validation performed:
make styleruff,ruff-format,mypy, and migration validation.python -m pytest tests/core/test_snapshot.py -k 'categorize_change_sql' -q8passed.500generated projection and non-projection mutations against the current implementation.make fast-test2,576tests successfully, skipped4,and reported one order-dependent error in
tests/dbt/cli/test_selectors.py::test_select_by_dbt_names[dbt_select6-expected6].macrosremains in the samexdistworker and
dbt-commonattempts to inspect it. No changed code is present in the traceback.14test_select_by_dbt_namescases serially passes.2,563passed and4skipped.make fast-testalso pass when invoked separately:3isolated,1registry_isolation, and159dialect_isolatedtests.Checklist
make styleand fixed any issues.make fast-test).git commit -s) per the DCO.