One of the uses for the Query Store, added in SQL 2016, is to force plans. Once forced, plans are supposed to remain unchanged, however there are cases where a forced plan will not be applied and a new plan will be generated.
Statistics changes, which are one of the things that usually cause recompiles, don’t disable a forced plan. It would be kinda weird if it did and against the point of a forced plan.
Schema changes are another matter.
Let’s look at a couple of cases.
First, schema changes that make the plan invalid, in other words, schema changes that affect something that the plan explicitly references. There aren’t that many schema changes that can make the plan invalid without making the query invalid as well, but there are a couple. Index changes, for example.
I want to test a few things:
- An index change that won’t make the plan invalid (eg adding a column)
- An index change that does make the plan invalid (eg removing a column that the query needs)
- Renaming the index without changing its definition
- Adding an index that would be better for the query than the one referenced by the forced plan.
First, the setup. My Interstellar Trading database with an extra index added:
CREATE INDEX idx_ForcingTest on Shipments (ClientID, HasHazardous, HasLiveStock, HasTemperatureControlled)
The query I’ll be running to test is
DECLARE @Storage TABLE (ID INT, Priority TINYINT, CountShipments INT);
INSERT INTO @Storage
SELECT OriginStationID, Priority, COUNT(*) FROM Shipments WHERE ClientID = 17 AND HasHazardous = 1
GROUP BY OriginStationID, Priority
It’s inserting into a table variable to prevent any problems with the resultsets in SSMS.
I’ve been running the query for a while, and its plan is forced.
CREATE INDEX idx_ForcingTest ON Shipments (ClientID, HasHazardous, HasLiveStock, HasTemperatureControlled, ReferenceNumber)
WITH (DROP_EXISTING = ON)
No change. Forced plan is still forced.
Now, let’s make that index less useful, by removing a column that the query does need. There’s a key lookup in the plan, so there is a way for the column to be obtained, but it would change what columns come from each operator and where the filters are being done. Same plan shape, but different details.
CREATE INDEX idx_ForcingTest ON Shipments (ClientID)
WITH (DROP_EXISTING = ON)
We get a new plan. The forced one is invalid, because the index no longer allows for the seek predicates defined in the plan, and so the forcing is ignored and we get a new plan.
The query still runs without error, which is better than we’d have had using the old USE PLAN hint.
Once I revert the index back to its original definition, the forced plan starts being used again.
How about renaming the index? Since the plan references the index by name, this will probably also cause the plan forcing to fail.
And indeed it does.
One last test. I’m going to rename the index back to its old name, and then add a new one that’s better for the query than the index referenced in the forced plan.
And we’re still using the forced plan. The addition of a new index did not invalidate the existing plan, and hence the forced plan will still be used, even when there’s a better index.
This is the reason why I recommend using plan forcing only to fix stuff that’s broken in prod, and to find a solution without forced plans for the long term. It’s not always possible but where it is I’d prefer not to leave the plan forcing in place, because it does mean that new indexes are not considered. Plus, if the query store is ever cleared, the forced plan (along with the forcing) are gone.