6.4 Reporting Log Events
6.4.1 Problem
You need a human-readable report of the activity on the Stock table. You
want the report to show activity in the correct order, and you want
to see only one row on the report for each update operation. A
row-level log table, such as the StockLog table from the previous
recipe, records events in the order in which they occur. You can
easily generate reports of these events.
6.4.2 Solution
Based on the Stock table, you can use the following query to generate
a human readable report of the audit log in the StockLog table:
SELECT
MAX(UserID) AS UserID,
MAX(
CASE
WHEN Type='D' THEN 'Deleted'
WHEN Type='I' THEN 'Inserted'
ELSE 'Updated' END) AS Type,
MAX(ProductID) AS ProductID,
MAX(CASE
WHEN Type!='N' THEN Qty
ELSE null END) AS Qty1,
MAX(CASE
WHEN Type='N' THEN Qty
ELSE null END) AS Qty2,
MAX(EventTime) EventTime
FROM StockLog
GROUP BY EventId
ORDER BY max(LogId)
|
When you execute this query, you may receive the following message:
"Warning: Null value eliminated from
aggregate." This is nothing to worry about. The
message is informing you that some of the values summarized using MAX
were NULL. This is, in fact, the intended use, since we are
explicitly setting the qty values in some rows to NULL to ignore
them.
|
|
As an example of how this query functions, let's
assume that the following SQL statements had been executed against
the stock table. Each INSERT and DELETE statement would have
generated one log entry, while the UPDATE statement would have
generated two.
INSERT INTO Stock(ProductID,Qty)
VALUES('Banana',10)
INSERT INTO Stock(ProductID,Qty)
VALUES('Apple',20)
INSERT INTO Stock(ProductID,Qty)
VALUES('Orange',30)
UPDATE STOCK SET Qty=2
WHERE ProductID='Banana'
DELETE FROM Stock
WHERE ProductID='Banana'
Given the operations shown here, the query in this recipe will
produce the following results:
UserID Type ProductID Qty1 Qty2 EventTime
------- -------- ---------- ----- ----- ---------------------------
Max Inserted Banana 10 NULL 2000-06-27 22:51:30.017
Justin Inserted Apple 20 NULL 2000-06-27 22:51:30.023
Joshua Inserted Orange 30 NULL 2000-06-27 22:51:30.027
Ryan Updated Banana 10 2 2000-06-27 22:51:30.027
Gerard Deleted Banana 2 NULL 2000-06-27 22:51:30.030
This report is more readable than what you would get were you simply
to query the raw data from the audit-log table, and it can be easily
understood by users. Of course, the query shown in this recipe can
easily be adapted to narrow down the results to just the data needed
to answer specific questions. For example, you might want to report
on the activity of a single user, or you might want to report on a
single product.
6.4.3 Discussion
There are four key things to notice about the query in this recipe:
The first CASE statement translates the type codes.
CASE statements divide quantity values into two columns.
The grouping functions combine updates into one row.
The ORDER BY clause sorts the activity by LogId.
The CASE statements in this query translate the one-letter type codes
to words that will have meaning to the person reading the report.
Thus, D is translated to Deleted, I to Inserted, and so forth.
You'll notice that this query returns two quantity
columns. This is done to report both the old and new quantities for
an update operation. The first quantity column, named Qty1, will
contain the quantities for any operation with a type code not equal
to N. This includes insert and delete operations.
It also includes the original quantity (type code
E) from an update operation. The following CASE
statement, embedded in the query, sets the value for this column:
MAX(CASE WHEN Type!='N' THEN Qty
ELSE null END) AS qty1
If the type is not equal to N, the qty1 column
returns the quantity; otherwise, it returns a NULL value. A similar
CASE statement is used to place the quantity value into the Qty2
column whenever it is not placed in the Qty1 column. In other words,
the second quantity column takes on the quantity from type
N log records. These represent the updated values
from update operations.
The effects of the CASE statements are fairly obvious. The use of
grouping functions to convert two update-related log records into one
output record is not so obvious, and it deserves some extra
explanation. The following example illustrates the output that this
query would generate were the grouping functions not used. The two
records in the example are types N and
E, which means they were generated as the result
of an UPDATE statement.
UserID Type ProductID Qty1 Qty2 EventTime
------- -------- ---------- ----- ----- ---------------------------
Ryan Updated Banana 10 NULL 2000-06-27 22:51:30.027
Ryan Updated Banana NULL 2 2000-06-27 22:51:30.027
Notice that the CASE statement has translated the type codes
N and E both to the word
Updated. The result is that all columns in both rows are identical
except for the Qty1 and Qty2 columns. You can combine these two rows
by using the SQL GROUP BY clause. Group the two rows based on the
EventID, and simply take the maximum value of each column. In the
case of the UserID, Type, and ProductID columns, both vales are the
same, so applying the MAX function returns just one occurrence of
each value. In the case of Qty1 and Qty2, applying the MAX function
returns the numbers, eliminating the NULLs. That's
because numbers are always higher than NULLs. The resulting single
row would be:
UserID Type ProductID Qty1 Qty2 EventTime
------- -------- ---------- ----- ----- ---------------------------
Ryan Updated Banana 10 2 2000-06-27 22:51:30.027
The use of grouping in this particular example to combine the two log
entries from an update operation into one row on the report depends
on two things. Each operation must have a unique EventId, and update
operations must not have changed any values other than the quantity
for a product. As long as each insert and delete operation has a
unique EventId, the log entries for those operations will never be
grouped with any others. Similarly, as long as each update record
only represents a change in quantity, the other columns in the log
entries will be identical, and the log entries will be successfully
combined into one record for the report.
|
The query used in this section to report on the log is designed under
the assumption that updates do not change the primary key for a
row — in this case, the ProductID. If changing the primary key is
a possibility, you will need to treat the primary-key columns in the
same manner as the Qty column was treated.
|
|
|