October 23, 2026
The Halloween Problem: why your UPDATE's plan has a spool you didn't ask for
An UPDATE that modifies the same column it's filtering on has a real correctness hazard, named after the date it was discovered in 1976 — and the operator SQL Server adds to defend against it has a real, visible performance cost.
The name has nothing to do with the bug — it's named for the date a group of IBM researchers found it, Halloween 1976, while working on query optimization. The problem itself is a real correctness hazard that every major relational engine has to defend against, and seeing SQL Server's defense against it in your own execution plans is common enough to be worth recognizing on sight.
The problem, concretely
UPDATE employees SET salary = salary * 1.1 WHERE salary < 30000;Give everyone earning under $30,000 a 10% raise. Imagine the engine
executes this by scanning rows in order of an index on salary, updating
each matching row as it goes. An employee earning $28,000 gets updated to
$30,800 — and if the scan is moving through the index in ascending order
and that row's new, higher value now sorts later in the same index the
scan is still traversing, the scan can encounter that same row again
later in its pass, see $30,800, and — since that's still arguably within
scope of a scan that started before the row moved — potentially evaluate
it against the filter again or process it a second time. The row the
UPDATE already handled gets revisited because the act of updating it
moved it to a different position in the very structure being used to find
rows to update.
This isn't about salary < 30000 specifically — it's the general shape:
any UPDATE (or DELETE/INSERT ... SELECT) that modifies a column
which is also used, via an index, to identify which rows to process has
this hazard. A statement that should touch each qualifying row exactly
once can, without protection, touch some rows more than once or skip
others, depending on scan order and exactly when a row's new value moves
it relative to where the scan currently is.
SQL Server's defense: a Spool
Every major engine solves this the same fundamental way: fully separate
"figure out which rows qualify" from "apply the change" into two distinct
phases, so the second phase can't influence what the first phase is still
in the middle of discovering. SQL Server's version of that separation
shows up in an execution plan as a Spool operator (commonly an
Eager Table Spool) sitting between the scan/seek that identifies
qualifying rows and the actual Update operator — the spool fully
materializes the set of rows to change into a temporary work area first,
so the update phase that runs afterward is working from a frozen
snapshot, not a structure it's actively modifying mid-scan.
That protection has a real cost: the spool means writing every qualifying
row's data to tempdb (or an in-memory work table for a small enough set)
before the update even starts, which is genuine extra I/O and time that
wasn't part of either "find the rows" or "update the rows" individually —
it's purely the overhead of making those two phases safe to separate.
Recognizing it, and when it's actually needed
Not every UPDATE gets a spool — SQL Server's optimizer only adds one
when it determines the Halloween Problem is actually possible for this
specific statement, which depends on whether the column being written is
also reachable from the index path used to find the rows. An UPDATE
filtering on customer_id while only writing to an unrelated notes
column, for instance, has no Halloween Problem to defend against at all —
no spool needed, because updating notes can't move a row relative to an
index being scanned on customer_id.
When you do see an unexpected spool in an execution
plan for a straightforward
UPDATE or DELETE, that's the sign: the column being written is also
part of what the engine used to identify the rows to touch, directly or
through an index. The spool isn't a bug or a sign something's configured
wrong — it's correctness protection the optimizer determined this specific
statement actually needs.
The one case worth actively avoiding it for
For a large UPDATE where the extra tempdb write is a genuine,
measured cost (not just a plan detail to understand, but something
actually showing up in wait stats or I/O), restructuring the statement to
avoid the hazard in the first place — doing the identification into an
already-materialized temp table yourself, then updating from that, with
no column overlap between "what identifies the row" and "what the update
touches" in the final UPDATE statement — can let the optimizer skip the
spool it would otherwise have to add. This is a real but narrow
optimization: worth reaching for on a provably hot, large-scale batch
update, not as a reflexive habit on every UPDATE statement.
Can't demonstrate this in the playground
Recognizing an Eager Table Spool specifically requires a real SQL Server
graphical execution plan — SQLite's planner and EXPLAIN QUERY PLAN
output don't expose an equivalent named operator for this, even though
SQLite (like every correct relational engine) also has to handle the
underlying hazard internally somehow. See the actual spool by running the
salary-style example above against a real SQL Server instance with a
supporting index and an Actual execution plan turned on.
- Reading a SQL Server execution plan — the operator vocabulary this post adds one specific icon to.
- UPDATE — the statement this hazard applies to, alongside DELETE and INSERT ... SELECT.
- Indexes — what makes a column "reachable from the index path" in the first place.