Query Tuning SQL Server 2019 Part 3: Who Died And Made You The Optimizer?

Be Yourself


We’ve got a problem, Sam Houston. We’ve got a problem with a query that has some strange issues.

It’s not parameter sniffing, but it sure could feel like it.

  • When the procedure compiles and runs with VoteTypeId 5, it runs for 12 minutes
  • Other VoteTypeIds run well with the same plan that VoteTypeId 5 gets
  • When VoteTypeId 5 runs with a “small” plan, it does okay at 10 seconds

Allow me to ruin a graph to illustrate. The Y axis is still seconds, but… it goes up a little higher now.

SQL Server Query Execution Times
weigh-in

The Frustration (A Minor Digression)


Here’s where life can be tough when it comes to troubleshooting actual parameter sniffing.

If you’re relying solely on the plan cache, you’re screwed. You’ll see the plan, and the compile value, but you won’t have the runtime value anywhere that “caused” the problem. In other words, the set of parameters that were adversely affected by the query plan that didn’t fit.

There are some things that can help, like if you’re watching it happen live, or if you have a monitoring tool that might capture runtime parameters.

OR IF YOU USE SP UNDERSCORE HUMANEVENTS.

Like I said, this isn’t parameter sniffing, but it feels like it.

It could extra-feel like it because you might see a misbehaving query, and a compile-time parameter that runs quickly on its own when you test it, e.g. VoteTypeId 6.

It would be really hard to tell that even if a plan were to compile specifically for a different parameter, it would still run for 12 minutes.

Heck, that’d even catch me off-guard.

But that’s what we have here: VoteTypeId 5 gets a bad plan special for VoteTypeId 5.

Examiner


Let’s dig in on what’s happening to cause us such remarkable grief. There has to be a reason.

I don’t need more grief without reason; I’ve already got a public school education.

SQL Server Query Plan
I WANT TO KNOW

If we were to summarize the problem here: that Hash Match Left Anti Semi Join runs for 12 minutes on its own.

No other operator, or group of operators, is responsible for a significant amount of time comparatively.

Magnifier


Some things to note:

  • The bad estimates aren’t from predicates, they’re from Batch Mode Bitmaps
  • Those bad estimates end up producing a much larger number of rows from the Adaptive Join
  • The Hash Match ends up needing to probe 932 million rows

 

SQL Server Query Plan
el disastero

Taking 12 minutes to probe 932 million rows is probably to be expected, now that I think about it.

If the optimizer had a good estimate from the Bitmaps here, it may have done the opposite of what a certain Pacific Island Dwelling Bird said:

Getting every nuance of this sort of relational transformation correct can be tricky. It is very handy that the optimizer team put the effort in so we do not have to explore these tricky rewrites manually (e.g. by changing the query text). If nothing else, it would be extremely tedious to write all the different query forms out by hand just to see which one performed better in practice. Never mind choosing a different version depending on current statistics and the number of changes to the table.

In this case, the Aggregate happens after the join. If the estimate were correct, or even in the right spacetime dimension, this would be fine.

We can gauge the general efficiency of it by looking at when this plan is used for other parameters that produce numbers of rows that are closer to this estimate.

SQL Server Query Plan
huey

If the optimizer had made a good guess for this parameter, it may have changed the plan to put an aggregate before the join.

Unfortunately we have very little control over estimates for Bitmaps, and the guesses for Batch Mode Bitmaps are a Shrug of Atlassian proportions.

Finisher


We’ve learned some things:

  1. This isn’t parameter sniffing
  2. Batch Mode Bitmaps wear pants on their head
  3. Batch Mode Bitmaps set their head-pants on fire
  4. Most of the time Batch Mode performance covers this up
  5. The plan for VoteTypeId 5 is not a good plan for VoteTypeId 5
  6. The plan for VoteTypeId 5 is great for a lot of other VoteTypeIds

In tomorrow’s post, we’ll look at how we can fix the problem.

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.

Query Tuning SQL Server 2019 Part 2: Big Databases, Big Ideas

Are We Still Friends?


When I first wrote this demo, I called it dbo.ParameterSniffingMonstrosity.

Because , you know, it’s really terrible.

CREATE OR ALTER PROCEDURE dbo.VoteSniffing( @VoteTypeId INT )
AS
SET XACT_ABORT, NOCOUNT ON;
    BEGIN
        SELECT   ISNULL(v.UserId, 0) AS UserId,
                 SUM(CASE WHEN v.CreationDate >= '20190101'
                          AND  v.CreationDate < '20200101'
                          THEN 1
                          ELSE 0
                     END) AS Votes2019,
                 SUM(CASE WHEN v.BountyAmount IS NULL
                          THEN 0
                          ELSE 1
                     END) AS TotalBounty,
                 COUNT(DISTINCT v.PostId) AS PostCount,
                 @VoteTypeId AS VoteTypeId
        FROM     dbo.Votes AS v
        WHERE    v.VoteTypeId = @VoteTypeId
        AND      NOT EXISTS
                (   
                    SELECT 1/0
                    FROM dbo.Posts AS p
                    JOIN dbo.Badges AS b 
                        ON b.UserId = p.OwnerUserId 
                    WHERE  p.OwnerUserId = v.UserId
                    AND    p.PostTypeId = 1 
                )
        GROUP BY v.UserId;
    END;
GO

The only parameter is for VoteTypeId, which has some pretty significant skew towards some types, especially in the full size Stack Overflow database.

SQL Server Query Results
Ask me about my commas

It’s like, when people tell you to index the most selective column first, well.

  • Sometimes it’s pretty selective.
  • Sometimes it’s not very selective

But this is exactly the type of data that causes parameter sniffing issues.

With almost any data set like this, you can draw a line or three, and values within each block can share a common plan pretty safely.

But crossing those lines, you run into issues where either little plans do far too much looping and seeking and sorting for “big” values, and big plans do far too much hashing and scanning and aggregating for “little” values.

This isn’t always the exact case, but generally speaking you’ll observe something along these lines.

It’s definitely not the case for what we’re going to be looking at this week.

This week is far more interesting.

That’s why it’s a monstrosity.

Fertilizer


The indexes that I create to support this procedure look like so — I’ve started using compression since at this point in time, 2016 SP1 is commonplace enough that even people on Standard Edition can use them — and they work quite well for the majority of values and query plans.

CREATE INDEX igno
ON dbo.Posts 
    (OwnerUserId, PostTypeId)
    WHERE PostTypeId = 1 
WITH(MAXDOP = 8, SORT_IN_TEMPDB = ON, DATA_COMPRESSION = ROW);
GO

CREATE INDEX rant
ON dbo.Votes 
    (VoteTypeId, UserId, PostId)
INCLUDE 
    (BountyAmount, CreationDate) 
WITH(MAXDOP = 8, SORT_IN_TEMPDB = ON, DATA_COMPRESSION = ROW);
GO 

CREATE INDEX clown ON dbo.Badges( UserId ) 
WITH(MAXDOP = 8, SORT_IN_TEMPDB = ON, DATA_COMPRESSION = ROW);
GO

If there are other indexes you’d like to test, you can do that locally.

What I want to point out is that for many values of VoteTypeId, the optimizer comes up with very good, very fast plans.

Good job, optimizer.

In fact, for any of these runs, you’ll get a good enough plan for any of the other values. They share well.

EXEC dbo.VoteSniffing @VoteTypeId = 4;
EXEC dbo.VoteSniffing @VoteTypeId = 6;
EXEC dbo.VoteSniffing @VoteTypeId = 7;
EXEC dbo.VoteSniffing @VoteTypeId = 9;
EXEC dbo.VoteSniffing @VoteTypeId = 11;
EXEC dbo.VoteSniffing @VoteTypeId = 12;
EXEC dbo.VoteSniffing @VoteTypeId = 13;
EXEC dbo.VoteSniffing @VoteTypeId = 14;
EXEC dbo.VoteSniffing @VoteTypeId = 15;
EXEC dbo.VoteSniffing @VoteTypeId = 16;

VoteTypeIds 1, 2, 3, 5, 8, and 10 have some quirks, but even they mostly do okay using one of these plans.

There are two plans you may see occur for these.

Plan 1

SQL Server Query Plan
teeny tiny

Plan 2

SQL Server Query Plan
it has adapted

Particulars & Peculiars


Plan 1 is first generated when the procedure is compiled with VoteTypeId 4, and Plan 2 is first generated when the procedure is compiled with VoteTypeId 6.

There’s a third plan that only gets generated when VoteTypeId 2 is compiled first, but we’ll have to save that for another post, because it’s totally different.

Here’s how each of those plans works across other possible parameters.

SQL Server Query Execution Times
this is my first graph

Plan 1 is grey, Plan 2 is blue. It’s pretty easy to see where each one is successful, and then not so much. Anything < 100ms got a 0.

The Y axis is runtime in seconds. A couple are quite bad. Most are decent to okay.

Plans for Type 2 & 8 obviously stick out, but for different plans.

This is one of those things I need to warn people about when they get wrapped up in:

  • Forcing a plan (e.g. via Query Store or a plan guide)
  • Optimizing for unknown
  • Optimizing for a specific value
  • Recompiling every time (that backfires in a couple cases here that I’m not covering right now)

One thing I need to point out is that Plan 2 doesn’t have an entry here for VoteTypeId 5. Why?

Because when it inherits the plan for VoteTypeId 6, it runs for 17 minutes.

SQL Server Query Plan
singalong

This is probably where you’re wondering “okay, so what plan does 5 get when it runs on its own? Is this the mysterious Plan 4 From Outer Space?”

Unfortunately, the plan that gets generated for VoteTypeId 5 is… the same one that gets generated for VoteTypeId 6, but 6 has a much smaller memory grant.

If you’re not used to reading operator times in execution plans, check out my video here.

Since this plan is all Batch Mode operators, each operator will track its time individually.

The Non-Switch


VoteTypeId 5 runtime, VoteTypeId 6 compile time

If I were to put a 17 minute runtime in the graph (>1000 seconds), it would defeat the purpose of graphing things.

Note the Hash Match has, by itself, 16 minutes and 44 seconds of runtime.

SQL Server Query Plan
pyramids

VoteTypeId 5 runtime, and compile time

This isn’t awesome, either.

The Hash Join, without spilling, has 12 minutes and 16 seconds of runtime.

SQL Server Query Plan
lost

Big Differentsiz


You have the same plan shape and operators. Even the Adaptive Join follows the same path to hash instead of loop.

Sure, the spills account for ~4 minutes of extra time. They are fairly big spills.

But the plan for VoteTypeId 5, even when compiled specifically for VoteTypeId 5… sucks, and sucks royally.

There are some dismally bad estimates, but where do they come from?

We just created these indexes, and data isn’t magically changing on my laptop.

TUNE IN TOMORROW!

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.

Query Tuning SQL Server 2019 Part 1: Changing Databases

Teeth To Grit


I’ve always had trouble standing still on SQL Server versions, but most companies don’t. Hardly anyone I talk to is on SQL Server 2017, though these days SQL Server 2016 seems more common than SQL Server 2012, so at least there’s that. Mostly I’m happy to not see SQL Server 2014. God I hate SQL Server 2014.

Despite the lack of adoption, I’ve been moving all my training material to SQL Server 2019. Heck, in a few years, my old posts might come in handy for you.

But during that process, I kept running into the same problem: The demos generally still worked for the OLTP-ish queries, but for the report-ish queries Batch Mode On Rowstore (BMOR, from here) was kicking butt (most of the time anyway, we’re gonna look at some hi-jinks this week).

The problem, so far as I could tell, was that the Stack Overflow 2013 database just wasn’t enough database for SQL Server 2019 (at least with my hardware). My laptop is quad core (8 with HT) @2.9GHz, with 64GB of RAM, and max server memory set to 50GB. The SO2013 database is… just about 50GB.

While it’s fun to be able to create performance problems even with the whole database in memory, it doesn’t match what lot of people are dealing with in real life.

Especially you poor saps on Standard Edition.

My options seemed to be:

  • Drop max server memory down
  • Use a VM with lower memory
  • Use the full size Stack Overflow database

Flipping and Flopping


Each of these has problems, though.

Dropping max server memory down is okay for the buffer pool, but SQL Server (it seems especially with column store/batch mode) is keen to use memory above that for other things like memory grants.

A lot of the interesting struggle I see on client servers between the buffer pool and query memory grants didn’t happen when I did that.

Using a VM with lower memory, while convenient, just didn’t seem as fun. Plus, part of the problem is that, while I make fun of other sample databases for being unrealistically tiny, at least they have relatively modern dates in some of them.

I was starting to feel really goofy having time stop on January 31st, 2013.

I suppose I could have updated all the CreationDate columns to modernize things, but who knows what that would have thrown off.

Plus, here’s a dirty little secret: all the date columns that start with “Last” that track stuff like when someone last logged in, or when a post was last active/edited, they don’t stop at 2013-12-31. They extend up to when the database was originally chopped down to size, in 2017 or so. I always found that a bit jarring, and I’d have to go and add time to them, too, to preserve the gaps.

It all starts to feel a bit like revisionist history.

The End Is Thigh


In the end, I settled on using the most recent version available here, but with a couple of the tables I don’t regularly use in demos cut out: PostHistory, and PostLinks. Once you drop those out, a 360GB database drops down to a much more manageable 150Gb or so.

If you’d like to get a copy, here’s the magnet link.

SQL Server Database Properties
Four users, huh?

The nice thing is that the general cadence of the data is the same in many ways and places, so it doesn’t take a lot to adjust demos to work here. Certain Post and Vote Types, User Ids, Reputations, etc. remain skewed, and outliers are easy to find. Plus, at 3:1 data to memory, it’s a lot harder to keep everything safely in the buffer pool.

This does present different challenges, like index create time to set up for things, database distribution, etc.

But if I can give you better demos, that seems worth it.

Plus, I hear everything is in the cloud now anyway.

Alluding To


In the process of taking old demos and seeing how they work with the new database, I discovered some interesting stuff that I want to highlight a little bit. So far as I can tell, they’re not terribly common (yet), but that’s what makes them interesting.

If you’re the kind of person who’s looking forward to SQL Server 2019’s performance features solving some problems for you auto-magick-ally, these may be things you need to watch out for, and depending on your workload they may end up being quite a bit more common than I perceive.

I’m going to be specifically focusing on how BMOR (and to some extent Adaptive Joins) can end up not solving performance issues, and how you may end up having to do some good ol’ fashion query tuning on your own.

In the next post, we’ll look at how one of my favorite demos continues to keep on giving.

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.

Raising Errors And Eyebrows With RAISERROR In SQL Server

Part One


I had a client get in touch recently because they thought they had corruption. That’s not normally my cup of tea, but I offered to jump on and take a look.

We got to talking, and they said users had been getting some nasty error messages. Stuff like this:

Msg 2745, Level 16, State 2, Procedure dbo.oh_no, Line 8 [Batch Start Line 13]
Process ID 56 has raised user error 50000, severity 20. SQL Server is terminating this process.
Msg 50000, Level 20, State 1, Procedure dbo.oh_no, Line 8 [Batch Start Line 13]
SCARY THINGS
Msg 596, Level 21, State 1, Line 13
Cannot continue the execution because the session is in the kill state.
Msg 0, Level 20, State 0, Line 13
A severe error occurred on the current command.  The results, if any, should be discarded

I don’t know if you’ve ever seen corruption error messages, but they’re… uh. Look, there’s a reason I send people to Microsoft. It’s $500 well spent.

Thankfully this was just an overzealous new developer who wanted to show people the wonders and miracles of error handling.

If you use RAISERROR with a severity level of 20 or higher, and the WITH LOG clause, it kills the connection. Without the WITH LOG CLAUSE, you won’t get a proper error, and it also wouldn’t enter a CATCH block.

CREATE OR ALTER PROCEDURE dbo.oh_no
AS
SET NOCOUNT, XACT_ABORT ON; 
BEGIN

    IF 1 < 2
        BEGIN
            RAISERROR('SCARY THINGS', 20, 1) WITH LOG, NOWAIT;
        END;

END;
GO 

EXEC dbo.oh_no;
GO

What you return to people can be mighty confusing. Especially if your application only shows the first error message. Ahem.

A SQL Server Error Message
Mercy

I’d probably be worried if I started seeing that pop up, too.

Part Two


Only somewhat related to part one, but probably a good time to mention it: If you want RAISERROR to get you to a TRY/CATCH block and throw an error, you have to use a severity 16 or higher and WITH LOG. For instance, this won’t work:

CREATE OR ALTER PROCEDURE dbo.oh_yes
AS
SET NOCOUNT, XACT_ABORT ON; 
BEGIN

    IF 1 < 2
        BEGIN TRY
            
            RAISERROR('IS THIS THING ON?', 10, 1) WITH LOG, NOWAIT;
        
            SELECT 'NO SHUT UP' AS NO_U;

        END TRY
        BEGIN CATCH
            THROW;
        END CATCH;
END;

EXEC dbo.oh_yes;

Your code will keep right on chugging along.

SQL Server Query Results
Chippy

Despite the fact that an “error” was raised.

Are you German?

You need to give this old hound a higher severity (and WITH LOG) to get it to bark.

RAISERROR('IS THIS THING ON?', 16, 1) WITH LOG, NOWAIT;

Then you get it back in red.

SQL Server Error Message
I used to be like you.

Parting


Error handling is good for all sorts of things. You can retry deadlocks, handle and log edge cases, keep loops alive, and more.

RAISERROR is useful for figuring out where you’re at in some logic — and I know, it’s deprecated — but I find using the combination of THROW and FORMATMESSAGE tedious and cumbersome. I don’t want to add things to sys.messages. I don’t want to use two functions instead of one to pretty print some information. What’s the point?

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.

Erik Does SQL Server Devops

No, Not Really


But I did write a silly helper procedure for myself, that I figured I’d share here. I’m not putting it on GitHub right now (unless you all find it incredibly useful).

I often have multiple copies of StackOverflow for demos. Smaller databases, ones in different compat levels, and other weird tweaks to test things (this is easier and less confusing than remembering to reset everything, and freaking out when old demos stop working because I forgot).

What I always got annoyed with was going to try something in a different database, and remembering that a procedure, function, or view I created wasn’t there, or had changed.

With that, I give you sp_MoveGuts.

A simple call looks like this:

EXEC dbo.sp_MoveGuts @SourceDatabase = N'StackOverflow2013', --where to take stuff
                     @TargetDatabase = N'StackOverflow', --where to make stuff
                     @CreateOnly = 0, --Will alter objects if they already exist
                     @Debug = 1 --optional;

You need to give it a source database, a target database, and tell it whether you want to only create new objects, or also alter any existing objects.

There are certain things I’m not going to spend time trying to fix, like dependencies. If you have code that references tables that don’t exist in other databases, errors will be logged and you’ll be notified. But I’m not going to move physical data in this thing. No way, no how. That part is up to you.

Anyway, here it is.

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.

The Code


CREATE OR ALTER PROCEDURE dbo.sp_MoveGuts
  @SourceDatabase sysname = N'', 
  @TargetDatabase sysname = N'',
  @CreateOnly BIT = 0,
  @Debug BIT = 0
  AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
 
DECLARE @get_sql NVARCHAR(MAX) = N'';

CREATE TABLE #mover
(
    id INT IDENTITY PRIMARY KEY CLUSTERED,
    object_id BIGINT,
    target_database sysname,
    object_name sysname,
    definition NVARCHAR(MAX),
    alter_definition 
           AS STUFF(definition, CHARINDEX(N'CREATE', definition), 6, N'ALTER')
);

DECLARE @logger TABLE
(
    id INT IDENTITY PRIMARY KEY CLUSTERED, 
    object_name sysname, 
    error_number INT, 
    error_message NVARCHAR(2048)
);


SET @get_sql += N'
SELECT asm.object_id,
       @iTargetDatabase,
       ISNULL(OBJECT_NAME(asm.object_id, DB_ID(@iSourceDatabase)), ''a_trigger'') AS object_name,
       LTRIM(RTRIM(asm.definition)) as definition
FROM ' + QUOTENAME(@SourceDatabase) + N'.sys.sql_modules AS asm;
' + CHAR(13);

IF @Debug = 1 BEGIN RAISERROR(@get_sql, 0, 1) WITH NOWAIT; END;

INSERT #mover ( object_id, target_database, object_name, definition )
EXEC sys.sp_executesql @get_sql, N'@iTargetDatabase sysname, @iSourceDatabase sysname', @TargetDatabase, @SourceDatabase;


IF @Debug = 1 BEGIN SELECT * FROM #mover AS m; END;

DECLARE @min_id INT;
DECLARE @max_id INT;
DECLARE @spe NVARCHAR(MAX) = QUOTENAME(@TargetDatabase) + N'.sys.sp_executesql ';
DECLARE @def_sql NVARCHAR(MAX) = N'';
DECLARE @def_sql_alt NVARCHAR(MAX) = N'';
DECLARE @object_name sysname = N''

SELECT @min_id = MIN(m.id), 
       @max_id = MAX(m.id)
FROM #mover AS m
OPTION(RECOMPILE);

    WHILE @min_id <= @max_id
    BEGIN
    BEGIN TRY 
    
        SELECT @object_name = m.object_name,
               @def_sql = m.definition,
               @def_sql_alt = m.alter_definition
        FROM #mover AS m
        WHERE m.id = @min_id
        OPTION (RECOMPILE);
           
        IF @Debug = 1 BEGIN RAISERROR(N'creating %s using %s', 0, 1, @object_name, @spe) WITH NOWAIT; END;        
        EXEC @spe @def_sql;

        IF @Debug = 1 BEGIN RAISERROR(N'Setting next id after %i out of %i total', 0, 1, @min_id, @max_id) WITH NOWAIT; END;
        
        SET @min_id = 
        (
            SELECT TOP (1) m.id
            FROM #mover AS m
            WHERE m.id > @min_id
            ORDER BY m.id
        );

        IF (@min_id IS NULL 
              OR @min_id = @max_id)
        BREAK;
     
     END TRY

     BEGIN CATCH
     IF XACT_STATE() <> 0 
         ROLLBACK TRANSACTION;

         IF (ERROR_NUMBER() = 2714
                AND @CreateOnly = 0 )
             BEGIN

                 IF @Debug = 1 BEGIN RAISERROR(N'Object exists, altering %s using %s instead', 0, 1, @object_name, @spe) WITH NOWAIT; END;        
                 EXEC @spe @def_sql_alt;

                IF @Debug = 1 BEGIN RAISERROR(N'Setting next id after %i out of %i total', 0, 1, @min_id, @max_id) WITH NOWAIT; END;
                
                SET @min_id = 
                (
                    SELECT TOP (1) m.id
                    FROM #mover AS m
                    WHERE m.id > @min_id
                    ORDER BY m.id
                );
                
                IF (@min_id IS NULL 
                      OR @min_id = @max_id)
                BREAK;
                
             END
         ELSE 
         BEGIN
             INSERT @logger 
                  ( object_name, error_number, error_message )
             SELECT @object_name, ERROR_NUMBER(), ERROR_MESSAGE();

             SET @min_id = 
             (
                 SELECT TOP (1) m.id
                 FROM #mover AS m
                 WHERE m.id > @min_id
                 ORDER BY m.id
             );
             
             IF (@min_id IS NULL 
                   OR @min_id = @max_id)
             BREAK;
         END

     END CATCH
END;
    
    IF EXISTS (SELECT 1/0 FROM @logger AS l)
        BEGIN
            SELECT N'errors' AS errors, * FROM @logger AS l ORDER BY l.id;
        END

END;
GO

 

SQL Server’s Optimizer Could Be Smarter About Constraints

Couldn’t We All


Let me ask you a question: If I told you that all the numbers in an integer column were either:

  • > 0
  • >= 1

You’d probably agree that the lower number you could possible see is 1.

And that’s exactly the case with the Reputation column in Stack Overflow.

Non-Alignment Pact


Assume that I am being truthful about creating this index:

CREATE INDEX constraints_are_silly
ON dbo.Users
(
Reputation,
UpVotes
) INCLUDE (DisplayName);

Also assume that this is the most important query ever written by human hands for the benefit of humanity:

SELECT TOP (1000)
u.Reputation,
u.UpVotes,
u.DisplayName
FROM dbo.Users AS u
WHERE u.Reputation <= 1
ORDER BY u.UpVotes;

However, I’m dissatisfied with the query plan. This requires no assumption. It’s a bit much for what I’m asking, I think.

SQL Server Query Plan
Overkill

Since a current implementation rule for the database is that no one can have a Reputation of 0 or less, I add this constraint hoping that SQL Server will see this and stop sorting data, because it knows that 1 is the lowest integer it will find, and the order of UpVotes won’t reset for Reputation = 0.

ALTER TABLE dbo.Users ADD CONSTRAINT checko CHECK (Reputation > 0);

But I still end up with the same execution plan. In neither case is the plan a) trivial, or b) simple parameterized. We can’t blame the optimizer trying to be helpful.

Now assume that I get really mad and change my constraint. This requires minimal assumption.

ALTER TABLE dbo.Users ADD CONSTRAINT checko CHECK (Reputation >= 1);

And now I get a query plan that does not have a sort in it. My approval does not require assumption.

SQL Server Query Plan
Hi I’m over here

Why does one constraint remove the need to sort, and one not?

Over My Head


The answer is in the query plan. Sometimes I have to be reminded to look at these.

SQL Server Query Plan
Life Stinks
  • The Seek Predicate on the left is from when we defined the constraint as > 0. It has a <= 1 predicate.
  • The Seek Predicate on the right is an equality on = 1

For a little more detail, I asked a question. Apparently the optimizer… doesn’t consider data types here.

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.

Enabling Optimize For Sequential Key For A SQL Server Index

Sweetness, Shortness


Recently I blogged about last page contention solutions, and one thing I mentioned is that turning on OPTIMIZE_FOR_SEQUENTIAL_KEY doesn’t require you to rebuild an index.  That’s awesome, because a whole lot of changes to indexes require you to rebuild them.

So how exactly do you do that?

Either when you create the table:

CREATE TABLE dbo.Votes_Insert
(
    Id INT IDENTITY(1, 1) NOT NULL,
    PostId INT NOT NULL,
    UserId INT NULL,
    BountyAmount INT NULL,
    VoteTypeId INT NOT NULL,
    CreationDate DATETIME NOT NULL,
    CONSTRAINT PK_Votes_Insert_Id
        PRIMARY KEY CLUSTERED (Id ASC) 
    WITH (OPTIMIZE_FOR_SEQUENTIAL_KEY  = ON) 
);

When you create the index:

CREATE INDEX so_optimized 
    ON dbo.Votes_Insert (Id) WITH (OPTIMIZE_FOR_SEQUENTIAL_KEY = ON);

Or if you need to alter an existing index:

ALTER INDEX so_optimized 
    ON dbo.Votes_Insert SET(OPTIMIZE_FOR_SEQUENTIAL_KEY  = ON);

Get Back To Work


You’ll find this post again in a few years when you finally migrate to SQL Server 2019.

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.

Check Out My Presentation: Better Troubleshooting With sp_HumanEvents

Group Me Baby


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.

Updates To sp_HumanEvents and sp_PressureDetector And More!

Ain’t Nothing To Do


I’ve made some changes to my scripts! Exciting. Sorta. Kinda.

sp_PressureDetector

Got a name change. The code hasn’t changed at all. I started getting annoyed by the formatting and the underscore between pressure and detector.

Now it looks a whole lot more like other scripts in your arsenal.

Heh. Arse… nal.

Go get it here.

sp_HumanEvents

There have been a ton of improvements and fixes here. I realize it’s bad form to generalize like this, but I went into overdrive making sure things were nice and tidy for GroupBy. There have been improvements to XML querying and processing, how data gets pulled in and correlated, and finally how it gets displayed.

Go get it here.

get_letters and get_numbers

What sweet little helper functions! They’ve been updated to include versions that don’t require a numbers table.

Go get them here.

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.

Troubleshooting Parameter Sniffing In SQL Server With sp_WhoIsActive

THANKS ADAM


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.