I’ve posted previously about how a foreign key constraint can change how a SELECT query behaves. Logically that just makes sense. But other types of constraints don’t affect execution plans do they?
Yes.
Let’s take this constraint as an example:
ALTER TABLE Sales.SalesOrderDetail WITH CHECK ADD Â CONSTRAINT CK_SalesOrderDetail_UnitPrice CHECK Â ((UnitPrice>=(0.00)))
That will ensure that no values less than zero can slip in there. We can even validate it:
INSERT Sales.SalesOrderDetail (SalesOrderID, CarrierTrackingNumber, OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate ) VALUES (60176, -- SalesOrderID - int N'XYZ123', -- CarrierTrackingNumber - nvarchar(25) 1, -- OrderQty - smallint 873, -- ProductID - int 1, -- SpecialOfferID - int -22, -- UnitPrice - money 0.0, -- UnitPriceDiscount - money NEWID(), -- rowguid - uniqueidentifier GETDATE() -- ModifiedDate - datetime );
Will give me an error:
Msg 547, Level 16, State 0, Line 470
The INSERT statement conflicted with the CHECK constraint “CK_SalesOrderDetail_UnitPrice”. The conflict occurred in database “AdventureWorks2014”, table “Sales.SalesOrderDetail”, column ‘UnitPrice’.
Let’s look at a SELECT query now. If we run this:
SELECT soh.OrderDate, soh.ShipDate, sod.OrderQty, sod.UnitPrice, p.Name AS ProductName FROM Sales.SalesOrderHeader AS soh JOIN Sales.SalesOrderDetail AS sod ON sod.SalesOrderID = soh.SalesOrderID JOIN Production.Product AS p ON p.ProductID = sod.ProductID WHERE p.Name = 'Water Bottle - 30 oz.';
The resulting execution plan looks like this:
But, if I modify the query to look like this, adding an additional AND filter on the constrained UnitPrice column:
SELECT soh.OrderDate, soh.ShipDate, sod.OrderQty, sod.UnitPrice, p.Name AS ProductName FROM Sales.SalesOrderHeader AS soh JOIN Sales.SalesOrderDetail AS sod ON sod.SalesOrderID = soh.SalesOrderID JOIN Production.Product AS p ON p.ProductID = sod.ProductID WHERE p.Name = 'Water Bottle - 30 oz.' AND sod.UnitPrice > $0.0;
You know what happens to the execution plan? Nothing. It stays exactly the same. The optimizer knows that in order to satisfy the query, it can safely ignore the change in the WHERE clause. In fact, you can look at the SELECT operator properties for the two different plans and note that while the Query Hash values changes, the Plan Hash value stays the same. The plans are identical.
With that knowledge, I’m going to modify the query to look like this, reversing the UnitPrice reference to look for data that violates the constraint:
SELECT soh.OrderDate, soh.ShipDate, sod.OrderQty, sod.UnitPrice, p.Name AS ProductName FROM Sales.SalesOrderHeader AS soh JOIN Sales.SalesOrderDetail AS sod ON sod.SalesOrderID = soh.SalesOrderID JOIN Production.Product AS p ON p.ProductID = sod.ProductID WHERE p.Name = 'Water Bottle - 30 oz.' AND sod.UnitPrice < $0.0;
And now we have a new execution plan:
The optimizer recognized that there is no way that any data can be returned with the WHERE clause above because there is an enforced constraint (note the use of the WITH CHECK clause on the constraint). This completely changes the execution plan in every possible way. Now, instead of attempting to access the data, a Constant Scan operator is put in as a place holder for an empty result set.
To sum up, yes, constraints absolutely affect the choices made by the optimizer when those constraints would have an affect on the plan, even a SELECT query. Also, I would argue, this means that the use of enforced constraints can be a performance enhancer since the optimizer can make intelligent choices about how a given query is dealt with.
UPDATE: Fixed a typo that said the constraint prevented data that was equal to or less than zero. It’s only for data less than zero.
Great post Grant!
It is worth noting that if the database option parameterization is set to “forced” then the optimizer can’t check for constraint violations at runtime because it parameterizes the literal $0.0.
If you do have forced parameterization you can give the optimizer the literal value by using OPTION(RECOMPILE) which will allow it to check for constraint violations.
Cheers!
Thanks Chris. I actually wasn’t aware of that one. I appreciate you posting it here.
A bit of SQL history here. Ingres began the idea of putting all — and I mean ALL — available constraints into the WHERE clauses. They added DDL and DCL stuff to each clause, then sent “The Query From Hell” to the SQL engine for processing.
I recently saw an article about Prolog getting revised in newer logic languages for business rules, etc
That sounds really wild. Glad I just have to deal with monsters that modern technology creates.
A minor point but your DDL actually creates the price constraint as >= to 0.0 not strictly greater than.
Perhaps a typo or cut & paste from earlier example 🙂
Yeah, you’re right, minor typo there.
Note that if the check constraint is not trusted SQL Server will need to run the regular query plan rather than the Constant Scan one.
SELECT * FROM sys.check_constraints WHERE is_not_trusted = 1
Yes. You’re 100% correct.