It’s that time of year again when ghouls and spooks surface and database bloggers write about an issue that shares its name with Halloween. Around mid to late October, you often come across database blogs about a phenomenon that dates all the way back to the mid-1970s: the Halloween Problem. This is my take on the topic.

The What?

The Halloween Problem gets its name from the day it was discovered – 31st October (some reports differ between 1975 and 1976). On this day, Donald Chamberlin, Patricia Selinger, and Morton M. Astrahan were working on the System R database project in an IBM lab and wrote a SQL query which updated a database, giving each staff member who earned less than $25,000 a 10% pay rise. The query completed, but every staff member then had a salary of at least $25,000 – this was not the expected output of the query.

They had inadvertently found a flaw in how the update was being executed – some affected records were updated more than once, until they no longer matched the filter criteria.

To demonstrate how this happens, let’s assume we have 4 employees in an Employees table and the table has an index on salary. The data in the index would look like the table below (names generated by Anthropic Claude, model: Sonnet 5.5):

First NameLast NameSalary
JaneDoe20,000
JohnSmith21,000
BobJones23,000
AishaKhan27,000

If we were to run the following SQL query to update the table, the database engine would read through the rows one by one and add 10% to the value:

UPDATE	Employees
SET		Salary = Salary * 1.1
WHERE	Salary < 25000;

As the index is sorted by salary, that ordering should be maintained as records are updated.

The Halloween Problem in Action

The first record to be updated is Jane Doe, whose salary is 20,000. The arrow marks the next row the scan will update:

Row to update this iterationFirst NameLast NameSalary
➜JaneDoe20,000
JohnSmith21,000
BobJones23,000
AishaKhan27,000

Jane is updated to 22,000 so that record moves along in the index to maintain the order (the bold text marks the row just updated):

Row to update this iterationFirst NameLast NameSalary
➜JohnSmith21,000
JaneDoe22,000
BobJones23,000
AishaKhan27,000

John is updated to 23,100. His row moves below Bob. Jane is now the next row in the scan, even though she has already been updated:

Row to update this iterationFirst NameLast NameSalary
➜JaneDoe22,000
BobJones23,000
JohnSmith23,100
AishaKhan27,000

Jane is updated a second time, from 22,000 to 24,200:

Row to update this iterationFirst NameLast NameSalary
➜BobJones23,000
JohnSmith23,100
JaneDoe24,200
AishaKhan27,000

Bob is updated to 25,300, which is no longer below 25,000:

Row to update this iterationFirst NameLast NameSalary
➜JohnSmith23,100
JaneDoe24,200
BobJones25,300
AishaKhan27,000

John is updated a second time, from 23,100 to 25,410:

Row to update this iterationFirst NameLast NameSalary
➜JaneDoe24,200
BobJones25,300
JohnSmith25,410
AishaKhan27,000

Jane is updated a third time, from 24,200 to 26,620. The scan then finds no rows under 25,000, so it stops.

First NameLast NameSalary
BobJones25,300
JohnSmith25,410
JaneDoe26,620
AishaKhan27,000

The table below shows the end result – each person who started with a salary below 25,000 is now over 25,000, some receiving a much greater increase than we expected. Let’s hope Jane is going to give generously to the trick-or-treaters at her door.

EmployeeStartExpected (one 10% raise)ActualTimes updated
Jane Doe20,00022,00026,6203
John Smith21,00023,10025,4102
Bob Jones23,00025,30025,3001
Aisha Khan27,00027,00027,0000

How SQL Server Prevents This Behaviour

Clearly, the above is not the desired behaviour and something must be done to prevent it. Let’s see what SQL Server does to prevent this from happening.

We’ll work through a similar example in the Stack Overflow database, provided under cc-by-sa 4.0 licence from the Stack Exchange Data Dump. The database is in compatibility level 160 on a SQL Server 2022 instance installed on a VM with 8 cores and 46GB RAM. I will use the 2013 version of the database for this example.

The following query increases the reputation of those users who already have a reputation over 1,000,000 by 10%

UPDATE	dbo.Users
SET		Reputation = Reputation * 1.1
WHERE	Reputation > 1000000;

To support this query, we may create the index below so SQL Server can seek directly to those users with a reputation over 1,000,000. The index allows SQL Server to seek directly to those users that need to be updated, but it also means the query reads and modifies the keys of the same index, which is what creates the Halloween Problem.

The WHERE clause is highly selective as there is only one user in this version of the database that has a reputation in that range, so this index should mean we can jump straight to that user and update their record:

CREATE INDEX IX_Reputation ON dbo.Users
(
	Reputation
);

As we’re going to run a few tests here, I’ll just grab the estimated execution plan as it illustrates SQL Server’s strategy without actually changing the data – this way each test is being run against the same data.

The estimated plan for the update query above is:

We can see a Table Spool operator and that is what prevents the Halloween Problem from occurring.

As we are retrieving the data we want to update using an index on the column we are modifying, that index must then be updated. SQL Server needs to separate the read dataset from the update dataset.

An Eager Table Spool reads all the rows of its child operator (the Index Seek in our case) before the next operator can start processing, as such, the Eager Table Spool operator is known as a blocking operator in an execution plan. What happens in this execution plan is that the records to be updated (one user with a reputation over 1,000,000 in this example) are read from the index and written to the Spool operator (this is written to tempdb) The dataset in the Eager Table Spool is then worked on (the Compute Scalars are there to support the 10% calculation) and then the output is pushed into the Clustered Index Update. The key is that the spool is a separate object from the index itself and therefore is unaffected by the index update.

The Clustered Index Update operator actually updates both the clustered index and the non-clustered index on Reputation:

A spool isn’t the only operator that provides Halloween Protection in SQL Server, other blocking operators such as a sort can give the same effect.

Let’s change the criteria of the WHERE clause so that every user with a reputation above 1 gets a 10% uplift. I’m forcing the optimizer to use the same index as it did before – rarely is this a good idea in reality, but I am doing it here to keep the demonstration on topic. Note the form of the update has changed slightly as there is now a FROM clause – this is to allow me to provide the index hint:

UPDATE	dbo.Users
SET		Reputation = Reputation * 1.1
FROM	dbo.Users WITH (INDEX = IX_Reputation)
WHERE	Reputation > 1;

The plan is below:

SQL Server still seeks the index (because of our index hint) but rather than perform a spool, it sorts the data that comes out of the Index Seek. It sorts by the Id column (which is the column the table is clustered on) so that it can update the clustered index. Like the spool, the sort creates a copy of the data and it is that which is used to drive the update, rather than the index which was read at the beginning of the plan. While the spool copies the data to tempdb, the sort is done in memory, though it can spill to tempdb if not enough memory is granted. In both cases, reading the secondary dataset, rather than the original index, is what provides the Halloween Protection.

As an aside, without forcing the index above, SQL Server chose a different plan:

UPDATE	dbo.Users
SET		Reputation = Reputation * 1.1
FROM	dbo.Users
WHERE	Reputation > 1;

The plan is below:

SQL Server chose to scan the entire clustered index. We can see the root operator in the plan for this query and the plan for our hint query that it has costed the clustered index plan slightly cheaper and as the optimizer is cost-based, SQL Server picks that plan.

Conclusion

This post was my take on how SQL Server protects against the Halloween Problem – a platform agnostic issue that could happen when using an index to retrieve records from a database where those are the same records being updated by the query. The post is a short overview of the problem, with some simple examples. For a more detailed overview, I would recommend Paul White’s excellent four-part article on the topic in the references below.

References / Further Reading

Andy Brownsword – The Halloween Problem and SQL Server

Charlie Custer – The Halloween Problem: a spooky (and true) SQL story

Craig Freedman – Halloween Protection

Engineering and Technology History Wiki – Morton M. Astrahan

Ernie Smith – Spooky Database Relations

Hugo Kornelis – Table Spool

Paul White – The Eager Index Spool and The Optimizer

Paul White – Halloween Protection: The Complete Series

Wikipedia – Donald Chamberlin

Wikipedia – Halloween Problem

Wikipedia – IBM System R

Wikipedia – Patricia Selinger

Posted in