|
2 | 2 |
|
3 | 3 | # Benchmarks |
4 | 4 |
|
5 | | -> Complete compiler performance on projects with thousands of models, SQL tests, and audit attachments. |
| 5 | +> SQLBuild and dbt v2 compiling the same 10,000-model project: cold, unchanged, one-model and 1% edits, with CPU time and peak memory. |
6 | 6 |
|
7 | 7 | Online: https://sqlbuild.com/docs/benchmarks/ |
8 | 8 |
|
9 | 9 | ## Contents |
10 | 10 |
|
11 | | -- Compiler performance |
12 | | -- Rules performance |
| 11 | +- SQLBuild vs dbt v2 with column checking |
| 12 | +- dbt v2 `--dirty` |
| 13 | +- dbt v2 out of the box and with remote sources |
| 14 | +- SQLBuild with every rule |
| 15 | +- Correctness |
| 16 | +- What is compared and what isn't |
| 17 | +- The project |
13 | 18 | - Methodology |
14 | | -- Reproduce the benchmarks |
| 19 | +- Reproduce the benchmark |
| 20 | +- Compiler guards in CI |
15 | 21 |
|
16 | | -SQLBuild compiles a 5,000-model project with nearly 5,000 SQL tests and more than 8,000 audit |
17 | | -attachments from scratch in **19 seconds** in our CI benchmark. An unchanged recompile takes |
18 | | -**13 seconds**. |
| 22 | +These numbers come from one run of the [benchmark workflow](https://github.com/chio-labs/sqlbuild/blob/main/.github/workflows/benchmark-dbt.yml): |
| 23 | +[run 39543189999](https://github.com/chio-labs/sqlbuild/actions/runs/39543189999), October 21, 2026, |
| 24 | +on a GitHub-hosted `ubuntu-24.04` runner with 4 CPUs (AMD EPYC 9V45) and 15 GiB RAM. It compared |
| 25 | +**SQLBuild 0.164.3** with **dbt 2.0.9** (the `dbt` package on PyPI), both installed from PyPI, |
| 26 | +on the same generated 10,000-model DuckDB project. Every cell is the median of three rounds. |
| 27 | + |
| 28 | +## SQLBuild vs dbt v2 with column checking |
| 29 | + |
| 30 | +Both tools here bind every column and type in every model. SQLBuild runs `sqb compile` with its |
| 31 | +defaults: compile with column and type checks, configurable rules off. dbt runs |
| 32 | +`dbt compile --static-analysis strict` with its parse cache turned on (`--partial-parse --partial-load`; |
| 33 | +the cache is opt-in and the command compiles the whole project). |
| 34 | + |
| 35 | +| Scenario | SQLBuild wall | CPU | Peak memory | dbt v2 strict, parse cache wall | CPU | Peak memory | dbt / SQLBuild | |
| 36 | +|---|---:|---:|---:|---:|---:|---:|---:| |
| 37 | +| Cold (no `target/`, no caches) | **15.7 s** | 30.3 s | 3.1 GiB | 64.7 s | 167.0 s | 4.7 GiB | 4.1× | |
| 38 | +| Unchanged | **0.46 s** (replay) | 0.5 s | 0.07 GiB | 43.8 s | 132.2 s | 4.1 GiB | 95× | |
| 39 | +| One model edited | **2.4 s** | 2.5 s | 0.8 GiB | 45.9 s | 137.2 s | 4.1 GiB | 19× | |
| 40 | +| 100 models edited (1%) | **2.4 s** | 2.5 s | 0.8 GiB | 72.9 s | 176.1 s | 6.9 GiB | 30× | |
| 41 | + |
| 42 | +SQLBuild's unchanged run replays the stored result of the previous compile. Its edit runs compile |
| 43 | +only the changed files against the previous compile and report the same result as a full compile. |
| 44 | + |
| 45 | +The gap isn't only replay. With reuse and delta compile turned off, every SQLBuild run is a full |
| 46 | +compile that takes per-model analyses from its cache: |
| 47 | + |
| 48 | +| Scenario | SQLBuild full compile wall | CPU | Peak memory | dbt v2 strict, parse cache wall | dbt / SQLBuild | |
| 49 | +|---|---:|---:|---:|---:|---:| |
| 50 | +| Cold | **14.2 s** | 28.4 s | 2.8 GiB | 64.7 s | 4.6× | |
| 51 | +| Unchanged | **8.6 s** | 12.9 s | 2.8 GiB | 43.8 s | 5.1× | |
| 52 | +| One model edited | **8.0 s** | 12.4 s | 2.8 GiB | 45.9 s | 5.7× | |
| 53 | +| 100 models edited (1%) | **8.1 s** | 12.6 s | 2.8 GiB | 72.9 s | 9.0× | |
| 54 | + |
| 55 | +## dbt v2 `--dirty` |
| 56 | + |
| 57 | +`dbt compile --dirty` compiles only models whose files changed since the last parse-cache run, |
| 58 | +plus all their downstream dependents. It needs the cache, so its cold run is a |
| 59 | +`--partial-parse --partial-load` compile. |
| 60 | + |
| 61 | +| Scenario | SQLBuild | dbt v2 `--dirty`, strict | dbt v2 `--dirty` | |
| 62 | +|---|---:|---:|---:| |
| 63 | +| Cold | **15.7 s** | 63.3 s | 40.1 s | |
| 64 | +| Unchanged | **0.46 s** (replay) | 0.86 s (nothing to do) | 0.80 s (nothing to do) | |
| 65 | +| One model edited | 2.4 s | **1.6 s** (48 models) | **1.5 s** (48 models) | |
| 66 | +| 100 models edited (1%) | **2.4 s** | 60.7 s (2,800 models) | 48.6 s (2,800 models) | |
| 67 | + |
| 68 | +`--dirty` is faster on a single-model edit: strict analysis of the edited model and its 47 |
| 69 | +descendants took 1.6 s. SQLBuild took 2.4 s and reported the result for the whole project, the same |
| 70 | +report a full compile produces; models whose inputs didn't change kept their stored analysis. With |
| 71 | +100 edited files, dbt 2.0.9 logged `[partial-parse] ... exceeds incremental threshold (100), falling |
| 72 | +back to full parse` and compiled the 2,800 selected models after a full parse. |
| 73 | + |
| 74 | +## dbt v2 out of the box and with remote sources |
| 75 | + |
| 76 | +For context, dbt v2 without the parse cache, and strict analysis reading source schemas from the |
| 77 | +warehouse (dbt's default) instead of from their declarations: |
| 78 | + |
| 79 | +| Scenario | dbt v2 default | dbt v2 default, strict | dbt v2 default, strict, sources from warehouse | |
| 80 | +|---|---:|---:|---:| |
| 81 | +| Cold | 36.9 s | 61.5 s | 67.4 s | |
| 82 | +| Unchanged | 45.9 s | 67.2 s | 74.3 s | |
| 83 | +| One model edited | 44.8 s | 67.4 s | 73.9 s | |
| 84 | +| 100 models edited (1%) | 43.4 s | 65.7 s | 73.5 s | |
| 85 | + |
| 86 | +dbt v2's default `compile` doesn't report unknown columns. |
| 87 | + |
| 88 | +## SQLBuild with every rule |
| 89 | + |
| 90 | +The same project with every built-in rule and one custom Python rule turned on. These rules check |
| 91 | +contract, declaration, graph, model, project, SQL and test conventions that dbt compile doesn't |
| 92 | +check. |
| 93 | + |
| 94 | +| Scenario | SQLBuild with rules | CPU | Peak memory | Full compile with per-model caches | |
| 95 | +|---|---:|---:|---:|---:| |
| 96 | +| Cold | 19.5 s | 42.0 s | 3.6 GiB | 18.0 s | |
| 97 | +| Unchanged | 0.46 s (replay) | 0.5 s | 0.07 GiB | 9.9 s | |
| 98 | +| One model edited | 2.9 s | 2.8 s | 0.9 GiB | 9.8 s | |
| 99 | +| 100 models edited (1%) | 3.1 s | 3.0 s | 0.9 GiB | 9.9 s | |
| 100 | + |
| 101 | +Cold, it is still 3.3× faster than dbt v2 strict with its parse cache (64.7 s). |
| 102 | + |
| 103 | +## Correctness |
| 104 | + |
| 105 | +The workflow also runs both tools on three 50-model projects before timing anything: |
| 106 | + |
| 107 | +| Project | `sqb compile` | `dbt compile` | `dbt compile --static-analysis strict` | |
| 108 | +|---|---|---|---| |
| 109 | +| Clean | exit 0 | exit 0 | exit 0 | |
| 110 | +| Reference to a missing model | exit 1, `P001` | exit 1, `dbt1048` | exit 1, `dbt1048` | |
| 111 | +| Column the upstream model doesn't have | exit 1, `B002 Unknown column 'missing_col' in b` | exit 0, nothing reported | exit 1, `dbt0227 No column b.missing_col found` | |
| 112 | + |
| 113 | +Strict analysis must also report the missing column in the parse-cache and `--dirty` modes, and no |
| 114 | +strict run may mention a fallback, login or skipped analysis. Otherwise the workflow fails. SQLBuild |
| 115 | +reports the same errors with rules on and off. |
| 116 | + |
| 117 | +## What is compared and what isn't |
| 118 | + |
| 119 | +- Both tools bind columns and types. dbt does this only with `--static-analysis strict`. |
| 120 | +- SQLBuild compile also plans and binds its 10,000 SQL tests. dbt `compile` excludes unit tests. |
| 121 | +- SQLBuild reports contract, type and nullability problems as errors. dbt strict warns on contract |
| 122 | + type mismatches, and enforced contracts are checked at run time. |
| 123 | +- SQLBuild computes column lineage by default. |
| 124 | +- Sources are declared locally in both: dbt's sources set `schema_origin: local` and every column |
| 125 | + has a `data_type`. The remote-sources column above shows dbt's default. |
| 126 | +- The DuckDB warehouse holds every source and model relation, empty, as after `dbt build`. dbt reads |
| 127 | + it; `sqb compile` is offline and doesn't. |
| 128 | +- The measured dbt is the `dbt` distribution on PyPI. Strict static analysis isn't in its Apache-2.0 |
| 129 | + build. |
| 130 | +- dbt's parse cache and `--dirty` are opt-in. SQLBuild's reuse and delta compile are on by default. |
| 131 | + |
| 132 | +### Why SQLBuild can reuse work by default |
| 133 | + |
| 134 | +SQLBuild records what each compile depended on, then checks all of it before reusing anything: |
| 135 | + |
| 136 | +- **Every input is recorded during the compile itself.** That covers the project's files, the |
| 137 | + command line, the variables passed with `--vars`, every environment variable a template read, |
| 138 | + provider settings files, the Python interpreter and every loaded module, and the SQLBuild build. |
| 139 | + Nothing is inferred afterwards. |
| 140 | +- **Files are checked by type, size, modification time, change time and inode**, and by content |
| 141 | + digest when a recently touched file's stamps move. If anything doesn't match, the stored result |
| 142 | + isn't replayed. |
| 143 | +- **Edits recompile only what they affect.** SQLBuild knows each model's references and the |
| 144 | + columns, types and nullability it reads. A changed model is compiled again, together with every |
| 145 | + model whose inputs changed shape. An edit outside model and SQL test files, such as a macro, |
| 146 | + config or a new file, runs a full compile. That choice is made before any work starts, apart from |
| 147 | + a few edits found part-way through to need a full compile. |
| 148 | +- **The result is the same as a full compile.** A delta compile produces byte-identical output to |
| 149 | + `sqb compile --no-cache`, and a replay prints the previous report again. CI checks this by |
| 150 | + compiling a copy in full after every step of a seeded edit series and comparing the exit code and |
| 151 | + everything printed. |
| 152 | + |
| 153 | +Reuse assumes macros, hooks, providers, adapters and custom rules are deterministic. It has |
| 154 | +[documented limits](cli/compile.md#reusing-an-unchanged-compile), for example on network |
| 155 | +filesystems or with clocks that differ between machines, and `--no-cache` always compiles in full. |
| 156 | + |
| 157 | +## The project |
| 158 | + |
| 159 | +The project comes from the dense generator SQLBuild's compile-performance CI jobs use, with the |
| 160 | +adapter switched to DuckDB. The dbt project is derived from it file by file: the same model names, |
| 161 | +folders, SQL bodies and DAG, with Jinja macros, `_models.yml` contracts, `data_tests` and |
| 162 | +`unit_tests`. |
| 163 | + |
| 164 | +| Resource | Count | |
| 165 | +|---|---:| |
| 166 | +| Models | 10,000 (8 to 472 columns, 31 on average) | |
| 167 | +| Sources | 2,400 | |
| 168 | +| Seeds | 500 | |
| 169 | +| SQL functions | 250 | |
| 170 | +| Macros | 120 | |
| 171 | +| `not_null` checks | 17,000 | |
| 172 | +| SQL tests (dbt unit tests) | 10,000 | |
| 173 | + |
| 174 | +Each edit changes one constant in a model's SQL body, so its columns, types and references stay the |
| 175 | +same. The 1% edit changes 100 models spread evenly across the project. |
19 | 176 |
|
20 | | -These measurements run the complete `sqb compile` command, including project discovery, dependency |
21 | | -resolution, SQL expansion and analysis, contract validation, test and audit preparation, configured |
22 | | -Rules, cache publication, and artifact generation. |
23 | | - |
24 | | -## Compiler performance |
25 | | - |
26 | | -| Compile time | **3k models**<br />2.9k tests · 5.1k audit attachments | **5k models**<br />4.9k tests · 8.4k audit attachments | |
27 | | -|---|---:|---:| |
28 | | -| First compile / cache fill | **12.48s** | **19.01s** | |
29 | | -| Unchanged recompile | **8.79s** | **12.76s** | |
30 | | - |
31 | | -Timings include test and audit preparation, not warehouse execution. |
32 | | - |
33 | | -The 5,000-model compiler process used approximately **1.3 GiB peak resident memory** on its first |
34 | | -compile and **0.7 GiB** on its unchanged recompile. |
35 | | - |
36 | | -### Workload details |
37 | | - |
38 | | -The deterministic generated projects exercise compiler work at scale, with synthetic SQL, |
39 | | -contracts, macros, functions, tests, and audits. |
| 177 | +## Methodology |
40 | 178 |
|
41 | | -| Resource | 3k-model project | 5k-model project | |
42 | | -|---|---:|---:| |
43 | | -| Models | 3,000 | 5,000 | |
44 | | -| Sources | 713 | 1,189 | |
45 | | -| Seeds | 141 | 236 | |
46 | | -| SQL functions | 71 | 118 | |
47 | | -| Python macros | 37 | 61 | |
48 | | -| SQL test cases | 2,945 | 4,908 | |
49 | | -| Audit attachments | 5,056 | 8,427 | |
| 179 | +For every variant and round, the workflow copies the pristine project, runs the cold compile, runs |
| 180 | +it again unchanged, edits one model and compiles, then edits 100 models and compiles. Rounds |
| 181 | +alternate the variant order. `/usr/bin/time` records wall time, user plus system CPU time, and the |
| 182 | +peak resident memory of the largest process. dbt runs use `DBT_SEND_ANONYMOUS_USAGE_STATS=false` and |
| 183 | +a DuckDB profile with `threads: 4`. |
50 | 184 |
|
51 | | -The projects use repeated generated structures. Individual project timings depend on SQL |
52 | | -complexity, macros, configuration, and hardware as well as model count. |
| 185 | +The exact commands, run in each project copy: |
53 | 186 |
|
54 | | -### Incremental compilation |
| 187 | +```bash |
| 188 | +# SQLBuild default |
| 189 | +sqb --no-color compile |
55 | 190 |
|
56 | | -SQLBuild reuses cached analysis for unaffected models. In the 5,000-model benchmark, a single-model |
57 | | -file edit reused **4,999 model analyses**. A scoped Python-macro edit required fresh analysis for |
58 | | -**7 models**. Both scenarios completed in approximately **9.8 seconds**. |
| 191 | +# SQLBuild full compile with per-model caches |
| 192 | +SQLBUILD_DISABLE_COMPILE_REUSE=1 SQLBUILD_DISABLE_DELTA_COMPILE=1 sqb --no-color compile |
59 | 193 |
|
60 | | -The single-model scenario appends a SQL comment to exercise file-change invalidation. Separate |
61 | | -regression coverage checks expression-changing edits against uncached compilation results. |
62 | | -Scenarios run in a fixed sequence and use different cache-loading paths; an edit timing below the |
63 | | -initial unchanged timing does not mean editing inherently requires less work. |
| 194 | +# SQLBuild with every rule: the same command in the project with [rules] select = ["SQBR", "XSQBR"] |
| 195 | +sqb --no-color compile |
64 | 196 |
|
65 | | -## Rules performance |
| 197 | +# dbt v2 strict with parse cache |
| 198 | +dbt compile --profiles-dir ../profiles --no-version-check --static-analysis strict --partial-parse --partial-load |
66 | 199 |
|
67 | | -Rules participate in ordinary compilation. Model Rules are cached per model, and SQLBuild tracks |
68 | | -their dependencies so unaffected findings can be reused after edits. Project Rules evaluate the |
69 | | -complete project view and have broader invalidation. |
| 200 | +# dbt v2 --dirty: the cold run builds the cache, later runs use --dirty |
| 201 | +dbt compile --profiles-dir ../profiles --no-version-check --static-analysis strict --partial-parse --partial-load |
| 202 | +dbt compile --profiles-dir ../profiles --no-version-check --static-analysis strict --partial-load --dirty |
70 | 203 |
|
71 | | -A separate custom-Rules benchmark exercises 20- and 100-Rule packs over 5,000 models. It measures |
72 | | -first compilation, unchanged compilation, model edits, and Rule implementation changes. This is a |
73 | | -different workload from the compiler table above; its results should be compared within that profile. |
| 204 | +# dbt v2 default, with and without strict |
| 205 | +dbt compile --profiles-dir ../profiles --no-version-check |
| 206 | +dbt compile --profiles-dir ../profiles --no-version-check --static-analysis strict |
| 207 | +``` |
74 | 208 |
|
75 | | -See [Execution and caching](concepts/rules/execution-and-caching.md) for the Rules lifecycle, or run |
76 | | -the publication profile below to collect current custom-Rules measurements. |
| 209 | +The non-strict parse-cache and `--dirty` rows drop `--static-analysis strict`. The remote-sources row |
| 210 | +runs the default strict command in a copy of the dbt project without `schema_origin: local`. |
77 | 211 |
|
78 | | -## Methodology |
| 212 | +## Reproduce the benchmark |
79 | 213 |
|
80 | | -The compiler results above are individual scenario measurements from the |
81 | | -[passing CI run on September 22, 2026](https://github.com/chio-labs/sqlbuild/actions/runs/35764207007), |
82 | | -using the compiler code released in **SQLBuild 0.113.3** and Python 3.12 on Linux. They are not |
83 | | -multi-run medians. |
| 214 | +The harness is in [`benchmarks/dbt-compare/`](https://github.com/chio-labs/sqlbuild/tree/main/benchmarks/dbt-compare), |
| 215 | +and its README describes every variant. Start the workflow with the versions measured here: |
84 | 216 |
|
85 | | -Each scenario launches a fresh process. The first compile starts with an empty project cache; |
86 | | -subsequent scenarios reuse that cache. The CI job enforces a **4 GiB combined memory limit**. |
87 | | -Reported peak resident memory is for the compiler process, not the entire CI job. |
| 217 | +```bash |
| 218 | +gh workflow run benchmark-dbt.yml -f sqlbuild_version=0.164.3 -f dbt_version=2.0.9 -f rounds=3 |
| 219 | +``` |
88 | 220 |
|
89 | | -CI checks cache-hit and miss expectations and pinned semantic fingerprints alongside timing and |
90 | | -memory budgets. The varied 1,000-model guard additionally compares SQL-expression and macro edits |
91 | | -with uncached analysis and Rules results. |
| 221 | +The run uploads the per-run files and a `summary.md` with the environment, the correctness table |
| 222 | +and the full results table. |
92 | 223 |
|
93 | | -These measurements cover compilation and static analysis. SQL tests and audits are prepared, not |
94 | | -executed against a warehouse. |
| 224 | +To run it locally, from a checkout of the repository: |
95 | 225 |
|
96 | | -## Reproduce the benchmarks |
| 226 | +```bash |
| 227 | +uv venv -p 3.12 venv-sqb && uv pip install -p venv-sqb/bin/python sqlbuild==0.164.3 pytest duckdb |
| 228 | +uv venv -p 3.12 venv-dbt2 && uv pip install -p venv-dbt2/bin/python dbt==2.0.9 |
97 | 229 |
|
98 | | -Run the fresh-process compiler cache guards from the |
99 | | -[SQLBuild repository](https://github.com/chio-labs/sqlbuild): |
| 230 | +export SQB=$PWD/venv-sqb/bin/sqb DBT_V2=$PWD/venv-dbt2/bin/dbt PYTHON=$PWD/venv-sqb/bin/python |
100 | 231 |
|
101 | | -```bash |
102 | | -make test-e2e-cache-compile-performance SQLBUILD_BENCHMARK_MODELS=3000 |
103 | | -make test-e2e-cache-compile-performance SQLBUILD_BENCHMARK_MODELS=5000 |
| 232 | +"$PYTHON" benchmarks/dbt-compare/write_dbt_dense.py --models 10000 --out /tmp/p10k |
| 233 | +benchmarks/dbt-compare/correctness.sh /tmp/correctness |
| 234 | +python3 benchmarks/dbt-compare/check_strict.py correctness /tmp/correctness |
| 235 | +benchmarks/dbt-compare/bench.sh /tmp/p10k /tmp/results 3 |
| 236 | +python3 benchmarks/dbt-compare/check_strict.py runs /tmp/results |
| 237 | +python3 benchmarks/dbt-compare/summarize.py /tmp/results |
104 | 238 | ``` |
105 | 239 |
|
106 | | -Run the five-iteration custom-Rules publication profile: |
| 240 | +Use an otherwise idle machine: other load changes wall times far more than CPU times. |
107 | 241 |
|
108 | | -```bash |
109 | | -uv run python -m scripts.benchmark_rules \ |
110 | | - --models 5000 \ |
111 | | - --rule-counts 20 100 \ |
112 | | - --iterations 5 \ |
113 | | - --output rules-5000.json |
114 | | -``` |
| 242 | +## Compiler guards in CI |
115 | 243 |
|
116 | | -Run the bounded Rules profile used by required CI: |
| 244 | +SQLBuild's CI also runs fresh-process compile guards with time and memory budgets on dense and |
| 245 | +varied projects, and a custom-Rules benchmark with 20- and 100-rule packs. Run them from the |
| 246 | +repository: |
117 | 247 |
|
118 | 248 | ```bash |
119 | | -uv run python -m scripts.benchmark_rules_ci \ |
120 | | - --output rules-performance-ci.json \ |
121 | | - --summary-output rules-performance-ci.md |
| 249 | +make test-e2e-dense-compile-performance SQLBUILD_BENCHMARK_MODELS=10000 |
| 250 | +make test-e2e-cache-compile-performance SQLBUILD_BENCHMARK_MODELS=5000 |
| 251 | +uv run python -m scripts.benchmark_rules --models 5000 --rule-counts 20 100 --iterations 5 --output rules-5000.json |
122 | 252 | ``` |
123 | | - |
124 | | -The benchmark generators, guard thresholds, and CI workflow are maintained in the public |
125 | | -[SQLBuild repository](https://github.com/chio-labs/sqlbuild). |
|
0 commit comments