Libérer de l'espace dans les fichiers de bases de données Oracle

Les fichiers de bases de données Oracle s’agrandissent automatiquement tant que la taille maximale n’a pas été atteinte (et si l’auto extension en activée).
Cependant, ce n’est pas la même chose dans l’autre sens. En effet, si vous supprimez des données, la taille sur le disque physique restera inchangée.
Voici un jeu de test qui présente ce scénario, et qui va permettre de récupérer l’espace « perdue » lors de la suppression de données :
(Pré requis pour le test : le tablespace est  en ASSM et non en MANUAL (sinon le shrink n’est pas possible) être en version 10 ou supérieure)
Création d’un tablespace, d’un utilisateur et d’une table.

create tablespace TBS_TEST_SIZE datafile 'C:\TBS_TEST_SIZE.dbf' size 1M autoextend on maxsize 10G SEGMENT SPACE MANAGEMENT AUTO; 
create user MY_USER identified by "ORACLE" default tablespace TBS_TEST_SIZE;
grant connect to MY_USER;
grant resource to MY_USER;
grant unlimited tablespace to MY_USER;
create table MY_USER.TEST_TABLE_SIZE (col1 VARCHAR2(250), col2 NUMBER);

Vérification de l’espace occupé dans le tablespace avant génération des données :

SELECT dbasegment.segment_libre AS "Espace Utilise en Mo",
   (SELECT ROUND(SUM(bytes)/1024/1024) Mo
      FROM dba_free_space
      WHERE tablespace_name = 'TBS_TEST_SIZE'
   ) AS "Espace libre en Mo"
FROM
   (SELECT ROUND(SUM(bytes)/1024/1024) segment_libre
      FROM dba_segments
      WHERE tablespace_name = 'TBS_TEST_SIZE'
   ) dbasegment;
Espace Utilise en Mo   Espace libre en Mo
---------------------- ----------------------
0                      1

Insertion de près de 25Mo de données physique :

DECLARE
   compteur NUMBER := 0;
BEGIN
  WHILE compteur < 1000000 LOOP
     compteur:=compteur+1;
     insert into MY_USER.TEST_TABLE_SIZE values ('Jeu de test',compteur);
  END LOOP;
COMMIT;
END;
/

Vérification de l’espace occupé dans le tablespace après génération des données

SELECT dbasegment.segment_libre AS "Espace Utilise en Mo",
   (SELECT ROUND(SUM(bytes)/1024/1024) Mo
      FROM dba_free_space
      WHERE tablespace_name = 'TBS_TEST_SIZE'
   ) AS "Espace libre en Mo"
FROM
   (SELECT ROUND(SUM(bytes)/1024/1024) segment_libre
      FROM dba_segments
      WHERE tablespace_name = 'TBS_TEST_SIZE'
   ) dbasegment;
Espace Utilise en Mo   Espace libre en Mo
---------------------- ----------------------
26                     2

Taille sur le disque :

C:\Users\BSO>dir C:\TBS_TEST_SIZE.DBF
14/03/2014 12:09 28 385 280 TBS_TEST_SIZE.DBF
==> 27 Mo

Maintenant, vidons la table complètement (Une table qui a subit un cycle de vie normal (suppression modification ajout…) peut aussi bien faire l’affaire).
Pour ça, nous avons 2 méthodes, un DELETE ou un TRUNCATE.

  • Si vous utilisez un TRUNCATE (et que vous êtes en 11.2), vous pouvez directement retailler le fichier de données.
  • Si vous utilisez un DELETE, il faudra alors effectuer un SHRINK de la table.

En effet, le TRUNCATE en 11.2 a évolué, il récupère automatiquement l’espace libre dans le tablespace. Alors que le DELETE ne récupère pas cet espace directement.
Démonstration :

  • Truncate
TRUNCATE TABLE MY_USER.TEST_TABLE_SIZE;
table MY_USER.TEST_TABLE_SIZE vidé(e).
Espace Utilise en Mo   Espace libre en Mo
---------------------- ----------------------
0                      28
  • Delete
DELETE FROM MY_USER.TEST_TABLE_SIZE;
COMMIT;
Espace Utilise en Mo    Espace libre en Mo
---------------------- ----------------------
26                      2

Dans le cas du DELETE il faudra effectuer un SHRINK de la table.
Le SHRINK va générer du trafic sur la base et s’il s’agit du grosse table, ça risque de prendre du temps (ce n’est pas vraiment une référence, mais pour info, sur ma XE pour récupérer 26Mo, il aura fallu moins de 5 secondes).

alter table MY_USER.TEST_TABLE_SIZE enable row movement;
alter table MY_USER.TEST_TABLE_SIZE shrink space;
alter table MY_USER.TEST_TABLE_SIZE disable row movement;
Espace Utilise en Mo   Espace libre en Mo
---------------------- ----------------------
0                      28

/!\ Attention /!\
Avant d’utiliser le SHRINK, vérifier certains « pre requis » sur la doc Oracle concernant l’utilisation du shrink : http://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_3001.htm#i2192484
Dès que vous avez de l’espace libre dans un fichier de données, vous pourrez redimensionner le fichier.
Vous pouvez utiliser la requête ci dessous pour connaitre la taille minimale que vous pouvez attribuer à votre fichier de données :

set serveroutput on;
DECLARE
   v_stmt VARCHAR2(500);
   tbs VARCHAR2(50) := 'MON_TABLESPACE';
   CURSOR c1 IS
      SELECT file_id FROM dba_data_files WHERE tablespace_name=tbs;
BEGIN
  FOR line IN c1 LOOP SELECT 'ALTER DATABASE DATAFILE ' || '''' ||
    D.file_name || '''' || ' RESIZE ' ||
    NVL(CEIL(D.bytes/1024/1024 - take_back.take_back_mb), D.bytes/1024/1024) || 'M;' SQL
    INTO v_stmt FROM dba_data_files D, (SELECT SUM (BYTES) / 1024 / 1024 take_back_mb
    FROM dba_free_space
    WHERE tablespace_name = tbs
    AND file_id = line.file_id
    AND block_id >=
    NVL ((SELECT (block_id + (BYTES / 32768))
    FROM dba_extents
    WHERE block_id =
    (SELECT MAX (block_id)
      FROM dba_extents
      WHERE file_id = line.file_id
      AND tablespace_name = tbs)
      AND file_id = line.file_id
      AND tablespace_name = tbs),
     0
    )) take_back
    WHERE D.file_id =line.file_id; DBMS_OUTPUT.PUT_LINE(v_stmt);
 END LOOP;
END;
/
bloc anonyme terminé
ALTER DATABASE DATAFILE 'C:\TBS_TEST_SIZE.DBF' RESIZE 1M;

Il n’y a plus qu’à exécuter le ALTER DATABASE :

ALTER DATABASE DATAFILE 'C:\TBS_TEST_SIZE.DBF' RESIZE 1M;
database datafile 'C:\TBS_TEST_SIZE.DBF' modifié(e).
Espace Utilise en Mo   Espace libre en Mo
---------------------- ----------------------
0                      1

Vérification de la taille du fichier de données sur le disque :

C:\Users\BSO>dir C:\TBS_TEST_SIZE.DBF
14/03/2014 13:06 1 056 768 TBS_TEST_SIZE.DBF

Mais comment peut on savoir quel objet nécessite un shrink ?
En utilisant une requête de ce type :

set serveroutput on
declare
 unf number;
 unfb number;
 fs1 number;
 fs1b number;
 fs2 number;
 fs2b number;
 fs3 number;
 fs3b number;
 fs4 number;
 fs4b number;
 full number;
 fullb number;
begin
   dbms_space.space_usage('MY_USER','TEST_TABLE_SIZE','TABLE',unf,unfb,fs1,fs1b,fs2,fs2b,fs3,fs3b,fs4,fs4b,full,fullb);
   dbms_output.put_line('Nombre total de blocks non formates: '||unf);
   dbms_output.put_line('Nombre de blocks qui ont entre 0 et 25% d espace libre: '||fs1);
   dbms_output.put_line('Nombre de blocks qui ont entre 25 to 50% d espace libre: '||fs2);
   dbms_output.put_line('Nombre de blocks qui ont entre 50 to 75% d espace libre: '||fs3);
   dbms_output.put_line('Nombre de blocks qui ont entre 75 to 100% d espace libre: '||fs4);
   dbms_output.put_line('Nombre de blocks plein dans le segment : '||full);
end;
/

En ajoutant un curseur, on peut par exemple faire une boucle pour interroger tous les objets d’un schéma / d’un tablespace etc…