The PL/SQL version of the above script looks like this: SET VERIFY OFF ACCEPT s_delete_confirm PROMPT Delete project hours data Y/N?. In this case, the command: @&&s_next_script will be
Trang 2SET FEEDBACK ON
CLEAR COLUMNS
TTITLE OFF
You have to be very careful when using this technique to turn off anything that could cause extraneous text to be written
to the temporary command file This includes page headings, column headings, and verification That's why the script in the example included these commands:
SET TERMOUT OFF
SET PAGESIZE 0
SET HEADING OFF
SET VERIFY OFF
Terminal output was turned off to prevent the user from seeing the results of the SELECT on the display One last thing you have to worry about is the filename itself In the example shown above, the filename is hardwired into the script and does not include a path Because no path is specified, the file will be written to the current directory That's why a single ampersand or @ was used to run the intermediate file Using @ causes SQL*Plus to look in the current directory for the script
Having the filename hardwired into the script can cause problems if multiple users execute the script at the same time and from the same directory If you are concerned about this, you could write some SQL or PL/SQL code to generate a unique filename based on the Oracle username or perhaps the session identifier (SID) from the V $ SESSION view
Be creative with this technique You don't need to limit yourself to writing SQL*Plus scripts, either You can use
SQL*Plus to generate shell script files, SQL*PLoader files, DOS batch files, or any other type of text file
Using PL/SQL
Always consider the possibility of using PL/SQL to implement any type of complex procedural logic After all, that's the reason PL/SQL was invented in the first place If you can manage to prompt the user up front for any needed
information, and if you don't need to interact with the user during the operation, PL/SQL is the way to go
Trang 3
Page 253
The reports menu could not possibly be implemented in Pl/SQL because the menu needs to run another SQL*Plus script corresponding to the user's choice PL/SQL runs inside the database, and cannot invoke a SQL*Plus script
An ideal candidate for the use of PL/SQL would be the example where we asked the user a simple yes/no question, and then deleted data from the PROJECT_HOURS table if the user responded with a Y Here's how that script looked:
SET VERIFY OFF
ACCEPT s_delete_confirm PROMPT Delete project hours data (Y/N)?
DELETE
FROM project_hours
WHERE UPPER(&&s_delete_confirm) = Y;
This script works, and because the DELETE statement is so simple, it's not too hard to understand Still, there are
people who would look at it and become very confused The more complicated the WHERE clause gets, the greater the likelihood of confusion Wrapping a simple IF statement, which everyone understands, around the DELETE statement would add clarity to the script The PL/SQL version of the above script looks like this:
SET VERIFY OFF
ACCEPT s_delete_confirm PROMPT Delete project hours data (Y/N)?
SET SERVEROUTPUT ON
DECLARE
users_yn_response CHAR := UPPER(&&s_delete_confirm);
BEGIN
IF users_yn_response = Y THEN
DELETE
FROM project_hours;
COMMIT;
DBMS_OUTPUT.PUT_LINE(All PROJECT_HOURS data has been deleted.);
ELSIF users_yn_response = N THEN
DBMS_OUTPUT.PUT_LINE(No data was deleted.);
ELSE
DBMS_OUTPUT.PUT_LINE(You must answer with a Y or N.);
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(The PROJECT_HOURS data could not be deleted
¦¦ SQLERRM);
ROLLBACK;
END;
/
Trang 5Page 254
This script is a bit longer, but it's also more robust The script will roll back the operation if the DELETE fails for any
reason It's also very clear now that the DELETE statement is going to delete all rows from the table.
Using a Scripting Language Instead
Don't overlook the possibility that you can use your operating-system scripting language to good advantage Any Unix shell will allow you to write more complex scripts than you could using SQL*Plus alone Here's an implementation of the user security report menu using the Unix Korn shell:
while:
do
print 1 - List users
print 2 - List users and table privileges
print 3 - List users and system privileges
print 4 - Quit
print -n Enter your choice (1,2,3,4) >
read
case $ REPLY in
1 )
sqlplus -s jgennick/beaner @user_security_1
;;
2 )
sqlplus -s jgennick/beaner @user_security_2
;;
3 )
sqlplus -s jgennick/beaner @user_security_3
;;
4 )
exit
;;
* )
print Please enter 1 , 2, 3, or 4
;;
esac
done
Peri is also something you should consider The Perl scripting language is available for both Unix and Windows It has the advantages of being widely used, and of not tying you to one specific operating system
Looping in SQL*Plus
There is no way to write a real loop using SQL*Plus Your best option, if you need to do something iteratively, is to use PL/SQL PL/SQL, however, doesn't allow you
Trang 6
Recursive execution
Generating a file of commands, and then executing it
The first option has some severe limitations, and I don't recommend it too strongly The second option I use all the time, especially when performing database maintenance tasks
Recursive Execution
You can't loop, but you can execute the same script recursively Say you have a script that displays some useful
information, and you want to give the user the option of running it again You can do that by recursively executing the script Take a look at the following interaction, in which the user is looking at indexes for various tables It looks like a loop Each time through, the user is prompted for another table name, and the indexes on that table are displayed
SQL> @list_indexex employee
INDEX_NAME COLUMN_NAME
- -
EMPLOYEE_PK EMPLOYEE_ID
Next table >project
INDEX_NAME COLUMN_NAME
- -
PROJECT_PK PROJECT_ID
PROJECT_BY_NAME PROJECT_NAME
Next table >project_hours
INDEX_NAME COLUMN_NAME
- -
PROJECT_HOURS_PK PROJECT_ID
EMPLOYEE_ID
TIME_LOG_DATE
Next table >
Thank you for using LIST_INDEXES Goodbye!
It sure does look like a loop, but it's not Here is the LIST_INDEXES script that is being run:
COLUMN index_name FORMAT A30
COLUMN column_name FORMAT A30
BREAK ON index_name NODUPLICATES
Trang 7
Page 256
SELECT index_name, column_name
FROM user_ind_columns
WHERE table_name = UPPER (1);
Ask the user if he wants to do this again
PROMPT
ACCEPT s_next_table PROMPT Next table >
Execute either list_indexes.sql or empty.sql,
depending on the user's response
COLUMN next_script NOPRINT NEW_VALUE s_next_script
SET TERMOUT OFF
SELECT DECODE (&&s_next_table,
,empty.sql,
list_indexes ¦¦ UPPER (&&s_next_table)) next_script
FROM dual;
SET TERMOUT ON
@&&s_next_script
The key to the looping is in the last part of the script, following the ACCEPT statement If the user enters another
tablename, the SELECT statement will return another call to the LIST_INDEXES script So when the user types project
in response to the prompt, the s_next_script substitution variable ends up being:
list_indexes PROJECT
The only thing missing is the ampersand sign, and that is supplied by the command at the bottom of the script In this case, the command:
@&&s_next_script
will be translated to:
@list_indexes PROJECT
If the user doesn't enter a table name at the prompt, the s_next_table variable will be null, and the DECODE statement
will return empty.sql EMPTY.SQL is necessary because the @ command must be executed EMPTY.SQL gives you a clean way out of the recursion In this case EMPTY.SQL prints a message, and is implemented like this:
PROMPT
PROMPT Thank you for using LIST_INDEXES Goodbye!
PROMPT
Recursive execution is a very limited technique You can't nest scripts forever SQL*Plus limits you to nesting only 20 scripts, and on some older versions the limit may be as low as 5 Exceed that limit, and you will get the following
message:
SQL*Plus command procedures may only be nested to a depth of 20
Trang 8
Generating a File of Commands
If you need to loop a fixed number of times, say once for each table you own, you can use a SQL query to build a
second file of SQL commands, then execute that file This is known as using SQL to write SQL, and it's a very powerful
scripting technique You've already seen an example of this technique in a previous section, where it was used to
implement the equivalent of an IF statement in a SQL*Plus script This technique can also be used to perform a
repetitive operation, if the basis for that operation can be the results of a SQL query
Suppose, for instance, that you want to write a script that will analyze and compute statistics for all your tables You don't want to hardcode the table names in the script, because then you would need to remember to change the script each time you drop or create a table Thinking in terms of pseudocode, you might envision a script like this:
FOR xxx = FIRST_TABLE TO LAST_TABLE
ANALYZE xxx COMPUTE STATISTICS;
NEXT XXX
Of course, that script would never fly in SQL*Plus Instead, you need a way to execute the ANALYZE command for each table without really looping One way to do that is to write a SQL query to create that command Take a look at the follow ing query:
SELECT ANALYZE ¦¦ table_name ¦¦ COMPUTE STATISTICS;
FROM user_tables;
Since SQL is a set-oriented language, it will return a result set consisting of one instance of the ANALYZE command for each table you own Running this query against the sample database used for this book will give the following
results:
ANALYZE EMPLOYEE COMPUTE STATISTICS;
ANALYZE PROJECT COMPUTE STATISTICS;
ANALYZE PROJECT_HOURS COMPUTE STATISTICS;
At this point, if this were just a once-off job, and if you were using a GUI version of SQL*Plus, you could simply copy the output and paste it back in as input The commands would execute and all your tables would be analyzed To make a script you can run periodically, all you need to do is to spool the ANALYZE commands to a file and then execute that file The following script does that
Trang 9
Page 258
SET ECHO OFF
DESCRIPTION
Analyze and compute statistics for all tables
owned by the user
We only want the results of the query written to the
file Headings, titles, feedback, etc aren't valid
commands, so turn all that stuff off
SET HEADING OFF
SET PAGESIZE 0
SET VERIFY OFF
SET FEEDBACK OFF
SET TERMOUT OFF
Create a file of ANALYZE commands, one for
each table PROMPT commands are used to write
SET ECHO ON and OFF commands to the spool file
SPOOL analyze_each_table.sql
PROMPT SET ECHO ON
SELECT ANALYZE TABLE ¦¦ table_name ¦¦ COMPUTE STATISTICS;
FROM user_tables;
PROMPT SET ECHO OFF
SPOOL OFF
Execute the ANALYZE commands
SET TERMOUT ON
@analyze_each_table
Reset settings back to defaults
SET HEADING ON
SET PAGESIZE 14
SET VERIFY ON
SET FEEDBACK ON
Most of the commands in the script are there to prevent any extraneous information, such as column headings or page titles, from being written to the spool file The real work is done by the SPOOL and SELECT commands, and also by the command used to run the resulting script file The ECHO setting is turned on prior to running the ANALYZE
commands so you can watch the ANALYZE commands as they execute A PROMPT command is used to write a SET ECHO ON command to the output file Here are the results from running the script:
SQL> @analyze_tables
SQL> ANALYZE TABLE EMPLOYEE COMPUTE STATISTICS;
SQL> ANALYZE TABLE PROJECT COMPUTE STATISTICS;
SQL> ANALYZE TABLE PROJECT_HOURS COMPUTE STATISTICS;
SQL> SET ECHO OFF
All the tables in the schema were analyzed Once this is set up, you never need to worry about it again Each time the script is run, ANALYZE commands are generated for all the tables currently in the schema
Trang 10
Looping Within PL/SQL
You should always consider PL/SQL when you need to implement any type of complex procedural logic, and that includes looping Because PL/SQL executes in the database, you can't use it for any type of loop that requires user interaction The table index example shown earlier in this chapter, where the user was continually prompted for another table name, could never be implemented in PL/SQL It's also impossible to call another SQL*Plus script from PL/SQL However, if you can get around those two limitations, PL/SQL may be the best choice for the task
The ANALYZE TABLE script revisited
As an example of what you can do using PL/SQL, let's revisit the ANALYZE_TABLE script shown earlier It's very easy, using PL/SQL, to write a loop to iterate through all the tables you own Here's one way to do that:
SQL> SET SERVEROUTPUT ON
SQL> BEGIN
2 FOR my_table in (
3 SELECT table_name
4 FROM user_tables) LOOP
5
5 Print the table name
6 DBMS_OUTPUT.PUT_LINE(my_table.table_name);
7 END LOOP;
8 END;
9 /
EMPLOYEE
PROJECT
PROJECT_HOURS
PL?SQL procedure successfully completed
This example uses what is called a cursor FOR loop A cursor FOR loop executes once for each row returned by the
query you give it In this example, that query returns a list of tables you own
You might think you could just put an ANALYZE TABLE command inside the loop and pass it the table name as a parameter, but it's not quite that simple The ANALYZE command is a DDL command, and prior to version 8.1 of Oracle, PL/SQL did not allow you to embed DDL commands within your code The following is an example of what will happen if you try