In my previous article, ‘SQL View Basics‘, I discussed a VIEW
as being a virtual table that does not exist until it is invoked by name in SQL statements, and which disappears at the end of that statement. The strong advantage of a VIEW
is that it will produce the correct results when it is invoked, based on the current data. Trying to do the same sort of things with temporary tables or computed columns within a table can be subject to errors and is likely to be slower to read from disk. Let’s keep going and look at some code.
Types of VIEWs
We can classify a view as being either read-only or updatable. This is handy, but we can also use the type of SELECT
statement in the VIEW
s to classify them. These two methods can overlap, but let’s start with the SELECT
statements method.
Single-Table Projection and Restriction
In practice, many VIEW
s are projections or restrictions on a single base table. This is a common method for implementing security-control by removing rows or columns that a particular group of users is not allowed to see. These VIEW
s are usually updatable and represent the most common VIEW
pattern.
Translated Columns
Another common use of a VIEW
is to translate codes into text or other codes by doing table look ups. This is a special case of a joined VIEW
based on a FOREIGN KEY
relationship between two tables. For example, an order table might use a part number that we wish to display with a part name on an order entry screen. This is done with a JOIN
between the Orders
table and the Inventory
table, thus:
1 2 3 4 |
CREATE VIEW Screen (part_nbr, part_name, ...) AS SELECT Orders.part_nbr, Inventory.part_name, ... FROM Inventory, Orders WHERE Inventory.part_nbr = Orders.part_nbr; |
The idea of JOIN VIEW
s to translate codes can be expanded to show more than just one translated column. The result is often a “star” query with one table in the center, joined by FOREIGN KEY
relations to many other tables to produce a result that is more readable than the original central table.
Missing values are a problem. If there is no translation for a given encoding, no row appears in the VIEW
, or if an OUTER
JOIN
was used, a NULL will appear. The programmer should establish a referential integrity constraint to CASCADE
changes between the tables to prevent loss of data.
Grouped VIEWs
A grouped VIEW
is based on a query with a GROUP BY
clause. Since each of the groups may have more than one row in the base from which it was built, these are necessarily read-only VIEW
s. Such VIEW
s usually have one or more aggregate functions and they are used for reporting purposes. They are also handy for working around weaknesses in SQL. Consider a VIEW
that shows the largest sale in each state. The query is straightforward:
1 2 3 4 |
CREATE VIEW BigSales (state_code, sales_amt_total) AS SELECT state_code, MAX(sales_amt) FROM Sales GROUP BY state_code; |
SQL does not require that the grouping column(s) appear in the select clause, but it is a good idea in this case.
These VIEW
s are also useful for “flattening out” one-to-many relationships. For example, consider a Personnel table, keyed on the employee number (emp_nbr
), and a table of dependents, keyed on a combination of the employee number for each dependent’s parent (emp_nbr
) and the dependent’s own serial number (dep_id
). The goal is to produce a report of the employees by name with the number of dependents each has.
1 2 3 4 |
CREATE VIEW DepTally1 (emp_nbr, dependent_cnt) AS SELECT emp_nbr, COUNT(*) FROM Dependents GROUP BY emp_nbr; |
The report is simply an OUTER JOIN
between this VIEW
and the Personnel table.
The OUTER
JOIN
is needed to account for employees without dependents with a NULL
value, like this.
1 2 3 4 5 |
SELECT emp_name, dependent_cnt FROM Personnel AS P1 LEFT OUTER JOIN DepTally1 AS D1 ON P1.emp_nbr = D1.emp_nbr; |
UNION-ed VIEWs
VIEW
s based on a UNION
or UNION ALL
operation are read-only because there is no single way to map a change onto just one row in one of the base tables. The UNION
operator will remove duplicate rows from the results. Both the UNION
and UNION ALL
operators hide which table the rows came from. Such VIEW
s must use a <view column list>,
because the columns in a UNION [ALL]
have no names of their own. In theory, a UNION
of two disjoint tables, neither of which has duplicate rows in itself should be updatable.
The problem given in the section on grouped VIEW
s, could also be done with a UNION
query that would assign a count of zero to employees without dependents, thus:
1 2 3 4 5 6 7 8 9 10 11 |
CREATE VIEW DepTally2 (emp_nbr, dependent_cnt) AS (SELECT emp_nbr, COUNT(*) FROM Dependents GROUP BY emp_nbr) UNION (SELECT emp_nbr, 0 FROM Personnel AS P2 WHERE NOT EXISTS (SELECT * FROM Dependents AS D2 WHERE D2.emp_nbr = P2.emp_nbr)); |
The report is now a simple INNER JOIN
between this VIEW
and the Personnel table. The zero value, instead of a NULL value, will account for employees without dependents.
Calculated Columns
The main reason for hiding computations in a VIEW
is so that the computation is done one way, one place, one time and so it gets a unique data element name. One common use for a VIEW
is to provide summary data across a row. For example, given a table with measurements in metric units, we can construct a VIEW
that hides the calculations to convert them into English units.
It is important to be sure that you have no problems with NULL
values when constructing a calculated column. For example, given a Personnel table with columns for both salary and commission, you might construct this VIEW
:
1 2 3 4 |
CREATE VIEW Payroll (emp_nbr, paycheck_amt) AS SELECT emp_nbr, (salary + COALESCE(commission), 0.00) FROM Personnel; |
Office workers do not get commissions, so the value of their commission column will be NULL
, so we use the COALESCE()
function to change the NULLs
to zeros.
SQL Server introduced a computed column construct in their table declaration syntax, <expression> AS <column name>
. This is proprietary and has some limitations in that it can only reference columns in the same row. This can be a way to avoid a VIEW
and is a good idea to reduce the number of objects in a schema.
Computed VIEWs not just for fancy math; strings and temporal data also have computations. For example a VIEW
can hide all but the last four digits of a 16-digit credit card number:
1 2 |
masked_creditcard_nbr AS '****-****-****-' + SUBSTRING (creditcard_nbr, 13, 16) |
likewise, the following Monday from a weekend date is easy to put into a computation business_date
1 2 3 4 5 |
AS CASE WHEN DATEPART(DW, sale_date) = 7 THEN DATEADD (DD, 1, sale_date) WHEN DATEPART(DW, sale_date) = 6 THEN DATEADD (DD, 2, sale_date) ELSE sale_date END |
Updatable and Read-Only VIEWs
Unlike base tables, VIEW
s are either updatable or read-only, but not both. INSERT
, UPDATE
, and DELETE
operations are allowed on updatable VIEW
s and base tables, subject to any other constraints. INSERT
, UPDATE
, and DELETE
are not allowed on read-only VIEW
s, but you can change their base tables, as you would expect.
For the computer science majors, this is an NP-complete problem. Since we have no general solution, we need a way to handle each VIEW
with particular code. We have a way to do that!
The INSTEAD OF Trigger
The INSTEAD OF
trigger is how to resolve the VIEW
updating problem. It is possible for a user to write INSTEAD OF
triggers on VIEW
s, which catch the changes and route them to the base tables that make up the VIEW
. The database designer has complete control over the way VIEW
s are handled.
These triggers are added to a VIEW
and are executed on base tables instead of making changes directly to the VIEW
it self. If you think about it, how would a program change the VIEW
anyway? It does not exist.
The basic syntax is fairly simple; T-SQL has a lot of other options but we will not be concerned with them.
1 2 3 4 5 |
CREATE TRIGGER <trigger_name> ON <view_name> INSTEAD OF } {[INSERT] [,] [UPDATE] [,] [DELETE]} AS <SQL statement>; |
Triggers have a global scope for obvious reasons. The options INSERT
, UPDATE
or DELETE
are called “database events” and they cause the trigger to fire. As a general heuristic, you will want to have your SQL statement fire on all three events.
The trigger’s SQL statement is executed instead of the triggering SQL statement. The attempted INSERT
, UPDATE
or DELETE
has no effect at all. If you feel fancy, you can define views on views where each view has its own INSTEAD OF
trigger.
INSTEAD OF
triggers are not allowed on updatable views that use WITH CHECK OPTION
. SQL Server raises an error when an INSTEAD OF
trigger is added to an updatable view WITH CHECK OPTION
specified. The user must remove that option by using ALTER VIEW
before defining the INSTEAD OF
trigger.
For INSTEAD OF
triggers, the DELETE
option is not allowed on tables that have an ON DELETE CASCADE
referential action. Similarly, the UPDATE
option is not allowed on tables that have ON UPDATE CASCADE
referential action.
This can be complicated, so let us start with a very example of a read-only VIEW
and its base tables.
1 2 3 4 5 6 7 |
CREATE TABLE Alpha (alpha_key VARCHAR(10) NOT NULL PRIMARY KEY, alpha_amt INTEGER NOT NULL); CREATE TABLE Beta (beta_key VARCHAR(10) NOT NULL PRIMARY KEY, beta_amt INTEGER NOT NULL); |
Now let’s declare a VIEW
that can’t be updated.
1 2 3 4 5 6 7 8 |
CREATE VIEW Combined_Shares (ab_key, ab_tot) AS SELECT ab_key, SUM(ab_tot) FROM (SELECT * FROM Alpha UNION ALL SELECT * FROM Beta) AS X (ab_key, ab_tot) GROUP BY ab_key; |
As a simple example, when you do an insertion, put half of an amount into each of the two hidden tables, Alpha and Beta. If the amount is an odd number, favor one of the two hidden tables.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
CREATE TRIGGER Split_Loot ON Combined_Shares INSTEAD OF INSERT AS BEGIN INSERT INTO Alpha -- favor alpha SELECT I.ab_key, CEILING (I.ab_tot/2.0) FROM INSERTED AS I; INSERT INTO Beta SELECT I.ab_key, FLOOR(I.ab_tot/2.0) FROM INSERTED AS I; END; |
Put in one row of data via the view and not the base tables.
1 2 |
INSERT INTO Combined_Shares (ab_key, ab_tot) VALUES ('Jerry', 51); |
Now look at what happened by looking at the VIEW
and the hidden tables in it.
1 |
SELECT * FROM Combined_Shares; |
Jerry |
51 |
1 |
SELECT * FROM Alpha; |
Jerry |
25 |
1 |
SELECT * FROM Beta; |
Jerry |
26 |
That was nice. Now let’s do a delete trigger
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
CREATE TRIGGER Delete_Loot ON Combined_Shares INSTEAD OF DELETE AS BEGIN MERGE INTO Alpha USING DELETED AS D ON D.ab_key = Alpha.ab_key WHEN MATCHED THEN DELETE; MERGE INTO Beta USING DELETED AS D ON D.ab_key = Beta.ab_key WHEN MATCHED THEN DELETE; END; |
And fire the trigger:
1 2 |
DELETE FROM Combined_Shares WHERE ab_key = 'Jerry'; |
Please notice the use of the MERGE
in this trigger. You need to learn how to write standard SQL. Also, when this fires you will get three messages, one for the VIEW
and one for each table.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
CREATE TRIGGER Update_Loot ON Combined_Shares INSTEAD OF UPDATE AS BEGIN MERGE INTO Alpha USING INSERTED AS I ON I.ab_key = Alpha.alpha_key WHEN MATCHED THEN UPDATE SET alpha_amt = CEILING (I.ab_tot/2.0); MERGE INTO Beta USING INSERTED AS I ON I.ab_key = Beta.beta_key WHEN MATCHED THEN UPDATE SET beta_amt = FLOOR (I.ab_tot/2.0); END; |
WITH CHECK OPTION Example
In my previous article, ‘SQL View Basics‘,, I mentioned the use of the WITH CHECK OPTION
but did not give an example. Let us define a simple Organizational Chart table in a nested sets model. I am going to assume that you know this SQL idiom and so I will not explain it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
CREATE TABLE OrgChart (dept_name CHAR(10) NOT NULL PRIMARY KEY, lft INTEGER NOT NULL UNIQUE CHECK (lft > 0), rgt INTEGER NOT NULL UNIQUE CHECK (rgt > 1), CONSTRAINT order_okay CHECK (lft < rgt)); INSERT INTO OrgChart VALUES ('Corporate', 1, 120), ('Acct', 20, 30), ('Sales', 40, 110), ('Retail', 50, 60), ('Wholesale', 70, 80), ('Internal', 90, 100); |
Let us define a simple Organizational Chart table in a nested sets model. I am going to assume that you know this SQL idiom and I will not explain it in detail.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
CREATE TABLE OrgChart (dept_name CHAR(10) NOT NULL PRIMARY KEY, lft INTEGER NOT NULL UNIQUE CHECK (lft > 0), rgt INTEGER NOT NULL UNIQUE CHECK (rgt > 1), CONSTRAINT order_okay CHECK (lft < rgt)); INSERT INTO OrgChart VALUES ('Corporate', 1, 120), ('Acct', 20, 30), ('Sales', 40, 110), ('Retail', 50, 60), ('Wholesale', 70, 80), ('Internal', 90, 100); |
The (lft
, rgt
) pairs are like tags in a mark-up language, or parentheses in algebra, BEGIN-END
blocks in Algol-family programming languages, etc. — they bracket a sub-set. This is a set-oriented approach to trees in a set-oriented language.
Notice all of the constraints on the columns at the column and table level. But we are missing two constraints we need. The first is that we have a single root. The second is that the (lft
, rgt
) pairs do not overlap .. that is, we really have nesting.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
CREATE VIEW OrgChart_2 AS SELECT O1.dept_name, O1.lft, O1.rgt FROM OrgChart AS O1 WHERE NOT EXISTS (SELECT * FROM OrgChart AS O2 WHERE O1.lft BETWEEN O2.lft AND O2.rgt AND O1.rgt NOT BETWEEN O2.lft AND O2.rgt) AND O1.lft BETWEEN 1 AND (SELECT rgt FROM OrgChart WHERE lft = 1) AND O1.rgt BETWEEN 1 AND (SELECT rgt FROM OrgChart WHERE lft = 1) WITH CHECK OPTION; |
This assumes that we start with a root node that has (lft
= 1) and build from that. That could be enforced with another predicate in the VIEW
, if you wish.
Try these statements and watch the WITH CHECK OPTION
errors. The first one is a dangling node not under the root node, the second is an overlapping range.
1 2 3 4 5 |
INSERT INTO OrgChart_2 VALUES ('dangle', 130, 131); INSERT INTO OrgChart_2 VALUES ('overlap', 51, 61); |
giving the error…
1 2 3 |
Msg 550, Level 16, State 1, Line 2 The attempted insert or update failed because the target view either specifies WITH CHECK OPTION or spans a view that specifies WITH CHECK OPTION and one or more rows resulting from the operation did not qualify under the CHECK OPTION constraint. The statement has been terminated. |
SUMMARY
VIEW
s are not as simple as most SQL programmers first think they are. But they are worth the effort because they are powerful and, because they are declarative, they can be optimized.
Load comments