Applying Subquery Factoring to PL/SQL 在PL/SQL中运用子查询分解
Even PL/SQL can present golden opportunities for optimization using subquery factoring. Something
that most of us have done at one time or another is to write a PL/SQL routine when we cannot figure out
how to do what we want in a single SQL query. Sometimes it can be very difficult to capture everything
in a single statement. It’s often just easier to think procedurally rather than in sets of data, and just write
some code to do what we need. As you gain experience, you will rely less and less on thinking in terms of
“How would I code this in PL/SQL?” and more along the lines of “How do I capture this problem in a
single SQL statement?” The more advanced features that Oracle has packed into SQL can help as well.
即使在PL/SQL中子查询分解也有绝好的时机一显身手。有时候我们能一次做好一件事,而当不能想出用一条SQL解决想要的问题的时候,就会写PL /SQL程序。有时候,在一条SQL中获取所有是非常困难的。通常过程化的思维较数据集合的要容易,且只要这样写我们需要的代码即可。随着你经验的积累, 你将越来越少的依照“我将如何用PL/SQL编码”而更倾向于“我如何能用一条SQL语句解决这个问题”? Oracle封装于SQL中的越来越多的高级特性也将有作为。
Here’s an example. You’ve been asked to create a report with the following criteria:
• Only include customers that have purchased products in at least three different years.
• Compute total aggregate sales per customer, broken down by product category.
这里是一个例子,要求你按照如下标准创建一个报告:
只包含至少在三个不同年份购买产品的顾客。
计算每个顾客总的聚合销售额,按产品目录分
At first, this doesn’t seem too difficult. But you may struggle for a bit trying to capture this in one
SQL statement, so you decide to use a PL/SQL routine to get the needed data. The results may be similar
to those in Listing 10-10. The logic is simple. Find all customers that fit the criteria and store their IDs in
a temporary table. Then loop through the newly saved customer IDs and find all their sales, sum them
up, and add them to another temporary table. The results are then joined to the CUSTOMERS and
PRODUCTS tables to generate the report.
最初,这似乎不太难。但是你可能会苦恼试图用一句SQL获取,因此你决定用PL/SQL程序获取所需的数据。结果可能相似于列表10-10所示。逻辑较简 单。找出所有顾客匹配标准的顾客再存储他们的ID到临时表。再遍历新保存的顾客ID找出他们所有的销售额,汇总,再把它们加到另一个临时表。结果再连接到 CUSTOMERS和products表产生报告。
Listing 10-10. PL/SQL to Generate Customer Report
SQL> create global temporary table cust3year ( cust_id number );
Table created.
SQL> create global temporary table sales3year(
2 cust_id number ,
3 prod_category varchar2(50),
4 total_sale number
5 )
6 /
Table created.
SQL> begin
2 execute immediate 'truncate table cust3year';
3 execute immediate 'truncate table sales3year';
4
5 insert into cust3year
6 select cust_id --, count(cust_years) year_count
7 from (
8 select distinct cust_id, trunc(time_id,'YEAR') cust_years
9 from sh.sales
10 )
11 group by cust_id
12 having count(cust_years) >= 3;
13
14 for crec in ( select cust_id from cust3year)
15 loop
16 insert into sales3year
17 select s.cust_id,p.prod_category, sum(co.unit_price * s.quantity_sold)
18 from sh.sales s
19 join sh.products p on p.prod_id = s.prod_id
20 join sh.costs co on co.prod_id = s.prod_id
21 and co.time_id = s.time_id
22 join sh.customers cu on cu.cust_id = s.cust_id
23 where s.cust_id = crec.cust_id
24 group by s.cust_id, p.prod_category;
25 end loop;
26 end;
27 /
PL/SQL procedure successfully completed.
Elapsed: 00:01:17.48
SQL> break on report
SQL> compute sum of total_sale on report
SQL> select c3.cust_id, c.cust_last_name, c.cust_first_name, s.prod_category, s.total_sale
2 from cust3year c3
3 join sales3year s on s.cust_id = c3.cust_id
4 join sh.customers c on c.cust_id = c3.cust_id
5 order by 1,4;
CUST ID LAST NAME FIRST NAME PRODUCT CATEGORY TOTAL SALE
--------- --------------- --------------- ------------------------------ ---------------
6 Charles Harriett Electronics 2,838.57
6 Charles Harriett Hardware 19,535.38
...
50833 Gravel Grover Photo 15,469.64
50833 Gravel Grover Software/Other 9,028.87
---------------
sum 167,085,605.71
16018 rows selected.
The code in Listing 10-10 is fairly succinct, and it only takes 1:17 minutes to run. That’s not too bad,
is it? While this is a nice little chunk of PL/SQL, take another look at it and think in terms of subfactored
subqueries. The section that determines the correct customer IDs can be captured in a WITHclause fairly
easily. Once the customers are identified, it is a fairly easy job to then use the results of the subquery to
lookup the needed sales, product, and customer information to create the report.
在列表10-10中的代码相当简洁,只花费了1:17分运行。不太糟糕,是么?虽然这是一小段PL/SQL,换个角度,从子查询分解的角度看它。确定正确 的顾客ID的那段可用WITH子句相当容易的捕获。一旦顾客确认了,接下来的工作就容易了,使用子查询的结果查找所需的sales,product,和顾客信息创建报告。
Listing 10-11 has a single SQL statement that captures what is done with the PL/SQL routine from
Listing 10-10—without the need to manually create temporary tables or use PL/SQL loops. Should the
use of temporary tables make for a more efficient query, Oracle will do so automatically, or you can
choose how Oracle preserves the subquery results via the INLINE and MATERIALIZE hints. It is somewhat
more efficient, too, with an elapsed time of 6.13 seconds.
列表10-11是一条SQL语句完成了列表10-10的PL/SQL程序完成的任务--而不需要手动的创建临时表或使用PL/SQL循环。使用临时表本该 使得查询更有效率,Oracle将自动的完成,或者你可选择Oracle阻止子查询结果通过INLINE和MATERIALIZE提示。 它更有效率,只耗时6.13s 。
The WITH clause in Listing 10-11 actually uses two subqueries. These could be combined into a
single query, but I thought it easier to read broken out into two queries. Notice the use of the EXTRACT()
function—it simplifies comparing years by extracting the year from a date and converting it to an
integer.
在列表10-11中的WITH子句实际使用了两个子查询。本可以组成一个单独的查询,但是我想它分成两个查询更容易读。注意使用EXTRACT()函数--它简化比较years通过从数据中提取年份再转换它成一个整数。
Listing 10-11. Use WITH Clause to Generate Customer Report
1 with custyear as (
2 select cust_id, extract(year from time_id) sales_year
3 from sh.sales
4 where extract(year from time_id) between 1998 and 2002
5 group by cust_id, extract(year from time_id)
6 ),
7 custselect as (
8 select distinct cust_id
9 from (
10 select cust_id, count(*) over ( partition by cust_id) year_count
11 from custyear
12 )
13 where year_count >= 3 -- 3 or more years as a customer during period
14 )
15 select cu.cust_id, cu.cust_last_name, cu.cust_first_name, p.prod_category,
sum(co.unit_price * s.quantity_sold) total_sale
16 from custselect cs
17 join sh.sales s on s.cust_id = cs.cust_id
18 join sh.products p on p.prod_id = s.prod_id
19 join sh.costs co on co.prod_id = s.prod_id
20 and co.time_id = s.time_id
21 join sh.customers cu on cu.cust_id = cs.cust_id
22 group by cu.cust_id, cu.cust_last_name, cu.cust_first_name, p.prod_category
23 order by cu.cust_id;
CUST ID LAST NAME FIRST NAME PRODUCT CATEGORY TOTAL SALE
--------- --------------- --------------- ------------------------------ ---------------
6 Charles Harriett Electronics 2,838.57
6 Charles Harriett Hardware 19,535.38
...
50833 Gravel Grover Photo 15,469.64
50833 Gravel Grover Software/Other 9,028.87
---------------
sum 167,085,605.71
16018 rows selected.
Elapsed: 00:00:06.13
The SQL examples in this section of the chapter are not meant to be tuning exercises, but merely
demonstrations showing how subquery factoring may be used. When refactoring legacy SQL to take
advantage of the WITH clause, be sure to test the results. Subquery factoring can be used to better
organize some queries, and in some cases can even be used as an optimization tool. Learning to use it
adds another tool to your Oracle toolbox.
本节的SQL例子不是调优实践,而仅仅演示如何使用子查询分解。当利用WITH子句重构历史遗留的SQL时,确保测试了结果。子查询分解可用于更好组织某些查询,某些情况下可作为一个优化工具。学习使用它又为你Oracle工具箱增加了一把工具。
EXPERIMENT WITH SUBQUERY FACTORING 子查询分解实验
Included in this chapter are two scripts in the Exercises folder that you may want to experiment with.
These scripts both run against the SH demo schema.
在本章的Exercises文件夹中包含两个脚本,你可用于实验。这两个脚本都运行在SH演示模式下。
Run these scripts with both the MATERIALIZE and INLINE hints to compare performance. In the tsales
subquery, a WHERE clause limits the data returned to a single year. Comment out the WHERE clause and
run the queries again. How does the efficiency of the two hints compare now? Would you feel
comfortable using these hints when the size of the data set is set at runtime by user input?
用MATERIALIZE和INLINE提示运行这两个脚本比较性能。在tsales子查询中,WHERE子句限制返回单个年的数据。注释掉WHERE子 句再次运行。现在两个提示的效率相比如何?当数据集的大小由运行时用户输入确定时,你是否对这些提示的使用感到满意。
• Exercises/l_10_exercise_1.sql
• Exercises/l_10_exercise_2.sql