DBMS_UTILITY Appendix A

advertisement
Appendix A
DBMS_UTILITY
The DBMS_UTILITY package is a collection of miscellaneous procedures. It is where many, standalone
procedures are placed. The DBMS_UTILITY package is installed in the database by default, and has
EXECUTE granted to PUBLIC. The procedures in this package are not related to one another as they
typically are in the other packages. For example, all of the entry points in the UTL_FILE package have a
common goal and meaning – to perform I/O on a file. The entry points in DBMS_UTILITY are pretty
much independent of one another.
In this section, we will look at many of these functions, and the important caveats and issues will be
pointed out.
COMPILE_SCHEMA
The goal of the COMPILE_SCHEMA procedure is to attempt to make valid all invalid procedures,
packages, triggers, views, types, and so on in a schema. This procedure works in Oracle 8.1.6 by using
the SYS.ORDER_OBJECT_BY_DEPENDENCY view. This view returns objects in the order they depend on
each other. In Oracle 8.1.7 and higher, this view is no longer used (why this is relevant will be shown
below). If we compile the objects in the order that this view returns them, then at the end, all objects
that can be valid, should be valid. This procedure runs the ALTER COMPILE command as the user who
invoked the procedure (invoker rights).
It should be noted that COMPILE_SCHEMA demands you pass in a case-sensitive username. If you call:
scott@TKYTE816> exec DBMS_UTILITY.compile_schema( 'scott' );
It is probable that nothing will happen, unless you have a lowercase user named scott. You must pass
in SCOTT.
1172
5254AppAL.pdf 1
2/28/2005 6:49:55 PM
DBMS_UTILITY
There is however another issue with COMPILE_SCHEMA in 8.1 versions of the database prior to 8.1.6.2
(that is all 8.1.5, 8.1.6.0, and 8.1.6.1 versions). If you have a Java-enabled database, this will introduce
some recursive dependencies into your system. This will cause COMPILE_SCHEMA to raise the error:
scott@TKYTE816> exec dbms_utility.compile_schema( user );
BEGIN dbms_utility.compile_schema( user ); END;
*
ERROR at line 1:
ORA-01436: CONNECT BY loop in user data
ORA-06512: at "SYS.DBMS_UTILITY", line 195
ORA-06512: at line 1
This is coming from the SYS.ORDER_OBJECT_BY_DEPENDENCY view, and is the reason why Oracle
8.1.7 and up do not use this view. If you encounter this error, we can create our own COMPILE_SCHEMA
procedure that behaves exactly as the real COMPILE_SCHEMA. We can do this by compiling the objects
in any order we feel like it. It is a common misconception that we must compile objects in some specific
order – we can in fact do them in any arbitrary order, and still end up with the same outcome we would
have, if we ordered by dependency. The logic is:
1.
Pick any invalid object from a schema that we have not yet tried to compile.
2.
Compile it.
3.
Go back to step one until there are no more invalid objects that we have not yet tried to
compile.
It is that simple – we need no special ordering. This is because a side effect of compiling an invalid
object is that all invalid objects it depends on will be compiled in order to validate this one. We just
have to keep compiling objects until we have no more invalid ones (well, we might have invalid ones,
but that would be because they cannot be successfully compiled no matter what). What we might
discover is that we need only to compile a single procedure to get 10 or 20 other objects compiled. As
long as we don't attempt to manually recompile those 10 or 20 other objects (as this would invalidate
the first object again) we are OK.
Since the implementation of this procedure is somewhat interesting, we'll demonstrate it here. We need
to rely on an invoker rights routine to do the actual ALTER COMPILE command. However, we need
access to the DBA_OBJECTS table to find the 'next' invalid object, and report on the status of the justcompiled object. We do not necessarily want the invoker of the routine to have to have access to
DBA_OBJECTS. In order to achieve this, we will use a mixture of invoker rights routines and definer
rights routines. We need to make sure that the top-level routine, the one called by the end user, is the
invoker rights routine however, to ensure that roles are enabled.
Here is my implementation of a COMPILE_SCHEMA.
The user who runs this script must have had SELECT granted to them on the SYS.DBA_OBJECTS
view directly (refer to Chapter 23, Invoker and Definer Rights for details on why this is).
1173
5254AppAL.pdf 2
2/28/2005 6:49:55 PM
Appendix A
Since this is a SQL*PLUS script, with some SQL*PLUS directives in it, I'll show the script here this
time, not the results of actually running the script. I am using a SQL*PLUS substitution variable to fill in
the schema name as we compile objects. I am doing this because of the invoker rights routine (the need
to fully qualify objects if they should always access the same table, regardless of who is running it), and
the fact that I personally do not like to rely on public synonyms. The script will be given to you in
pieces below with commentary in between:
column u new_val uname
select user u from dual;
drop table compile_schema_tmp
/
create global temporary table compile_schema_tmp
( object_name varchar2(30),
object_type varchar2(30),
constraint compile_schema_tmp_pk
primary key(object_name,object_type)
)
on commit preserve rows
/
grant all on compile_schema_tmp to public
/
We start the script by getting the currently logged in user's username into a SQL*PLUS substitution
variable. We will use this later in our CREATE OR REPLACE procedures. We need to do this because our
procedure is going to run as an invoker rights routine, and needs to access the table we just created
above. If you recall in Chapter 23 on Invoker and Definer Rights, we discussed how references to tables in
the procedure are be done using the default schema of the person running the procedure. Well, we only
have one temporary table that all users will use, and it will be owned by whoever installs this package.
Therefore, we need to hard code the username into the PL/SQL routine. The temporary table is used by
our procedures to 'remember' what objects we have attempted to compile. We need to use ON COMMIT
PRESERVE ROWS because of the fact that we are going to do DDL in our procedure (the ALTER
COMPILE command is DDL), and DDL commits. Next, we can start in on the procedures we need:
create or replace
procedure get_next_object_to_compile( p_username in varchar2,
p_cmd out varchar2,
p_obj out varchar2,
p_typ out varchar2 )
as
begin
select 'alter ' || object_type || ' '
|| p_username || '.' || object_name ||
decode( object_type, 'PACKAGE BODY', ' compile body',
' compile' ), object_name, object_type
into p_cmd, p_obj, p_typ
from dba_objects a
where owner = upper(p_username)
and status = 'INVALID'
and object_type <> 'UNDEFINED'
1174
5254AppAL.pdf 3
2/28/2005 6:49:55 PM
DBMS_UTILITY
and not exists ( select
from
where
and
)
and rownum = 1;
null
compile_schema_tmp b
a.object_name = b.object_name
a.object_type = b.object_type
insert into compile_schema_tmp
( object_name, object_type )
values
( p_obj, p_typ );
end;
/
This is a definer rights procedure that accesses the DBA_OBJECTS view for us. This will return 'some'
invalid object to be compiled, as long as we have not yet attempted to compile it. It just finds the first
one. As we retrieve them, we 'remember' them in our temporary table. Note that this routine will throw
the exception NO_DATA_FOUND when there are no objects left to be compiled in the requested schema –
we'll use this fact in our next routine to stop processing. Next, we have our invoker rights routine that
will actually do the compilation. This also shows why we needed the COLUMN U NEW_VAL UNAME
directive above– we need to physically insert the owner of the temporary table in here to avoid having to
use a synonym. Since we do this dynamically upon compiling the procedure, it makes it better than a
synonym:
create or replace procedure compile_schema( p_username in varchar2 )
authid current_user
as
l_cmd varchar2(512);
l_obj dba_objects.object_name%type;
l_typ dba_objects.object_type%type;
begin
delete from &uname..compile_schema_tmp;
loop
get_next_object_to_compile( p_username, l_cmd, l_obj, l_typ );
dbms_output.put_line( l_cmd );
begin
execute immediate l_cmd;
dbms_output.put_line( 'Successful' );
exception
when others then
dbms_output.put_line( sqlerrm );
end;
dbms_output.put_line( chr(9) );
end loop;
exception – get_next_object_to_compile raises this when done
when no_data_found then NULL;
end;
/
grant execute on compile_schema to public
/
1175
5254AppAL.pdf 4
2/28/2005 6:49:55 PM
Appendix A
And that's it. Now you can go into any schema that is able to compile some objects, and execute:
scott@TKYTE816> exec tkyte.compile_schema('scott')
alter PROCEDURE scott.ANALYZE_MY_TABLES compile
Successful
alter PROCEDURE scott.CUST_LIST compile
ORA-24344: success with compilation error
alter TYPE scott.EMP_MASTER compile
ORA-24344: success with compilation error
alter PROCEDURE scott.FOO compile
Successful
alter PACKAGE scott.LOADLOBS compile
Successful
alter PROCEDURE scott.P compile
Successful
alter PROCEDURE scott.RUN_BY_JOBS compile
Successful
PL/SQL procedure successfully completed.
So, this shows me the objects it attempted to compile, and the outcome. According to the above, we
compile seven objects, two of which failed, and five of which succeeded. We compiled them in any
order – the order was simply not relevant. This procedure should work in all situations.
ANALYZE_SCHEMA
The ANALYZE_SCHEMA routine does pretty much what it sounds like it would do – it performs an
ANALYZE to collect statistics for the objects in a user's schema. It is recommended that you never do this
on either SYS or SYSTEM. This is especially for SYS, as the recursive SQL generated by Oracle over the
years was optimized for execution using the rule-based optimizer. Having statistics on SYS-owned tables
will cause your database to operate slower than it should. You may use this procedure to analyze
application schemas you have yourself developed.
The ANALYZE_SCHEMA procedure accepts five arguments:
❑
SCHEMA – The schema to be analyzed.
❑
METHOD – ESTIMATE, COMPUTE, or DELETE. If ESTIMATE, then either ESTIMATE_ROWS or
ESTIMATE_PERCENT must be non-zero.
❑
ESTIMATE_ROWS – Number of rows to estimate.
❑
ESTIMATE_PERCENT – Percentage of rows to estimate. If ESTIMATE_ROWS is specified, then
this parameter is ignored.
❑
METHOD_OPT [ FOR TABLE ] [ FOR ALL [INDEXED] COLUMNS] [SIZE n] [ FOR ALL
INDEXES] – These options are the same options as you use with the ANALYZE command
itself. You will find these options fully documented in the Oracle8i SQL Reference manual under
the ANALYZE commands, for clause.
1176
5254AppAL.pdf 5
2/28/2005 6:49:55 PM
DBMS_UTILITY
So, for example, to analyze all of the objects in SCOTT's schema, we can do the following. We start by
first deleting and then collecting statistics:
scott@TKYTE816> exec dbms_utility.analyze_schema(user,'delete');
PL/SQL procedure successfully completed.
scott@TKYTE816> select table_name, num_rows, last_analyzed
2 from user_tables;
TABLE_NAME
NUM_ROWS LAST_ANAL
------------------------------ ---------- --------BONUS
CREATE$JAVA$LOB$TABLE
DEPT
...
12 rows selected.
scott@TKYTE816> exec dbms_utility.analyze_schema(user,'compute');
PL/SQL procedure successfully completed.
scott@TKYTE816> select table_name, num_rows, last_analyzed
2 from user_tables;
TABLE_NAME
NUM_ROWS LAST_ANAL
------------------------------ ---------- --------BONUS
0 03-FEB-01
CREATE$JAVA$LOB$TABLE
58 03-FEB-01
DEPT
4 03-FEB-01
...
12 rows selected.
This simple shows that the ANALYZE COMPUTE actually did its job – the NUM_ROWS and
LAST_ANALYZED columns got filled in.
In general, the ANALYZE_SCHEMA procedure is as straightforward as it sounds. If you have the need to
specifically analyze certain objects in certain ways, it will not apply. This procedure does the same sort
of analysis to each object type, and does not have exceptions. For example, if you are a large data
warehouse, and you make use of histograms on specific columns, or sets of columns on certain tables
only, ANALYZE_SCHEMA is not what you want. You can use ANALYZE_SCHEMA to get histograms either
for every column or none of the columns – not just certain columns. Once you go beyond the 'simple'
with regards to analyzing objects, ANALYZE_SCHEMA will not be useful any more. This routine works
well for small to medium sized applications, where small to medium is a measure of the amount of data
you have. If you have large volumes of data, you will want to analyze in parallel or use special options
to analyze on various tables. This will exclude ANALYZE_SCHEMA from being of use to you.
If you do use ANALYZE_SCHEMA, you should be aware of the following two issues. The first has to do
with ANALYZE_SCHEMA against a schema that is changing. The second is with respect to objects
ANALYZE_SCHEMA does not analyze. We will look at both of these caveats in turn.
1177
5254AppAL.pdf 6
2/28/2005 6:49:55 PM
Appendix A
ANALYZE_SCHEMA with a Changing Schema
Suppose you start an ANALYZE_SCHEMA in the SCOTT schema. You've added some large tables so it will
take a while. In another session, you drop or add some objects to SCOTT's schema. The object you drop
hasn't been reached by ANALYZE_SCHEMA yet. When it does, you will receive the somewhat misleading
message:
scott@TKYTE816> exec dbms_utility.analyze_schema(user,'compute');
BEGIN dbms_utility.analyze_schema(user,'compute'); END;
*
ERROR at line 1:
ORA-20000: You have insufficient privileges for an object in this schema.
ORA-06512: at "SYS.DBMS_UTILITY", line 258
ORA-06512: at line 1
Obviously, you have all of the privileges you need; you own the objects after all. The error here is that a
table, which it is trying to analyze no longer exists, when it gets round to analyzing it. Instead of
recognizing that the table does not exist anymore, it assumes it does, and the error must be that you do
not have sufficient privileges to analyze it. Currently, there is nothing you can do about this other than:
❑
Restart the ANALYZE_SCHEMA.
❑
Do not drop objects while ANALYZE_SCHEMA is executing.
The other thing to be aware of is that objects added to the schema after the ANALYZE_SCHEMA begins
will not be analyzed – it will not see them yet. This is fairly harmless, as the ANALYZE_SCHEMA will run
to completion successfully.
ANALYZE_SCHEMA does not Analyze Everything
There is an open issue with respect to ANALYZE_SCHEMA. It will not analyze an index-organized table
that has an overflow segment (see Chapter 7, Indexes, for more information regarding IOTs and
overflows). For example if you run the following code:
scott@TKYTE816> drop table t;
Table dropped.
scott@TKYTE816> create table t ( x int primary key, y date )
2 organization index
3 OVERFLOW TABLESPACE TOOLS
4 /
Table created.
scott@TKYTE816> execute dbms_utility.analyze_schema('SCOTT','COMPUTE')
PL/SQL procedure successfully completed.
scott@TKYTE816> select table_name, num_rows, last_analyzed
2
from user_tables
3
where table_name = 'T';
TABLE_NAME
NUM_ROWS LAST_ANAL
------------------------------ ---------- --------T
1178
5254AppAL.pdf 7
2/28/2005 6:49:56 PM
DBMS_UTILITY
it did not get analyzed. However, if you leave off the OVERFLOW clause:
scott@TKYTE816> drop table t;
Table dropped.
scott@TKYTE816> create table t ( x int primary key, y date )
2 organization index
3 /
Table created.
scott@TKYTE816> execute dbms_utility.analyze_schema('SCOTT','COMPUTE')
PL/SQL procedure successfully completed.
scott@TKYTE816> select table_name, num_rows, last_analyzed
2
from user_tables
3
where table_name = 'T';
TABLE_NAME
NUM_ROWS LAST_ANAL
------------------------------ ---------- --------T
0 03-FEB-01
it does. This does not mean you should leave the OVERFLOW off of your IOTs, but rather that you will
have to manually analyze these objects.
ANALYZE_DATABASE
This will be an exceptionally short section. Do not use this procedure. It is not realistic on a database of
any size, and has a nasty side effect of analyzing the data dictionary (these are SYS owned objects, and
we should never analyze these). Do not use it. Simply ignore its existence.
FORMAT_ERROR_STACK
FORMAT_ERROR_STACK is a function that, at first glance, would appear to be very useful, but in
retrospect, is not at all. In actuality, FORMAT_ERROR_STACK is simply a less functional implementation
of SQLERRM (SQL ERRor Message). A simple demonstration will help you to understand what I mean:
scott@TKYTE816> create or replace procedure p1
2 as
3 begin
4
raise program_error;
5 end;
6 /
Procedure created.
1179
5254AppAL.pdf 8
2/28/2005 6:49:56 PM
Appendix A
scott@TKYTE816> create or replace procedure p2
2 as
3 begin
4
p1;
5 end;
6 /
Procedure created.
scott@TKYTE816> create or replace procedure p3
2 as
3 begin
4
p2;
5 end;
6 /
Procedure created.
scott@TKYTE816> exec p3
BEGIN p3; END;
*
ERROR at line 1:
ORA-06501: PL/SQL: program error
ORA-06512: at "SCOTT.P1", line 4
ORA-06512: at "SCOTT.P2", line 4
ORA-06512: at "SCOTT.P3", line 4
ORA-06512: at line 1
If we have an error, and we do not catch it in an exception handle, the entire error stack is displayed for
us, and would be available to use in a Pro*C, OCI, JDBC, and so on, program. You would expect that
the DBMS_UTILITY.FORMAT_ERROR_STACK routine would return similar information. You will find
however that it loses this important information:
scott@TKYTE816> create or replace procedure p3
2 as
3 begin
4
p2;
5 exception
6
when others then
7
dbms_output.put_line( dbms_utility.format_error_stack );
8 end;
9 /
Procedure created.
scott@TKYTE816> exec p3
ORA-06501: PL/SQL: program error
PL/SQL procedure successfully completed.
As you can see, we actually lost the error stack by calling FORMAT_ERROR_STACK! This routine returns
the same information SQLERRM would return:
1180
5254AppAL.pdf 9
2/28/2005 6:49:56 PM
DBMS_UTILITY
scott@TKYTE816> create or replace procedure p3
2 as
3 begin
4
p2;
5 exception
6
when others then
7
dbms_output.put_line( sqlerrm );
8 end;
9 /
Procedure created.
scott@TKYTE816> exec p3
ORA-06501: PL/SQL: program error
PL/SQL procedure successfully completed.
Before, I said FORMAT_ERROR_STACK was a less functional SQLERRM. This is because SQLERRM can, not
only return the current error message, but it can also return any error message:
scott@TKYTE816> exec dbms_output.put_line( sqlerrm(-1) );
ORA-00001: unique constraint (.) violated
PL/SQL procedure successfully completed.
Unfortunately, there simply is no way currently to get the real error stack in PL/SQL. You must let fatal
errors propagate up to the calling client routine, in order to get the actual line number of the code that
raised the error in the first place.
FORMAT_CALL_STACK
Fortunately this function is truly useful compared to FORMAT_ERROR_STACK. This returns to us the
current call stack. Using this, we can write some utility procedures such as MY_CALLER and WHO_AM_I.
These routines call a procedure to determine what source code from which line number invoked it. This
is very useful for debugging and logging purposes. Also, a procedure could modify its behavior based
on who called it, or where it was called.
Before we introduce the code for MY_CALLER and WHO_AM_I, let us look at what the call stack provides
for us, and what the output from these routines is destined to be. If we use the P1, P2, P3 example from
above, and rewrite P1 to be:
scott@TKYTE816> create or replace procedure p1
2 as
3
l_owner
varchar2(30);
4
l_name
varchar2(30);
5
l_lineno
number;
6
l_type
varchar2(30);
7 begin
8
dbms_output.put_line( '----------------------' );
9
dbms_output.put_line( dbms_utility.format_call_stack );
10
dbms_output.put_line( '----------------------' );
11
who_called_me( l_owner, l_name, l_lineno, l_type );
1181
5254AppAL.pdf 10
2/28/2005 6:49:56 PM
Appendix A
12
13
14
15
16
17
18
19
20
dbms_output.put_line( l_type || ' ' ||
l_owner || '.' || l_name ||
'(' || l_lineno || ')' );
dbms_output.put_line( '----------------------' );
dbms_output.put_line( who_am_i );
dbms_output.put_line( '----------------------' );
raise program_error;
end;
/
Procedure created.
we will receive output such as:
scott@TKYTE816> exec p3
-------------------------- PL/SQL Call Stack ----object
line object
handle
number name
2f191e0
9 procedure
39f0a9c
4 procedure
3aae318
4 procedure
3a3461c
1 anonymous
---------------------PROCEDURE SCOTT.P2(4)
---------------------SCOTT.P1(16)
---------------------BEGIN p3; END;
SCOTT.P1
SCOTT.P2
SCOTT.P3
block
*
ERROR at line 1:
ORA-06501: PL/SQL: program error
ORA-06512: at "SCOTT.P2", line 8
ORA-06512: at "SCOTT.P3", line 4
ORA-06512: at line 1
So, we can see the entire call stack in P1. This shows that P1 was called by P2, P2 was called by P3, and
P3 was called by an anonymous block. Additionally, we can procedurally retrieve the fact that our
caller in P1 was the procedure SCOTT.P2, and that they called us from line 4. Lastly, we can see simply
that we are the procedure SCOTT.P1.
So, now that we see what the call stack looks like, and what kind of output we would like to get, we can
present the code to do it:
tkyte@TKYTE816> create or replace function my_caller return varchar2
2
3 as
4
owner
varchar2(30);
5
name
varchar2(30);
6
lineno
number;
7
caller_t
varchar2(30);
8
call_stack varchar2(4096) default dbms_utility.format_call_stack;
1182
5254AppAL.pdf 11
2/28/2005 6:49:56 PM
DBMS_UTILITY
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
n
found_stack
line
cnt
begin
number;
BOOLEAN default FALSE;
varchar2(255);
number := 0;
loop
n := instr( call_stack, chr(10) );
exit when ( cnt = 3 or n is NULL or n = 0 );
line := substr( call_stack, 1, n-1 );
call_stack := substr( call_stack, n+1 );
if ( NOT found_stack ) then
if ( line like '%handle%number%name%' ) then
found_stack := TRUE;
end if;
else
cnt := cnt + 1;
-- cnt = 1 is ME
-- cnt = 2 is MY Caller
-- cnt = 3 is Their Caller
if ( cnt = 3 ) then
lineno := to_number(substr( line, 13, 6 ));
line
:= substr( line, 21 );
if ( line like 'pr%' ) then
n := length( 'procedure ' );
elsif ( line like 'fun%' ) then
n := length( 'function ' );
elsif ( line like 'package body%' ) then
n := length( 'package body ' );
elsif ( line like 'pack%' ) then
n := length( 'package ' );
elsif ( line like 'anonymous block%' ) then
n := length( 'anonymous block ' );
else -- must be a trigger
n := 0;
end if;
if ( n <> 0 ) then
caller_t := ltrim(rtrim(upper(substr(line,1,n-1))));
line := substr( line, n );
else
caller_t := 'TRIGGER';
line := ltrim( line );
end if;
n := instr( line, '.' );
owner := ltrim(rtrim(substr( line, 1, n-1 )));
name := ltrim(rtrim(substr( line, n+1 )));
end if;
end if;
end loop;
return owner || '.' || name;
end;
/
1183
5254AppAL.pdf 12
2/28/2005 6:49:56 PM
Appendix A
Function created.
tkyte@TKYTE816> create or replace function who_am_i return varchar2
2 as
3 begin
4
return my_caller;
5 end;
6 /
Function created.
Now that you have these routines, you can do some interesting things. It has been used to
❑
Perform auditing – The audit routines log not only the user that performed some operation,
but also the code that did it as well.
❑
Perform debugging – For example, if you litter your code with calls to
DBMS_APPLICATION_INFO.SET_CLIENT_INFO(WHO_AM_I), you can query V$SESSION in
another session to see where in your code you are currently. See the earlier section of this
appendix on DBMS_APPLICATION_INFO for details on this package.
GET_TIME
This function returns a ticker that measures time in hundredths of a second. You cannot use GET_TIME
to tell what time it is, a function it's name may imply, but rather you can use this to measure elapsed
time. A common way to do this is:
scott@TKYTE816> declare
2
l_start number;
3
n
number := 0;
4 begin
5
6
l_start := dbms_utility.get_time;
7
8
for x in 1 .. 100000
9
loop
10
n := n+1;
11
end loop;
12
13
dbms_output.put_line( ' it took ' ||
14
round( (dbms_utility.get_time-l_start)/100, 2 ) ||
15
' seconds...' );
16 end;
17 /
it took .12 seconds...
PL/SQL procedure successfully completed.
1184
5254AppAL.pdf 13
2/28/2005 6:49:56 PM
DBMS_UTILITY
so you can use GET_TIME to measure elapsed time in hundredths of a second. You should realize
however, that GET_TIME will wrap around to zero and start counting again if your database is up long
enough. Now, on most platforms this time to wrap is well over a year in length. The counter is a 32-bit
integer, and this can hold hundredths of seconds for about 497 days. After that, the 32-bit integer will
roll over to zero, and start over again. On some platforms, the operating system supplies this ticker in a
smaller increment than hundredths of seconds. On these platforms the ticker may roll over sooner than
497 days. For example, on Sequent it is known that the timer will roll over every 71.58 minutes, since
this operating system's ticker measures time in microseconds, leaving significantly less room in the 32bit integer. On a 64-bit platform, the time may very well not roll over for many thousands of years.
A last note about GET_TIME. The same value that GET_TIME returns may be retrieved from a SELECT *
FROM V$TIMER. The dynamic view and GET_TIME return the same values:
tkyte@TKYTE816> select hsecs, dbms_utility.get_time
2 from v$timer;
HSECS
GET_TIME
---------- ---------7944822
7944822
GET_PARAMETER_VALUE
This API allows anyone to get the value of a specific init.ora parameter. Even if you have no access
to V$PARAMETER, and cannot run the SHOW PARAMETER command, you can use this to get the value of
an init.ora parameter. It works like this:
scott@TKYTE816> show parameter utl_file_dir
ORA-00942: table or view does not exist
scott@TKYTE816> select * from v$parameter where name = 'utl_file_dir'
2 /
select * from v$parameter where name = 'utl_file_dir'
*
ERROR at line 1:
ORA-00942: table or view does not exist
scott@TKYTE816> declare
2
intval number;
3
strval varchar2(512);
4 begin
5
if ( dbms_utility.get_parameter_value( 'utl_file_dir',
6
intval,
7
strval ) = 0)
8
then
9
dbms_output.put_line( 'Value = ' || intval );
10
else
11
dbms_output.put_line( 'Value = ' || strval );
12
end if;
13 end;
14 /
Value = c:\temp\
PL/SQL procedure successfully completed.
1185
5254AppAL.pdf 14
2/28/2005 6:49:56 PM
Appendix A
As you can see, even though SCOTT cannot query V$PARAMETER and the call to show parameter failed,
he can still use this call to get the value. It should be noted that parameters set with True/False strings in
the init.ora file will be reported back as returning a number type (this particular function will return
0), and a value of 1 indicates True while a value of 0 indicates False. Additionally, for multi-valued
parameters, such as UTL_FILE_DIR, this routine only returns the first value. If I use an account that can
do a SHOW PARAMETER in the same database:
tkyte@TKYTE816> show parameter utl_file_dir
NAME
TYPE
VALUE
------------------------------------ ------- -----------------------------utl_file_dir
string c:\temp, c:\oracle
I can see more values.
NAME_RESOLVE
This routine will take the name of a:
❑
Top-level procedure
❑
Top-level function
❑
Database package name
❑
A synonym that points to a database package, or a top level procedure or function
and fully resolve the name for you. It can tell you if the object name you gave it is a procedure,
function, or package, and what schema it belongs to. Here is a simple example:
scott@TKYTE816> declare
2
type vcArray is table of varchar2(30);
3
l_types vcArray := vcArray( null, null, null, null, 'synonym',
4
null, 'procedure', 'function',
5
'package' );
6
7
l_schema
varchar2(30);
8
l_part1
varchar2(30);
9
l_part2
varchar2(30);
10
l_dblink
varchar2(30);
11
l_type
number;
12
l_obj#
number;
13 begin
14
dbms_utility.name_resolve( name => 'DBMS_UTILITY',
15
context
=> 1,
16
schema
=> l_schema,
17
part1
=> l_part1,
18
part2
=> l_part2,
19
dblink
=> l_dblink,
20
part1_type
=> l_type,
21
object_number => l_obj# );
22
if l_obj# IS NULL
1186
5254AppAL.pdf 15
2/28/2005 6:49:56 PM
DBMS_UTILITY
23
then
24
dbms_output.put_line('Object not found or not valid.');
25
else
26
dbms_output.put( l_schema || '.' || nvl(l_part1,l_part2) );
27
if l_part2 is not null and l_part1 is not null
28
then
29
dbms_output.put( '.' || l_part2 );
30
end if;
31
32
dbms_output.put_line( ' is a ' || l_types( l_type ) ||
33
' with object id ' || l_obj# ||
34
' and dblink "' || l_dblink || '"' );
35
end if;
36 end;
37 /
SYS.DBMS_UTILITY is a package with object id 2408 and dblink ""
PL/SQL procedure successfully completed.
In this case, NAME_RESOLVE took our synonym DBMS_UTILITY, and figured out for us that this was in
fact a database package that is owned by SYS.
It should be noted that NAME_RESOLVE works only on procedures, functions, packages, and synonyms
that point to one of these three object types. It explicitly will not work on a database table for example.
You will receive the following error:
declare
*
ERROR at line 1:
ORA-06564: object emp does not exist
ORA-06512: at "SYS.DBMS_UTILITY", line 68
ORA-06512: at line 9
if you attempt to use it on the EMP table in the SCOTT schema for example.
In addition to not being able to do tables, indexes, and other objects, NAME_RESOLVE does not function
as documented when it comes to resolving synonyms that point to remote objects over a database link.
It is documented that if you pass NAME_RESOLVE a synonym to a remote package/procedure, then the
TYPE will be set to synonym, and they will tell us the name of the database link. This is an issue with the
NAME_RESOLVE code (the documentation is correct, the procedure does not function as it should).
Currently, NAME_RESOLVE will never return SYNONYM as the type. Rather, it will resolve the remote
object and return its name and an object ID of –1. For example, I have a database link set up, and I
create a synonym X for DBMS_UTILITY@ora8i.world. When I NAME_RESOLVE this, I receive:
SYS.DBMS_UTILITY is a package with object id -1 and dblink ""
PL/SQL procedure successfully completed.
I should have been told that X was a synonym and the DBLINK OUT parameter would have been filled in. As you
can see however, the DBLINK is Null, and the only indication we have that this is not a local package, is the fact
that the object ID is set to –1. You should not rely on this behavior persisting in future releases of Oracle. It has
been determined as an issue in the NAME_RESOLVE implementation, and is not a documentation issue. The
documentation is correct, the observed behavior is wrong. When this gets corrected, NAME_RESOLVE will function
differently on remote objects. For this reason, you will want to either avoid using NAME_RESOLVE on remote
objects or make sure to 'wrap' the NAME_RESOLVE routine in some function of your own. This will make it so that
when, and if, the behavior changes, you can easily modify your code to provide yourself with the old functionality,
if that is what you depend on.
1187
5254AppAL.pdf 16
2/28/2005 6:49:57 PM
Appendix A
One last comment about NAME_RESOLVE. The parameters CONTEXT and OBJECT_NUMBER are underdocumented and not documented, respectively. The CONTEXT parameter is documented briefly as:
.. . must be an integer between 0 and 8
In fact, it must be an integer between 1 and 7 or you'll receive:
declare
*
ERROR at line 1:
ORA-20005: ORU-10034: context argument must be 1 or 2 or 3 or 4 or 5 or
6 or 7
ORA-06512: at "SYS.DBMS_UTILITY", line 66
ORA-06512: at line 14
And if it is anything other than 1, you will receive one of the two following error messages:
ORA-04047: object specified is incompatible with the flag specified
ORA-06564: object OBJECT-NAME does not exist
So, the only valid value for context is 1. The OBJECT_NUMBER parameter is not documented at all. This
is the OBJECT_ID value found in DBA_OBJECTS, ALL_OBJECTS and USER_OBJECTS. For example,
given our first example where the OBJECT_ID was shown to be 2048 I can query:
scott@TKYTE816> select owner, object_name
2 from all_objects
3 where object_id = 2408;
OWNER
OBJECT_NAME
------------------------------ -----------------------------SYS
DBMS_UTILITY
NAME_TOKENIZE
This utility routine simply takes a string that represents some object name, and breaks it into its
component pieces for you. Objects are referenced via:
[schema].[object_name].[procedure|function]@[database link]
NAME_TOKENIZE simply takes a string in this form, and breaks it out into the three leading pieces and
the last (database link) piece. Additionally, it tells us what byte it stopped parsing the object name at.
Here is a small example showing what you might expect back from various object names you pass to it.
Note that you do not have to use real object names (these tables and procedures do not have to exist),
but you must use valid object identifiers. If you do not use a valid object identifier, NAME_TOKENIZE will
raise an error. This makes NAME_TOKENIZE suitable as a method to discover whether a given string of
characters will be a valid identifier or not:
scott@TKYTE816> declare
2
l_a
varchar2(30);
3
l_b
varchar2(30);
4
l_c
varchar2(30);
5
l_dblink varchar2(30);
1188
5254AppAL.pdf 17
2/28/2005 6:49:57 PM
DBMS_UTILITY
6
l_next
number;
7
8
type vcArray is table of varchar2(255);
9
l_names vcArray :=
10
vcArray( 'owner.pkg.proc@database_link',
11
'owner.tbl@database_link',
12
'tbl',
13
'"Owner".tbl',
14
'pkg.proc',
15
'owner.pkg.proc',
16
'proc',
17
'owner.pkg.proc@dblink with junk',
18
'123' );
19 begin
20
for i in 1 .. l_names.count
21
loop
22
begin
23
dbms_utility.name_tokenize(name
=> l_names(i),
24
a
=> l_a,
25
b
=> l_b,
26
c
=> l_c,
27
dblink => l_dblink,
28
nextpos=> l_next );
29
30
dbms_output.put_line( 'name
' || l_names(i) );
31
dbms_output.put_line( 'A
' || l_a );
32
dbms_output.put_line( 'B
' || l_b );
33
dbms_output.put_line( 'C
' || l_c );
34
dbms_output.put_line( 'dblink ' || l_dblink );
35
dbms_output.put_line( 'next
' || l_next || ' ' ||
36
length(l_names(i)));
37
dbms_output.put_line( '-----------------------' );
38
exception
39
when others then
40
dbms_output.put_line( 'name
' || l_names(i) );
41
dbms_output.put_line( sqlerrm );
42
end;
43
end loop;
44 end;
45 /
name
owner.pkg.proc@database_link
A
OWNER
B
PKG
C
PROC
dblink DATABASE_LINK
next
28 28
As you can see, this breaks out the various bits and pieces of our object name for us. Here the NEXT is
set to the length of the string – parsing ended when we hit the end of the string in this case. Since we
used every possible piece of the object name, all four components are filled in. Now for the remaining
examples:
name
A
B
C
dblink
next
owner.tbl@database_link
OWNER
TBL
DATABASE_LINK
23 23
1189
5254AppAL.pdf 18
2/28/2005 6:49:57 PM
Appendix A
----------------------name
tbl
A
TBL
B
C
dblink
next
3 3
-----------------------
Notice here how B and C are left Null. Even though an object identifier is
SCHEMA.OBJECT.PROCEDURE, NAME_TOKENIZE makes no attempt to put TBL into the B OUT
parameter. It simply takes the first part it finds, and puts it in A, the next into B, and so on. A, B, and C
do not represent specific pieces of the object name, just the first found, next found, and so on.
name
"Owner".tbl
A
Owner
B
TBL
C
dblink
next
11 11
-----------------------
Here is something interesting. In the previous examples, NAME_TOKENIZE uppercased everything. This
is because identifiers are in uppercase unless you use quoted identifiers. Here, we used a quoted
identifier. NAME_TOKENIZE will preserve this for us, and remove the quotes!
name
pkg.proc
A
PKG
B
PROC
C
dblink
next
8 8
----------------------name
owner.pkg.proc
A
OWNER
B
PKG
C
PROC
dblink
next
14 14
----------------------name
proc
A
PROC
B
C
dblink
next
4 4
----------------------name
owner.pkg.proc@dblink with junk
A
OWNER
B
PKG
C
PROC
dblink DBLINK
next
22 31
-----------------------
1190
5254AppAL.pdf 19
2/28/2005 6:49:57 PM
DBMS_UTILITY
Here is an example where the parsing stopped before we ran out of string. NAME_TOKENIZE is telling us
it stopped parsing at byte 22 out of 31. This is the space right before with junk. It simply ignores the
remaining pieces of the string for us.
name
123
ORA-00931: missing identifier
PL/SQL procedure successfully completed.
And lastly, this shows if we use an invalid identifier, NAME_TOKENIZE will raise an exception. It checks
all tokens for being valid identifiers before returning. This makes it useful as a tool to validate object
names if you are building an application that will create objects in the Oracle database. For example, if
you are building a data modeling tool, and would like to validate that the name the end user wants to
use for a table or column name is valid, NAME_TOKENIZE will do the work for you.
COMMA_TO_TABLE, TABLE_TO_COMMA
These two utilities either take a comma-delimited string of identifiers and parse them into a PL/SQL
table (COMMA_TO_TABLE), or take a PL/SQL table of any type of string, and make a comma-delimited
string of them (TABLE_TO_COMMA). I stress the word identifiers above, because COMMA_TO_TABLE uses
NAME_TOKENIZE to parse the strings, hence as we saw in that section, we need to use valid Oracle
identifiers (or quoted identifiers). This still limits us to 30 characters per element in our commadelimited string however.
This utility is most useful for applications that want to store a list of table names in a single string for
example, and have them easily converted to an array in PL/SQL at run-time. Otherwise, it is of limited
use. If you need a general purpose COMMA_TO_TABLE routine that works with comma-delimited strings
of data, see Chapter 20, Using Object Relational Features. In the SELECT * from PLSQL_FUNCTION
section, I demonstrate how to do this.
Here is an example using this routine, demonstrating how it deals with long identifiers and invalid
identifiers:
scott@TKYTE816> declare
2
type vcArray is table of varchar2(4000);
3
4
l_names vcArray := vcArray( 'emp,dept,bonus',
5
'a, b ,
c',
6
'123, 456, 789',
7
'"123", "456", "789"',
8
'"This is a long string, longer then 32 characters","b",c');
9
l_tablen number;
10
l_tab
dbms_utility.uncl_array;
11 begin
12
for i in 1 .. l_names.count
13
loop
14
dbms_output.put_line( chr(10) ||
15
'[' || l_names(i) || ']' );
16
begin
17
18
dbms_utility.comma_to_table( l_names(i),
19
l_tablen, l_tab );
20
1191
5254AppAL.pdf 20
2/28/2005 6:49:57 PM
Appendix A
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
for j in 1..l_tablen
loop
dbms_output.put_line( '[' || l_tab(j) || ']' );
end loop;
l_names(i) := null;
dbms_utility.table_to_comma( l_tab,
l_tablen, l_names(i) );
dbms_output.put_line( l_names(i) );
exception
when others then
dbms_output.put_line( sqlerrm );
end;
end loop;
end;
/
[emp,dept,bonus]
[emp]
[dept]
[bonus]
emp,dept,bonus
So, that shows that it can take the string emp,dept,bonus, break it into a table, and put it back
together again.
[a, b,
[a]
[ b ]
[
c]
a, b,
c]
c
This example shows that if you have whitespace in the list, it will be preserved. You would have to use
the TRIM function to remove leading and trailing white space if you do not want any.
[123, 456, 789]
ORA-00931: missing identifier
This shows that to use this procedure on a comma-delimited string of numbers, we must go one step
further as demonstrated below:
["123", "456", "789"]
["123"]
[ "456"]
[ "789"]
"123", "456", "789"
Here, it is able to extract the numbers from the string. Note however, how it not only retains the leading
whitespace, but it also retains the quotes. It would be up to you to remove them if you so desire.
["This is a long string, longer than 32 characters","b",c]
ORA-00972: identifier is too long
PL/SQL procedure successfully completed.
1192
5254AppAL.pdf 21
2/28/2005 6:49:57 PM
DBMS_UTILITY
And this last example shows that if the identifier is too long (longer than 30 characters), it will raise an
error as well. These routines are only useful for strings of 30 characters or less. While it is true that
TABLE_TO_COMMA will take larger strings than 30 characters, COMMA_TO_TABLE will not be able to undo
this work.
DB_VERSION and PORT_STRING
The DB_VERSION routine was added in Oracle 8.0, in order to make it easier for applications to figure
out what version of the database they were running in. We could have used this in our CRYPT_PKG (see
the DBMS_OBFUSCATION_TOOLKIT section) for example, to tell users who attempted to use the DES3
routines in an Oracle 8.1.5 database that it would not work, instead of just trying to execute the DES3
routines and failing. It is a very simple interface as follows:
scott@TKYTE816> declare
2
l_version
varchar2(255);
3
l_compatibility varchar2(255);
4 begin
5
dbms_utility.db_version( l_version, l_compatibility );
6
dbms_output.put_line( l_version );
7
dbms_output.put_line( l_compatibility );
8 end;
9 /
8.1.6.0.0
8.1.6
PL/SQL procedure successfully completed.
And provides more version detail than the older function, PORT_STRING:
scott@TKYTE816> select dbms_utility.port_string from dual;
PORT_STRING
--------------------------IBMPC/WIN_NT-8.1.0
Using the PORT_STRING, not only would you have to parse the string, but you also cannot tell if you are
in version 8.1.5 versus 8.1.6 versus 8.1.7. DB_VERSION will be more useful for this. On the other hand,
the PORT_STRING does tell you what operating system you are on.
GET_HASH_VALUE
This function will take any string as input, and return a numeric HASH value for it. You could use this to
build your own 'index by table' that was indexed by a string, or as we did in the DBMS_LOCK section, to
facilitate the implementation of some other algorithm.
You should be aware that the algorithm used to implement GET_HASH_VALUE can, and has, changed
from release to release, so you should not use this function to generate surrogate keys. If you find
yourself storing the return value from this function in a table, you might be setting yourself up for a
problem in a later release when the same inputs return a different hash value!
1193
5254AppAL.pdf 22
2/28/2005 6:49:57 PM
Appendix A
This function takes three inputs:
❑
The string to hash.
❑
The 'base' number to be returned. If you want the numbers to range from 0 to some number,
use 0 for your base.
❑
The size of the hash table. Optimally this number would be a power of two.
As a demonstration of using the GET_HASH_VALUE, we will implement a new type, HASHTABLETYPE, to
add to the PL/SQL language a hash type. This is very similar to a PL/SQL table type that is indexed by
a VARCHAR2 string instead of a number. Normally, PL/SQL table elements are referenced by subscripts
(numbers). This new type of PL/SQL table will have elements that are referenced by arbitrary strings.
This will allow us to declare variables of type HASHTABLETYPE and GET and PUT values into it. We can
have as many of these table types as we like. Here is the specification for our package:
tkyte@TKYTE816> create or replace type myScalarType
2 as object
3 ( key varchar2(4000),
4
val varchar2(4000)
5 );
6 /
Type created.
tkyte@TKYTE816> create or replace type myArrayType
2 as varray(10000) of myScalarType;
3 /
Type created.
tkyte@TKYTE816> create or replace type hashTableType
2 as object
3 (
4
g_hash_size
number,
5
g_hash_table
myArrayType,
6
g_collision_cnt number,
7
8
static function new( p_hash_size in number )
9
return hashTableType,
10
11
member procedure put( p_key in varchar2,
12
p_val in varchar2 ),
13
14
member function get( p_key in varchar2 )
15
return varchar2,
16
17
member procedure print_stats
18 );
19 /
Type created.
An interesting implementation detail here is the addition of the static member function NEW. This will
allow us to create our own constructor. You should note that there is absolutely nothing special about
the name NEW that I used. It is not a keyword or anything like that. What NEW will allow us to do is to
declare a HASHTABLETYPE like this:
1194
5254AppAL.pdf 23
2/28/2005 6:49:57 PM
DBMS_UTILITY
declare
l_hashTable hashTableType := hashTableType.new( 1024 );
instead of like this:
declare
l_hashTable hashTableType := hashTableType( 1024, myArrayType(), 0 );
It is my belief that the first syntax is in general, more readable and clearer than the second. The second
syntax makes the end user aware of many of the implementation details (that we have an array type in
there, that there is some variable G_COLLISION_CNT that must be set to zero, and so on). They neither
need to know that nor do they really care.
So, now onto the type body itself:
scott@TKYTE816> create or replace type body hashTableType
2 as
3
4 -- Our 'friendly' constructor.
5
6 static function new( p_hash_size in number )
7 return hashTableType
8 is
9 begin
10
return hashTableType( p_hash_size, myArrayType(), 0 );
11 end;
12
13 member procedure put( p_key in varchar2, p_val in varchar2 )
14 is
15
l_hash number :=
16
dbms_utility.get_hash_value( p_key, 1, g_hash_size );
17 begin
18
19
if ( p_key is null )
20
then
21
raise_application_error( -20001, 'Cannot have NULL key' );
22
end if;
23
This next piece of code looks to see if we need to 'grow' the table to hold this new, hashed value. If we
do, we grow it out big enough to hold this index:
27
28
29
30
31
if ( l_hash > nvl( g_hash_table.count, 0 ) )
then
g_hash_table.extend( l_hash-nvl(g_hash_table.count,0)+1 );
end if;
Now, there is no guarantee that the index entry our key hashed to is empty. What we do upon detecting
a collision is to try and put it in the next collection element. We search forward for up to 1,000 times to
put it into the table. If we hit 1,000 collisions, we will fail. This would indicate that the table is not sized
properly, if this is the case:
1195
5254AppAL.pdf 24
2/28/2005 6:49:58 PM
Appendix A
35
36
37
38
39
40
41
42
43
for i in 0 .. 1000
loop
-- If we are going to go past the end of the
-- table, add another slot first.
if ( g_hash_table.count <= l_hash+i )
then
g_hash_table.extend;
end if;
The next bit of logic says 'if no one is using this slot or our key is in this slot already, use it and return.'
It looks a tad strange to check if the G_HASH_TABLE element is Null, or if the
G_HASH_TABLE(L_HASH+I).KEY is Null. This just shows that a collection element may be Null, or it
may contain an object that has Null attributes:
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
if ( g_hash_table(l_hash+i) is null OR
nvl(g_hash_table(l_hash+i).key,p_key) = p_key )
then
g_hash_table(l_hash+i) := myScalarType(p_key,p_val);
return;
end if;
-- Else increment a collision count and continue
-- onto the next slot.
g_collision_cnt := g_collision_cnt+1;
end loop;
-- If we get here, the table was allocate too small.
-- Make it bigger.
raise_application_error( -20001, 'table overhashed' );
end;
member function get( p_key in varchar2 ) return varchar2
is
l_hash number :=
dbms_utility.get_hash_value( p_key, 1, g_hash_size );
begin
When we go to retrieve a value, we look in the index element we think the value should be in, and then
look ahead up to 1,000 entries in the event we had collisions. We short circuit this look-ahead search if
we ever find an empty slot – we know our entry cannot be beyond that point:
71
72
73
74
75
76
77
78
79
80
for i in l_hash .. least(l_hash+1000, nvl(g_hash_table.count,0))
loop
-- If we hit an EMPTY slot, we KNOW our value cannot
-- be in the table. We would have put it there.
if ( g_hash_table(i) is NULL )
then
return NULL;
end if;
-- If we find our key, return the value.
1196
5254AppAL.pdf 25
2/28/2005 6:49:58 PM
DBMS_UTILITY
81
82
83
84
85
86
87
88
89
90
if ( g_hash_table(i).key = p_key )
then
return g_hash_table(i).val;
end if;
end loop;
-- Key is not in the table. Quit.
return null;
end;
The last routine is used to print out useful information, such as how many slots you've allocated versus
used, and how many collisions we had. Note that collisions can be bigger than the table itself!
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
member procedure print_stats
is
l_used number default 0;
begin
for i in 1 .. nvl(g_hash_table.count,0)
loop
if ( g_hash_table(i) is not null )
then
l_used := l_used + 1;
end if;
end loop;
dbms_output.put_line( 'Table Extended To.....' ||
g_hash_table.count );
dbms_output.put_line( 'We are using..........' ||
l_used );
dbms_output.put_line( 'Collision count put...' ||
g_collision_cnt );
end;
end;
/
Type body created.
As you can see, we simply used the GET_HASH_VALUE to turn the string into some number we could use
to index into our table type, to get the value. Now we are ready to see how this new type can be used:
tkyte@TKYTE816> declare
2
l_hashTbl hashTableType := hashTableType.new(power(2,7));
3 begin
4
for x in ( select username, created from all_users )
5
loop
6
l_hashTbl.put( x.username, x.created );
7
end loop;
8
9
for x in ( select username, to_char(created) created,
10
l_hashTbl.get(username) hash
11
from all_users )
1197
5254AppAL.pdf 26
2/28/2005 6:49:58 PM
Appendix A
12
loop
13
if ( nvl( x.created, 'x') <> nvl(x.hash,'x') )
14
then
15
raise program_error;
16
end if;
17
end loop;
18
19
l_hashTbl.print_stats;
20 end;
21 /
Table Extended To.....120
We are using..........17
Collision count put...1
PL/SQL procedure successfully completed.
And that's it. We've just extended the PL/SQL language again, giving it a hash table using the built-in
packages.
Summary
This wraps up our overview of many of the procedures found in the DBMS_UTILITY package. Many,
such as GET_TIME, GET_PARAMETER_VALUE, GET_HASH_VALUE, and FORMAT_CALL_STACK are in my
list of 'frequently given answers'. This is to say, they are frequently the answer to many a question –
people just weren't even aware they even existed.
1198
5254AppAL.pdf 27
2/28/2005 6:49:58 PM
Download