Skip to content
dbm.lt
Open menu
Back to blog
performance t-sql benchmarks

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:

#MethodShape
1Separate declaresDECLARE @v0 int; DECLARE @v1 int; …
2Combined declareDECLARE @v0 int, @v1 int, …;
3Combined declare + inline assignDECLARE @v0 int = 1, @v1 int = 2, …;
4Separate declares + inline assignDECLARE @v0 int = 1; DECLARE @v1 int = 2; …
5Declare, then SET eachDECLARE …; SET @v0 = 1; SET @v1 = 2; …
6Declare, then SELECT eachDECLARE …; SELECT @v0 = 1; SELECT @v1 = 2; …
7Declare, then one combined SELECTDECLARE …; 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):

MethodStatementsµs/batch
Combined DECLARE with inline assigns1105
One combined SELECT1119
Separate DECLAREs with inline assigns8160
SET per variable8168
SELECT per variable8192

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):

Method1 var4 vars16 vars64 vars
Combined DECLARE + inline assigns30 µs61 µs227 µs1.68 ms
One combined SELECT40 µs75 µs246 µs1.81 ms
Separate DECLAREs + inline assigns31 µs88 µs340 µs2.32 ms
SET per variable31 µs89 µs350 µs2.26 ms
SELECT per variable37 µs104 µs402 µs2.51 ms

Three things to read off that table:

  1. 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.
  2. 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.
  3. 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 single SELECT @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 SET vs SELECT on 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.

Book a 30-min consult