Here's the query to return a list of people employed as of January 1, 1998: SELECT employee_id, employee_name, employee_hire_date, employee_termination_date FROM employee WHERE empl
Trang 1Page 117
Taking Advantage of Unions
A union is a SQL construct that allows you to knit together the results of several SQL queries and treat those results as
if they had been returned by just one query I find them invaluable when writing queries, and one of the more creative uses I've discovered involves using unions to produce reports that need to show data grouped by categories, and that may need to show the same records in more than one of those categories
A typical example
A good example of this type or report would be one that fulfills the following request:
Produce an employee turnover report that lists everyone employed at the beginning of the year, everyone hired during the year, everyone terminated during the year, and everyone still employed at the end of the year The report should be divided into four sections, one for each of those categories
This is not an unusual type of request, not for me at least The interesting thing about this request, though, is that every employee will need to be listed in exactly two categories That means you would need to write a query that returned each employee record twice, in the correct categories
When you are faced with this type of query, it can be helpful to simplify the problem by thinking in terms of separate queries, one for each category It's fairly easy to conceive of a query to bring back a list of employees that were on board at the beginning of the year You just need to make sure the first of the year is between the hire and termination dates, and account for the fact that the termination date might be null Here's the query to return a list of people
employed as of January 1, 1998:
SELECT employee_id,
employee_name,
employee_hire_date,
employee_termination_date
FROM employee
WHERE employee_hire_date < TO_DATE(1-Jan-1988, dd-mon-yyyy)
AND (employee_termination_date IS NULL
OR employee_termination_date >= TO_DATE(1-Jan-1988, dd-mon-yyyy))
This gives you the first section of the reportthose employed at the beginning of the year Retrieving the data for the remaining sections is a matter of using a different WHERE clause for each section Table 3-3 shows the selection
criteria for each section of the report
Trang 2
Table 3-3 Union Query Selection Criteria
Employed at beginning of year WHERE employee_hire date < TO_DATE(1-Jan-1998, dd-mon-yyyy)
AND (employee_termination_date IS NULL
OR employee_termination_date >=TO_DATE(1-Jan-1998, dd-mon-yyyy)) Hired during the year WHERE employee_hire_date >= TO_DATE (1-Jan-1998 dd-mon-yyyy)
AND (employee_hire_date < TO_DATE(1-Jan-1999, dd-mon-yyyy))
Terminated during the year WHERE employee_termination_date >=
TO_DATE (1-Jan-1998, dd-mm-yyyy) AND (employee_termination_date <
TO_DATE(1-Jan-1999, dd-mm-yyyy)) Employed at end of year WHERE employee_hire_date < TO_DATE (1-Jan-1998, dd-mm-yyyy)
AND (employee_termination_date IS NULL
OR employee_termination_date >= TO_DATE(1-Jan-1999, dd-mm-yyyy))
The UNION query
After separately developing the four queries, one for each section of the report, you can use SQL's UNION operator to link those four
queries together into one large query There are four things to consider when doing this:
1 You need to return all the records retrieved by all four queries.
2 You need to be able to group the retrieved records by category.
3 You need to be able to control which category prints first.
4 You need to identify each category on the printed report so the end user knows what's what.
To be certain of getting all the records back from the query, use the UNION ALL operator to tie the queries together Using UNION by itself
causes SQL to filter out any duplicate rows in the result set That's not really an issue with this example there won't be any duplicate rowsbut it's
an important point to consider.
In order to properly group the records, you can add a numeric constant to each of the four queries For example, the query to return the list of
those employed at the beginning of the year could return an arbitrary value of 1:
SELECT 1 sort_column,
employee_id,
employee_name,
The other queries would return values of 2, 3, and 4 in the sort column Sorting the query results on these arbitrary numeric values serves
two purposes First, the records for each section of the report will be grouped together because they will all have the same constant Second,
the value of the sort column controls the order in which the sections print Use a value of 1 for the section to be printed first, a value of 2 for
the second section, etc.
Trang 3
Page 119 The final thing to worry about is identifying the results to the reader of the report The values used in the sort column won't mean anything to the reader, so you also need to add a column with some descriptive text Here's how the final query for people employed at the beginning of the year looks with that text added:
SELECT 1 sort_column,
Employed at Beginning of Year employee_status_text,
employee_id,
employee_name,
employee_hire_date,
employee_termination_date
FROM employee
WHERE employee_hire_date < TO_DATE(1-Jan-1998,dd-mon-yyyy)
AND (employee_termination_date IS NULL
OR employee_termination_date >= TO_DATE(1-Jan-1998,dd-mon-yyyy))
The first column returned by this query is used to sort these records to the top of the report, while the second column serves to identify those records for the reader The full-blown UNION query to produce all four sections of the report looks like this:
SELECT 1 sort_column,
Employed at Beginning of Year employee_status_text,
employee_id,
employee_name,
employee_hire_date,
employee_termination_date
FROM employee
WHERE employee_hire_date < TO_DATE(1-Jan-1998, dd-mon-yyyy)
AND (employee_termination_date IS NULL
OR employee_termination_date >= TO_DATE(1-Jan-1998, dd-mon-yyyy)) UNION ALL
SELECT 2 as sort_column,
Hired During Year as employee_status_text,
employee_id,
employee_name,
employee_hire_date,
employee_termination_date
FROM employee
WHERE employee_hire_date >= TO_DATE(1-Jan-1998, dd-mon-yyyy)
AND (employee_hire_date < TO_DATE(1-Jan-1999,dd-mon-yyyy))
UNION ALL
SELECT 3 as sort_column,
Terminated During Year as employee_status_text,
employee_id,
employee_name,
employee_hire_date,
employee_termination_date
FROM employee
WHERE employee_termination_date >= TO_DATE(1-Jan-1998, dd-mon-yyyy)
AND (employee_termination_date < TO_DATE (1-Jan-1999, dd-mon-yyyy))
UNION ALL
SELECT 4 as sort_column,
Trang 4
Employed at End of Year as employee_status_text,
employee_id,
employee_name,
employee_hire_date,
employee_termination_date
FROM employee
WHERE employee_hire_date < TO_DATE(1-Jan-1999, dd-mon-yyyy)
AND (employee_termination_date IS NULL
OR employee_termination_date >= TO_DATE (1-Jan-1999, dd-mon-yyyy)) ORDER BY sort_column, employee_id, employee_hire_date;
As you can see, the four queries have been unioned together in the same order in which the report is to be printed That's done for readability, though It's the ORDER BY clause at the bottom that ensures that the records are returned in the proper order
The final report
All that's left now that the query has been worked out is to follow the remaining steps in the report development
methodology to format and print the report To produce a fairly basic, columnar report, precede the query with the
following commands:
Setup pagesize parameters
SET NEWPAGE 0
SET PAGESIZE 55
Set the linesize, which must match the number of equal signs used
for the ruling lines in the headers and footers
SET LINESIZE 75
TTITLE CENTER The Fictional Company SKIP 2 -
CENTER Employee Turnover Report SKIP 1 -
LEFT ================================ -
===================================== -
SKIP 3
Format the columns
CLEAR COLUMNS
COLUMN sort_column NOPRINT
COLUMN employee_status_text HEADING Status FORMAT A29
COLUMN employee_name HEADING Employee Name FORMAT A20
COLUMN employee_hire_date HEADING Hire Date FORMAT A11
COLUMN employee_termination_date HEADING Term Date FORMAT A11
Breaks and computations
BREAK ON EMPLOYEE_status_text SKIP 2 NODUPLICATES
CLEAR COMPUTES
COMPUTE NUMBER LABEL Total Count OF employee_name ON employee_status_text
Set the date format to use
ALTER SESSION SET NLS_DATE_FORMAT = dd-Mon-yyyy;
Trang 5
Page 121
When you execute this report, the output will look like this:
The Fictional Company
Employee Turnover Report
=============================================================================
Status Employee Name Hire Date Term date
- - - -
Employed at beginning of year Jonathan Gennick 15-Nov-1961
Jenny Gennick 16-Sep-1964 05-May-1998
Jeff Gennick 29-Dec-1987 01-Apr-1998
Pavlo Chubynsky 01-Mar-1994 15-Nov-1998
Taras Shevchenko 23-Agu-1976
Hermon Goche 15-Nov-1961 04-Apr-1998
**************************** -
Total Count 6
Hired During Year Hourace Walker 15-Jun-1998
Bohdan Khmelnytsky 02-Jan-1998
Ivan Mazepa
04-Apr-1998 30-Sep-1998
Jacob Marley 03-Mar-1998 31-oct-1998
**************************** -
Total Count 4
Terminated During Year Jenny Gennick 16-Sep-1964 05-May-1998 Jeff Gennick 29-Dec-1987 01-Apr-1998 Pavlo Chubynsky 01-Mar-1994 15-Nov-1998 Ivan Mazepa 04-Apr-1998 30-Sep-1998 Hermon Goche 15-Nov-1961 04-Apr-1998 Jacob Marley 03-Mar-1998 31-Oct-1998
**************************** -
Total Count 6
Employed at End of Year Jonathan Gennick 15-Nov-1961
Horace Walker 15-Jun-1998
Bohdan Khmelnytsky 02-Jan-1998
Taras Shevchenko 23-Aug-1976
**************************** -
Total Count 4
That's all there is to it It wouldn't be a big leap to turn this report into a master/ detail report, with each section starting on a new page Using this technique, you can develop similar reports with any number of sections you need.
Trang 6
4
Writing SQL*Plus Scripts
In this chapter:
Why Write Scripts?
Using Substitution Variables
Prompting for Values
Cleaning Up the Display
Packaging Your Script
The DEFINE and UNDEFINE Commands
Controlling Variable Substitution
Commenting Your Scripts
In the previous chapter, you saw how to write a script to produce a report This chapter delves more deeply into the subject of scripting, and shows you how to write interactive scripts You will learn how to use substitution variables, which allow the user to dynamically supply values to a script at runtime You will learn how to prompt the user for those values, and how to display other messages for the user to see Finally, you will learn how to package your script for easy access when you need it
Why Write Scripts?
The most compelling reason to write scripts, in my mind, is to encapsulate knowledge Say, for example, that you have developed a query that returns index definitions for a table You certainly don't want to have to think through the entire process of developing that query each time you need to see an index If you have a good script available, you just run it Likewise, if someone asks you how to see index definitions for a table, just give them a copy of the script
A second reason for developing scripts is that they save time Look at the script to produce the first report in Chapter 3,
Generating Reports with SQL*Plus It contains 17 separate commands, some quite long By placing those commands in
a script, you save yourself the time and effort involved in retyping all of them each time you run the report
Lastly, scripts can simplify tasks both for you and for others When you know you have a good, reliable script, you can just run it, answer the questions, then sit back
Trang 7
Page 123 while it does all the work You don't need to worry, thinking did I enter the correct command?, did I log on as the
correct user?, or did I get that query just right?
Anytime you find yourself performing a task over and over, think about writing a script to do it for you You'll save yourself time You'll save yourself stress You'll be able to share your knowledge more easily
A good source of ready-to-run scripts for Unix users is Oracle Scripts by Brian Lomasky and
David C Kreines, O'Reilly & Associates, 1998
Using Substitution Variables
Substitution variables allow you to write generic SQL*Plus scripts They allow you to mark places in a script where you want to substitute values at runtime
What Is a Substitution Variable?
A substitution variable is the same thing as a user variable In the previous chapter, you saw how to get the contents of a
database column into a user variable and how to place the contents of that user variable into the page header of a report SQL*Plus also allows you to place user variables in your script to mark places where you want to supply a value at runtime When you use them this way, they are called substitution variables
A substitution variable is not like a true variable used in a programming language Instead, a substitution variable marks places in the text where SQL*Plus does the equivalent of a search and replace at runtime, replacing the reference to a substitution variable with its value
Substitution variables are set off in the text of a script by preceding them with either one or two ampersand characters Say, for example, that you had this query to list all projects to which employee #107 had charged time:
SELECT DISTINCT p.project_id, p.project_name
FROM project p,
project_hours ph
WHERE ph.employee_id = 107
AND p.project_id = ph.project_id;
As you can see, this query is specific to employee number 107 To run the query for a different employee, you would need to edit your script file, change the ID number, save the file, then execute it That's a pain You don't want to do that
Trang 8Instead, you can generalize the script by rewriting the SELECT statement with a substitution variable in place of the employee ID number It would look like this:
SELECT DISTINCE p.project_id, p.project_name
FROM project p,
project_hours ph
WHERE ph.employee_id = &employee_id
AND p.project_id = ph.project_id;
The ampersand in front of the word employee_id marks it as a variable At runtime, when it reads the statement,
SQL*Plus will see the substitution variable and replace it with the current value of the specified user variable If the employee_id user variable contained a value of 104, then &employee_id would be replaced by 104, and the resulting line would look like this:
WHERE ph.employee_id = 104
As stated earlier, and as you can see now, SQL*Plus truly does a search and replace operation The Oracle database does not know that a variable has been used Nor does SQL*Plus actually compare the contents of the employee_id column against the value of the variable SQL*Plus simply does the equivalent of a search and replace operation on each statement before that statement is executed As far as the Oracle database is concerned, you might just as well have included constants in your script
Substitution variables are the workhorse of SQL*Plus scripts They give you a place to store user input, and they give you a way to use that input in SQL queries, PL/ SQL code blocks, and other SQL*Plus commands
Using Single-Ampersand Variables.
The easiest way to generalize a script is to take one you have working for a specific case and modify it by replacing specific values with substitution variables In this section, we will revisit the Labor Hours and Dollars Detail report shown in Chapter 3 You will see how you can modify the script to print the report for only one employee, and you will see how you can use a substitution variable to generalize that script by making it prompt for the employee ID number at runtime
When SQL*Plus encounters a variable with a single leading ampersand, it always prompts you for a value This is true even when you use the same variable multiple times in your script If you use it twice, you will be prompted twice Doubleampersand variables allow you to prompt a user only once for a given value, and are explained later in this chapter
The report for one specific employee
The report in the previous chapter produced detailed hours and dollars information for all employees To reduce the scope to one employee, you can add this line to the WHERE clause:
Trang 9
Page 125 AND e.employee_id = 107
Since this report is now only for one employee, the grand totals don't make sense, so the COMPUTES to create them can be removed Finally, a SPOOL command has been added to capture the output in a file to be printed later The complete script for the report looks like this:
Setup pagesize parameters
SET NEWPAGE 0
SET PAGESIZE 55
Set the linesize, which must match the number of equal signs used
for the ruling lines in the headers and footers
SET LINESIZE 71
Get the date for inclusion in the page footer
SET TERMOUT OFF
ALTER SESSION SET NLS_DATE_FORMAT = DD-Mon_YYYY;
COLUMN SYSDATE NEW_VALUE report_date
SELECT SYSDATE FROM DUAL;
SET TERMOUT ON
Setup page headings and footings
TTITLE CENTER The Ficional Company SKIP 3 -
LEFT I.S.Department -
RIGHT Project Hours and Dollars Detail SKIP 1 -
LEFT ============================================================= -
SKIP 2 Employee: FORMAT 9999 emp_id_var emp_name_var SKIP 3
BTITLE LEFT ============================================================= -
SKIP 1 -
LEFT report_date -
RIGHT Page FORMATE 999 SQL.PNO
Format the columns
COLUMN employee_id NEW_VLUE emp_id_var NOPRINT
COLUMN employee_name NEW_VALUE emp_name_var NOPRINT
COLUMN project_id HEADING Proj ID FORMAT 9999
COLUMN project_name HEADING Project Name FORMAT A26 WORD_WRAPPED
COLUMN time_log_date HEADING Date FORMAT All
COLUMN hours_logged HEADING Hours FORMAT 9,999
COLUMN dollars_charged HEADING Dollars¦Charged FORMAT $999,999.99
Breaks and Computations
BREAK ON employee_id SKIP PAGE NODUPLICATES -
ON employee_name NODUPLICATES -
ON project_id SKIP 2 NODUPLICATES -
ON project_name NODUPLICATES
CLEAR COMPUTES
COMPUTE SUM LABEL Project Totals OF hours_logged ON project_name
COMPUTE SUM LABEL Project Totals OF dollars_charged ON project_name
COMPUTE SUM LABEL Totals OF hours_logged ON employee_id
COMPUTE SUM LABEL Totals OF dollars_charged ON employee_id
Trang 10
Execute the query to generate the report
SPOOL C:\A\HOURS_DOLLARS
SELECT P.PROJECT_ID
P.PROJECT_NAME,
TO_CHAR(PH.TIME_LOG_DATE, dd-Mon_yyyy) time_log_date,
PH.HOURS_LOGGED,
PH.DOLLARS_CHARGED,
E.EMPLOYEE_ID,
E.EMPLOYEE_NAME
FROM EMPLOYEE E,
PROJECT P,
PROJECT_HOURS PH
WHERE E.EMPLOYEE_ID = PH.EMPLOYEE_ID
AND P.PROJECT_ID=PH.PROJECT_ID
AND E.EMPLOYEE_ID = 107
ORDER BY.E.EMPLOYEE_ID, P.PROJECT_ID, PH.TIME_LOG_DATE;
SPOOL OFF
Reset everything back to the defaults
CLEAR BREAKS
CLEAR COMPUTES
TTITLE OFF
BTITLE OFF
SET NEWPAGE 1
SET PAGESIZE 24
SET LINESIZE 80
Running this script as shown will produce a report specifically for employee 107
Generalizing the report with substitution variables
You don't want to edit the script file and modify your script every time you need to produce a report for a different employee, and you don't have to Instead, you can replace the reference to a specific employee number with a
substitution variable and let SQL*Plus prompt you for a value at runtime Here's how the affected line of script looks with a substitution variable instead of a hardcoded value:
AND E.EMPLOYEE_ID = &employee_id
The variable name should be descriptive, and it needs to serve two purposes It needs to inform the user and it needs to inform you First and foremost, the variable name is used in the prompt, and must convey to the user the specific
information needed In this case, for example, using &id for the variable would leave the user wondering whether to enter an employee ID or a project ID The second thing to keep in mind is that you will need to look at the script again someday, so make sure the name is something that will jog your memory as well
Running the report
When you run the report, SQL*Plus will prompt you for the value of the &employee_id substitution variable Assume
that the script is in a file named HOURS_DOLLARS.SQL Here's how the output will look: