Fine-grained Space Management in Oracle

By J.B. Sastry

This article provides a glimpse into one of the concerns in Oracle�s storage management, a primary responsibility of DBAs and certain application developers. I promise not to repeat basic concepts of Oracle�s storage in this article. A more formidable introduction to the ideas presented in here and other primary issues/concepts of storage like storage clause, efficient deployment of pctfree and pctused criteria, is available in voluminous detail through Oracle manuals and several books written by DBA gurus.

This article addresses the question of "How much data can be loaded into a database without having to add any more files to the tablespaces?" It also discusses the diagnostic information that can be derived while answering this question. The article will also address relevance to costs arising out of performance degradation from fragmentation and ill-effects of unscientific file additions to tablespaces. This article includes the code to accomplish the above task. The script in its entirety yields a highly resolved data to facilitate space administration and serves as a diagnostic tool that can be deployed if low performance is suspected due to fragmentation. This approach can be highly valuable in a datawarehousing environment with hundreds of gigabytes of data and where simplistic solutions like export and reimport or CTAS are not conducive operations. The timeless advise of prevention being better than cure is once again the ultimate truth. Scientific planning of tablespaces that is in sync with database growth is more important than ad hoc cures.

Tablespaces are the basic logical containers of Oracle data in the databases. Tablespaces reside in file-spaces on the servers and store segments of logical data such as tables and indexes. In other words, users create tables and indexes in tablespaces that are "opened" for them. As the space available for a "container" is consumed, new data files are added. Ultimately, in a moderately large database you will find that tablespaces are spread across several files and disks . Such a spread might not, after all, have occurred from intentional striping, but from a poor or inadequate planning.

Let us begin our discussion with certain assumptions in a hypothetical scenario, typically deploying sql*loader for loading into a multitude of tables. The goal is to develop a script that would evaluate the situation and forewarn us with information such as "some tablespace can accommodate up to 12,550,000 rows in its present state and if our load is in excess of this volume, please add some data files to this tablespace." It might also throw out a report which says that "clusters of blocks starting at of 25,345,000 bytes in file_id are worthless because they can not be used any more for a new extent and you have not only wasted so much space but also created a fragmented disk space."

Normally, we would assume that in our database we have dedicated tablespaces, i.e., each table has its own. For instance, a table "employee" resides in tablespace "employee_ts" and its associated indexes reside in tablespace "employee_is." This type of database design is not only useful but imperative for databases of any significant size. Enough can never be said about how easier it would be to manage such systems.

By the same token, we have to realize that it is impossible to visualize each index of a table residing in its own tablespace, as the sizes of indexes are much smaller. Fine-graining to such an extent is also not desirable and probably will exceed the MAX_FILES limitation. As the script development progresses in our present venture, we will see how the computation gets slightly complicated when multiple segments in a single tablespace extent are involved.

Let us commence our study with a typical layout of a database segment. In order to determine the number of rows of data that can be loaded into this object- a table or a set of indexes, the following factors need to be considered.

1. The amount of free space within a data block in the existing segment. This arises out of the rows DELETED and that are listed in freelists for Oracle for INSERT operations. We are not considering this particular factor in the present analysis for various reasons. A few reasons include: a typical datawarehousing application does not involve many DELETE transactions, the volume of such a space is not bound to be high and does not warrant attention in a bulk loading scenario, or we might not even consider conventional Oracle loading and prefer directpath loads.

2. The second chunk of space lies in the segment itself, unused.- See the area marked as Unused space in Fig:1. This is the space between the sum of the extents already formed for this object and the utilized space. In this case there is no extension of any segment involved and it is a straight forward computation.

3. The third chunk of space to be analyzed is the space beyond the table segment but below the tablespace- marked as free-space in the figure. This arises when the extent sizes of objects are smaller than that of the tablespace. This is the space in which the table segment will extend next, provided there are enough data blocks available to accommodate its next extent. The free chunks are assumed to be coalesced by Oracle . When there are no contiguous blocks to accommodate a table extent, this space remains unused forever, at least till some repair action is taken by the DBA. The next extent to grow would size to the next_extent parameter of the table. When there are multiple tables in a tablespace or several indexes with different next_extent sizes in a tablespace, the growth would take place for the size of the next object due. Both these situations involving multiple tables and multiple indexes are similar to a degree and in the script we are going to develop we will be addressing the indexes case. The script can be "tweaked" for the other case, if need be.

4. The fourth chunk of space is the space available in the data file (file_space in the figure 1) and ready to be used by the tablespace in its future extensions. Same conditions that are mentioned earlier apply to this space too. We need to keep in mind that the extent size in this case is the next_extent size of the tablespace and not of the table.

The principle we assume is that as dataload progresses, Oracle will insert rows into the free unused space in the segment. This is followed by growing the extents of the table until the tablespace can accommodate and then extend the tablespace until the OS file can provide, after which the dataload will fail. We need to find the number of rows that can be loaded into various tablesbeforee such a failure so that we can come back to a pleasant greeting on Monday morning!

5. For accurate computations we will be reading the following values from data dictionary:

Let us briefly consider the relative complexity in computing the index segments. Assume that three index segments(indexes) exist in a tablespace created with equal Initial_Extent sizes. Due to the differences in the widths and number of columns in their sizes, the used spaces in their segments will differ. Accordingly, the number of rows they can and have stored for a given chunk of blocks will also differ. If their next_extent sizes are different, they grow almost at random. Their growth pattern will resemble Fig.2. The first segment to grow will be the index that can accommodate least number of rows in its unused space(see note 2 above). The next growth would occur in a segment with next least rows, and so on . Therefore, we will have to capture this sequence in an algorithm. This pattern will remain even in a new extent of the tablespace. This point will be referred to later in this article.

The code is developed for Oracle 7.3 and differs with earlier versions in a few ways, including:

As stated earlier, let us assume that our tablespaces are well organized and named with _IS extents for the tablespaces housing the indexes and _TS for tables. Further, only one table exists for each _TS. The treatment of a tablespace where multiple tables are grouped in a table is somewhat similar to the treatment for _IS and is left to the reader as an exercise.

The computational parts of the code will be in a package, which we will refer to as pkg_compute_rows. Its main procedure would be proc_compute_rows with input parameter of the tablespace name. Each index or table tablespace name needs to be passed to this package in a loop. The technique to develop these names is largely related to the application. In a situation involving partitions of tables, the partitions are generally named after the lastname alphabet or the month or quarter of operation which serves as a check constraint for creating the partitions and views. If such is the case, the list of tablespaces can be generated out of a PL/SQL routine. Otherwise we need to SELECT DISTINCT TABLESPACE_NAMEs form user_tablespaces view. We can then use a FOR or WHILE loop to pass each tablespace name and call the procedure with the following command:

pkg_compute_rows.proc_compute_rows(v_tsname);

The specification of the package will be simple and involve just the main procedure, the code for which follows below:

CREATE OR REPLACE PACKAGE pkg_compute_rows AS PROCEDURE proc_compute_rows(v_tsname IN VARCHAR2); END pkg_compute_rows;

Before we start defining the body of the package, let us dwell on the package variables that we need:

A complete list of variables would therefore contain-

CREATE OR REPLACE PACKAGE BODY pkg_compute_rows AS
  --Declare some variables
  blk_size 			INTEGER 	:=16384 or whatever; --Constant, DB specific
  v_row_len			INTEGER 	:=0;
  v_avg_row_len			INTEGER 	:=0;
  v_num_cols			INTEGER 	:=0; --(either in a table or in an index)
  tbl_row_hdr			INTEGER 	:=8; --(Constant- for row_id information)
  ind_row_hdr			INTEGER 	:=3; --(Constant- for index header info.)
  v_table_name    		VARCHAR2(30):=' ';
  v_index_name    		VARCHAR2(40):=' '; 
  v_next_ext	   		NUMBER 		:=0;
  v_next_blks   		NUMBER 		:=0;
  v_ext_possible		INTEGER 	:=0; --(number of extents possible)
  v_rows_possible		NUMBER 		:=0;
  v_blks_per_ext		NUMBER 		:=0;
  v_rows_per_block		INTEGER 	:=0;
  v_length 			NUMBER 		:=0;
  v_least_rows 			NUMBER 		:=0;
  v_index_with_least_rows 	VARCHAR2(40):=' ';
  v_index_id_with_least_rows BINARY_INTEGER:=0;
 
  --Variables for receiving values from procedure DBMS_SPACE.UNUSED_SPACE
  ds_tblocks 		NUMBER :=0; -- (Total blocks in the segment)
  ds_tbytes 		NUMBER :=0; -- (Total bytes in the segment)
  ds_ublocks 		NUMBER :=0; -- (Unused blocks in the segment)
  ds_ubytes 		NUMBER :=0; -- (Unused bytes in the segment)
  ds_lufid 		NUMBER :=0; -- Last used file_id)
  ds_lubid 		NUMBER :=0; -- (Last used block_id)
  ds_lublock 		NUMBER :=0; -- (Last used block)
Variables for PL/SQL tables used in index segment computations

  TYPE ind_rec is RECORD (
    ind_name			VARCHAR2(30),
    avglen			INTEGER,
    ublocks			NUMBER,
    next_extent			NUMBER,
    rows_possible		NUMBER,
    num_cols			INTEGER );
    TYPE ind_tab_type IS TABLE OF ind_rec INDEX BY BINARY_INTEGER; 
    ind_tab 			ind_tab_type;
    idx 			BINARY_INTEGER :=0;
The main procedure in the package does little. It checks the v_tsname parameter to see if this is an index space or a table tablespace and calls the relevant program.

PROCEDURE proc_compute_rows( v_tsname	VARCHAR2) IS
  v_tsname_ind		VARCHAR2(2);

 BEGIN
 --Determine if the tablespace  is for an index or a table segment
   v_tsname_ind := SUBSTR(v_tsname,(LENGTH(v_tsname)-1));

   ind_tab.DELETE; --Initialize

   IF v_tsname_ind = 'TS' THEN
      pkg_compute_rows.proc_compute_table(v_tsname);

   ELSE
      pkg_compute_rows.proc_compute_index(v_tsname);

   END IF;

  ind_tab.DELETE;--To ensure that we do not waste disk /memory space by leaving
 some values behind in these global PL/SQL tables.
END  proc_compute_rows; 

Now to the procedure pkg_compute_rows.proc_compute_table(v_tsname). This is an unspecified procedure internal to the package and has three action segments. The first one is for computing the rows that can be loaded from unused space within the table segment already formed.

PROCEDURE proc_compute_table( v_tsname VARCHAR2) IS

This procedure is called when the main procedure evaluates that the segment passed belongs to a table. Begin by initializing some variables:

BEGIN
v_table_name := SUBSTR(v_tsname, 1, LENGTH(v_tsname)-3);

SELECT avg_row_len INTO v_avg_row_len FROM tabs WHERE table_name = v_table_name;

  SELECT next_extent INTO v_next_ext FROM tabs WHERE table_name= v_table_name;

  v_next_blks := TRUNC(v_next_ext/blk_size);--Number of blocks required for next_extent
We need this information for computing the required row lengths. See the Oracle documentation on space calculations for details.

SELECT MAX(column_id) INTO v_num_cols FROM cols WHERE table_name = v_table_name;

There is a small approximation in the line below. The num_cols are just added where as Oracle advises differently for some data types.

  v_rows_per_block := TRUNC(blk_size/(tbl_row_hdr + v_avg_row_len + 					v_num_cols));

Compute from DBMS_SPACE first for the rows that can be inserted without any extension to the table or the tablespace.

 DBMS_SPACE.UNUSED_SPACE('APPOWNER', �table_name�, 'TABLE', ds_tblocks,
	  ds_tbytes, ds_ublocks, ds_ubytes, ds_lufid, ds_lubid, ds_lublock);

 v_rows_possible := TRUNC( ds_ublocks * v_rows_per_block );
At this point we have the information that can perhaps be stored in a result- summary table for querying later.
 INSERT INTO summary_compute_rows(tablespace_name, bytes, blocks, rows_possible, check_date) 
 VALUES(v_tsname, ds_ubytes,ds_ublocks,v_rows_possible,SYSDATE);
 COMMIT;
 v_rows_possible :=0;

Now check if the tablespace can accommodate any more table extents. We are assuming that Oracle has coalesced all contiguous chunks of space. We are computing this information using user_free_space view.

 FOR rec IN ( SELECT DISTINCT file_id, bytes, blocks, block_id start_blk, 
   				(block_id+blocks) end_blk FROM user_free_space
					WHERE tablespace_name =  v_tsname ) LOOP

  IF rec.blocks >= v_next_blks THEN
     v_ext_possible  := TRUNC(rec.blocks/v_next_blks);  --Extents possible
     v_rows_possible := TRUNC(v_rows_per_block * v_next_blks * v_ext_possible);
     INSERT  INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, rec.bytes, rec.blocks,v_next_ext,
              v_rows_possible,SYSDATE,rec.start_blk, rec.end_blk);
     COMMIT;
     v_rows_possible :=0;
  ELSE  
When this condition is hit, we know that we have a chunk of space that does not have enough blocks to extend a table segment. Though this chunk of space is useless for loading, we need this information to estimate and diagnose fragmentation. More of these empty and useless spaces would warrant corrective action.

     INSERT INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, rec.bytes, rec.blocks,v_next_ext,
              0,SYSDATE,rec.start_blk, rec.end_blk);
     COMMIT;
  END IF;
 END LOOP;

The next chunk of space of interest is the space between the OS file for the tablespace and the already formed tablespace extents. We now check to see if the files can accommodate any more extents of the tablespace. Note that v_ext_possible and v_next_ext refer to the tablespace and not table. We are checking the difference between total blocks and used blocks in each file to determine if a tablespace can grow in that free area.

  SELECT next_extent INTO v_next_ext FROM user_tablespaces WHERE tablespace_name= v_tsname;

  v_next_blks := TRUNC(v_next_ext/blk_size);

These are the blocks required for the next tablespace extent. There are bound to be a number of non-contiguous chunks of blocks in a file system that has been put to use for a while and we need to examine of these spaces. The way wee accomplish this is to check the file_id of each space and then query the file$ table for total blocks and uet$ for used blocks.

  FOR rec IN (SELECT DISTINCT file_id FROM sys.dba_extents WHERE
					tablespace_name=v_tsname) LOOP

	SELECT f.blocks into ds_tblocks FROM sys.file$ --Total blocks from file
		WHERE	file#= rec.file_id ;
----------------------------------
	SELECT SUM(u.length) INTO ds_ublocks FROM sys.uet$  --Used blocks 											from extents
		WHERE  file# = rec.file_id;
----------------------------------
   IF (ds_tblocks- ds_ublocks) >= v_next_blks THEN
     v_ext_possible  := TRUNC((ds_tblocks- ds_ublocks)/v_next_blks);
     v_rows_possible := TRUNC(v_rows_per_block * v_next_blks * v_ext_possible);
     INSERT INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, (ds_tblocks- ds_ublocks)*blk_size,
              (ds_tblocks- ds_ublocks), v_next_ext,v_rows_possible,
              SYSDATE,0,0);
     COMMIT;
     v_rows_possible :=0;
   ELSE

Though this chunk of space is useless for loading, we need this information to estimate and diagnose fragmentation. More of these empty and useless spaces would warrant corrective action.

INSERT INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, (ds_tblocks- ds_ublocks)*blk_size,
              (ds_tblocks- ds_ublocks), v_next_ext,0,SYSDATE,0,0);
     COMMIT;
   END IF;
     END LOOP;
END proc_compute_table;

We shall now move on to the computation for index spaces. The three steps are the same but with a small twist because of the existence of multiple indexes in each tablespace. We therefore initially �load� the details of all indexes in a tablespace in to PL/SQL table ind_tab, defined among the variables.

This procedure is called when the main program detects that the segment passed to it belongs to indexes

PROCEDURE proc_compute_index(v_tsname VARCHAR2) IS 
 v_blocks NUMBER;

Please note that "hwm" is a variable serving as a watermark for the rows that are already entered into summary_compute_rows table. This will eliminate duplication of rows already considered. Its use will become evident later.

hwm      NUMBER;  

--Initialize some variables defined in PL/SQL table

 BEGIN
v_table_name := SUBSTR(v_tsname, 1, LENGTH(v_tsname)-3);

We need to find all the indexes for this table and load them into PL/SQL tables as multiple indexes share the same tablespace

 idx :=0;
FOR rec IN (SELECT index_name, next_extent FROM ind WHERE table_name = 			v_table_name) LOOP
   idx := idx + 1;
   ind_tab(idx).ind_name 		:= rec.index_name;
   ind_tab(idx).next_extent 		:= rec.next_extent;
 END LOOP;

Pick up the number of columns in each index and their length.

FOR I IN 1..idx LOOP
   v_index_name := ind_tab(I).ind_name;
   SELECT COUNT(*), SUM(column_length) INTO v_num_cols, v_length FROM 	user_ind_columns WHERE
   index_name= v_index_name;
   ind_tab(I).num_cols 	:= v_num_cols;
   ind_tab(I).avglen 		:= v_length;

Extract from DBMS_SPACE first for the rows that can be inserted without any extension to the table or the tablespace.

   DBMS_SPACE.UNUSED_SPACE('APPOWNER', �table_name�, 'TABLE',ds_tblocks, 	ds_tbytes, ds_ublocks,ds_ubytes,  ds_lufid,ds_lubid,ds_lublock);
    ind_tab(I).ublocks := ds_ublocks;

Compute the rows_possible without any extension to the segments.

   v_rows_per_block := TRUNC(blk_size/(ind_row_hdr + v_length + v_num_cols));
   v_rows_possible := TRUNC( ds_ublocks * v_rows_per_block);
   ind_tab(I).rows_possible := v_rows_possible; 
   v_rows_possible :=0;
 END LOOP ;

It is likely that the indexes have differing load capacities. To post the results of above check, we therefore consider the lowest possible rows among all indexes and post it for entire IS tablespace. This is accomplished by calling the procedure to find the index with the least rows. The code for this procedure is given at the end. This procedure can be included in the package as a stand-alone module to which other programs can refer to as needed.

pkg_compute_rows.proc_find_least_rows;

 INSERT INTO summary_compute_rows (tablespace_name, rows_possible, 
 		check_date) VALUES (v_tsname, v_least_rows,SYSDATE); 
 hwm := v_least_rows; --Assign the entry value(rows already considered)

Next we need to evaluate the possibility of extensions to various index segments to the extent the tablespace can accommodate. The extension would start with the index with least capable rows and then on the next one. The way this is accomplished is by checking for the index with least rows and extending its rows and reducing the size of tablespace then recheck the lowest index, and so on. the program for identifying the least row index is called repetitiously.

  FOR rec IN ( SELECT DISTINCT file_id, bytes, blocks, block_id start_blk, 
   				(block_id+blocks) end_blk FROM user_free_space
					WHERE tablespace_name =  v_tsname ) LOOP

    v_blocks 			:= rec.blocks;
    v_next_blks 		:= TRUNC(ind_tab(v_index_id_with_lst_rows).next_extent/blk_size); 

We need a second loop to extend the index segments individually, based on their status in the v_least_rows extraction. The loop exits when a particular segment is completely allocated by the diminishing v_blocks check in the WHILE loop.

   WHILE v_blocks >= v_next_blks LOOP
       v_blocks 			:= v_blocks - v_next_blks ;
       v_row_len		:= (ind_row_hdr + 										    ind_tab(v_index_id_with_lst_rows).avglen +
               			    ind_tab(v_index_id_with_lst_rows).num_cols);
       v_rows_per_block 	:= TRUNC(blk_size/v_row_len);

       ind_tab(v_index_id_with_lst_rows).rows_possible :=   
	ind_tab(v_index_id_with_lst_rows).rows_possible + (v_next_blks * 			v_rows_per_block );
---------------------------------
       pkg_compute_rows.proc_find_least_rows;
---------------------------------
    v_next_blks := TRUNC(ind_tab(v_index_id_with_lst_rows).next_extent/blk_size); 
    END LOOP;

At this stage all the relevant records in PL/SQL table have been updated. We are ready to move on to the next file_id. Now we should record the new information, i.e., the latest rows possible(once again the least record) into the dataload table.

    pkg_compute_rows.proc_find_least_rows;
---------------------------------
    INSERT INTO summary_compute_rows 
       VALUES(v_tsname, rec.file_id, rec.bytes, rec.blocks,0,
              (v_least_rows-hwm),SYSDATE,rec.start_blk, rec.end_blk);
---------------------------------
       hwm := v_least_rows;
---------------------------------
    COMMIT;
  END LOOP;

The final step is the possibility of extension to the tablspace itself within the file system. We compute the difference between the file blocks (total) and used blocks for the tablespace as the blocks available. The rows possible is estimated by summing up the over all row lengths, num_cols for all indexes and estimating the rows possible. This is because the extension might take place on any index in the given tablespace. For a more accurate calculation, we might incorporate the same technique used in the above program block.

  SELECT next_extent INTO v_next_ext FROM user_tablespaces WHERE 	tablespace_name= v_tsname;
  v_next_blks := TRUNC(v_next_ext/blk_size);

FOR rec IN (SELECT DISTINCT file_id FROM sys.dba_extents WHERE
					tablespace_name=v_tsname) LOOP

	SELECT f.blocks into ds_tblocks FROM sys.file$ f 
		WHERE	f.file#= rec.file_id ;
	SELECT SUM(u.length) INTO ds_ublocks FROM sys.uet$ u
		WHERE  u.file# = rec.file_id;

   IF (ds_tblocks- ds_ublocks) >= v_next_blks THEN
     v_ext_possible  := TRUNC((ds_tblocks- ds_ublocks)/v_next_blks);

Compute the overall row length for a more generic estimate of rows because at this stage we are only looking at an entire tablespace extent growth.

     v_row_len 			:= 0;
     FOR I IN 1..idx LOOP
       v_row_len:= v_row_len + (ind_row_hdr + ind_tab(I).avglen + 						ind_tab(I).num_cols);
     END LOOP;

v_rows_per_block := TRUNC(blk_size/v_row_len);
       v_rows_possible := TRUNC(v_rows_per_block * v_next_blks * v_ext_possible);

     INSERT INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, (ds_tblocks- ds_ublocks)*blk_size,
              (ds_tblocks- ds_ublocks), v_next_ext,v_rows_possible,
              SYSDATE,0,0);
     COMMIT;
     v_rows_possible :=0;
    END IF;
   END LOOP;

END proc_compute_index;

PROCEDURE proc_find_least_rows IS

This procedure is called a number of times by index computing procedure Methodology is to start with the first number and then substitute it if with subsequent smaller numbers. the values in the PL/SQL table at the point of calling this procedure are used for this extraction.

BEGIN
--start with the first row
   v_least_rows 			:= ind_tab(1).rows_possible;
   v_index_with_lst_rows 		:= ind_tab(1).ind_name;
   v_index_id_with_lst_rows 		:= 1;

 FOR I IN 1..idx LOOP
    IF  ind_tab(I).rows_possible < v_least_rows THEN
      v_least_rows 			:= ind_tab(I).rows_possible;
      v_index_with_lst_rows 		:= ind_tab(I).ind_name;
      v_index_id_with_lst_rows 	:= I;
   END IF;
 END LOOP;
END;

The summary_compute_rows table now has all kinds of useful information. It not only projects the number of rows any dataload can sustain but also can map the chunks of empty spaces that are forming in the database. One can write simple SQL*PLUS scripts to report formatted information from this table. A simple sample report is shown below.

15,859,712968188,743,6809,338,880570188,743,680016,3841188,743,68008,192,000500188,743,6800
TablespaceFileBytesBlocksExtent_SizeRows_Possible
SAMPLE_TABLE_TS278,528171,700
180
19
20
21
sum33,685,5042,0561,700

From the above report we can see that the sample_table can load only up to 1700 rows from the unused space in its already formed segments(file_id is NULL). The other rows pertain to the different file segments of the tablespace. In all these cases there are free blocks available but none of them is useful in letting an extent grow. Without a detailed analysis, one might have wrongfully concluded that 240,000 rows could have been added to the table over a combined free space of 33.6MB for a row-size of 140 bytes. In reality the dataload would have failed. Taking a look at the number of files that carry useless empty-spaces, we know that they typically result in fragmentation, if more files get added without corrective action. Fragmentation of tablespaces is bad for two reasons- it creates performance degradation as disk reads of non-contiguous blocks is inefficient and also the trend can only continue if corrective actions are not made.

In summary, we can conclude that space management can become an intricate task as one tends to develop fine-grained analysis and solutions to data loading and fragmentation problems. A simple addition of more file spaces is never a solution, at least not an intelligent one. One must decide what needs to be done based on facts. The above developed script is one such value-added tool that can quickly show the database�s health and can provide intelligible information.

(Author�s Note: I have not considered or come across any Third Party tool/utility which would have answered the requirement mentioned in this paper directly. It is very possible that some innovative entrepreneur has already developed and marketed a suitable tool, in which case I accept my ignorance -JB.)

Acknowledgments:

Thanks to Bob Desatnik of BCBSO.

About The Author:
With more than 8 years of Business and Information Systems design and management back-ground, JB Sastry works for Megasys as a consultant and am on assignment at BCBSO, Cleveland. Readers can reach me at [email protected]. My Address:

J. B. Sastry
7605 Lucerne Drive, Apt- A16
Middleburg Hts., OH- 44130

Phones:
Day: +1.216.292.0400 x 3455
Evening:+1.216.816.1642

Full listing of the code:

---------------------------------------------------------------------------------------------------------
CREATE OR REPLACE PACKAGE pkg_compute_rows AS
PROCEDURE proc_compute_rows(v_tsname IN VARCHAR2);
END pkg_compute_rows;
---------------------------------------------------------------------------------------------------------
CREATE OR REPLACE PACKAGE BODY pkg_compute_rows AS
  --Declare some variables
  blk_size 			INTEGER 	:=16384 or whatever; --Constant, DB specific
  v_row_len			INTEGER 	:=0;
  v_avg_row_len		INTEGER 	:=0;
  v_num_cols			INTEGER 	:=0; --either in a table or in an index
  tbl_row_hdr			INTEGER 	:=8; --Constant- for row_id information
  ind_row_hdr			INTEGER 	:=3; --Constant- for index header info
  v_table_name    		VARCHAR2(30):=' ';
  v_index_name    		VARCHAR2(40):=' '; 
  v_next_ext	   		NUMBER 	:=0;
  v_next_blks   			NUMBER 	:=0;
  v_ext_possible		INTEGER 	:=0; --number of extents possible
  v_rows_possible		NUMBER 	:=0;
  v_blks_per_ext		NUMBER 	:=0;
  v_rows_per_block		INTEGER 	:=0;
  v_length 			NUMBER 	:=0;
  v_least_rows 			NUMBER 	:=0;
  v_index_with_least_rows 	VARCHAR2(40):=' ';
  v_index_id_with_least_rows BINARY_INTEGER:=0;
 
  --Variables for receiving values from procedure DBMS_SPACE.UNUSED_SPACE
  ds_tblocks 		NUMBER :=0; --Total blocks in the segment
  ds_tbytes 		NUMBER :=0; --Total bytes in the segment
  ds_ublocks 		NUMBER :=0; --Unused blocks in the segment
  ds_ubytes 		NUMBER :=0; --Unused bytes in the segment
  ds_lufid 		NUMBER :=0; --Last used file_id
  ds_lubid 		NUMBER :=0; --Last used block_id
  ds_lublock 		NUMBER :=0; --Last used block

  --Variables for  PL/SQL tables used in index segment computations
  TYPE ind_rec is RECORD (
    ind_name			VARCHAR2(30),
    avglen			INTEGER,
    ublocks			NUMBER,
    next_extent			NUMBER,
    rows_possible		NUMBER,
    num_cols			INTEGER );

  TYPE ind_tab_type IS TABLE OF ind_rec INDEX BY BINARY_INTEGER; 
  ind_tab 			ind_tab_type;
  idx 				BINARY_INTEGER :=0;
PROCEDURE proc_compute_rows ( v_tsname	VARCHAR2) IS
  v_tsname_ind		VARCHAR2(2);

 BEGIN
 --Determine if the tablespace  is for an index or a table segment
   v_tsname_ind := SUBSTR(v_tsname,(LENGTH(v_tsname)-1));

   ind_tab.DELETE; --Initialize

   IF v_tsname_ind = 'TS' THEN
      pkg_compute_rows.proc_compute_table(v_tsname);

   ELSE
      pkg_compute_rows.proc_compute_index(v_tsname);

   END IF;

  ind_tab.DELETE;
END  proc_compute_rows; 
---------------------------------------------------------------------------------------------------------
PROCEDURE proc_compute_table( v_tsname VARCHAR2) IS
---------------------------------------------------------------------------------
BEGIN
--Initialize some variables
  v_table_name := SUBSTR(v_tsname, 1, LENGTH(v_tsname)-3);
--------------------------------
SELECT avg_row_len INTO v_avg_row_len FROM tabs WHERE table_name = v_table_name;
--------------------------------
  SELECT next_extent INTO v_next_ext FROM tabs WHERE table_name= v_table_name;
--------------------------------
  v_next_blks := TRUNC(v_next_ext/blk_size);--Number of blocks required for next_extent
--------------------------------
  SELECT MAX(column_id) INTO v_num_cols FROM cols WHERE table_name = 	v_table_name;
--------------------------------
v_rows_per_block := TRUNC(blk_size/(tbl_row_hdr + v_avg_row_len + 					v_num_cols));
---------------------------------------------------------------------------------
 DBMS_SPACE.UNUSED_SPACE('APPOWNER', �table_name�, 'TABLE', ds_tblocks,
	  ds_tbytes, ds_ublocks, ds_ubytes, ds_lufid, ds_lubid, ds_lublock);
--------------------------------
 v_rows_possible := TRUNC( ds_ublocks * v_rows_per_block );

INSERT INTO summary_compute_rows(tablespace_name, bytes, blocks, rows_possible, check_date) 
VALUES(v_tsname, ds_ubytes,ds_ublocks,v_rows_possible,SYSDATE);
 COMMIT;
 v_rows_possible :=0;
----------------------------------
 FOR rec IN ( SELECT DISTINCT file_id, bytes, blocks, block_id start_blk, 
   				(block_id+blocks) end_blk FROM user_free_space
					WHERE tablespace_name =  v_tsname ) LOOP

  IF rec.blocks >= v_next_blks THEN
     v_ext_possible  := TRUNC(rec.blocks/v_next_blks);  --Extents possible
     v_rows_possible := TRUNC(v_rows_per_block * v_next_blks * v_ext_possible);
     INSERT  INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, rec.bytes, rec.blocks,v_next_ext,
              v_rows_possible,SYSDATE,rec.start_blk, rec.end_blk);
     COMMIT;
     v_rows_possible :=0;
  ELSE  
----------------------------------
     INSERT INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, rec.bytes, rec.blocks,v_next_ext,
              0,SYSDATE,rec.start_blk, rec.end_blk);
     COMMIT;
  END IF;
 END LOOP;
---------------------------------------------------------------------------------
SELECT next_extent INTO v_next_ext FROM user_tablespaces WHERE tablespace_name= v_tsname;
--------------------------------
  v_next_blks := TRUNC(v_next_ext/blk_size);
--------------------------------
  FOR rec IN (SELECT DISTINCT file_id FROM sys.dba_extents WHERE
					tablespace_name=v_tsname) LOOP

	SELECT f.blocks into ds_tblocks FROM sys.file$ --Total blocks from file
		WHERE	file#= rec.file_id ;
----------------------------------
	SELECT SUM(u.length) INTO ds_ublocks FROM sys.uet$  --Used blocks 											from extents
		WHERE  file# = rec.file_id;
----------------------------------
   IF (ds_tblocks- ds_ublocks) >= v_next_blks THEN
     v_ext_possible  := TRUNC((ds_tblocks- ds_ublocks)/v_next_blks);
     v_rows_possible := TRUNC(v_rows_per_block * v_next_blks * v_ext_possible);
     INSERT INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, (ds_tblocks- ds_ublocks)*blk_size,
              (ds_tblocks- ds_ublocks), v_next_ext,v_rows_possible,
              SYSDATE,0,0);
     COMMIT;
     v_rows_possible :=0;
   ELSE
----------------------------------
     INSERT INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, (ds_tblocks- ds_ublocks)*blk_size,
              (ds_tblocks- ds_ublocks), v_next_ext,0,SYSDATE,0,0);
     COMMIT;
   END IF;
     END LOOP;
END proc_compute_table;
---------------------------------------------------------------------------------------------------------

PROCEDURE proc_find_least_rows IS
----------------------------------
BEGIN
--start with the first row
   v_least_rows 			:= ind_tab(1).rows_possible;
   v_index_with_lst_rows 		:= ind_tab(1).ind_name;
   v_index_id_with_lst_rows 		:= 1;

 FOR I IN 1..idx LOOP
    IF  ind_tab(I).rows_possible < v_least_rows THEN
      v_least_rows 			:= ind_tab(I).rows_possible;
      v_index_with_lst_rows 		:= ind_tab(I).ind_name;
      v_index_id_with_lst_rows 	:= I;
   END IF;
 END LOOP;
END proc_find_least_rows;

----------------------------------------------------------------------------------------------------
PROCEDURE proc_compute_index(v_tsname VARCHAR2) IS 
 v_blocks NUMBER;
hwm      NUMBER;  
---------------------------------
 BEGIN
v_table_name := SUBSTR(v_tsname, 1, LENGTH(v_tsname)-3);
---------------------------------
 idx :=0;
FOR rec IN (SELECT index_name, next_extent FROM ind WHERE table_name = 			v_table_name) LOOP
   idx := idx + 1;
   ind_tab(idx).ind_name 		:= rec.index_name;
   ind_tab(idx).next_extent 		:= rec.next_extent;
 END LOOP;
---------------------------------
 FOR I IN 1..idx LOOP
   v_index_name := ind_tab(I).ind_name;
   SELECT COUNT(*), SUM(column_length) INTO v_num_cols, v_length FROM 	user_ind_columns WHERE
   index_name= v_index_name;
   ind_tab(I).num_cols 	:= v_num_cols;
   ind_tab(I).avglen 		:= v_length;
---------------------------------
DBMS_SPACE.UNUSED_SPACE('APPOWNER', �table_name�, 'TABLE',ds_tblocks, 	ds_tbytes, ds_ublocks,ds_ubytes,  ds_lufid,ds_lubid,ds_lublock);
    ind_tab(I).ublocks := ds_ublocks;
---------------------------------
v_rows_per_block := TRUNC(blk_size/(ind_row_hdr + v_length + v_num_cols));
   v_rows_possible := TRUNC( ds_ublocks * v_rows_per_block);
   ind_tab(I).rows_possible := v_rows_possible; 
   v_rows_possible :=0;
 END LOOP ;
---------------------------------
 pkg_compute_rows.proc_find_least_rows;
---------------------------------
 INSERT INTO summary_compute_rows (tablespace_name, rows_possible, 
 		check_date) VALUES (v_tsname, v_least_rows,SYSDATE); 
 hwm := v_least_rows; --Assign the entry value(rows already considered)
---------------------------------------------------------------------------------
FOR rec IN ( SELECT DISTINCT file_id, bytes, blocks, block_id start_blk, 
   				(block_id+blocks) end_blk FROM user_free_space
					WHERE tablespace_name =  v_tsname ) LOOP

    v_blocks 			:= rec.blocks;
    v_next_blks 		:= TRUNC(ind_tab(v_index_id_with_lst_rows).next_extent/blk_size); 
---------------------------------
WHILE v_blocks >= v_next_blks LOOP
       v_blocks 			:= v_blocks - v_next_blks ;
       v_row_len		:= (ind_row_hdr + 										    ind_tab(v_index_id_with_lst_rows).avglen +
               			    ind_tab(v_index_id_with_lst_rows).num_cols);
       v_rows_per_block 	:= TRUNC(blk_size/v_row_len);

       ind_tab(v_index_id_with_lst_rows).rows_possible :=   
	ind_tab(v_index_id_with_lst_rows).rows_possible + (v_next_blks * 			v_rows_per_block );
---------------------------------
       pkg_compute_rows.proc_find_least_rows;
---------------------------------
    v_next_blks := TRUNC(ind_tab(v_index_id_with_lst_rows).next_extent/blk_size); 
    END LOOP;
---------------------------------
pkg_compute_rows.proc_find_least_rows;
---------------------------------
    INSERT INTO summary_compute_rows 
       VALUES(v_tsname, rec.file_id, rec.bytes, rec.blocks,0,
              (v_least_rows-hwm),SYSDATE,rec.start_blk, rec.end_blk);
---------------------------------
       hwm := v_least_rows;
---------------------------------
    COMMIT;
  END LOOP;
---------------------------------------------------------------------------------
SELECT next_extent INTO v_next_ext FROM user_tablespaces WHERE 	tablespace_name= v_tsname;
  v_next_blks := TRUNC(v_next_ext/blk_size);
---------------------------------------------------------------------------------
  FOR rec IN (SELECT DISTINCT file_id FROM sys.dba_extents WHERE
					tablespace_name=v_tsname) LOOP
---------------------------------
	SELECT f.blocks into ds_tblocks FROM sys.file$ f 
		WHERE	f.file#= rec.file_id ;
	SELECT SUM(u.length) INTO ds_ublocks FROM sys.uet$ u
		WHERE  u.file# = rec.file_id;
---------------------------------
   IF (ds_tblocks- ds_ublocks) >= v_next_blks THEN
     v_ext_possible  := TRUNC((ds_tblocks- ds_ublocks)/v_next_blks);
v_row_len 			:= 0;
     FOR I IN 1..idx LOOP
       v_row_len:= v_row_len + (ind_row_hdr + ind_tab(I).avglen + 						ind_tab(I).num_cols);
     END LOOP;
---------------------------------
       v_rows_per_block := TRUNC(blk_size/v_row_len);
       v_rows_possible := TRUNC(v_rows_per_block * v_next_blks * v_ext_possible);
---------------------------------
     INSERT INTO summary_compute_rows
       VALUES(v_tsname, rec.file_id, (ds_tblocks- ds_ublocks)*blk_size,
              (ds_tblocks- ds_ublocks), v_next_ext,v_rows_possible,
              SYSDATE,0,0);
     COMMIT;
     v_rows_possible :=0;
    END IF;
   END LOOP;
---------------------------------------------------------------------------------  
END proc_compute_index;
END pkg_compute_rows;
---------------------------------------------------------------------------------


This is a copy of an article published @ http://www.ioug.org/