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.
|
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.
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.
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.
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.
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.
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.
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.
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.
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
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.