SET vs SELECT vs DECLARE: does it matter how you assign T-SQL variables?
I benchmarked seven ways of declaring and assigning T-SQL variables on SQL Server 2025 — almost 3,000 timed runs. The keyword barely matters; the statement count does, and mostly at compile time.
Every SQL Server team has the argument eventually. Someone writes:
DECLARE @a INT;
DECLARE @b INT;
SET @a = 1;
SET @b = 2;
and someone else “fixes” it in review to:
DECLARE @a INT = 1, @b INT = 2;
Is that a performance fix, or just style? I got curious enough to measure it properly: seven ways of getting to the same end state — a set of declared, assigned variables — benchmarked on SQL Server 2025 across variable counts from 1 to 64, a dozen data types, and string lengths from 1 to 4,000 characters. Almost 3,000 timed runs.
The short version: the keyword you use barely matters. The number of statements does — and mostly at compile time, not execution time. Here’s the data.
The seven contenders
Same end state, seven roads there:
| # | Method | Shape |
|---|---|---|
| 1 | Separate declares | DECLARE @v0 int; DECLARE @v1 int; … |
| 2 | Combined declare | DECLARE @v0 int, @v1 int, …; |
| 3 | Combined declare + inline assign | DECLARE @v0 int = 1, @v1 int = 2, …; |
| 4 | Separate declares + inline assign | DECLARE @v0 int = 1; DECLARE @v1 int = 2; … |
| 5 | Declare, then SET each | DECLARE …; SET @v0 = 1; SET @v1 = 2; … |
| 6 | Declare, then SELECT each | DECLARE …; SELECT @v0 = 1; SELECT @v1 = 2; … |
| 7 | Declare, then one combined SELECT | DECLARE …; SELECT @v0 = 1, @v1 = 2, …; |
Notice the real axis hiding in that table: methods 3 and 7 do all the assignments in one statement; methods 4, 5, and 6 use one statement per variable. Keep that in mind — it’s the whole story.
How do you measure something this small?
A single DECLARE costs well under a microsecond, so you can’t time one. The harness
runs each batch in a tight loop until a wall-time budget elapses (~400 ms per
configuration per pass, hundreds of thousands of iterations for the cheap methods),
takes the minimum time per iteration across eight interleaved passes (the estimator
least disturbed by OS scheduling), and subtracts a no-op baseline — the smallest
possible batch, timed the same way — so the loop and dispatch overhead cancels out.
What’s left is each method’s own cost.
That’s the short version; the harness design has enough traps to deserve its own write-up — see How to benchmark something that takes a microsecond for the full methodology and code.
Every method is timed in two modes, because they answer different real-world questions:
- Cached — the plan is compiled once and reused. This is what a hot stored procedure pays on every call.
- Recompile — a plan-cache miss is forced on every iteration. This is what ad-hoc batches, or procs hit by recompilation, pay.
One honesty note before the numbers: this ran on a shared 4-core box, so absolute values are comparative, not a hardware spec. Trust the ordering and the scaling; your microseconds will differ.
Result 1: declaring is free, assigning is not
First, declarations alone. With a cached plan, declaring variables — whether in one
DECLARE or sixty-four separate ones — costs essentially nothing: it sits on the
measurement floor at one variable, reaches a few tens of microseconds at 16, and only
~170–210 µs even at 64. If you’ve ever wondered whether SQL Server “does work” for each
DECLARE at runtime: not enough to ever notice. Declaration placement is a readability
decision, full stop.
Assignment is where actual cost appears. And once you compare the five assigning methods, a clean pattern emerges. Cached, at 8 variables (net cost per batch):
| Method | Statements | µs/batch |
|---|---|---|
Combined DECLARE with inline assigns | 1 | 105 |
One combined SELECT | 1 | 119 |
Separate DECLAREs with inline assigns | 8 | 160 |
SET per variable | 8 | 168 |
SELECT per variable | 8 | 192 |
The two one-statement forms win; the three N-statement forms cluster together behind
them; and the gap widens as the variable count grows. SET versus single-variable
SELECT — the version of this argument people actually have — is a near-tie at every
variable count. The keyword is not the lever. The statement count is.
Result 2: compilation is where the choice actually bites
The cached numbers above are small — for a handful of variables you’re talking a few microseconds of difference per call, which in almost any real procedure is rounding error. The recompile numbers are not small.
When the batch has to be parsed and compiled each time (net cost, by variable count):
| Method | 1 var | 4 vars | 16 vars | 64 vars |
|---|---|---|---|---|
Combined DECLARE + inline assigns | 30 µs | 61 µs | 227 µs | 1.68 ms |
One combined SELECT | 40 µs | 75 µs | 246 µs | 1.81 ms |
Separate DECLAREs + inline assigns | 31 µs | 88 µs | 340 µs | 2.32 ms |
SET per variable | 31 µs | 89 µs | 350 µs | 2.26 ms |
SELECT per variable | 37 µs | 104 µs | 402 µs | 2.51 ms |
Three things to read off that table:
- Compilation dwarfs execution. At 64 variables, the compile-every-time forms cost 1.7–2.5 milliseconds per batch, where the same batches run from a cached plan in a few hundred microseconds at most — the compiler is doing several times more work than the runtime ever will.
- The one-statement forms compile meaningfully cheaper, and the margin grows with variable count — at 64 variables, separate-statement forms cost ~40–50% more to compile than the combined ones.
- The ordering is identical to the cached case. There’s one rule, not two: fewer statements, less work — it just matters ~10× more when compilation is in play.
So if you have ad-hoc batches, frequently-recompiled procedures, or generated SQL with dozens of variables, statement count is a real (if modest) cost. If your procs compile once and run a million times, it’s noise.
Result 3: data types and value lengths — nothing to see
I also swept a dozen data types (bit through datetime2 to uniqueidentifier) and string
lengths from 1 to 4,000 characters. The honest summary: on this host, any per-type or
per-length effect was smaller than the noise floor of a shared machine. Fixed-width
types cluster together with no convincing size ordering, and varchar versus nvarchar
was a wash. Whatever effect exists, it’s small compared to the statement-count effect —
which is itself small compared to anything your queries actually do. I’ll re-run this
sweep on a quiet dedicated box before claiming more.
The asterisk that matters more than any of this: SELECT’s silent semantics
One non-performance difference deserves more attention than every number above. When assigning from a query:
SELECT @status = Status FROM dbo.Orders WHERE CustomerId = @cust;
if that query returns multiple rows, SELECT silently assigns the last one and
moves on. The equivalent SET with a subquery raises an error. And if the query returns
zero rows, SELECT leaves the variable’s previous value untouched — it doesn’t set
it to NULL — which is a classic source of stale-value bugs in loops.
A correctness footgun is worth more than 50 microseconds of compile time. If your assignment query could ever return anything other than exactly one row, that — not performance — should decide the syntax.
What I’d actually recommend
- For a handful of variables in a stored procedure: write whatever reads clearest. The cached-plan difference is single-digit microseconds; no human will ever observe it.
- If you’re assigning many variables at once, prefer one statement — a combined
DECLARE @a INT = 1, @b INT = 2, …or a singleSELECT @a = 1, @b = 2, …. It’s never slower, it’s meaningfully cheaper to compile, and it’s usually more readable anyway. - In generated SQL or ad-hoc-heavy workloads, statement count is a real lever, because you pay compilation every time.
- Choose
SETvsSELECTon semantics, not speed — they cost the same per statement, but they behave differently on zero and multiple rows. - And the meta-lesson: this entire effect is microseconds. If a procedure is slow, its variable declarations are not why. Look at the queries, the indexes, and the execution plan — that’s where the milliseconds live.
The benchmark harness — batch generator, timing loop, no-op baseline subtraction, and the full result set (374 configurations × 8 passes) — is reproducible end-to-end against a disposable SQL Server 2025 container. If you’d like the scripts or want this kind of measurement done on your own workload, get in touch.
Need help with something like this?
I take on focused SQL Server engagements — performance, migrations, and CI/CD.