Filler
This is a short post to “document” something interesting I noticed about… It’s kind of a mouthful.
See, hash joins will bail out when they spill to disk enough. What they bail to is something akin to Nested Loops (the hashing function stops running and partitioning things).
This usually happens when there are lots of duplicates involved in a join that makes continuing to partition values ineffective.
It’s a pretty atypical situation, and I really had to push (read: hint the crap out of) a query in order to get it to happen.
I also had to join on some pretty dumb columns.
Dupe-A-Dupe
Here’s a regular row store query. Bad idea hints and joins galore.
SELECT * FROM dbo.Posts AS p LEFT JOIN dbo.Votes AS v ON p.PostTypeId = v.VoteTypeId WHERE ISNULL(v.UserId, v.VoteTypeId) IS NULL OPTION ( HASH JOIN, -- If I don't force this, the optimizer chooses Sort Merge. Smart! USE HINT('ENABLE_PARALLEL_PLAN_PREFERENCE'), MAX_GRANT_PERCENT = 0.0 );
As it runs, the duplicate-filled columns being forced to hash join with a tiny memory grant cause a bunch of problems.
This behavior is sort of documented, at least.
The value is a constant, hard coded in the product, and its value is five (5). This means that before the hash scan operator resorts to a sort based algorithm for any given subpartition that doesn’t fit into the granted memory from the workspace, five previous attempts to subdivide the original partition into smaller partitions must have happened.
At runtime, whenever a hash iterator must recursively subdivide a partition because the original one doesn’t fit into memory the recursion level counter for that partition is incremented by one. If anyone is subscribed to receive the Hash Warning event class, the first partition that has to recursively execute to such level of depth produces a Hash Warning event (with EventSubClass equals 1 = Bailout) indicating in the Integer Data column what is that level that has been reached. But if any other partition later also reaches any level of recursion that has already been reached by other partition, the event is not produced again.
It’s also worth mentioning that given the way the event reporting code is written, when a bail-out occurs, not only the Hash Warning event class with EventSubClass set to 1 (Bailout) is reported but, immediately after that, another Hash Warning event is reported with EventSubClass set to 0 (Recursion) and Integer Data reporting one level deeper (six).
But It’s Different With Batch Mode
If I get batch mode involved, that changes.
CREATE TABLE #hijinks (i INT NOT NULL, INDEX h CLUSTERED COLUMNSTORE); SELECT * FROM dbo.Posts AS p LEFT JOIN dbo.Votes AS v ON p.PostTypeId = v.VoteTypeId LEFT JOIN #hijinks AS h ON 1 = 0 WHERE ISNULL(v.UserId, v.VoteTypeId) IS NULL OPTION ( HASH JOIN, -- If I don't force this, the optimizer chooses Sort Merge. Smart! USE HINT('ENABLE_PARALLEL_PLAN_PREFERENCE'), MAX_GRANT_PERCENT = 0.0 );
The plan yields several batch mode operations, but now we start bailing out after three recursions.
I’m not sure why, and I’ve never seen it mentioned anywhere else.
My only guess is that the threshold is lower because column store and batch mode are a bit more memory hungry than their row store counterparts.
Thanks for reading!
Going Further
If this is the kind of SQL Server stuff you love learning about, you’ll love my training. I’m offering a 75% discount to my blog readers if you click from here. I’m also available for consulting if you just don’t have time for that and need to solve performance problems quickly.