

- Description
- A data center seen from the roof
- Source
- Rsparks3 · original page
- Licence
- CC0 Public domain
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.
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.
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#
- A table is a set of rows, with no order. No row is “the first” until a sort has been requested.
- Each operation takes sets and returns a set. They can therefore be composed: filter, join, group, then reuse the result.
- You describe the property of the result (“orders more than two years old”), not the algorithm to find them.
- The optimizer chooses the execution plan based on statistics and indexes.
- 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#
| Aspect | Procedural (row-by-row) | Set-based |
|---|---|---|
| Reasoning | Steps, loops, variables, intermediate states | Relations, joins, filters, groupings |
| Execution order | Decided by the programmer | Decided by the optimizer |
| Cost per row | One statement, one execution context and often one log write per row | Amortized over the set: batched reads and writes |
| Parallelism | Very limited | Possible, handled by the engine |
| Volume | Rapid degradation | Generally almost linear growth |
| Typical use case | Orchestration, per-object calls, external effects | Data 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.
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#
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#
- Codd, E. F. (1970), “A Relational Model of Data for Large Shared Data Banks”, CACM 13(6), 377–387 — foundation of the relational model
- Microsoft Learn — OVER Clause (Transact-SQL) — window functions
- Microsoft Learn — Scalar UDF Inlining — cost of scalar functions, SQL Server 2019 improvements