Another Example:
I was reviewing the performance of a procedure recently and stumbled over another pumpkin.
My last blog post was about the Halloween Problem, and we saw its effects on an UPDATE statement. In this case, it was the same issue but with an INSERT statement. Here’s the code being executed:
INSERT INTO Schema1.Object1 (
Column1,
Column2,
Column3,
Column4 )
SELECT
Object2.Column1,
Object2.Column2,
Object2.Column3,
Object2.Column4
FROM Object3 Object2
LEFT LOOP JOIN Schema1.Object1 Object4 WITH(INDEX(Column5))
ON Object4.Column3 = Object2.Column3
AND Object4.Column1 = Object2.Column1
AND Object4.Column2 = Object2.Column2
AND Object4.Column4 = Object2.Column4
WHERE
Object4.Column6 IS NULL
The gist is, we’re trying to insert a record into Object1, assuming said record doesn’t already exist. We’re querying the data we want to insert from a temp table, but joining to the base table to make sure a record doesn’t already exist with the same values.
In the case of an UPDATE statement, if we update fields that are in our query’s search criteria, we could update the same row multiple times. SQL Server’s Halloween protections prevent this, but result in extra work that affect our performance.
The INSERT statement here is similar, trying to insert a record while querying to see if the same record exists. So, again SQL Server adds Halloween protections to our plan:
Plan Analysis
I would have expected us to scan the temp table, then have a LEFT JOIN to the base table. The Table Spool is the red flag that we have an issue with the plan, and is frequently seen with Halloween protections.
The index scan on the base table seems to be overkill since we’re joining on the primary key columns (the key lookup isn’t much of a concern). But we’re likely doing the scan because of the spool; it’s SQL Server’s way of getting all relevant records in one place at one time, breaking the normal flow of row mode operation, to make sure we don’t look up the same record multiple times.
Easy Fix
The data we are trying to insert is being passed into the procedure using a memory-optimized table valued parameter. We’ve queried that into the temp table as another step before our final INSERT SELECT query, because SQL Server will sometimes make poor optimizations when TVP’s are involved (because they have no statistics).
The solution then is an easy one. We move our LEFT JOIN out of the final INSERT, and we make that check as we query the TVP’s data into the temp table. We separate the SELECT against that table from the INSERT; they are now in separate operations, and the Halloween protections are no longer necessary.
If you liked this post, please follow me on twitter or contact me if you have questions.