It is well known that using constraints is
the most reliable way to enforce data integrity rules. So how do we go
about enforcing our previous business rule (the height of a box must be
less than, or equal to, the width; and the width must be less than, or
equal to, the length) in a constraint?
Our first attempt might look as shown in Listing 1.
The attempt to add this constraint fails, because some data in the table does not validate against it.
First, we need to remove the offending data, as shown in Listing 3.
In real life, we would probably want to clean up that data instead of
just deleting it, but we shall skip this step for brevity.
Unfortunately, in its current form our constraint is still flawed, as demonstrated by the fact that Listing 4 succeeds in adding a box that is taller than it is long.
In order to use constraints effectively, we need to understand how they work;
otherwise we may end up, as here, with a false sense of security and some dirty data in our database.
The problem here, as you may have guessed, is a failure to handle NULL
values correctly. This is one of several very common mistakes that are
made when using constraints, and over the coming sections we'll learn
what they are, and how to avoid them. Along the way, we'll fix our
constraint so that it works reliably.
1. Handling nulls in CHECK constraints
Logical conditions in CHECK constraints work differently from logical conditions in the WHERE clause. If a condition in a CHECK constraint evaluates to "unknown," then the row can still be inserted, but if a condition in a WHERE
clause evaluates to "unknown," then the row will not be included in the
result set. To demonstrate this difference, add another CHECK constraint to our Boxes table, as shown in Listing 5.
However, the condition used in the CHECK constraint will not prevent us from inserting a row with NULL length, as demonstrated in Listing 6.
However, this row will not validate against exactly the same condition in a WHERE clause.
Many SQL developers get into trouble because they fail to consider how NULL evaluates in logical expressions. For example, we've already proven that our constraint Boxes_ConsistentDimensions does not quite work; it validated a box with height greater than length.
Now we know enough to understand how that happened: the constraint will only forbid rows where the CHECK clause (HeightInInches <= WidthInInches AND WidthInInches <= LengthInInches) evaluates to FALSE. If either condition in the clause evaluates to UNKNOWN, and another evaluates to UNKNOWN or TRUE, then the overall clause evaluates to UNKNOWN and the row can be inserted. In this case, both conditions in our CHECK constraint evaluated as UNKNOWN.
When we develop constraints, we must take added care if the columns involved are nullable. The script in Listing 8 fixes our Boxes_ConsistentDimensions constraint.
Rerun Listing 4 and you'll find that, this time, the constraint will prevent invalid data from saving. The lesson here is simple: when testing CHECK constraints, always include in your test cases rows with NULLs.