Still Not A Developer
I’m going to use a funny example to show you something weird that I see often in EF queries.
I’m not going to use EF to do it, because I have no idea how to. Please use your vast imagination.
In this case, I’m going to figure out if a user is trusted, and only if they are will I show them certain information.
The first part of the query establishes if the user is trusted or not.
I think this is silly because no one should ever trust users.
DECLARE @UserId INT = 22656, --2788872 @PostId INT = 11227809, @IsTrusted BIT = 0, @SQL NVARCHAR(MAX) = N''; SELECT @IsTrusted = CASE WHEN u.Reputation >= 10000 THEN 1 ELSE 0 END FROM dbo.Users AS u WHERE u.Id = @UserId;
The second part will query and join a few tables, but one of the joins (to the Votes table) will only run if a user is trusted.
SET @SQL = @SQL + N' SELECT p.Title, p.Score, c.Text, c.Score, v.* FROM dbo.Posts AS p LEFT JOIN dbo.Comments AS c ON p.Id = c.PostId LEFT JOIN dbo.Votes AS v ON p.Id = v.PostId AND 1 = @iIsTrusted WHERE p.Id = @iPostId AND p.PostTypeId = 1; '; EXEC sys.sp_executesql @SQL, N'@iIsTrusted BIT, @iPostId INT', @iIsTrusted = @IsTrusted, @iPostId = @PostId;
See where 1 = @iIsTrusted? That determines if the join runs at all.
Needless to say, adding an entire join in to the query might slow things down if we’re not prepared.
First I’m going to run it for user 2788872, who isn’t trusted.
This query finishes rather quickly (2 seconds), and has an interesting operator in it.
The filter has a startup expression in it, which means it’s sort of a gatekeeper, here. If the parameter is 0, we don’t touch Votes.
If it’s 1… Boy, do we touch Votes. This is another case of where cached plans can lie to us.
If we run this for user 22656 (Jon Skeet) afterwards, we will definitely need to touch the Votes table.
I grabbed the Live Query Plan to show you just how little progress it makes over 5 minutes.
The cached plan will look identical. And looking at the plan, it’ll be hard to believe there’s any way it could run >5 minutes.
If we clear the cache and run this for 22656 first, the plan runs relatively quickly, and looks a little different.
Running it for an untrusted user has a similar runtime. It’s not great, but it’s the better of the two.
It’s difficult to control EF queries with much granularity.
You could branch the application code to run two different queries based on if a user is trusted.
In a perfect world, you’d never even consider that join at all, and avoid having to worry about it.
On the plus side (at least in this case), the good plan for trusted users runs in the same time as the good plan for untrusted users, even though they’re different.
If you’re feeling extra confident, you can try adding an OPTIMIZE FOR hint to your code, or implementing a plan guide.
Thanks for reading!