Well-designed applications generally expose a public interface while keeping implementation details private, thereby enabling future design changes without impacting end users. When designing your database, you can achieve a similar result by keeping your tables private and allowing your users to access data only through a set of views. This chapter strives to define what views are, how they are created, and when and how you might want to use them.
A view is simply a mechanism for querying data. Unlike tables, views do not
involve data storage; you won’t need to worry about views filling up your disk
space. You create a view by assigning a name to a select statement, and then storing the query for others to use. Other
users can then use your view to access data just as though they were querying tables
directly (in fact, they may not even know they are using a view).
As a simple example, let’s say that you want to partially obscure the federal IDs
(Social Security numbers and corporate identifiers) in the customer table. The customer service department, for example, may
need access to just the last portion of the federal ID in order to verify the
identity of a caller, but exposing the entire number would violate the company’s
privacy policy. Therefore, instead of allowing direct access to the customer table, you define a view
called customer_vw and mandate that all bank
personnel use it to access customer data. Here’s the view definition:
CREATE VIEW customer_vw
(cust_id,
fed_id,
cust_type_cd,
address,
city,
state,
zipcode
)
AS
SELECT cust_id,
concat('ends in ', substr(fed_id, 8, 4)) fed_id,
cust_type_cd,
address,
city,
state,
postal_code
FROM customer;The first part of the statement lists the view’s column names, which may be
different from those of the underlying table (e.g., the customer_vw view has a column named zipcode which maps to the customer.postal_code column). The second part of the statement is a
select statement, which must contain one
expression for each column in the view.
When the create view statement is executed, the
database server simply stores the view definition for future use; the query is not
executed, and no data is retrieved or stored. Once the view has been created, users
can query it just like they would a table, as in:
mysql>SELECT cust_id, fed_id, cust_type_cd->FROM customer_vw;+---------+--------------+--------------+ | cust_id | fed_id | cust_type_cd | +---------+--------------+--------------+ | 1 | ends in 1111 | I | | 2 | ends in 2222 | I | | 3 | ends in 3333 | I | | 4 | ends in 4444 | I | | 5 | ends in 5555 | I | | 6 | ends in 6666 | I | | 7 | ends in 7777 | I | | 8 | ends in 8888 | I | | 9 | ends in 9999 | I | | 10 | ends in 111 | B | | 11 | ends in 222 | B | | 12 | ends in 333 | B | | 13 | ends in 444 | B | +---------+--------------+--------------+ 13 rows in set (0.02 sec)
The actual query that the server executes is neither the one submitted by the user nor the query attached to the view definition. Instead, the server merges the two together to create another statement, which in this case looks as follows:
SELECT cust_id,
concat('ends in ', substr(fed_id, 8, 4)) fed_id,
cust_type_cd
FROM customer;Even though the customer_vw view definition
includes seven columns of the customer table, the
query executed by the server retrieves only three of the seven. As you’ll see later
in the chapter, this is an important distinction if some of the columns in your view
are attached to functions or subqueries.
From the user’s standpoint, a view looks exactly like a table. If you want to know
what columns are available in a view, you can use MySQL’s (or Oracle’s) describe command to examine it:
mysql> describe customer_vw;
+--------------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+-------+
| cust_id | int(10) unsigned | NO | | 0 | |
| fed_id | varchar(12) | YES | | NULL | |
| cust_type_cd | enum('I','B') | NO | | NULL | |
| address | varchar(30) | YES | | NULL | |
| city | varchar(20) | YES | | NULL | |
| state | varchar(20) | YES | | NULL | |
| postal_code | varchar(10) | YES | | NULL | |
+--------------+------------------+------+-----+---------+-------+
7 rows in set (1.40 sec)You are free to use any clauses of the select
statement when querying through a view, including group
by, having, and order by. Here’s an example:
mysql>SELECT cust_type_cd, count(*)->FROM customer_vw->WHERE state = 'MA'->GROUP BY cust_type_cd->ORDER BY 1;+--------------+----------+ | cust_type_cd | count(*) | +--------------+----------+ | I | 7 | | B | 2 | +--------------+----------+ 2 rows in set (0.22 sec)
In addition, you can join views to other tables (or even to other views) within a query, as in:
mysql>SELECT cst.cust_id, cst.fed_id, bus.name->FROM customer_vw cst INNER JOIN business bus->ON cst.cust_id = bus.cust_id;+---------+-------------+------------------------+ | cust_id | fed_id | name | +---------+-------------+------------------------+ | 10 | ends in 111 | Chilton Engineering | | 11 | ends in 222 | Northeast Cooling Inc. | | 12 | ends in 333 | Superior Auto Body | | 13 | ends in 444 | AAA Insurance Inc. | +---------+-------------+------------------------+ 4 rows in set (0.24 sec)
This query joins the customer_vw view to the
business table in order to retrieve only
business customers.
In the previous section, I demonstrated a simple view whose sole purpose was to
mask the contents of the customer.fed_id column.
While views are often employed for this purpose, there are many reasons for using
views, as I demonstrate in the following subsections.
If you create a table and allow users to query it, they will be able to access every column and every row in the table. As I pointed out earlier, however, your table may include some columns that contain sensitive data, such as identification numbers or credit card numbers; not only is it a bad idea to expose such data to all users, but also it might violate your company’s privacy policies, or even state or federal laws, to do so.
The best approach for these situations is to keep the table private (i.e.,
don’t grant select permission to any users)
and then to create one or more views that either omit or obscure (such as the
'ends in ####' approach taken with the
customer_vw.fed_id column) the sensitive
columns. You may also constrain which rows a set of users
may access by adding a where clause to your
view definition. For example, the next view definition allows only business
customers to be queried:
CREATE VIEW business_customer_vw
(cust_id,
fed_id,
cust_type_cd,
address,
city,
state,
zipcode
)
AS
SELECT cust_id,
concat('ends in ', substr(fed_id, 8, 4)) fed_id,
cust_type_cd,
address,
city,
state,
postal_code
FROM customer
WHERE cust_type_cd = 'B'If you provide this view to your corporate banking department, they will be
able to access only business accounts because the condition in the view’s
where clause will always be included in
their queries.
Oracle Database users have another option for securing both rows and
columns of a table: Virtual Private Database (VPD). VPD allows you to attach
policies to your tables, after which the server will modify a user’s query
as necessary to enforce the policies. For example, if you enact a policy
that members of the corporate banking department can see only business
accounts, then the condition cust_type_cd =
'B' will be added to all of their queries against the customer table.
Reporting applications generally require aggregated data, and views are a great way to make it appear as though data is being pre-aggregated and stored in the database. As an example, let’s say that an application generates a report each month showing the number of accounts and total deposits for every customer. Rather than allowing the application developers to write queries against the base tables, you could provide them with the following view:
CREATE VIEW customer_totals_vw
(cust_id,
cust_type_cd,
cust_name,
num_accounts,
tot_deposits
)
AS
SELECT cst.cust_id, cst.cust_type_cd,
CASE
WHEN cst.cust_type_cd = 'B' THEN
(SELECT bus.name FROM business bus WHERE bus.cust_id = cst.cust_id)
ELSE
(SELECT concat(ind.fname, ' ', ind.lname)
FROM individual ind
WHERE ind.cust_id = cst.cust_id)
END cust_name,
sum(CASE WHEN act.status = 'ACTIVE' THEN 1 ELSE 0 END) tot_active_accounts,
sum(CASE WHEN act.status = 'ACTIVE' THEN act.avail_balance ELSE 0 END) tot_balance
FROM customer cst INNER JOIN account act
ON act.cust_id = cst.cust_id
GROUP BY cst.cust_id, cst.cust_type_cd;Using this approach gives you a great deal of flexibility as a database
designer. If you decide at some point in the future that query performance would
improve dramatically if the data were preaggregated in a table rather than
summed using a view, you can create a customer_totals table and modify the customer_totals_vw view definition to retrieve data from this
table. Before modifying the view definition, you can use it to populate the new
table. Here are the necessary SQL statements for this scenario:
mysql>CREATE TABLE customer_totals->AS->SELECT * FROM customer_totals_vw;Query OK, 13 rows affected (3.33 sec) Records: 13 Duplicates: 0 Warnings: 0 mysql>CREATE OR REPLACE VIEW customer_totals_vw->(cust_id,->cust_type_cd,->cust_name,->num_accounts,->tot_deposits->)->AS->SELECT cust_id, cust_type_cd, cust_name, num_accounts, tot_deposits->FROM customer_totals;Query OK, 0 rows affected (0.02 sec)
From now on, all queries that use the customer_totals_vw view will pull data from the new customer_totals table, meaning that users will see
a performance improvement without needing to modify their queries.
One of the most common reasons for deploying views is to shield end users from complexity. For example, let’s say that a report is created each month showing the number of employees, the total number of active accounts, and the total number of transactions for each branch. Rather than expecting the report designer to navigate four different tables to gather the necessary data, you could provide a view that looks as follows:
CREATE VIEW branch_activity_vw (branch_name, city, state, num_employees, num_active_accounts, tot_transactions ) AS SELECT br.name, br.city, br.state, (SELECT count(*) FROM employee emp WHERE emp.assigned_branch_id = br.branch_id) num_emps, (SELECT count(*) FROM account acnt WHERE acnt.status = 'ACTIVE' AND acnt.open_branch_id = br.branch_id) num_accounts, (SELECT count(*) FROM transaction txn WHERE txn.execution_branch_id = br.branch_id) num_txns FROM branch br;
This view definition is interesting because three of the six column values are
generated using scalar subqueries. If someone uses this view but does
not reference the num_employees, num_active_accounts, or tot_transactions column, then none of the subqueries will be
executed.
Some database designs break large tables into multiple pieces in order to
improve performance. For example, if the transaction table became large, the designers may decide to break
it into two tables: transaction_current,
which holds the latest six months’ of data, and transaction_historic, which holds all data up to six months ago.
If a customer wants to see all the transactions for a particular account, you
would need to query both tables. By creating a view that queries both tables and
combines the results together, however, you can make it look like all
transaction data is stored in a single table. Here’s the view
definition:
CREATE VIEW transaction_vw (txn_date, account_id, txn_type_cd, amount, teller_emp_id, execution_branch_id, funds_avail_date ) AS SELECT txn_date, account_id, txn_type_cd, amount, teller_emp_id, execution_branch_id, funds_avail_date FROM transaction_historic UNION ALL SELECT txn_date, account_id, txn_type_cd, amount, teller_emp_id, execution_branch_id, funds_avail_date FROM transaction_current;
Using a view in this case is a good idea because it allows the designers to change the structure of the underlying data without the need to force all database users to modify their queries.
If you provide users with a set of views to use for data retrieval, what should
you do if the users also need to modify the same data? It might seem a bit strange,
for example, to force the users to retrieve data using a view, but then allow them
to directly modify the underlying table using update or insert statements. For
this purpose, MySQL, Oracle Database, and SQL Server all allow you to modify data
through a view, as long as you abide by certain restrictions. In the case of MySQL,
a view is updatable if the following conditions are met:
No aggregate functions are used (max(),
min(), avg(), etc.).
The view does not employ group by or
having clauses.
No subqueries exist in the select or
from clause, and any subqueries in
the where clause do not refer to tables
in the from clause.
The view does not utilize union,
union all, or distinct.
The from clause includes at least one
table or updatable view.
The from clause uses only inner joins
if there is more than one table or view.
To demonstrate the utility of updatable views, it might be best to start with a simple view definition and then to move to a more complex view.
The view at the beginning of the chapter is about as simple as it gets, so let’s start there:
CREATE VIEW customer_vw
(cust_id,
fed_id,
cust_type_cd,
address,
city,
state,
zipcode
)
AS
SELECT cust_id,
concat('ends in ', substr(fed_id, 8, 4)) fed_id,
cust_type_cd,
address,
city,
state,
postal_code
FROM customer;The customer_vw view queries a single
table, and only one of the seven columns is derived via an expression. This view
definition doesn’t violate any of the restrictions listed earlier, so you can
use it to modify data in the customer table,
as in:
mysql>UPDATE customer_vw->SET city = 'Woooburn'->WHERE city = 'Woburn';Query OK, 1 row affected (0.34 sec) Rows matched: 1 Changed: 1 Warnings: 0
As you can see, the statement claims to have modified one row, but let’s check
the underlying customer table just to be
sure:
mysql>SELECT DISTINCT city FROM customer;+------------+ | city | +------------+ | Lynnfield | |Woooburn| | Quincy | | Waltham | | Salem | | Wilmington | | Newton | +------------+ 7 rows in set (0.12 sec)
While you can modify most of the columns in the view in this fashion, you will
not be able to modify the fed_id column,
since it is derived from an expression:
mysql>UPDATE customer_vw->SET city = 'Woburn', fed_id = '999999999'->WHERE city = 'Woooburn';ERROR 1348 (HY000): Column 'fed_id' is not updatable
In this case, it may not be a bad thing, since the whole point of the view is to obscure the federal identifiers.
If you want to insert data using the customer_vw view, you are out of luck; views that contain derived
columns cannot be used for inserting data, even if the derived columns are not
included in the statement. For example, the next statement attempts to populate
only the cust_id, cust_type_cd, and city columns
using the customer_vw view:
mysql>INSERT INTO customer_vw(cust_id, cust_type_cd, city)->VALUES (9999, 'I', 'Worcester');ERROR 1471 (HY000): The target table customer_vw of the INSERT is not insertable -into
Now that you have seen the limitations of simple views, the next section will demonstrate the use of a view that joins multiple tables.
While single-table views are certainly common, many of the views that you come
across will include multiple tables in the from clause of the underlying query. The next view, for example,
joins the business and customer tables so that all the data for business
customers can be easily queried:
CREATE VIEW business_customer_vw (cust_id, fed_id, address, city, state, postal_code, business_name, state_id, incorp_date ) AS SELECT cst.cust_id, cst.fed_id, cst.address, cst.city, cst.state, cst.postal_code, bsn.name, bsn.state_id, bsn.incorp_date FROM customer cst INNER JOIN business bsn ON cst.cust_id = bsn.cust_id WHERE cust_type_cd = 'B';
You may use this view to update data in either the customer or the business
table, as the following statements demonstrate:
mysql>UPDATE business_customer_vw->SET postal_code = '99999'->WHERE cust_id = 10;Query OK, 1 row affected (0.09 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql>UPDATE business_customer_vw->SET incorp_date = '2008-11-17'->WHERE cust_id = 10;Query OK, 1 row affected (0.11 sec) Rows matched: 1 Changed: 1 Warnings: 0
The first statement modifies the customer.postal_code column, whereas the second statement
modifies the business.incorp_date column. You
might be wondering what happens if you try to update columns from
both tables in a single statement, so let’s find
out:
mysql>UPDATE business_customer_vw->SET postal_code = '88888', incorp_date = '2008-10-31'->WHERE cust_id = 10;ERROR 1393 (HY000): Can not modify more than one base table through a join view 'bank.business_customer_vw'
As you can see, you are allowed to modify both of the underlying tables, as
long as you don’t try to do it with a single statement. Now let’s try to
insert data into both tables for a new customer
(cust_id = 99):
mysql>INSERT INTO business_customer_vw->(cust_id, fed_id, address, city, state, postal_code)->VALUES (99, '04-9999999', '99 Main St.', 'Peabody', 'MA', '01975');Query OK, 1 row affected (0.07 sec) mysql>INSERT INTO business_customer_vw->(cust_id, business_name, state_id, incorp_date)->VALUES (99, 'Ninety-Nine Restaurant', '99-999-999', '1999-01-01');ERROR 1393 (HY000): Can not modify more than one base table through a join view 'bank.business_customer_vw'
The first statement, which attempts to insert data into the customer table, works fine, but the second
statement, which attempts to insert a row into the business table, raises an exception. The second statement fails
because both tables include a cust_id column,
but the cust_id column in the view definition
is mapped to the customer.cust_id column.
Therefore, it is not possible to insert data into the business table using the preceding view definition.
Oracle Database and SQL Server also allow data to be inserted and updated
through views, but, like MySQL, there are many restrictions. If you are
willing to write some PL/SQL or Transact-SQL, however, you can use a feature
called instead-of triggers, which allows you to
essentially intercept insert, update, and delete statements against a view, and write custom code to
incorporate the changes. Without this type of feature, there are simply too
many restrictions to make updating through views a feasible strategy for
nontrivial applications.
Test your understanding of views by working through the following exercises. When you’re done, compare your solutions with those in Appendix C.
Create a view that queries the employee
table and generates the following output when queried with no where clause:
+-----------------+------------------+ | supervisor_name | employee_name | +-----------------+------------------+ | NULL | Michael Smith | | Michael Smith | Susan Barker | | Michael Smith | Robert Tyler | | Robert Tyler | Susan Hawthorne | | Susan Hawthorne | John Gooding | | Susan Hawthorne | Helen Fleming | | Helen Fleming | Chris Tucker | | Helen Fleming | Sarah Parker | | Helen Fleming | Jane Grossman | | Susan Hawthorne | Paula Roberts | | Paula Roberts | Thomas Ziegler | | Paula Roberts | Samantha Jameson | | Susan Hawthorne | John Blake | | John Blake | Cindy Mason | | John Blake | Frank Portman | | Susan Hawthorne | Theresa Markham | | Theresa Markham | Beth Fowler | | Theresa Markham | Rick Tulman | +-----------------+------------------+ 18 rows in set (1.47 sec)