5.4 Inline Views

Most texts covering SQL define the FROM clause of a SELECT statement as containing a list of tables and/or views. Please abandon this definition and replace it with the following:

The FROM clause contains a list of data sets.

In this light, it is easy to see how the FROM clause can contain tables (permanent data sets), views (virtual data sets), and SELECT statements (temporary data sets). SELECT statements, or inline views as mentioned earlier, are one of the most powerful, yet underutilized features of Oracle SQL.

In our opinion, the name "inline view" is confusing and tends to intimidate people. Since it is a subquery that executes prior to the containing query, a more palatable name might have been "pre-query."


Here's a simple example of an inline view:

SELECT d.dept_id, d.name, emp_cnt.tot

FROM department d INNER JOIN 

 (SELECT dept_id, COUNT(*) tot 

  FROM employee 

  GROUP BY dept_id) emp_cnt

  ON d.dept_id = emp_cnt.dept_id;



   DEPT_ID NAME                        TOT

---------- -------------------- ----------

        10 ACCOUNTING                    3

        20 RESEARCH                      5

        30 SALES                         6

In this example, the FROM clause references the department table and an inline view called emp_cnt, which calculates the number of employees in each department. The two sets are joined using dept_id and the ID, name, and employee count are returned for each department. While this example is fairly simple, inline views allow you to do things in a single query that might otherwise require multiple select statements or a procedural language to accomplish.

5.4.1 Inline View Basics

Because the result set from an inline view is referenced by other elements of the containing query, you must give your inline view a name and provide aliases for all ambiguous columns. In the previous example, the inline view was given the name "emp_cnt", and the alias "tot" was assigned to the COUNT(*) column. Similar to other types of subqueries, inline views may join multiple tables, call built-in and user-defined functions, specify optimizer hints, and include GROUP BY, HAVING, and CONNECT BY clauses. Unlike other types of subqueries, an inline view may also contain an ORDER BY clause, which opens several interesting possibilities (see Section 5.5 later in the chapter for an example using ORDER BY in a subquery).

Inline views are particularly useful when you need to combine data at different levels of aggregation. In the previous example, we needed to retrieve all rows from the department table and include aggregated data from the employee table, so we chose to do the aggregation within an inline view and join the results to the department table. Anyone involved in report generation or data warehouse extraction, transformation, and load (ETL) applications has doubtless encountered situations where data from various levels of aggregation needs to be combined; with inline views, you should be able to produce the desired results in a single SQL statement rather than having to break the logic into multiple pieces or write code in a procedural language.

When considering using an inline view, ask yourself the following questions:

  • What value does the inline view add to the readability and, more importantly, the performance of the containing query?

  • How large will the result set generated by the inline view be?

  • How often, if ever, will I have need of this particular data set?

Generally, using an inline view should enhance the readability and performance of the query, and it should generate a manageable data set that is of no value to other statements or sessions; otherwise, you may want to consider building a permanent or temporary table so that you can share the data between sessions and build additional indexes as needed.

5.4.2 Query Execution

Inline views are always executed prior to the containing query and, thus, may not reference columns from other tables or inline views from the same query. After execution, the containing query interacts with an inline view as if it were an unindexed, in-memory table. If inline views are nested, the innermost inline view is executed first, followed by the next-innermost inline view, and so on. Consider the following query:

SELECT d.dept_id dept_id, d.name dept_name, 

  dept_orders.tot_orders tot_orders

FROM department d INNER JOIN 

   (SELECT e.dept_id dept_id, SUM(emp_orders.tot_orders) tot_orders

    FROM employee e INNER JOIN 

     (SELECT sales_emp_id, COUNT(*) tot_orders 

      FROM cust_order 

      WHERE order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY') 

        AND cancelled_dt IS NULL

      GROUP BY sales_emp_id

     ) emp_orders

    ON e.emp_id = emp_orders.sales_emp_id

    GROUP BY e.dept_id

   ) dept_orders

ON d.dept_id = dept_orders.dept_id;



   DEPT_ID DEPT_NAME            TOT_ORDERS

---------- -------------------- ----------

        30 SALES                         6

If you're new to inline views, this query might be intimidating. Start with the innermost query, understand the result set generated by that query, and move outward to the next level. Since inline views must be noncorrelated, you can run each inline view's SELECT statement individually and look at the results. (From the standpoint of the inline view, this would constitute an "out-of-query experience.") For the previous query, executing the emp_orders inline view generates the following result set:

SELECT sales_emp_id, COUNT(*) tot_orders 

FROM cust_order 

WHERE order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY') 

  AND cancelled_dt IS NULL

GROUP BY sales_emp_id;



SALES_EMP_ID TOT_ORDERS

------------ ----------

        7354          4

        7368          4

        7654          6

The emp_orders set contains all salespeople who booked orders since 2001, along with the total number of orders booked. The next level up is the dept_orders inline view, which joins the emp_orders data set to the employee table and aggregates the number of orders up to the department level. The resulting data set looks as follows:

SELECT e.dept_id dept_id, SUM(emp_orders.tot_orders) tot_orders

FROM employee e INNER JOIN 

 (SELECT sales_emp_id, COUNT(*) tot_orders 

  FROM cust_order 

  WHERE order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY') 

    AND cancelled_dt IS NULL

  GROUP BY sales_emp_id

 ) emp_orders

ON e.emp_id = emp_orders.sales_emp_id

GROUP BY e.dept_id;



   DEPT_ID TOT_ORDERS

---------- ----------

        30          6

Finally, the dept_orders set is joined to the department table, and the final result set is:

   DEPT_ID DEPT_NAME            TOT_ORDERS

---------- -------------------- ----------

        30 SALES                         6

After query execution completes, the emp_orders and dept_orders result sets are discarded.

5.4.3 Data Set Fabrication

Along with querying existing tables, inline views may be used to fabricate special-purpose data sets that don't exist in the database. For example, you might want to aggregate orders over the past year by small, medium, and large orders, but the concept of order sizes may not have been defined in your database. You could build a table with three records to define the different sizes and their boundaries, but you only need this information for a single query, and you don't want to clutter the database with dozens of small, special-purpose tables. One solution is to use the UNION set operator to combine individual sets of data into a single set. (Set operators will be covered in detail in Chapter 7.) For example:

SELECT 'SMALL' name, 0 lower_bound, 29 upper_bound from dual

UNION ALL

SELECT 'MEDIUM' name, 30 lower_bound, 79 upper_bound from dual

UNION ALL

SELECT 'LARGE' name, 80 lower_bound, 9999999 upper_bound from dual;



NAME   LOWER_BOUND UPPER_BOUND

------ ----------- -----------

SMALL            0          29

MEDIUM          30          79

LARGE           80     9999999

You can then wrap this query in an inline view and use it to do your aggregations:

SELECT sizes.name order_size, SUM(co.sale_price) tot_dollars

FROM cust_order co INNER JOIN

 (SELECT 'SMALL' name, 0 lower_bound, 29 upper_bound from dual

  UNION ALL

  SELECT 'MEDIUM' name, 30 lower_bound, 79 upper_bound from dual

  UNION ALL

  SELECT 'LARGE' name, 80 lower_bound, 9999999 upper_bound from dual

 ) sizes

ON co.sale_price BETWEEN sizes.lower_bound AND sizes.upper_bound

WHERE co.cancelled_dt IS NULL 

  AND co.order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY')

  AND co.order_dt < TO_DATE('01-JAN-2002','DD-MON-YYYY') 

GROUP BY sizes.name

ORDER BY sizes.name DESC;



ORDER_ TOT_DOLLARS

------ -----------

SMALL          100

MEDIUM         292

LARGE          396

One word of caution: when constructing a set of ranges, make sure there are no gaps through which data may slip. For example, an order totaling $29.50 would not appear in either the small or medium categories, since $29.50 is neither between $0 and $29 nor between $30 and $79. One solution is to overlap the region boundaries so that there is no gap through which data can slip. Note that you can no longer use BETWEEN with this approach:

SELECT sizes.name order_size, SUM(co.sale_price) tot_dollars

FROM cust_order co INNER JOIN

(SELECT 'SMALL' name, 0 lower_bound, 30 upper_bound from dual

  UNION ALL

  SELECT 'MEDIUM' name, 30 lower_bound, 80 upper_bound from dual

  UNION ALL

  SELECT 'LARGE' name, 80 lower_bound, 9999999 upper_bound from dual

 ) sizes

ON co.sale_price >= sizes.lower_bound

    AND co.sale_price < sizes.upper_bound

WHERE co.cancelled_dt IS NULL

  AND co.order_dt >= TO_DATE('01-JAN-2001', 'DD-MON-YYYY')

  AND co.order_dt < TO_DATE('01-JAN-2002', 'DD-MON-YYYY')

GROUP BY sizes.name

ORDER BY sizes.name DESC;



ORDER_ TOT_DOLLARS

------ -----------

SMALL          100

MEDIUM         292

LARGE          396

Now that you have neither an overlap nor a gap between the buckets, you can be sure that no data will be left out of the aggregations.

Fabricated data sets can also be useful for determining what data is not stored in a database. For example, your manager might ask for a report listing the aggregate sales for each day of the year 2001, including days with no sales. Although the cust_order table contains records for every day that had orders, there is no table in the database containing a record for every day of the year. To provide your manager with an answer, you will need to fabricate a driving table containing a record for every day in 2001, and then outer join it to the set of aggregated sales for the same period.

Since a year contains either 365 or 366 days, we will build the set {0, 1, 2, ..., 399}, add each member of the set to the start date of 01-JAN-2001, and let Oracle throw away the rows that don't belong in 2001. To build the set {0, 1, 2, ..., 399}, we will create the sets {0, 1, 2, ..., 10}, {0, 10, 20, 30, ..., 90}, and {0, 100, 200, 300} and add members of the three sets across the Cartesian product:

SELECT ones.x + tens.x + hundreds.x tot

FROM

 (SELECT 0 x FROM dual UNION ALL

  SELECT 1 x FROM dual UNION ALL

  SELECT 2 x FROM dual UNION ALL

  SELECT 3 x FROM dual UNION ALL

  SELECT 4 x FROM dual UNION ALL

  SELECT 5 x FROM dual UNION ALL

  SELECT 6 x FROM dual UNION ALL

  SELECT 7 x FROM dual UNION ALL

  SELECT 8 x FROM dual UNION ALL

  SELECT 9 x FROM dual) ones 

CROSS JOIN

 (SELECT 0 x FROM dual UNION ALL

  SELECT 10 x FROM dual UNION ALL

  SELECT 20 x FROM dual UNION ALL

  SELECT 30 x FROM dual UNION ALL

  SELECT 40 x FROM dual UNION ALL

  SELECT 50 x FROM dual UNION ALL

  SELECT 60 x FROM dual UNION ALL

  SELECT 70 x FROM dual UNION ALL

  SELECT 80 x FROM dual UNION ALL

  SELECT 90 x FROM dual) tens

CROSS JOIN

 (SELECT 0 x FROM dual UNION ALL

  SELECT 100 x FROM dual UNION ALL

  SELECT 200 x FROM dual UNION ALL

  SELECT 300 x FROM dual) hundreds;



       TOT

----------

         0

         1

         2

         3

         4

         5

         6

         7

         8

         9

        10

 . . . 

       390

       391

       392

       393

       394

       395

       396

       397

       398

       399

Since this query has no join conditions, every combination of the rows in the ones, tens, and hundreds sets will be generated, and the sum of the three numbers in each row will produce the set {0, 1, 2, ..., 399}. The next query generates the set of days in 2001 by adding each number in the set to the base date and then discarding days that fall in 2002:

SELECT days.dt

FROM

 (SELECT TO_DATE('01-JAN-2001', 'DD-MON-YYYY') +

    ones.x + tens.x + hundreds.x dt

  FROM

   (SELECT 0 x FROM dual UNION ALL

    SELECT 1 x FROM dual UNION ALL

    SELECT 2 x FROM dual UNION ALL

    SELECT 3 x FROM dual UNION ALL

    SELECT 4 x FROM dual UNION ALL

    SELECT 5 x FROM dual UNION ALL

    SELECT 6 x FROM dual UNION ALL

    SELECT 7 x FROM dual UNION ALL

    SELECT 8 x FROM dual UNION ALL

    SELECT 9 x FROM dual) ones

  CROSS JOIN

   (SELECT 0 x FROM dual UNION ALL

    SELECT 10 x FROM dual UNION ALL

    SELECT 20 x FROM dual UNION ALL

    SELECT 30 x FROM dual UNION ALL

    SELECT 40 x FROM dual UNION ALL

    SELECT 50 x FROM dual UNION ALL

    SELECT 60 x FROM dual UNION ALL

    SELECT 70 x FROM dual UNION ALL

    SELECT 80 x FROM dual UNION ALL

    SELECT 90 x FROM dual) tens

  CROSS JOIN

   (SELECT 0 x FROM dual UNION ALL

    SELECT 100 x FROM dual UNION ALL

    SELECT 200 x FROM dual UNION ALL

    SELECT 300 x FROM dual) hundreds) days

WHERE days.dt < TO_DATE('01-JAN-2002', 'DD-MON-YYYY');



DT

---------

01-JAN-01

02-JAN-01

03-JAN-01

04-JAN-01

05-JAN-01

06-JAN-01

07-JAN-01

08-JAN-01

09-JAN-01

10-JAN-01

 . . . 

20-DEC-01

21-DEC-01

22-DEC-01

23-DEC-01

24-DEC-01

25-DEC-01

26-DEC-01

27-DEC-01

28-DEC-01

29-DEC-01

30-DEC-01

31-DEC-01

Since 2001 is not a leap year, the result set will contain 365 rows, one for each day of 2001. This query can then be wrapped in another inline view and used as the driving table for generating the report. Whether you would actually want to use such a strategy in your code is up to you; the main purpose of this example is to help get the creative juices flowing.

5.4.4 Overcoming SQL Restrictions

The use of certain features of Oracle SQL can impose restrictions on our SQL statements. When these features are isolated from the rest of the query inside an inline view, however, these restrictions can be sidestepped. This section explores how inline views can overcome limitations with hierarchical and aggregation queries.

5.4.4.1 Hierarchical queries

Hierarchical queries allow recursive relationships to be traversed. As an example of a recursive relationship, consider a table called region that holds data about sales territories. Regions are arranged in a hierarchy, and each record in the region table references the region in which it is contained, as illustrated by the following data:

SELECT * FROM region;



REGION_ID NAME                 SUPER_REGION_ID

---------- -------------------- ---------------

         1 North America

         2 Canada                             1

         3 United States                      1

         4 Mexico                             1

         5 New England                        3

         6 Mid-Atlantic                       3

         7 Southeast US                       3

         8 Southwest US                       3

         9 Northwest US                       3

        10 Central US                         3

        11 Europe

        12 France                            11

        13 Germany                           11

        14 Spain                             11

Each record in the customer table references the smallest of its applicable regions. Given a particular region, it is possible to construct a query that traverses up or down the hierarchy by utilizing the START WITH and CONNECT BY clauses:

SELECT region_id, name, super_region_id

  FROM region

  START WITH name = 'North America'

  CONNECT BY PRIOR region_id = super_region_id;



REGION_ID NAME                 SUPER_REGION_ID

---------- -------------------- ---------------

         1 North America

         2 Canada                             1

         3 United States                      1

         5 New England                        3

         6 Mid-Atlantic                       3

         7 Southeast US                       3

         8 Southwest US                       3

         9 Northwest US                       3

        10 Central US                         3

         4 Mexico                             1

The query just shown traverses the region hierarchy starting with the North America region and working down the tree. Looking carefully at the results, you can see that the Canada, United States, and Mexico regions all point to the North America region via the super_region_id field. The remainder of the rows all point to the United States region. Thus, we have identified a three-level hierarchy with one node at the top, three nodes in the second level, and six nodes in the third level underneath the United States node. For a detailed look at hierarchical queries, see Chapter 8.

Imagine that you have been asked to generate a report showing total sales in 2001 for each subregion of North America. However, hierarchical queries have the restriction that the table being traversed cannot be joined to other tables within the same query, so it might seem impossible to generate the report from a single query. Using an inline view, however, you can isolate the hierarchical query on the region table from the customer and cust_order tables, as in:

SELECT na_regions.name region_name, 

  SUM(co.sale_price) total_sales

FROM cust_order co INNER JOIN customer c

  ON co.cust_nbr = c.cust_nbr

INNER JOIN

 (SELECT region_id, name

  FROM region

  START WITH name = 'North America'

  CONNECT BY PRIOR region_id = super_region_id) na_regions

  ON c.region_id = na_regions.region_id

WHERE co.cancelled_dt IS NULL

  AND co.order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY')

  AND co.order_dt < TO_DATE('01-JAN-2002','DD-MON-YYYY')

GROUP BY na_regions.name;



REGION_NAME          TOTAL_SALES

-------------------- -----------

Mid-Atlantic                 198

New England                  590

Even though the na_regions set includes the North America and United States regions, customer records always point to the smallest applicable region, which is why these particular regions do not show up in the final result set.

By placing the hierarchical query within an inline view, you are able to temporarily flatten the region hierarchy to suit the purposes of the query, which allows you to bypass the restriction on hierarchical queries without resorting to splitting the logic into multiple pieces. The next section will demonstrate a similar strategy for working with aggregate queries.

5.4.4.2 Aggregate queries

Queries that perform aggregations have the following restriction: all nonaggregate columns in the SELECT clause must be included in the GROUP BY clause. Consider the following query, which aggregates sales data by customer and salesperson, and then adds supporting data from the customer, region, employee, and department tables:

SELECT c.name customer, r.name region, 

  e.fname || ' ' || e.lname salesperson, d.name department,

  SUM(co.sale_price) total_sales

FROM cust_order co INNER JOIN customer c

  ON co.cust_nbr = c.cust_nbr

  INNER JOIN region r

  ON c.region_id = r.region_id

  INNER JOIN employee e

  ON co.sales_emp_id = e.emp_id

  INNER JOIN department d

  ON e.dept_id = d.dept_id

WHERE co.cancelled_dt IS NULL 

  AND co.order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY')

GROUP BY c.name, r.name, e.fname || ' ' || e.lname, d.name;



CUSTOMER         REGION        SALESPERSON     DEPARTMENT   TOTAL_SALES

---------------- ------------- --------------- ------------ -----------

Zantech Inc.     Mid-Atlantic  KENNETH MARTIN  SALES        198

Flowtech Inc.    New England   KENNETH MARTIN  SALES        180

Since every nonaggregate in the SELECT clause must be included in the GROUP BY clause, you are forced to sort on five columns, since a sort is needed to generate the groupings. Because every customer is in one and only one region and every employee is in one and only one department, you really only need to sort on the customer and employee columns to produce the desired results. So the Oracle engine is wasting its time sorting on the region and department names.

However, by isolating the aggregation from the supporting tables, you can create a more efficient and more understandable query:

SELECT c.name customer, r.name region, 

  e.fname || ' ' || e.lname salesperson, d.name department, 

  cust_emp_orders.total total_sales

FROM 

 (SELECT cust_nbr, sales_emp_id, SUM(sale_price) total

  FROM cust_order

  WHERE cancelled_dt IS NULL 

    AND order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY')

  GROUP BY cust_nbr, sales_emp_id) cust_emp_orders

  INNER JOIN customer c 

  ON cust_emp_orders.cust_nbr = c.cust_nbr

  INNER JOIN region r

  ON c.region_id = r.region_id

  INNER JOIN employee e

  ON cust_emp_orders.sales_emp_id = e.emp_id

  INNER JOIN department d

  ON e.dept_id = d.dept_id;



CUSTOMER         REGION        SALESPERSON     DEPARTMENT   TOTAL_SALES

---------------- ------------- --------------- ------------ -----------

Flowtech Inc.    New England   KENNETH MARTIN  SALES        180

Zantech Inc.     Mid-Atlantic  KENNETH MARTIN  SALES        198

Since the cust_order table includes the customer number and salesperson ID, you can perform the aggregation against these two columns without the need to include the other four tables. Not only are you sorting on fewer columns, you are sorting on numeric fields (customer number and employee ID) rather than potentially lengthy strings (customer name, region name, employee name, and department name). The containing query uses the cust_nbr and sales_emp_id columns from the inline view to join to the customer and employee tables, which in turn are used to join to the region and department tables.

By performing the aggregation within an inline view, you have sidestepped the restriction that all nonaggregates be included in the GROUP BY clause. You have also shortened execution time by eliminating unnecessary sorts and minimized the number of joins to the customer, region, employee, and department tables. Depending on the amount of data in the tables, these improvements could yield significant performance gains.

5.4.5 Inline Views in DML Statements

Now that you are comfortable with inline views, it's time to add another wrinkle: inline views may also be used in INSERT, UPDATE, and DELETE statements. In most cases, using an inline view in a DML statement improves readability but otherwise adds little value to statement execution. To illustrate, we'll begin with a fairly simple UPDATE statement and then show the equivalent statement using an inline view:

UPDATE cust_order co 

SET co.expected_ship_dt = co.expected_ship_dt + 7

WHERE co.cancelled_dt IS NULL AND co.ship_dt IS NULL

  AND EXISTS (SELECT 1 

    FROM line_item li INNER JOIN part p

      ON li.part_nbr = p.part_nbr

    WHERE li.order_nbr = co.order_nbr 

      AND p.inventory_qty = 0);

This statement uses an EXISTS condition to locate orders that include out-of-stock parts. The next version uses an inline view called suspended_orders to identify the same set of orders:

UPDATE (SELECT co.expected_ship_dt exp_ship_dt 

  FROM cust_order co 

  WHERE co.cancelled_dt IS NULL AND co.ship_dt IS NULL

    AND EXISTS (SELECT 1 

      FROM line_item li INNER JOIN part p

      ON li.part_nbr = p.part_nbr

      WHERE li.order_nbr = co.order_nbr 

        AND p.inventory_qty = 0)) suspended_orders

SET suspended_orders.exp_ship_dt = suspended_orders.exp_ship_dt + 7;

In the first statement, the WHERE clause of the UPDATE statement determines the set of rows to be updated, whereas in the second statement, the result set returned by the SELECT statement determines the target rows. Otherwise, the two statements are identical. For the inline view to add extra value to the statement, it must be able to do something that the simple update statement cannot do: join multiple tables. The following version attempts to do just that by replacing the subquery with a three-table join:

UPDATE (SELECT co.expected_ship_dt exp_ship_dt 

  FROM cust_order co INNER JOIN line_item li

    ON co.order_nbr = li.order_nbr

    INNER JOIN part p

    ON li.part_nbr = p.part_nbr

  WHERE co.cancelled_dt IS NULL AND co.ship_dt IS NULL

    AND p.inventory_qty = 0) suspended_orders

SET suspended_orders.exp_ship_dt = suspended_orders.exp_ship_dt + 7;

However, statement execution results in the following error:

ORA-01779: cannot modify a column which maps to a non key-preserved table

As is often the case in life, we can't get something for nothing. To take advantage of the ability to join multiple tables within a DML statement, we must abide by the following rules:

  • Only one of the joined tables in an inline view may be modified by the containing DML statement.

  • To be modifiable, the target table's key must be preserved in the result set of the inline view.

Although the previous UPDATE statement attempts to modify only one table (cust_order), that table's key (order_nbr) is not preserved in the result set, since an order has multiple line items. In other words, rows in the result set generated by the three-table join cannot be uniquely identified using just the order_nbr field, so it is not possible to update the cust_order table via this particular three-table join. However, it is possible to update or delete from the line_item table using the same join, since the key of the line_item table matches the key of the result set returned from the inline view (order_nbr and part_nbr). The next statement deletes rows from the line_item table using an inline view nearly identical to the one that failed for the previous UPDATE attempt:

DELETE FROM (SELECT li.order_nbr order_nbr, li.part_nbr part_nbr

  FROM cust_order co INNER JOIN line_item li

    ON co.order_nbr = li.order_nbr

    INNER JOIN part p

    ON li.part_nbr = p.part_nbr

  WHERE co.cancelled_dt IS NULL AND co.ship_dt IS NULL

    AND p.inventory_qty = 0) suspended_orders;

The column(s) referenced in the SELECT clause of the inline view are actually irrelevant. Since the line_item table is the only key-preserved table of the three tables listed in the FROM clause, this is the table on which the DELETE statement operates. Although utilizing an inline view in a DELETE statement can be more efficient, it's somewhat disturbing that it is not immediately obvious which table is the focus of the DELETE statement. A reasonable convention when writing such statements would be to always select the key columns from the target table.

5.4.6 Restricting Access Using WITH CHECK OPTION

Another way in which inline views can add value to DML statements is by restricting both the rows and columns that may be modified. For example, most companies only allow members of Human Resources to see or modify salary information. By restricting the columns visible to a DML statement, we can effectively hide the salary column:

UPDATE (SELECT emp_id, fname, lname, dept_id, manager_emp_id 

  FROM employee) emp

SET emp.manager_emp_id = 11

WHERE emp.dept_id = 4;

Although this statement executes cleanly, attempting to add the salary column to the SET clause would yield the following error:

UPDATE (SELECT emp_id, fname, lname, dept_id, manager_emp_id 

  FROM employee) emp

SET emp.manager_emp_id = 11, emp.salary = 1000000000

WHERE emp.dept_id = 4;



ORA-00904: "EMP"."SALARY": invalid identifier

Of course, the person writing the UPDATE statement has full access to the table; the intent here is to protect against unauthorized modifications by the users. This might prove useful in an n-tier environment, where the interface layer interacts with a business-logic layer.

Although this mechanism is useful for restricting access to particular columns, it does not limit access to particular rows in the target table. To restrict the rows that may be modified using a DML statement, you can add a WHERE clause to the inline view and specify WITH CHECK OPTION. For example, you may want to restrict the users from modifying data for any employee in the Accounting department:

UPDATE (SELECT emp_id, fname, lname, dept_id, manager_emp_id 

  FROM employee 

  WHERE dept_id != 

   (SELECT dept_id FROM department WHERE name = 'ACCOUNTING')

  WITH CHECK OPTION) emp

SET emp.manager_emp_id = 7698

WHERE emp.dept_id = 30;

The addition of WITH CHECK OPTION to the inline view protects against any data modifications that would not be visible via the inline view. For example, attempting to modify an employee's department assignment from Sales to Accounting would generate an error, since the data would no longer be visible via the inline view:

UPDATE (SELECT emp_id, fname, lname, dept_id, manager_emp_id 

  FROM employee 

  WHERE dept_id != 

   (SELECT dept_id FROM department WHERE name = 'ACCOUNTING')

  WITH CHECK OPTION) emp

SET dept_id = (SELECT dept_id FROM department WHERE name = 'ACCOUNTING')

WHERE emp_id = 7900;

 

ORA-01402: view WITH CHECK OPTION where-clause 



violation

5.4.7 Global Inline Views

Earlier in the chapter, you saw how the WITH clause can be used to allow the same subquery to be referenced multiple times within the same query. Another way to utilize the WITH clause is as an inline view with global scope. To illustrate, we will rework one of the previous inline view examples to show how the subquery can be moved from the FROM clause to the WITH clause. Here's the original example, which comes from Section 5.4.4.1:

SELECT na_regions.name region_name, 

  SUM(co.sale_price) total_sales

FROM cust_order co INNER JOIN customer c

ON co.cust_nbr = c.cust_nbr

INNER JOIN

 (SELECT region_id, name

  FROM region

  START WITH name = 'North America'

  CONNECT BY PRIOR region_id = super_region_id) na_regions

ON c.region_id = na_regions.region_id

WHERE co.cancelled_dt IS NULL

  AND co.order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY')

  AND co.order_dt < TO_DATE('01-JAN-2002','DD-MON-YYYY') 

GROUP BY na_regions.name;



REGION_NAME          TOTAL_SALES

-------------------- -----------

Mid-Atlantic                 198

New England                  590

Here's the same query with the na_regions subquery moved to the WITH clause:

WITH na_regions AS (SELECT region_id, name

  FROM region

  START WITH name = 'North America'

  CONNECT BY PRIOR region_id = super_region_id)

SELECT na_regions.name region_name, 

  SUM(co.sale_price) total_sales

FROM cust_order co INNER JOIN customer c

ON co.cust_nbr = c.cust_nbr

INNER JOIN na_regions

ON c.region_id = na_regions.region_id

WHERE co.cancelled_dt IS NULL

  AND co.order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY')

  AND co.order_dt < TO_DATE('01-JAN-2002','DD-MON-YYYY')

GROUP BY na_regions.name;



REGION_NAME          TOTAL_SALES

-------------------- -----------

Mid-Atlantic                 198

New England                  590

Note that the FROM clause must include the inline view alias for you to reference the inline view's columns in the SELECT, WHERE, GROUP BY, or ORDER BY clauses.

To show how the na_regions subquery has global scope, the join between the na_regions inline view and the customer table has been moved to another inline view (called cust) in the FROM clause:

WITH na_regions AS (SELECT region_id, name

  FROM region

  START WITH name = 'North America'

  CONNECT BY PRIOR region_id = super_region_id)

SELECT cust.region_name region_name, 

  SUM(co.sale_price) total_sales

FROM cust_order co INNER JOIN 

 (SELECT c.cust_nbr cust_nbr, na_regions.name region_name

  FROM customer c INNER JOIN na_regions

  ON c.region_id = na_regions.region_id) cust

ON co.cust_nbr = cust.cust_nbr

WHERE co.cancelled_dt IS NULL

  AND co.order_dt >= TO_DATE('01-JAN-2001','DD-MON-YYYY')

  AND co.order_dt < TO_DATE('01-JAN-2002','DD-MON-YYYY')

GROUP BY cust.region_name;



REGION_NAME          TOTAL_SALES

-------------------- -----------

Mid-Atlantic                 198

New England                  590

Earlier in this section, we stated that inline views "are always executed prior to the containing query and, thus, may not reference columns from other tables or inline views from the same query." Using the WITH clause, however, you are able to break this rule, since the na_regions inline view is visible everywhere within the query. This makes the na_regions inline view act more like a temporary table than a true inline view.