When discussing 23ai features with my customers, I’ve noticed that DBAs show particular interest in the newly introduced tablespace shrink functionality for Oracle tablespaces. They are eager to learn about the capabalities and when and how they can be used.

A first use case is illustrated in the following scenario:
Imagine that database objects have been removed, resulting in unused space within the tablespace. In such situations, you may wish to reduce the actual data file size to free up space on your disks. With 23ai, you can now reorganize tablespaces both SMALLFILE oder BIGFILE tablespaces while resizing the corresponding data file(s) This is done by moving objects within the tablespace, which can be done either online or offline.

Just a reminder: A bigfile tablespace is defined as a tablespace that has one large data file(up to 4G blocks). Traditional smallfile tablespaces, in contrast, can contain multiple data files, but the files cannot be as large. And starting with Oracle Database 23ai, the BIGFILE functionality is the default for SYSAUX, SYSTEM, and USER tablespaces.

You can use the SHRINK_TABLESPACE procedure from the DBMS_SPACE package to resize both BIGFILE and SMALLFILE tablespaces or analyze them prior to resizing. This procedure utilizes online DDL to reorganize the objects within the data file, and after the objects have been successfully reorganized, it executes a resize of the data file. 

The usage is quite simple: You only need to specify the tablespace name and the shrink mode such as 

  • TS_MODE_ANALYZE for an analyze mode

  • TS_MODE_SHRINK, the default

  • TS_MODE_SHRINK_FORCE, this will move objects online by default, but if the online move fails, it will attempt to move them offline.

The following code snippet illustrates the arguments of the new procedure. A complete description can be found in the PL/SQL Packages and Types Reference DBMS_SPACE.

PROCEDURE SHRINK_TABLESPACE
	 Argument Name                  Type                    In/Out Default?
	 ------------------------------ ----------------------- ------ --------
	 TS_NAME                        VARCHAR2                IN
	 SHRINK_MODE                    NUMBER                  IN     DEFAULT
	 TARGET_SIZE                    NUMBER                  IN     DEFAULT
	 SHRINK_RESULT                  CLOB                    OUT

The following code snippet shows a first example.The end result is a CLOB and you may view it e.g. in SQL*Plus.

SQL> set long 10000
SQL> variable result clob
SQL> col result format a200
SQL> execute DBMS_SPACE.SHRINK_TABLESPACE('RECLAIMTS', SHRINK_MODE => DBMS_SPACE.TS_MODE_ANALYZE, SHRINK_RESULT => :result);
PL/SQL procedure successfully completed.

SQL> print result 
RESULT 
---------------------------------------------------------------------------------- 
Movable Objects: 
1. { RECLAIM_USER.T2 | type: TABLE | blocks: 20480 | tablespace_name: RECLAIMTS } 
2. { RECLAIM_USER.T2_PK | type: INDEX | blocks: 32 | tablespace_name: RECLAIMTS } 
Total Movable Objects: 2 
Total Movable Size(GB): .16 
Original Datafile Size(GB): 1.01 
Suggested Target Size(GB): .38 
Process Time: +00 00:00:01.669704

You may also find useful information in the Database Administrator’s Guide in chapter “Shrinking a bigfile tablespace“. In addition Tim Hall described the feature in his posting Bigfile Tablespace Shrink in Oracle Database 23ai
Using Tim’s simple setup as a guide, let’s look “under the hook” for additional context and examples. Data dictionary views such as DBA_FREE_SPACE and USER_SEGMENTS and the “Tablespace Content” View from Oracle Enterprise Manager Cloud control will be used to demonstrate the process. We’ll wrap up the post with some more details about the feature based on my initial testing experience.

Setup and example

You can also repeat the following example in the available 23ai database FREE version or in Oracle 23ai Database Cloud, on Exadata or ODA. 

Let’s connect to one PDB and create a BIGFILE tablespace named RECLAIMTS. The new user RECLAIM_USER will execute the user commands.

SQL> drop tablespace reclaimts including contents and datafiles;
Tablespace dropped.

SQL> drop user reclaim_user;
User dropped.

SQL> create bigfile tablespace reclaimts datafile size 10M autoextend on next 5M;
Tablespace created.
SQL> create user reclaim_user identified by reclaim default tablespace reclaimts quota unlimited on reclaimts;
User created.
SQL> grant connect, resource, dba to reclaim_user;
Grant succeeded.

Connect to user RECLAIM_USER and create the following 2 tables. Then insert some rows.

SQL> connect reclaim_user/reclaim@<servicename>
SQL> create table t1 (
     id   number,
     col1 varchar2(4000),
     col2 varchar2(4000),
     constraint t1_pk primary key (id));

SQL> create table t2 (
     id   number,
     col1 varchar2(4000),
     col2 varchar2(4000),
     constraint t2_pk primary key (id));

SQL> insert /*+append*/ into t1 select rownum, rpad('x', 4000, 'x'), rpad('x', 4000, 'x')
     from dual connect by level <= 10000;
10000 rows created.

SQL> commit;
SQL> insert /*+append*/ into t2 select rownum, rpad('x', 4000, 'x'), rpad('x', 4000, 'x')
     from dual connect by level <= 10000;
10000 rows created.

SQL> commit;

Let’s generate some statistics.

SQL> exec dbms_stats.gather_schema_stats('RECLAIM_USER');
PL/SQL procedure successfully completed.

Check the size now in DBA_DATA_FILES and USER_SEGMENTS.

SQL> col tablespace_name format a25
SQl> select tablespace_name, blocks, bytes/1024/1024 as size_mb
     from dba_data_files
     where tablespace_name = 'RECLAIMTS'; 

TABLESPACE_NAME               BLOCKS    SIZE_MB
------------------------- ---------- ----------
RECLAIMTS                     132480       1035

SQL> col segment_name format a10
SQL> select segment_name, segment_type, bytes/1024 size_kb 
     from user_segments;

SEGMENT_NA SEGMENT_TY    SIZE_KB
---------- ---------- ----------
T1         TABLE          163840
T2         TABLE          163840
T1_PK      INDEX             256
T2_PK      INDEX             256

Let’s truncate table T1 and collect statistics:

SQL> truncate table t1;
Table truncated.

SQL> exec dbms_stats.gather_schema_stats('RECLAIM_USER');
PL/SQL procedure successfully completed.

Let’s examine the space – first the segment space. Obviously T1 is truncated.

SQL> col segment_name format a10
SQL> select segment_name, segment_type, bytes/1024 size_kb 
     from user_segments;

SEGMENT_NA SEGMENT_TY    SIZE_KB
---------- ---------- ----------
T1         TABLE              64
T1_PK      INDEX              64
T2         TABLE          163840
T2_PK      INDEX             256

Let’s now investigate the space within the tablespace. Five free fragments are visible, and these could be potential locations for moving objects. The largest one is around 646 MB in size. 

SQL> select ts.tablespace_name, sum(nvl(fs.bytes,0))/1024/1024 as MB_FREE,
     count(*) as FRAGMENTS, max(nvl(fs.bytes,0))/1024/1024 as BIGGEST
     from user_free_space fs, user_tablespaces ts
     where fs.tablespace_name(+) = ts.tablespace_name and ts.tablespace_name='RECLAIMTS'
     group by ts.tablespace_name;  

TABLESPACE_NAME              MB_FREE  FRAGMENTS    BIGGEST
------------------------- ---------- ---------- ----------
RECLAIMTS                   806.5625          5        646

To get the full picture, we could use the following query to show the mapping of the segments in the tablespace.

SQL> col segment_type format a10
SQL> set pagesize 100
SQl> select block_id begin_block, block_id + blocks - 1 end_block, blocks, bytes/1024/1024 MB,
     segment_name, segment_type
     from dba_extents where tablespace_name = 'RECLAIMTS'
     union all
     select block_id begin_block, block_id + blocks - 1 end_block, blocks, bytes/1024/1024 MB,
     'free' segment_name, null segment_type
     from dba_free_space
     where tablespace_name = 'RECLAIMTS'
     order by 1 desc;

BEGIN_BLOCK  END_BLOCK     BLOCKS         MB SEGMENT_NA SEGMENT_TY
----------- ---------- ---------- ---------- ---------- ----------
      49792     132479      82688        646 free    <== first free space starting at Block id 49792
      48768      49791       1024          8 T2         TABLE
      47744      48767       1024          8 T2         TABLE
      46720      47743       1024          8 T2         TABLE
      45696      46719       1024          8 T2         TABLE
      44672      45695       1024          8 T2         TABLE
      43648      44671       1024          8 T2         TABLE
      42624      43647       1024          8 T2         TABLE
      41600      42623       1024          8 T2         TABLE
      40576      41599       1024          8 T2         TABLE
      39552      40575       1024          8 T2         TABLE
      ...      
      29312      29319          8      .0625 T2         TABLE
       9216      29311      20096        157 free   <== another notable free space starting with 9216
       1024       1279        256          2 free
       1016       1023          8      .0625 T2         TABLE
       1008       1015          8      .0625 T2         TABLE
       ...

Let’s use Oracle Enterprise Manager Cloud Control to display the tablespace content. Go to Administration => Storage=> Tablespace, click on tablespace RECLAIMTS and choose “Show Tablespace Content” in the menu on the right. Then you will get a new page with the Extent Map you can expand. Notice the free space is always colored in green.

Before Shrink - Extent Map

Shrinking Tablespace

What is the possible outcome of the shrink run now? You can get a list of objects by checking SHRINK_RESULT returned. It will list the objects that are not supported such as cluster tables, some advanced queuing tables etc.

SQL> set linesize window
SQL> variable result clob
SQL> col result format a200
SQL> execute DBMS_SPACE.SHRINK_TABLESPACE('RECLAIMTS', SHRINK_MODE => DBMS_SPACE.TS_MODE_ANALYZE, SHRINK_RESULT => :result);
PL/SQL procedure successfully completed.
SQL> print result
RESULT
----------------------------------------------------------------------------------
Movable Objects:
1. { RECLAIM_USER.T2 | type: TABLE | blocks: 20480 | tablespace_name: RECLAIMTS }
2. { RECLAIM_USER.T2_PK | type: INDEX | blocks: 32 | tablespace_name: RECLAIMTS }
Total Movable Objects: 2
Total Movable Size(GB): .16
Original Datafile Size(GB): 1.01
Suggested Target Size(GB): .38
Process Time: +00 00:00:01.669704

The table T2 and the index T2_PK are the two objects that can be moved. The data file size should then be approximately 0.38 GB after that. When checking out the Extent Map above, we could see that there are at least two bigger empty pieces which could be used for that – one starts with block id 9216 and one with block id 49792.

So let’s shrink the tablespace with SHRINK_TABLESPACE and the default setting to achieve the maximum possible shrink result. You can use a specific TARGET_SIZE to reach your own shrink size goal (see example at the end of the posting).

SQL> set serveroutput on
SQL> execute dbms_space.shrink_tablespace('RECLAIMTS');
-------------------SHRINK RESULT-------------------
Total Moved Objects: 2
Total Moved Size(GB): .16
Original Datafile Size(GB): 1.01
New Datafile Size(GB): .39
Process Time: +00 00:00:10.611905
PL/SQL procedure successfully completed.

Let’s do the same checks as before. There are still 5 fragments but the free space is less than before.

SQL> select ts.tablespace_name, sum(nvl(fs.bytes,0))/1024/1024 as MB_FREE,
     count(*) as FRAGMENTS, max(nvl(fs.bytes,0))/1024/1024 as BIGGEST
     from user_free_space fs, user_tablespaces ts
     where fs.tablespace_name(+) = ts.tablespace_name and ts.tablespace_name='RECLAIMTS'
     group by ts.tablespace_name;   

TABLESPACE_NAME              MB_FREE  FRAGMENTS    BIGGEST
------------------------- ---------- ---------- ----------
RECLAIMTS                   168.5625          5        167

Let’s get some more details.

SQL> col segment_type format a10
SQL> set pagesize 100
SQl> select block_id begin_block, block_id + blocks - 1 end_block, blocks, bytes/1024/1024 MB,
     segment_name, segment_type
     from dba_extents
     where tablespace_name = 'RECLAIMTS'    
     union all
     select block_id begin_block, block_id + blocks - 1 end_block, blocks, bytes/1024/1024 MB,
     'free' segment_name, null segment_type
     from dba_free_space
     where tablespace_name = 'RECLAIMTS'
     order by 1 desc;

BEGIN_BLOCK  END_BLOCK     BLOCKS         MB SEGMENT_NA SEGMENT_TY
----------- ---------- ---------- ---------- ---------- ----------
      49792      50815       1024          8 T2         TABLE
      28416      49791      21376        167 free        <== could be used for a segment move
      27392      28415       1024          8 T2         TABLE
      26368      27391       1024          8 T2         TABLE
      25344      26367       1024          8 T2         TABLE
      24320      25343       1024          8 T2         TABLE
      23296      24319       1024          8 T2         TABLE
      22272      23295       1024          8 T2         TABLE
      21248      22271       1024          8 T2         TABLE
      20224      21247       1024          8 T2         TABLE
      19200      20223       1024          8 T2         TABLE
...
       1056       1151         96        .75 free        <= free space
       1048       1055          8      .0625 T2_PK      INDEX
       1040       1047          8      .0625 T2_PK      INDEX
...

Let’s check the Extent Map.

After Shrink - Extent Map

 

So let’s analyze again. There is still some possible savings as we can see from the Extent Map above and the analyze command confirms that.

SQL> variable result clob
SQL> col result format a200
SQL> execute DBMS_SPACE.SHRINK_TABLESPACE('RECLAIMTS', SHRINK_MODE => DBMS_SPACE.TS_MODE_ANALYZE, SHRINK_RESULT => :result);
SQL> print result
RESULT
---------------------------------------------------------------------------------
Movable Objects:
1. { RECLAIM_USER.T2 | type: TABLE | blocks: 20480 | tablespace_name: RECLAIMTS }
2. { RECLAIM_USER.T2_PK | type: INDEX | blocks: 32 | tablespace_name: RECLAIMTS }
Total Movable Objects: 2
Total Movable Size(GB): .16
Original Datafile Size(GB): .39
Suggested Target Size(GB): .38
Process Time: +00 00:00:01.674684 

Therefore let’s shrink again.

SQL> set serveroutput on;
SQL> execute dbms_space.shrink_tablespace('RECLAIMTS');
-------------------SHRINK RESULT-------------------
Total Moved Objects: 2
Total Moved Size(GB): .16
Original Datafile Size(GB): .39
New Datafile Size(GB): .37
Process Time: +00 00:00:11.726847
PL/SQL procedure successfully completed.

Let’s check the result.

SQL> select ts.tablespace_name, sum(nvl(fs.bytes,0))/1024/1024 as MB_FREE,
            count(*) as FRAGMENTS, max(nvl(fs.bytes,0))/1024/1024 as BIGGEST
    from user_free_space fs, user_tablespaces ts
    where fs.tablespace_name(+) = ts.tablespace_name and ts.tablespace_name='RECLAIMTS'
    group by ts.tablespace_name;

TABLESPACE_NAME         MB_FREE  FRAGMENTS    BIGGEST
-------------------- ---------- ---------- ----------
RECLAIMTS              152.5625          5        150 

Let’s check the Extent Map. We can see that fragments are moved again.

SQL> select block_id begin_block, block_id + blocks - 1 end_block, blocks, bytes/1024/1024 MB,      
     segment_name, segment_type 
     from dba_extents where tablespace_name = 'RECLAIMTS' 
     union all 
     select block_id begin_block, block_id + blocks - 1 end_block, blocks, bytes/1024/1024 MB,      
     'free' segment_name, null segment_type 
     from dba_free_space where tablespace_name = 'RECLAIMTS' order by 1 desc;

BEGIN_BLOCK  END_BLOCK     BLOCKS         MB SEGMENT_NA SEGMENT_TYPE 
----------- ---------- ---------- ---------- ---------- --------------------       
      47744      48767       1024          8 T2         TABLE       
      46720      47743       1024          8 T2         TABLE       
      45696      46719       1024          8 T2         TABLE       
      44672      45695       1024          8 T2         TABLE       
      43648      44671       1024          8 T2         TABLE       
      42624      43647       1024          8 T2         TABLE       
      41600      42623       1024          8 T2         TABLE 
... 
      28416      28543        128          1 T2         TABLE        
       9216      28415      19200        150 free <= first free space...

And the Extent Map in Cloud Control. 

Shrink 2 - Extent Map

Summary and Additional Considerations

With the new procedure SHRINK_TABLESPACE you can reorganize tablespaces and resize the associated data file(s) to its smallest possible size by moving objects within the tablespace, either online or offline. You only need to use one command for this. Please note: When shrinking a smallfile tablespace, the data file sizes may increase or decrease depending on the size of the objects placed in the data files.

 As mentioned above you can use DBMS_SPACE.SHRINK_TABLESPACE in different ways:

  • Analyze a tablespace for a suggested target size.
  • Resize a tablespace to its minimum possible size.
  • Attempt to resize a tablespace to a specified target size.

Before trying to shrink, you should use the analyze command to find out the potential of a shrink operation. It will take much less time that actually shrinking it. The result of this analysis contains useful information including a list of movable objects, the list of unsupported objects, the total size of movable objects in the tablespace, and the suggested target size for the tablespace.

The shrink operation itself will reorganize the tablespace which will take some time. If you want to monitor long-running executions you may use V$SESSION_LONGOPS. OPNAME “Tablespace Shrink” defines our tablespace shrink operation. 

 SQL> select opname, sofar, time_remaining, 
             to_char(start_time,'dd.mm.yyyy-HH24:MI:SS') StartTime, message
      from v$session_longops 
      where time_remaining>=0 and opname like '%Table%' order by 4;

OPNAME
--------------------------------------------------------------------------------------------------------
     SOFAR TIME_REMAINING STARTTIME
---------- -------------- --------------------
MESSAGE
--------------------------------------------------------------------------------------------------------
Online Move Table
         9              0 22.09.2024-19:44:04
Online Move Table: T2 : 9 out of 9 Steps done

Transform Heap Mapping Table to IOT Mapping Table
         1              0 22.09.2024-19:44:10
Transform Heap Mapping Table to IOT Mapping Table: T2 : 1 out of 1 Step done

Wait for DMLs - Swap dobj# - Move Table Online
         1              0 22.09.2024-19:44:12
Wait for DMLs - Swap dobj# - Move Table Online: T2 : 1 out of 1 Step done

Tablespace Shrink
         1              0 22.09.2024-19:44:14
Tablespace Shrink: segment : 1 out of 1 segments done

Tablespace Shrink
         1              0 22.09.2024-19:44:19
Tablespace Shrink: segment : 1 out of 1 segments done

Here is an example how to use SHRINK_TABLESPACE with a specific TARGET_SIZE in bytes. It will always use and adjust the target size that is required to complete the operation successfully.  

SQL> set serveroutput on
SQL> execute dbms_space.shrink_tablespace('RECLAIMTS', target_size => 500000000);
Specified target size is too small, adjusting it to 1395654656
-------------------SHRINK RESULT-------------------
Total Moved Objects: 1
Total Moved Size(GB): 0
Original Datafile Size(GB): 2.01
New Datafile Size(GB): 1.3
Process Time: +00 00:00:06.728767

PL/SQL procedure successfully completed.

Partially failing DBMS_SPACE.SHRINK_TABLESPACE is possible. Nevertheless, if a move DDL fails, the command always reports the cause. The command still resizes the data file to a smaller size if it already successfully moved some objects.

For instance, in the following example it is not possible to move table T2 because a lock is held in another session.

SQL> set serveroutput on
SQL> execute dbms_space.shrink_tablespace('RECLAIMTS'); 
Procedure exited because it can't move an object: Failed Move DDL: alter table "RECLAIM_USER"."T2" move online 
Failed Reason: ORA-00054: Failed to acquire a lock (Type: "TM", Name: "DML", Description: "Synchronizes accesses to an object") 
because it is currently held by another session. The resource being locked can be identified by 132917 ("Table") and 0 ("operation")

Another option is DBMS_SPACE.TS_MODE_SHRINK_FORCE, which is useful when moving an object offline is possible but not when moving it online. DMLs and queries will be blocked by an offline move. Because some objects cannot be moved online or offline, this mode won’t always work.

In any case, my initial testing experience showed that using the Tablespace Shrink feature during a maintenance window produced the best results for me. Therefore, I would advise using a dedicated window for that.

Links to more information