clement13.net
Original photo: A data center seen from the roof
Description
A data center seen from the roof
Source
Rsparks3 · original page
Licence
CC0 Public domain
Original photo, unretouched. The background is a version of it stylised by the site.

Set-based programming in T-SQL

Describe the result, not the path: the way of thinking that changes the scale of what SQL can handle.

≈ 3 min readReading
In brief
  • Goal: express the expected result and let the optimizer choose the execution.
  • Principle: sets as input, one set as output, composed with one another.
  • Tools: joins, aggregates, window functions, CTEs, APPLY, set-based writes.
  • Safeguard: a loop over objects is legitimate; over data rows, rarely.
On this page
Two halves

This section first covers how to use the engine (principles, tools, examples). The other half is an approach: prepare the model first, then issue very few, very large queries. See The approach, design errors and disaster cases.

Goal#

Set-based programming means expressing the result you want (a set of rows defined by properties), rather than the sequence of steps that builds it row after row. In T-SQL, this amounts to entrusting the query optimizer with the choice of execution method.

Its benefits, when practiced well:

  • Performance: one statement processes thousands of rows in a single pass, with parallelism, suitable join algorithms and batched writes.
  • Readability: the intent (the “what”) is visible; the mechanics (the “how”) are left to the engine.
  • Reliability: a statement is atomic; no intermediate state to manage by hand.
  • Maintainability: fewer variables, fewer branches, fewer side effects.
  • Scalability: cost most often grows almost linearly with volume, instead of exploding.

The principle in five ideas#

  1. A table is a set of rows, with no order. No row is “the first” until a sort has been requested.
  2. Each operation takes sets and returns a set. They can therefore be composed: filter, join, group, then reuse the result.
  3. You describe the property of the result (“orders more than two years old”), not the algorithm to find them.
  4. The optimizer chooses the execution plan based on statistics and indexes.
  5. You think in terms of “grain”: what does one row of the result correspond to (a sale, a customer per month, a day)? Almost all errors come from a poorly defined grain.

Procedural and set-based at a glance#

AspectProcedural (row-by-row)Set-based
ReasoningSteps, loops, variables, intermediate statesRelations, joins, filters, groupings
Execution orderDecided by the programmerDecided by the optimizer
Cost per rowOne statement, one execution context and often one log write per rowAmortized over the set: batched reads and writes
ParallelismVery limitedPossible, handled by the engine
VolumeRapid degradationGenerally almost linear growth
Typical use caseOrchestration, per-object calls, external effectsData transformation, calculations, cleansing, loading

A minimal example#

Mark as archived the orders prior to 2020:

Procedural#

-- Procedural: mark old orders, one by one
DECLARE @Id int;
DECLARE c CURSOR LOCAL FAST_FORWARD FOR
    SELECT CommandeId FROM dbo.Commandes WHERE DateCommande < '2020-01-01';
OPEN c;
FETCH NEXT FROM c INTO @Id;
WHILE @@FETCH_STATUS = 0
BEGIN
    UPDATE dbo.Commandes SET Statut = 'ARCHIVEE' WHERE CommandeId = @Id;
    FETCH NEXT FROM c INTO @Id;
END;
CLOSE c;
DEALLOCATE c;

Set-based#

-- Set-based: a single statement, a single plan
UPDATE dbo.Commandes
SET    Statut = 'ARCHIVEE'
WHERE  DateCommande < '2020-01-01';

The second form expresses the same thing in one statement. The engine can use an index on DateCommande, batch the writes to the transaction log and, if the volume justifies it, parallelize. The first form executes as many statements as there are rows: at each iteration, a row is read, a statement is launched, a row is modified.

Why the loop is expensive

When it is not inside an explicit transaction, each statement is committed separately (autocommit mode): the engine must wait for the log write on every row. See the details of the cost.

Outline of this section#

Principles in detailRelational model, optimizer, cost of row-by-row processing, NULL, determinism and transactions: the foundations of set-based programming in T-SQL.The T-SQL toolkitJoins, window functions, CTEs, APPLY, set-based writes, series and batch processing: the T-SQL toolbox for set-based programming, with SQL Server versions.Determinism and deterministic objectsDeterministic objects in T-SQL: computed columns and indexed views, schema-bound functions, constant folding, inlining, and table types to freeze a procedure's execution context."Generating code: inlining and hard-coded values"Compile T-SQL code on the fly to hard-code the result of a reference SELECT: cardinality estimates, filtered indexes, deterministic functions, inlining, risks and best practices."The approach: prepare, then strike once"The set-based approach: prepare the model (scope, indexes, pure functions, materialized derivations, frozen inputs) before issuing very few, very large queries, with no iteration, in a fully deterministic process.An expert's design errors"Fifteen mistakes made even by developers who know the SQL engine perfectly: invisible outer loop, scalar function, recomputed derivations, shifting context, skewed estimates. For each, a method."Examples: procedural versus set-basedSix T-SQL examples comparing a procedural version (cursor, loop) with a set-based version: running total, update, deduplication, string aggregation, calendar, continuous ranges.6 pagesDisaster cases: the SQL is clean, the assembly is wrongThree T-SQL workloads where every query is clean but which accumulate several assembly errors, measured, then redesigned with the set-based approach: preparation, very few queries, no iteration.3 pagesWhen procedural is legitimateCases where a loop is justified in T-SQL (objects, batches, external effects) and fake set-based code to avoid (scalar functions, disguised cursors, row-by-row triggers).Measuring properlyMethod and reproducible T-SQL script to compare a procedural process with its set-based version: CPU, reads, growing volumes, equality of results.
Conventions and prerequisites

The examples use fictitious tables in the dbo schema. The required SQL Server versions are indicated next to each feature (for example SQL Server 2017+). Run them on a test database: several scripts create tables and delete rows.

Sources and verification#