The example is completely new and involves an inventory system
that is used to store the history of changes to an inventory of items. Listing 1 creates the InventoryLog table, for this purpose.
To record any change in the inventory, we insert rows into the Inventory table. At the very beginning, when we do not have any rows in the InventoryLog
table, we assume that we do not have any items in our inventory at all.
An increase or decrease in inventory for a given item is reflected by
adding new rows. For example, if we need to record the fact that we
added five items of a given type to our inventory, we add a row with ChangeQuantity=5. If we withdraw three items, we add another row with ChangeQuantity=-3.
Our business rule states that, for a given item, a
change in the inventory cannot be allowed if it will result in a net
inventory that is less than zero. Of course, we cannot take out more of
an item than we currently have in stock. In order to determine the
current inventory amount for a given item, we must query the history of
all inventory changes for the item, i.e. calculate the net total in the ChangeQuantity column, resulting from all previous transactions, and make sure that the proposed ChangeQuantity
would not result in negative inventory. This is the sort of calculation
that would traditionally be performed in a trigger but, in order to
make the trigger watertight, the code would be quite complex. Instead,
we'll perform the calculation as part of our INSERT statement, using a HAVING clause to prevent illegal withdrawals.
Listing 2
provides several examples of valid and invalid changes to the
inventory. For simplicity, we disallow retrospective logging. For
example, if we have recorded for Item1 an inventory change for 20100103
then we cannot subsequently log an inventory change for the same item
with a date earlier than 20100103. In a production system, we would
encapsulate our modification code in a stored procedure, which can be
called for all changes. Here, however, we shall just use the simplest
possible code that demonstrates the technique. Note that in several
places we use the HAVING clause without GROUP BY. This is a perfectly correct usage; we don't really need a GROUP BY clause to select aggregates such as SUM, MIN, and AVG, as long as we only select aggregates and not column values. Likewise, we don't need a GROUP BY clause if we want to use such aggregates in the HAVING clause, as long as column values are not used in that clause.
For the tiny number of rows in Listing 2, it is not a problem to perform a SUM on the ChangeQuantity
column every time we change the inventory. However, for a real
inventory system, with hundreds of thousands of transactions per item,
this becomes a huge performance drain.
To eliminate this expensive querying of historical
inventory changes, it is very tempting to store the new stock quantity
of a given item that results from an inventory change, along with the
change amount. Let's add one more column to InventoryLog table to store the current amount, as shown in Listing 3. At the same time, we add a CHECK constraint to make sure that CurrentQuantity is never negative.
Now, instead of querying the history of changes, we need only look up the CurrentQuantity value for the most recent row, and add it to the proposed ChangeQuantity, as shown in Listing 4.
This appears to suit our requirements, but unfortunately we have nothing that guarantees that the value of CurrentQuantity in the latest row is, indeed, the correct current quantity in stock.
To be more specific, there are currently many ways in which we can violate our business rules. To name just a few:
we can retrospectively delete or update a row
from the log, and end up invalidating the whole log trail – for
example, if we retrospectively deleted the log entry for Jan 1st in Listing 4, it immediately invalidates the withdrawal on Jan 5th
we can retrospectively update ChangeQuantity and fail to modify CurrentQuantity accordingly
we can manually update CurrentQuantity, or set it to the wrong value when adding a new row.
In order to make our inventory system robust, we
require a reasonably complex "network" of interacting constraints. To
fully understand how it all fits together will probably require some
careful thought and experimentation. With that forewarning, let's take a
look at the solution.
We need to find a way to ensure that the value stored in the CurrentQuantity
column is always correct, which is a bigger challenge than it may
sound. In order to guarantee this, we'll need to create several more
constraints, and add some additional columns to our table.
First, we need to add two new columns, PreviousQuantity and PreviousChangeDate, as shown in Listing 5, in order to accurately navigate the chain of rows that modify the same item.
In our first solution, the user simply had to enter a
change quantity and a date (alongside the ID of the item). In our new
system, they are required to enter two date values (the dates for the
current and for the previous entries) as well as three inventory values:
PreviousQuantity – the quantity in stock before the current change is made
ChangeQuantity – the quantity to be added or removed
CurrentQuantity – the quantity that will exist after the change is made.
Our system must make sure that all values entered are mutually consistent and abide by our business rules.
First, the CHK_InventoryLog_ValidChange
constraint will enforce the obvious relation between previous quantity,
current quantity, and the change being made, as shown in Listing 6.
Note that, instead of having CHK_InventoryLog_ValidChange enforce the validity of CurrentQuantity, we could implement CurrentQuantity as a persisted computed column. This is left as an advanced exercise.
Next, the CHK_InventoryLog_ValidPreviousChangeDate constraint ensures that changes occur in chronological order.
Clearly, for a given item, the current value for PreviousQuantity must match the previous value for CurrentQuantity. We'll use a FOREIGN KEY constraint, plus the required UNIQUE constraint or index, to enforce this rule. At the same time, this will also ensure that the PreviousChangeDate is a date that actually has an inventory change for the same item.
With these four constraints in place, in addition to our PRIMARY KEY constraint and original CHECK constraint (CHK_InventoryLog_NonnegativeCurrentQuantity), it's about time to run some tests.