Panorama: Determining candidates for storage reorganization in Oracle-DB

Knowing that Oracle-DB does not automatically release storage below the high water mark it would be necessary to do it yourself.
Usually you may scan your database from time to time to identify tables, indexes or lobs where reorganization of these objects can make significant amount of storage in tablespace available for other objects.

The PL/SQL package DBMS_SPACE allows consideration of free space in DB blocks for an object.
This information can be used to forecast the effect of object reorganization.

Unfortunately DBMS_SPACE.Space_Usage takes longer time for evaluation of larger objects, so it is quite expensive to scan a whole schema or system using DBMS_SPACE.

Following I'll show how the performance analysis tool Panorama handles determining candidates for reorganization.

To determine candidates for deeper inspection Panorama uses a simple calculation based on avg. row-length, Pct_Free and Ini_Trans to list suspected objects that are worth to reorganize.
Sorting this list descending by "MBytes unused" gives you a hit-list for reorganization.








Columns "% unused" and "MBytes unused" give a fuzzy view to possible potential for reorganization (1557 MB for the selected table).

At single object level Panorama then allows detailed object evaluation with DBMS_SPACE.Space_Usage by click in one of this two columns.




The 4 slots in result of DBMS_SPACE.Space_Usage (0..25, 25..50, 50..75 and 75..100% free space in blocks) allow a more detailed forecast of possible reorganization effect.

Based on this 4 slots for free space in DB-blocks is an approximate calculation of unused space im MBytes (column "Approx. unused MBytes").













Assuming the worst case that PCT_FREE is already used for content expansion the column "Freeable MBytes" show the possible storage decrease after reorganization of this object (with renewed allocation of PCT_FREE).



This check with DBMS_SPACE is also available by click at detail views for tables, indexes and LOBs.



For Securefile-LOBs the result of DBMS_SPACE.Space_Usage is different due to their special structure.


The following SQL statement allows determination of candidates by hand in a similar way like Panorama does in the first step. Column "MB_Unused" shows the amount of storage that can be released by reorganizing objects.
  SELECT /* Panorama-Tool Ramm */
    RowNum,
    CASE WHEN Segment_Name LIKE 'SYS_LOB%' THEN
          Segment_Name||' ('||(SELECT Object_Name FROM DBA_Objects WHERE Object_ID=TO_NUMBER(SUBSTR(Segment_Name, 8, 10)) )||')'
         WHEN Segment_Name LIKE 'SYS_IL%' THEN
          Segment_Name||' ('||(SELECT Object_Name FROM DBA_Objects WHERE Object_ID=TO_NUMBER(SUBSTR(Segment_Name, 7, 10)) )||')'
         WHEN Segment_Name LIKE 'SYS_IOT_OVER%' THEN
          Segment_Name||' ('||(SELECT Object_Name FROM DBA_Objects WHERE Object_ID=TO_NUMBER(SUBSTR(Segment_Name, 14, 10)) )||')'
    ELSE Segment_Name
    END Segment_Name_Qual,
    x.*,
    CASE 
      WHEN Segment_Type LIKE 'TABLE%' AND x.Avg_RowLen > 0 AND x.Num_Rows > 0 AND x.MBytes > 0 AND x.Ini_Trans > 0 THEN 
        x.MBytes - (
                    ( TRUNC(num_rows / ( (TRUNC((blocksize.val - ( (Type_Size.block_common_header_size + Type_Size.unsigned_byte_4_size + Type_Size.transaction_fixed_header_size + Type_Size.data_header_size
                                                                   ) /* block_header_size_without_ini_trans */ + Type_Size.transaction_var_header_size * (x.ini_trans - 1)
                                                                 ) /* block_header_size(ini_trans) */
                                                ) * (1 - x.pct_free/100.0) - Type_Size.table_directory_entry_size
                                               )
                                         ) /* data_size_per_block_without_row_dir */ / (x.avg_rowlen + 5) /* 2 Byte row directory + 3 byte row header */
                                       ) /* rows_per_block */) + 1
                    )/* needed_blocks */  * blocksize.val
                   )/ (1024 * 1024) 
      WHEN Segment_Type LIKE 'INDEX%' AND x.Avg_RowLen > 0 AND x.Num_Rows > 0 AND x.MBytes > 0 AND x.Ini_Trans > 0 AND leaf_blocks > 0 THEN 
        ( TRUNC((blocksize.val - ( (Type_Size.block_common_header_size + Type_Size.unsigned_byte_4_size + Type_Size.transaction_fixed_header_size + Type_Size.data_header_size
                                   ) /* block_header_size_without_ini_trans */ + Type_Size.transaction_var_header_size * (x.ini_trans - 1)
                                 ) /* block_header_size(ini_trans) */
                ) * (1 - x.pct_free/100.0) - Type_Size.table_directory_entry_size
               ) * x.leaf_blocks - (x.num_rows * (x.avg_rowlen + CASE WHEN x.avg_rowlen > 250 THEN 3 ELSE 1 END + rowid_size.val)) /* allocated size in leaf blocks by rows */
        ) / (1024.0 * 1024.0)         
      ELSE NULL 
    END MB_Unused
  FROM (
  SELECT
    Segment_Name,
    Tablespace_Name,
    Count(*) Partition_Count,
    SEGMENT_TYPE,
    Owner,                                                  
    SUM(EXTENTS)                    Used_Ext,               
    SUM(bytes)/(1024*1024)          MBytes,
    SUM(Blocks)                     Blocks,
    MIN(Initial_Extent)/1024        Min_Init_Ext_KB,
    MAX(Initial_Extent)/1024        Max_Init_Ext_KB,
    SUM(Initial_Extent)/1024        Sum_Init_Ext_KB,
    MIN(Next_Extent)/1024           Min_Next_Ext_KB,
    MAX(Next_Extent)/1024           Max_Next_Ext_KB,
    SUM(Next_Extent)/1024           Sum_Next_Ext_KB,
    MIN(Min_Extents)                Min_Min_Exts,
    MAX(Min_Extents)                Max_Min_Exts,
    SUM(Min_Extents)                Sum_Min_Exts,
    MIN(Max_Extents)                Min_Max_Exts,
    MAX(Max_Extents)                Max_Max_Exts,
    SUM(Max_Extents)                Sum_Max_Exts,
    CASE WHEN COUNT(DISTINCT InMemory) = 1 THEN MIN(InMemory) ELSE '<'||COUNT(DISTINCT InMemory)||'>' END InMemory,
    SUM(Num_Rows)                   Num_Rows,
    SUM(Blocks * PCT_Free)  / SUM(Blocks) Pct_Free,     /* weighted value by number of blocks of partition */
    SUM(Blocks * Ini_Trans) / SUM(Blocks) Ini_Trans,    /* weighted value by number of blocks of partition */
    CASE WHEN COUNT(DISTINCT Compression) <= 1 THEN MIN(Compression) ELSE '<several>' END Compression,
    AVG(Avg_Row_Len)                Avg_RowLen,
    SUM(Empty_Blocks)               Empty_Blocks,
    AVG(Avg_Space)                  Avg_Space,
    MIN(Last_Analyzed)              Last_Analyzed,
    MAX(Last_DML_Timestamp)         Last_DML_Timestamp,
    MIN(Created)                    Created,
    MAX(Last_DDL_Time)              Last_DDL_Time,
    MAX(Spec_TS)                    Spec_TS,
    SUM(Leaf_Blocks)                Leaf_Blocks
  FROM (
    /* Views moved to with clause due to performance problems with 18.3 */
    WITH Tab_Modifications AS (SELECT /*+ NO_MERGE MATERIALIZE */ Table_Owner, Table_Name, Partition_Name, SubPartition_Name, Timestamp FROM DBA_Tab_Modifications WHERE Partition_Name IS NULL),
         Segments          AS (SELECT /*+ NO_MERGE MATERIALIZE */ * FROM DBA_Segments s        WHERE s.SEGMENT_TYPE<>'CACHE'),
         Objects           AS (SELECT /*+ NO_MERGE MATERIALIZE */ Owner, Object_Name, SubObject_Name, Created, Last_DDL_Time, Timestamp FROM DBA_Objects),
         Tables            AS (SELECT /*+ NO_MERGE MATERIALIZE */ Owner, Table_Name, Num_Rows, Pct_Free, Ini_Trans, Avg_Row_Len, Empty_Blocks, Avg_Space, Last_Analyzed, Compression, Compress_For FROM DBA_Tables),
         Tab_Partitions    AS (SELECT /*+ NO_MERGE MATERIALIZE */ Table_Owner, Table_Name, Partition_Name, Num_Rows, Pct_Free, Ini_Trans, Avg_Row_Len, Empty_Blocks, Avg_Space, Last_Analyzed, Compression, Compress_For FROM DBA_Tab_Partitions),
         Tab_SubPartitions AS (SELECT /*+ NO_MERGE MATERIALIZE */ Table_Owner, Table_Name, SubPartition_Name, Num_Rows, Pct_Free, Ini_Trans, Avg_Row_Len, Empty_Blocks, Avg_Space, Last_Analyzed, Compression, Compress_For FROM DBA_Tab_SubPartitions),
         Indexes           AS (SELECT /*+ NO_MERGE MATERIALIZE */ Owner, Index_Name, Table_Owner, Table_Name, Index_Type, Num_Rows, Pct_Free, Ini_Trans, Compression, Last_Analyzed, Leaf_Blocks FROM DBA_Indexes),
         Ind_Partitions    AS (SELECT /*+ NO_MERGE MATERIALIZE */ Index_Owner, Index_Name, Partition_Name, Num_Rows, Pct_Free, Ini_Trans, Last_Analyzed, Compression, Leaf_Blocks FROM DBA_Ind_Partitions),
         Ind_SubPartitions AS (SELECT /*+ NO_MERGE MATERIALIZE */ Index_Owner, Index_Name, SubPartition_Name, Num_Rows, Pct_Free, Ini_Trans, Last_Analyzed, Compression, Leaf_Blocks FROM DBA_Ind_SubPartitions),
         Ind_Row_Len       AS (SELECT /*+ NO_MERGE MATERIALIZE */ ic.Index_Owner, ic.Index_Name, SUM(tc.Avg_Col_Len) Avg_Rows_Len
                               FROM   DBA_Ind_Columns ic
                               JOIN   DBA_Tab_Columns tc ON tc.Owner = ic.Table_Owner AND tc.Table_Name = ic.Table_Name AND tc.Column_Name = ic.Column_Name
                               GROUP BY ic.Index_Owner, ic.Index_Name ),
         Tab_Row_Len       AS (SELECT /*+ NO_MERGE MATERIALIZE */ Owner, Table_Name, SUM(Avg_Col_Len) Avg_Rows_Len
                               FROM   DBA_Tab_Columns
                               GROUP BY Owner, Table_Name
                              ),
         Lobs              AS (SELECT /*+ NO_MERGE MATERIALIZE */ Owner, Segment_Name, Compression FROM DBA_Lobs),
         Lob_Partitions    AS (SELECT /*+ NO_MERGE MATERIALIZE */ Table_Owner, Lob_Name, Lob_Partition_Name, Compression FROM DBA_Lob_Partitions),
         Lob_SubPartitions AS (SELECT /*+ NO_MERGE MATERIALIZE */ Table_Owner, Lob_Name, Lob_SubPartition_Name, Compression FROM DBA_Lob_SubPartitions)
    SELECT /*+ ORDERED USE_HASH(s o t tp tsp m i ip isp im l lp lsp) */
           s.Segment_Name,
           s.Partition_Name,                                
           s.Segment_Type,                                  
           s.Tablespace_Name,
           s.Owner,                                         
           s.Extents,                                       
           s.Bytes, s.Blocks,
           s.Initial_Extent, s.Next_Extent, s.Min_Extents, s.Max_Extents,
           o.Created, o.Last_DDL_Time, TO_DATE(o.Timestamp, 'YYYY-MM-DD:HH24:MI:SS') Spec_TS,
           s.InMemory,
           DECODE(s.Segment_Type,                           
             'TABLE',              t.Num_Rows,
             'TABLE PARTITION',    tp.Num_Rows,
             'TABLE SUBPARTITION', tsp.Num_Rows,
             'INDEX',              i.Num_Rows,
             'INDEX PARTITION',    ip.Num_Rows,
             'INDEX SUBPARTITION', isp.Num_Rows,
           NULL) num_rows,
           DECODE(s.Segment_Type,
             'TABLE',              t.Pct_Free,
             'TABLE PARTITION',    tp.Pct_Free,
             'TABLE SUBPARTITION', tsp.Pct_Free,
             'INDEX',              i.Pct_Free,
             'INDEX PARTITION',    ip.Pct_Free,
             'INDEX SUBPARTITION', isp.Pct_Free,
           NULL) Pct_Free,
           DECODE(s.Segment_Type,
             'TABLE',              t.Ini_Trans,
             'TABLE PARTITION',    tp.Ini_Trans,
             'TABLE SUBPARTITION', tsp.Ini_Trans,
             'INDEX',              i.Ini_Trans,
             'INDEX PARTITION',    ip.Ini_Trans,
             'INDEX SUBPARTITION', isp.Ini_Trans,
           NULL) Ini_Trans,
           DECODE(s.Segment_Type,
             'TABLE',              t.Compression  ||CASE WHEN   t.Compression != 'DISABLED' THEN ' ('||  t.Compress_For||')' END,
             'TABLE PARTITION',    tp.Compression ||CASE WHEN  tp.Compression != 'DISABLED' THEN ' ('|| tp.Compress_For||')' END,
             'TABLE SUBPARTITION', tsp.Compression||CASE WHEN tsp.Compression != 'DISABLED' THEN ' ('||tsp.Compress_For||')' END,
             'INDEX',              i.Compression,
             'INDEX PARTITION',    ip.Compression,
             'INDEX SUBPARTITION', isp.Compression,
             'LOBSEGMENT',         l.Compression,
             'LOB PARTITION',      lp.Compression,
             'LOB SUBPARTITION',   lsp.Compression,
           NULL) Compression,
           CASE WHEN s.Segment_Type = 'TABLE'              THEN t.Avg_Row_Len
                WHEN s.Segment_Type = 'TABLE PARTITION'    THEN tp.Avg_Row_Len
                WHEN s.Segment_Type = 'TABLE SUBPARTITION' THEN tsp.Avg_Row_Len
                WHEN s.Segment_Type IN ('INDEX', 'INDEX PARTITION', 'INDEX_SUBPARTITION') AND i.Index_Type = 'NORMAL'    THEN irl.Avg_Rows_Len
                WHEN s.Segment_Type = 'INDEX'              AND i.Index_Type = 'IOT - TOP' THEN trl.Avg_Rows_Len
                WHEN s.Segment_Type = 'INDEX PARTITION'    AND i.Index_Type = 'IOT - TOP' THEN trl.Avg_Rows_Len
                WHEN s.Segment_Type = 'INDEX SUBPARTITION' AND i.Index_Type = 'IOT - TOP' THEN trl.Avg_Rows_Len
           END avg_row_len,
           DECODE(s.Segment_Type,
             'TABLE',              t.Empty_blocks,
             'TABLE PARTITION',    tp.Empty_Blocks,
             'TABLE SUBPARTITION', tsp.Empty_Blocks,
           NULL) empty_blocks,
           DECODE(s.Segment_Type,
             'TABLE',              t.Avg_Space,
             'TABLE PARTITION',    tp.Avg_Space,
             'TABLE SUBPARTITION', tsp.Avg_Space,
           NULL) Avg_Space,
           DECODE(s.Segment_Type,                           
             'TABLE',              t.Last_analyzed,
             'TABLE PARTITION',    tp.Last_analyzed,
             'TABLE SUBPARTITION', tsp.Last_analyzed,
             'INDEX',              i.Last_analyzed,
             'INDEX PARTITION',    ip.Last_analyzed,
             'INDEX SUBPARTITION', isp.Last_analyzed,
           NULL) Last_Analyzed,
           DECODE(s.Segment_Type,
             'TABLE',              m.Timestamp,
             'TABLE PARTITION',    m.Timestamp,
             'TABLE SUBPARTITION', m.Timestamp,
             'INDEX',              im.Timestamp,
             'INDEX PARTITION',    im.Timestamp,
             'INDEX SUBPARTITION', im.Timestamp,
           NULL) Last_DML_Timestamp,
           DECODE(s.Segment_Type,
             'TABLE',              NULL,
             'TABLE PARTITION',    NULL,
             'TABLE SUBPARTITION', NULL,
             'INDEX',              i.Leaf_Blocks,
             'INDEX PARTITION',    ip.Leaf_Blocks,
             'INDEX SUBPARTITION', isp.Leaf_Blocks,
           NULL) Leaf_Blocks
    FROM Segments s
    LEFT OUTER JOIN Objects o                 ON o.Owner         = s.Owner       AND o.Object_Name          = s.Segment_name   AND NVL(s.Partition_Name, '-1') = NVL(o.SubObject_Name, '-1')
    LEFT OUTER JOIN Tables t                  ON t.Owner         = s.Owner       AND t.Table_Name           = s.segment_name
    LEFT OUTER JOIN Tab_Partitions tp         ON tp.Table_Owner  = s.Owner       AND tp.Table_Name          = s.segment_name   AND tp.Partition_Name        = s.Partition_Name
    LEFT OUTER JOIN Tab_SubPartitions tsp     ON tsp.Table_Owner = s.Owner       AND tsp.Table_Name         = s.segment_name   AND tsp.SubPartition_Name    = s.Partition_Name
    LEFT OUTER JOIN Tab_Modifications m       ON m.Table_Owner = t.Owner         AND m.Table_Name           = t.Table_Name     AND m.Partition_Name IS NULL    -- Summe der Partitionen wird noch einmal als Einzel-Zeile ausgewiesen
    LEFT OUTER JOIN Indexes i                 ON i.Owner         = s.Owner       AND i.Index_Name           = s.segment_name
    LEFT OUTER JOIN Ind_Partitions ip         ON ip.Index_Owner  = s.Owner       AND ip.Index_Name          = s.segment_name   AND ip.Partition_Name        = s.Partition_Name
    LEFT OUTER JOIN Ind_SubPartitions isp     ON isp.Index_Owner = s.Owner       AND isp.Index_Name         = s.segment_name   AND isp.SubPartition_Name    = s.Partition_Name
    LEFT OUTER JOIN Ind_Row_Len irl           ON irl.Index_Owner = i.Owner       AND irl.Index_Name         = i.Index_Name
    LEFT OUTER JOIN Tab_Row_Len trl           ON trl.Owner       = i.Table_Owner AND trl.Table_Name         = i.Table_Name     /* Sum of column sizes for IOTs */
    LEFT OUTER JOIN Tables it                 ON it.Owner        = i.Table_Owner AND it.Table_Name          = i.Table_Name
    LEFT OUTER JOIN Tab_Modifications im      ON im.Table_Owner  = it.Owner      AND im.Table_Name          = it.Table_Name    AND im.Partition_Name IS NULL    -- Summe der Partitionen wird noch einmal als Einzel-Zeile ausgewiesen
    LEFT OUTER JOIN Lobs l                    ON l.Owner         = s.Owner       AND l.Segment_Name         = s.Segment_Name
    LEFT OUTER JOIN Lob_Partitions lp         ON lp.Table_Owner  = s.Owner       AND lp.Lob_Name            = s.Segment_Name   AND lp.Lob_Partition_Name     = s.Partition_Name
    LEFT OUTER JOIN Lob_SubPartitions lsp     ON lsp.Table_Owner = s.Owner       AND lsp.Lob_Name           = s.Segment_Name   AND lsp.Lob_SubPartition_Name = s.Partition_Name
   )
  GROUP BY Owner, Segment_Name, Tablespace_Name, Segment_Type 
  ) x
  CROSS JOIN (SELECT /*+ NO_MERGE */ TO_NUMBER(Value) Val FROM v$parameter WHERE UPPER(Name) = 'DB_BLOCK_SIZE') blocksize
  CROSS JOIN (SELECT /*+ NO_MERGE */
                     MAX(CASE WHEN Type = 'KDBT'  THEN Type_Size END) Table_Directory_Entry_Size,
                     MAX(CASE WHEN Type = 'KTBIT' THEN Type_Size END) Transaction_Var_Header_Size /* Size of ITL entry */,
                     MAX(CASE WHEN Type = 'KCBH'  THEN Type_Size END) Block_Common_Header_Size,
                     MAX(CASE WHEN Type = 'UB4'   THEN Type_Size END) Unsigned_Byte_4_Size,
                     MAX(CASE WHEN Type = 'KTBBH' THEN Type_Size END) Transaction_Fixed_Header_Size,
                     MAX(CASE WHEN Type = 'KDBH'  THEN Type_Size END) Data_Header_Size
              FROM   v$Type_Size 
             ) Type_Size
  CROSS JOIN (SELECT VSIZE(rowid) Val FROM Dual) RowID_Size
  ORDER BY x.MBytes DESC
  ;

Comments

Popular posts from this blog

Common pitfalls using SQL*Net via Firewalls

Oracle-DB: How to check for appropriate sequence caching

Oracle-DB: Link between audit trail and active session history