Monday, January 19, 2026

Drop

 


The scripts below are explicitly designed with Heavy Parallelism using DBMS_SCHEDULER.

  • Tablespace Drops: All 8 tablespaces are dropped simultaneously (8 concurrent threads).

  • Object Cleanup: All 7 schemas are scrubbed simultaneously (7 concurrent threads).

Here is your Consolidated, Parallel Destruction Runbook.

Parallel Destruction Runbook

StepScriptActionParallelism
101_parallel_init.sqlConfig & Safety. Sets up control tables and defines the batches.N/A
202_parallel_drop.sqlThe Nuke. Launches 8 background jobs to drop tablespaces at the same time.8x Threads
303_wait_blocker.sqlTraffic Cop. Pauses your terminal until all Drop jobs are 100% finished.N/A
404_parallel_sweep.sqlThe Sweeper. Launches 7 background jobs to clean schemas at the same time.7x Threads
505_monitor.sqlVerification. Checks status and confirms 0 objects remain.N/A

SCRIPT 01: Config, Safety & Initialization

Filename: 01_parallel_init.sql

SQL
SET SERVEROUTPUT ON;

DECLARE
    -- [SAFETY VALVE] SET TO TRUE TO ENABLE DESTRUCTION
    v_i_have_backups BOOLEAN := FALSE; 

    v_fallback_ts VARCHAR2(30);
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== STEP 1: PARALLEL INIT & SAFETY ===');

    IF NOT v_i_have_backups THEN
        RAISE_APPLICATION_ERROR(-20000, 'STOP! You must edit Script 01 and set v_i_have_backups := TRUE.');
    END IF;

    -- Detect Fallback TS
    BEGIN
        SELECT property_value INTO v_fallback_ts 
        FROM database_properties WHERE property_name = 'DEFAULT_PERMANENT_TABLESPACE';
    EXCEPTION WHEN OTHERS THEN v_fallback_ts := 'USERS'; END;

    -- Cleanup Old Controls
    BEGIN EXECUTE IMMEDIATE 'DROP TABLE tablespace_control PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
    BEGIN EXECUTE IMMEDIATE 'DROP TABLE tablespace_log PURGE';     EXCEPTION WHEN OTHERS THEN NULL; END;
    BEGIN EXECUTE IMMEDIATE 'DROP TABLE tablespace_config PURGE';  EXCEPTION WHEN OTHERS THEN NULL; END;

    -- Config & Control Tables
    EXECUTE IMMEDIATE 'CREATE TABLE tablespace_config (config_key VARCHAR2(50) PRIMARY KEY, config_value VARCHAR2(500))';
    EXECUTE IMMEDIATE 'INSERT INTO tablespace_config VALUES (''FALLBACK_TS'', :1)' USING v_fallback_ts;

    EXECUTE IMMEDIATE q'[
        CREATE TABLE tablespace_control (
            item_name     VARCHAR2(50) PRIMARY KEY,
            item_type     VARCHAR2(20),
            batch_id      NUMBER,
            status        VARCHAR2(20) DEFAULT 'PENDING',
            error_msg     VARCHAR2(4000),
            CONSTRAINT chk_safe_items CHECK (
                UPPER(item_name) NOT IN ('SYSTEM','SYSAUX','UNDOTBS1','TEMP','USERS') 
            )
        )
    ]';
    
    EXECUTE IMMEDIATE q'[
        CREATE TABLE tablespace_log (
            log_id    NUMBER GENERATED ALWAYS AS IDENTITY,
            log_time  TIMESTAMP DEFAULT SYSTIMESTAMP,
            item_name VARCHAR2(50), 
            action    VARCHAR2(30), 
            status    VARCHAR2(20), 
            message   VARCHAR2(4000)
        )
    ]';

    -- LOAD PARALLEL BATCHES
    INSERT ALL
        -- Batch A: 8 Tablespaces (Will run on 8 threads)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('TS_REVANTH_CORE',      'TABLESPACE', 1)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('TS_REVANTH_INDEX',     'TABLESPACE', 2)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('TS_REVANTH_AUDIT',     'TABLESPACE', 3)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('TS_REVANTH_HISTORY',   'TABLESPACE', 4)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('TS_REVANTH_REPORTING', 'TABLESPACE', 5)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('TS_REVANTH_ARCHIVE',   'TABLESPACE', 6)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('TS_REVANTH_LARGE',     'TABLESPACE', 7)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('TS_REVANTH_RESERVE',   'TABLESPACE', 8)
        
        -- Batch B: 7 Schemas (Will run on 7 threads)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('SCHEMA_APP1',        'SCHEMA', 101)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('SCHEMA_APP2',        'SCHEMA', 102)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('SCHEMA_DATA',        'SCHEMA', 103)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('SCHEMA_REPORT',      'SCHEMA', 104)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('SCHEMA_AUDIT',       'SCHEMA', 105)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('SCHEMA_INTEGRATION', 'SCHEMA', 106)
        INTO tablespace_control (item_name, item_type, batch_id) VALUES ('SCHEMA_ARCHIVE',     'SCHEMA', 107)
    SELECT * FROM dual;
    COMMIT;

    -- Prepare Environment (Kill Sessions)
    FOR r IN (SELECT item_name FROM tablespace_control WHERE item_type = 'SCHEMA') LOOP
        BEGIN
            EXECUTE IMMEDIATE 'ALTER USER ' || r.item_name || ' ACCOUNT LOCK';
            FOR s IN (SELECT sid, serial# FROM v$session WHERE username = r.item_name) LOOP
                EXECUTE IMMEDIATE 'ALTER SYSTEM KILL SESSION ''' || s.sid || ',' || s.serial# || ''' IMMEDIATE';
            END LOOP;
        EXCEPTION WHEN OTHERS THEN NULL; END;
    END LOOP;

    DBMS_OUTPUT.PUT_LINE('>> Parallel Batches Configured. Ready to Drop.');
END;
/

SCRIPT 02: Launch Parallel Tablespace Drops

Filename: 02_parallel_drop.sql

SQL
SET SERVEROUTPUT ON;

DECLARE
    v_fallback_ts VARCHAR2(50);
    v_jobs_count  NUMBER := 0;
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== STEP 2: LAUNCHING PARALLEL DROPS ===');

    SELECT config_value INTO v_fallback_ts FROM tablespace_config WHERE config_key = 'FALLBACK_TS';

    -- Loop through control table and spawn a job for EACH tablespace immediately
    FOR rec IN (SELECT * FROM tablespace_control WHERE item_type = 'TABLESPACE' ORDER BY batch_id) LOOP
        DBMS_SCHEDULER.CREATE_JOB(
            job_name   => 'JOB_DROP_' || rec.batch_id,
            job_type   => 'PLSQL_BLOCK',
            job_action => q'[
                DECLARE
                    v_ts   VARCHAR2(50) := ']' || rec.item_name || q'[';
                    v_safe VARCHAR2(50) := ']' || v_fallback_ts || q'[';
                    v_cnt  NUMBER;
                BEGIN
                    -- 1. Evacuate Users
                    FOR u IN (SELECT username FROM dba_users WHERE default_tablespace = v_ts) LOOP
                        EXECUTE IMMEDIATE 'ALTER USER ' || u.username || ' DEFAULT TABLESPACE ' || v_safe;
                    END LOOP;

                    -- 2. Drop (The Heavy IO Operation)
                    SELECT count(*) INTO v_cnt FROM dba_tablespaces WHERE tablespace_name = v_ts;
                    IF v_cnt > 0 THEN
                        EXECUTE IMMEDIATE 'DROP TABLESPACE ' || v_ts || ' INCLUDING CONTENTS AND DATAFILES CASCADE CONSTRAINTS';
                        INSERT INTO tablespace_log (item_name, action, status, message) VALUES (v_ts, 'DROP_TS', 'SUCCESS', 'Dropped via Parallel Job');
                    ELSE
                        INSERT INTO tablespace_log (item_name, action, status, message) VALUES (v_ts, 'DROP_TS', 'SKIPPED', 'Not Found');
                    END IF;
                    
                    UPDATE tablespace_control SET status = 'COMPLETED' WHERE item_name = v_ts;
                    COMMIT;
                EXCEPTION WHEN OTHERS THEN
                    INSERT INTO tablespace_log (item_name, action, status, message) VALUES (v_ts, 'DROP_TS', 'FAILED', SQLERRM);
                    UPDATE tablespace_control SET status = 'FAILED', error_msg = SQLERRM WHERE item_name = v_ts;
                    COMMIT;
                END;
            ]',
            enabled    => TRUE,
            auto_drop  => TRUE
        );
        v_jobs_count := v_jobs_count + 1;
        DBMS_OUTPUT.PUT_LINE('>> Spawned Thread #' || v_jobs_count || ' for: ' || rec.item_name);
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('>> All ' || v_jobs_count || ' threads running. Proceed to Wait Script.');
END;
/

SCRIPT 03: The Blocker (Wait for Completion)

Filename: 03_wait_blocker.sql

SQL
SET SERVEROUTPUT ON;

DECLARE
    v_active_jobs NUMBER;
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== WAITING FOR PARALLEL JOBS TO FINISH ===');
    
    LOOP
        SELECT COUNT(*) INTO v_active_jobs 
        FROM dba_scheduler_jobs 
        WHERE job_name LIKE 'JOB_%';
        
        IF v_active_jobs = 0 THEN
            EXIT;
        END IF;
        
        DBMS_OUTPUT.PUT_LINE('... ' || v_active_jobs || ' threads still active ...');
        
        -- Sleep 10s (Universal compatible approach)
        BEGIN
            EXECUTE IMMEDIATE 'BEGIN DBMS_SESSION.SLEEP(10); END;';
        EXCEPTION WHEN OTHERS THEN
            NULL; -- Busy wait for older versions
        END;
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('>> ALL THREADS FINISHED.');
END;
/

SCRIPT 04: Launch Parallel Object Sweepers

Filename: 04_parallel_sweep.sql

SQL
SET SERVEROUTPUT ON;

DECLARE
    v_jobs_count NUMBER := 0;
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== STEP 4: LAUNCHING PARALLEL SWEEPERS ===');

    FOR rec IN (SELECT * FROM tablespace_control WHERE item_type = 'SCHEMA') LOOP
        DBMS_SCHEDULER.CREATE_JOB(
            job_name   => 'JOB_SWEEP_' || rec.batch_id,
            job_type   => 'PLSQL_BLOCK',
            job_action => q'[
                DECLARE
                    v_owner VARCHAR2(50) := ']' || rec.item_name || q'[';
                BEGIN
                    EXECUTE IMMEDIATE 'ALTER SESSION SET RECYCLEBIN = OFF';
                    
                    -- A. Parallel Schema Cleanup
                    FOR obj IN (
                        SELECT object_name, object_type FROM dba_objects WHERE owner = v_owner
                        AND object_type NOT LIKE 'SYSTEM%' AND object_type NOT LIKE 'LOB%' AND object_name NOT LIKE 'BIN$%'
                        ORDER BY CASE object_type 
                            WHEN 'SYNONYM' THEN 1 WHEN 'VIEW' THEN 2 WHEN 'SEQUENCE' THEN 3
                            WHEN 'PROCEDURE' THEN 4 WHEN 'PACKAGE' THEN 5 WHEN 'TABLE' THEN 6 
                            ELSE 7 END ASC
                    ) LOOP
                        BEGIN
                            IF obj.object_type = 'TABLE' THEN
                                EXECUTE IMMEDIATE 'DROP TABLE "'||v_owner||'"."'||obj.object_name||'" CASCADE CONSTRAINTS PURGE';
                            ELSE
                                EXECUTE IMMEDIATE 'DROP '||obj.object_type||' "'||v_owner||'"."'||obj.object_name||'"' || 
                                                  CASE WHEN obj.object_type = 'TYPE' THEN ' FORCE' ELSE '' END;
                            END IF;
                        EXCEPTION WHEN OTHERS THEN NULL; END;
                    END LOOP;

                    -- B. Parallel Public Synonym Cleanup
                    FOR p IN (SELECT synonym_name FROM dba_synonyms WHERE owner = 'PUBLIC' AND table_owner = v_owner) LOOP
                        BEGIN
                            EXECUTE IMMEDIATE 'DROP PUBLIC SYNONYM "' || p.synonym_name || '"';
                        EXCEPTION WHEN OTHERS THEN NULL; END;
                    END LOOP;

                    INSERT INTO tablespace_log (item_name, action, status, message) VALUES (v_owner, 'SWEEP', 'SUCCESS', 'Cleaned via Parallel Job');
                    UPDATE tablespace_control SET status = 'COMPLETED' WHERE item_name = v_owner;
                    COMMIT;
                END;
            ]',
            enabled    => TRUE,
            auto_drop  => TRUE
        );
        v_jobs_count := v_jobs_count + 1;
        DBMS_OUTPUT.PUT_LINE('>> Spawned Thread #' || v_jobs_count || ' for: ' || rec.item_name);
    END LOOP;
END;
/

SCRIPT 05: Status Monitor

Filename: 05_monitor.sql

SQL
SET LINESIZE 200 PAGESIZE 100;
COL item_name FORMAT A30
COL action FORMAT A15
COL status FORMAT A15
COL message FORMAT A60
COL log_time FORMAT A20

PROMPT === FINAL JOB STATUS ===
SELECT job_name, state, run_duration FROM dba_scheduler_jobs WHERE job_name LIKE 'JOB_%';

PROMPT === CONTROL STATUS ===
SELECT item_name, item_type, status, error_msg FROM tablespace_control ORDER BY batch_id;

PROMPT === OBJECT COUNT (Target: 0) ===
SELECT owner, count(*) as remaining_objects 
FROM dba_objects 
WHERE owner IN (SELECT item_name FROM tablespace_control WHERE item_type = 'SCHEMA')
GROUP BY owner;

OLTP Advanced Compression Diff


Quick copy-paste ready blocks for your script SQL

text
-- Modern recommended version (2025–2026 standard)
v_common_clause CONSTANT VARCHAR2(500) := 
    'AUTOEXTEND ON NEXT 1G MAXSIZE UNLIMITED LOGGING DEFAULT NOLOGGING '
 || 'NO INMEMORY ONLINE EXTENT MANAGEMENT LOCAL AUTOALLOCATE '
 || 'BLOCKSIZE 8K SEGMENT SPACE MANAGEMENT AUTO FLASHBACK ON '
 || 'ROW STORE COMPRESS ADVANCED';

SQL

text
-- Slightly shorter + old-school compatible version
v_common_clause CONSTANT VARCHAR2(400) := 
    'AUTOEXTEND ON NEXT 1G MAXSIZE UNLIMITED NOLOGGING ONLINE '
 || 'EXTENT MANAGEMENT LOCAL AUTOALLOCATE SEGMENT SPACE MANAGEMENT AUTO '
 || 'COMPRESS FOR OLTP';

what does this bothdo?


Both versions do almost exactly the same thing — they define the storage and compression options that will be applied when your script creates new bigfile tablespaces.

The only real difference is syntax style and how modern/future-proof the command is.

Here’s a clear comparison:

AspectModern version (ROW STORE COMPRESS ADVANCED)Old-school version (COMPRESS FOR OLTP)Which one is better in 2026?
Official Oracle syntaxCurrent / recommended (since ~12.1, strongly preferred in 19c/21c/23ai)Old syntax (still 100% working)Modern
Compression typeAdvanced Row Compression (aka OLTP compression)Advanced Row Compression (same thing)Same functionality
License requiredAdvanced Compression OptionAdvanced Compression OptionSame
Typical compression ratio2× – 4× (sometimes more)2× – 4× (exactly the same)Same
DML overhead (insert/update)Low to moderateLow to moderateSame
Future-proofYes — this is the syntax Oracle pushes nowStill supported, but considered legacyModern wins
Readability / clarityVery clear what kind of compression it isLess explicit (OLTP sounds like only for OLTP)Modern wins
LengthLongerShorterOld-school wins
Risk of deprecationVery lowLow (but possible in far future)Modern wins

Summary – What each one actually does

Both create tablespaces with these properties:

  • Bigfile tablespace
  • Starts with 1 GB autoextend chunks
  • Can grow without limit (MAXSIZE UNLIMITED)
  • Uses advanced row compression (good balance between compression + acceptable DML performance)
  • Local extent management + autoallocate (Oracle manages extent sizes automatically)
  • 8 KB block size
  • Automatic segment space management (ASSM)
  • Flashback enabled
  • Online creation
  • No In-Memory column store
  • Logging / Nologging setting (you have different preferences in each version)

Recommendation for January 2026


SQL
-- ★ This is the best choice right now ★
v_common_clause CONSTANT VARCHAR2(500) := 
    'AUTOEXTEND ON NEXT 1G MAXSIZE UNLIMITED '
 || 'NOLOGGING ONLINE '                           -- ← faster creation, common choice
 || 'NO INMEMORY '
 || 'EXTENT MANAGEMENT LOCAL AUTOALLOCATE '
 || 'BLOCKSIZE 8K SEGMENT SPACE MANAGEMENT AUTO '
 || 'FLASHBACK ON '
 || 'ROW STORE COMPRESS ADVANCED';

Why this one?

  • Uses current Oracle-recommended syntax
  • Very clear what compression you're getting
  • Least chance of any future surprise/warning
  • Everyone who reads your code in 2026–2030 will immediately understand it

The old COMPRESS FOR OLTP still works perfectly fine today — but it's like still writing VARCHAR2 instead of VARCHAR — it works, but it's not the modern way anymore.

Pick whichever style your team is more comfortable with — functionally they are identical.




Here is a bullet-proof, modern, production-grade version of the tablespace creation clause (2025–2026 best practices).

Recommended final version – most conservative & safe

SQL
v_common_clause CONSTANT VARCHAR2(400) := 
    'AUTOEXTEND ON NEXT 1024M MAXSIZE UNLIMITED '
 || 'NOLOGGING '
 || 'ONLINE '
 || 'NO INMEMORY '
 || 'EXTENT MANAGEMENT LOCAL AUTOALLOCATE '
 || 'BLOCKSIZE 8192 '
 || 'SEGMENT SPACE MANAGEMENT AUTO '
 || 'FLASHBACK ON '
 || 'ROW STORE COMPRESS ADVANCED';

Why these exact settings are considered "bullet-proof" today

SettingValue usedWhy this is safer / betterRisk if changed / omitted
AUTOEXTEND NEXT1024M (instead of 1G)1G = 1024M, but written as 1024M is more explicitVery low
MAXSIZEUNLIMITEDStandard for bigfile tablespacesAlmost none
NOLOGGINGYesMuch faster initial creation (especially 6 TB+)Redo generation during creation
ONLINEYesTablespace is usable immediatelyRequired anyway
NO INMEMORYExplicitPrevents accidental In-Memory populationVery useful in mixed environments
EXTENT MANAGEMENTLOCAL AUTOALLOCATEMost flexible, Oracle manages extent sizes bestVery safe choice
BLOCKSIZE8192 (instead of 8K)More explicit, zero chance of confusionPractically none
SEGMENT SPACE MANAGEMENTAUTOASSM – the only reasonable choice since ~11gNone
FLASHBACK ONYesAllows flashback operations on the tablespaceImportant for recovery scenarios
ROW STORE COMPRESS ADVANCEDYesBest balance compression vs DML performanceRequires Advanced Compression license

Even more conservative / extra-safe version (if you want maximum clarity)

SQL
v_common_clause CONSTANT VARCHAR2(500) := 
    'DATAFILE SIZE 10G '                                -- ← explicit initial size (recommended)
 || 'AUTOEXTEND ON NEXT 1G MAXSIZE UNLIMITED '
 || 'NOLOGGING '
 || 'ONLINE '
 || 'NO INMEMORY '
 || 'EXTENT MANAGEMENT LOCAL '
 || 'AUTOALLOCATE '
 || 'BLOCKSIZE 8192 '
 || 'SEGMENT SPACE MANAGEMENT AUTO '
 || 'FLASHBACK ON '
 || 'ROW STORE COMPRESS ADVANCED';

Quick checklist before you go live with this

text
☑ Advanced Compression license is enabled (DBA_FEATURE_USAGE_STATISTICS)
☑ You are creating BIGFILE tablespace
☑ Your database is >= 12.1 (preferably 19c+)
☑ You are okay with NOLOGGING during initial creation
☑ Your storage can handle ~6TB+ per tablespace (since initial_size_gb = 6268)
☑ You tested insert/update performance with compression in your environment
☑ You have monitoring for tablespace usage (especially autoextend events)

Most bullet-proof pattern used in large environments (2025–2026)

SQL
-- The one most careful DBAs are actually using right now
v_common_clause CONSTANT VARCHAR2(512) := 
    'BIGFILE '                                          -- explicit
 || 'DATAFILE SIZE 10G '                                -- safe starting size
 || 'AUTOEXTEND ON NEXT 1024M MAXSIZE UNLIMITED '
 || 'NOLOGGING ONLINE '
 || 'NO INMEMORY '
 || 'EXTENT MANAGEMENT LOCAL AUTOALLOCATE '
 || 'BLOCKSIZE 8192 '
 || 'SEGMENT SPACE MANAGEMENT AUTO '
 || 'FLASHBACK ON '
 || 'ROW STORE COMPRESS ADVANCED';

Pick whichever version feels most comfortable to you and your team — all of them are very safe when used with BIGFILE.

Good luck Revanth — may your tablespaces be fast, compressed, and never run out of space! 🚀

UNDO and TEMP Usage [Optimal Sizes]


You correctly identified the circular dependency in the previous formula: tuned_undoretention is an effect (what the database managed to retain given current space constraints), not a cause (what the business actually requires). Using it to calculate sizing often leads to results that just mirror the current size rather than the true requirement.

You also correctly noted that MAX(undoblks) in dba_hist_undostat refers to the consumption rate (blocks written during the interval), not the total active footprint of the tablespace. Therefore, the most mathematically accurate way to size UNDO is the Rate-Based Method (Your Method #2): finding the "Peak Generation Rate" and multiplying it by your "Target Retention."

Here is the Revised, Industry-Standard Script. It uses your Rate-Based Logic to give you sizing recommendations for specific retention goals (4h, 12h, 24h), which is far more actionable for an RDS environment.

Script: Oracle RDS Historical Undo & Temp Sizing (Corrected)

SQL
-- -----------------------------------------------------------------------------
-- Script:  rds_undo_temp_complete.sql
-- Purpose: Complete history (Max Used) AND Sizing Recommendations (Projections).
-- -----------------------------------------------------------------------------

SET PAGESIZE 5000
SET LINESIZE 300
SET FEEDBACK OFF
SET VERIFY OFF
SET HEADING ON
SET COLSEP ' '
SET NUMWIDTH 15

PROMPT 
PROMPT ================================================================================
PROMPT   ORACLE RDS: COMPLETE UNDO & TEMP USAGE REPORT (Actuals + Sizing)
PROMPT ================================================================================
PROMPT 

-- -----------------------------------------------------------------------------
-- SECTION 1: UNDO HISTORY & SIZING
-- Displays:
--   1. Analysis Range (Start/End Date)
--   2. Peak Actual Usage (The highest space used in the last month)
--   3. Sizing Recommendations (Based on peak generation rate)
-- -----------------------------------------------------------------------------
PROMPT 
PROMPT [ 1. UNDO: Historical Max Usage & Sizing Recommendations ]
PROMPT 

COLUMN "Start Date"          FORMAT A12
COLUMN "End Date"            FORMAT A12
COLUMN "Days"                FORMAT 999.9
COLUMN "Max Used (GB)"       FORMAT 99,990.99 HEADING 'Actual Peak|Used (GB)'
COLUMN "Snapshot Too Old"    FORMAT 999,990   HEADING 'ORA-01555|Errors'
COLUMN "Rec. Size 4Hr (GB)"  FORMAT 99,990.99 HEADING 'Size Needed|For 4Hrs (GB)'
COLUMN "Rec. Size 12Hr (GB)" FORMAT 99,990.99 HEADING 'Size Needed|For 12Hrs (GB)'
COLUMN "Rec. Size 24Hr (GB)" FORMAT 99,990.99 HEADING 'Size Needed|For 24Hrs (GB)'

WITH 
-- 1. Get Block Size Safely
params AS (
    SELECT value AS blk_size 
    FROM v$parameter 
    WHERE name = 'db_block_size'
),
-- 2. Get Raw Stats (Aggregated first)
raw_stats AS (
    SELECT
        MIN(begin_time) AS min_time,
        MAX(end_time)   AS max_time,
        -- How many days of data did we actually find?
        MAX(end_time) - MIN(begin_time) AS days_covered,
        -- Peak Active Blocks (For "Actual Max Used")
        MAX(undoblks) AS max_active_blocks,
        -- Peak Generation Rate (For "Sizing Projections")
        MAX(undoblks / GREATEST((end_time - begin_time) * 86400, 1)) AS peak_blocks_per_sec,
        SUM(ssolderrcnt) AS total_ora_1555
    FROM dba_hist_undostat
    WHERE begin_time >= TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM')
)
-- 3. Final Calculation
SELECT
    TO_CHAR(r.min_time, 'YYYY-MM-DD') AS "Start Date",
    TO_CHAR(r.max_time, 'YYYY-MM-DD') AS "End Date",
    ROUND(r.days_covered, 1)          AS "Days",
    -- Actual Max Used: (Max Active Blocks * BlockSize) / 1GB
    ROUND((r.max_active_blocks * p.blk_size) / 1073741824, 2) AS "Max Used (GB)",
    r.total_ora_1555                  AS "Snapshot Too Old",
    -- Projection: (Peak Rate * Seconds * BlockSize * 1.2 Buffer) / 1GB
    ROUND((r.peak_blocks_per_sec * 14400 * p.blk_size * 1.2) / 1073741824, 2) AS "Rec. Size 4Hr (GB)",
    ROUND((r.peak_blocks_per_sec * 43200 * p.blk_size * 1.2) / 1073741824, 2) AS "Rec. Size 12Hr (GB)",
    ROUND((r.peak_blocks_per_sec * 86400 * p.blk_size * 1.2) / 1073741824, 2) AS "Rec. Size 24Hr (GB)"
FROM raw_stats r
CROSS JOIN params p;

PROMPT 
PROMPT   * 'Actual Peak Used': The highest amount of space occupied at one time.
PROMPT   * 'Size Needed': Recommended size to guarantee retention (with 20% buffer).
PROMPT 

-- -----------------------------------------------------------------------------
-- SECTION 2: TEMPORARY TABLESPACE PEAK USAGE
-- -----------------------------------------------------------------------------
PROMPT --------------------------------------------------------------------------------
PROMPT [ 2. TEMP: Historical Max Usage & Recommendations ]
PROMPT 

COLUMN "Start Date"        FORMAT A12
COLUMN "End Date"          FORMAT A12
COLUMN "Days"              FORMAT 999.9
COLUMN "Max Temp Used (GB)" FORMAT 99,990.99 HEADING 'Actual Peak|Used (GB)'
COLUMN "Avg Temp Used (GB)" FORMAT 99,990.99 HEADING 'Avg Temp|Used (GB)'
COLUMN "Rec. Optimal (GB)"  FORMAT 99,990.99 HEADING 'Rec. Optimal|Size (GB)'

SELECT
    TO_CHAR(MIN(begin_time), 'YYYY-MM-DD') AS "Start Date",
    TO_CHAR(MAX(end_time), 'YYYY-MM-DD')   AS "End Date",
    ROUND(MAX(end_time) - MIN(begin_time), 1) AS "Days",
    ROUND(MAX(maxval) / 1073741824, 2)     AS "Max Temp Used (GB)",
    ROUND(AVG(average) / 1073741824, 2)    AS "Avg Temp Used (GB)",
    -- Optimal = Peak + 20% Headroom
    ROUND((MAX(maxval) / 1073741824) * 1.20, 2) AS "Rec. Optimal (GB)"
FROM dba_hist_sysmetric_summary
WHERE metric_name = 'Temp Space Used'
AND begin_time >= TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM');

PROMPT 
PROMPT ================================================================================
PROMPT Script Completed.
PROMPT ================================================================================
SET FEEDBACK ON

Changes from previous version:

  1. Eliminated TUNED_UNDORETENTION: The formula no longer relies on this variable. It now strictly calculates Demand (Bytes generated per second).

  2. Scenarios: Instead of giving you one "Optimal Size" (which is subjective), it gives you the required size for 4 hours, 12 hours, and 24 hours. This allows you to choose based on your business SLA (e.g., if you need Flashback Query to work for 24h, use the last column).

  3. Safety Buffer: I added a 1.2 multiplier (20% buffer) to the final recommendation. In RDS, extending storage is easy, but hitting ORA-30036 (Undo full) freezes the DB, so it's better to be slightly over-provisioned.

  4. ORA-01555 Check: Added a column for "Snapshot Too Old." If this number is greater than 0, it confirms your current sizing was definitely too small for the workload in the last month.

    ===================================================================

    -- =============================================================================

    -- COMBINED UNDO ANALYSIS - LAST MONTH (from start of previous month)

    -- Current date context: January 2026

    -- Covers: Historical peak usage, rate-based sizing, advisor, errors

    -- Run as user with access to DBA_HIST_* and DBMS_UNDO_ADV

    -- =============================================================================

    -- -----------------------------------------------------------------------------
    -- Script:  rds_undo_optimization_matrix.sql
    -- Purpose: The "Best of Both Worlds" report.
    --          - Uses DBA_HIST for long-term accuracy (Last 30 Days).
    --          - Presents the "Size vs. Retention" trade-off logic you requested.
    --          - Converts all metrics to GB for readability.
    -- -----------------------------------------------------------------------------

    SET PAGESIZE 100
    SET LINESIZE 250
    SET FEEDBACK OFF
    SET VERIFY OFF
    SET HEADING ON
    SET COLSEP ' | '
    SET NUMWIDTH 15

    PROMPT 
    PROMPT ================================================================================
    PROMPT   ORACLE RDS: UNDO OPTIMIZATION MATRIX (30-Day History)
    PROMPT ================================================================================
    PROMPT 

    -- -----------------------------------------------------------------------------
    -- SECTION 1: CURRENT STATE & WORKLOAD PEAKS
    -- -----------------------------------------------------------------------------
    COLUMN "Analysis Range"      FORMAT A25
    COLUMN "Current Size (GB)"   FORMAT 999,990.99
    COLUMN "Curr Retention (Sec)" FORMAT 999,990
    COLUMN "Peak Gen Rate (MB/s)" FORMAT 990.999
    COLUMN "ORA-01555 Errors"    FORMAT 999,990

    WITH 
        -- 1. Get Physical Configuration
        config AS (
            SELECT 
                (SELECT SUM(bytes) FROM dba_data_files df JOIN dba_tablespaces ts ON df.tablespace_name = ts.tablespace_name WHERE ts.contents = 'UNDO') AS total_bytes,
                (SELECT value FROM v$parameter WHERE name = 'undo_retention') AS curr_retention,
                (SELECT value FROM v$parameter WHERE name = 'db_block_size') AS blk_size
            FROM DUAL
        ),
        -- 2. Get Historical Workload (Peak Rate)
        workload AS (
            SELECT 
                MIN(begin_time) as start_date,
                MAX(end_time) as end_date,
                -- Peak Generation Rate (Bytes per second)
                MAX((undoblks * (SELECT blk_size FROM config)) / GREATEST((end_time - begin_time) * 86400, 1)) AS peak_bytes_per_sec,
                SUM(ssolderrcnt) as total_errors
            FROM dba_hist_undostat
            WHERE begin_time >= TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM')
        )
    SELECT
        TO_CHAR(w.start_date, 'YYYY-MM-DD') || ' to ' || TO_CHAR(w.end_date, 'MM-DD') AS "Analysis Range",
        ROUND(c.total_bytes / 1073741824, 2)    AS "Current Size (GB)",
        c.curr_retention                        AS "Curr Retention (Sec)",
        ROUND(w.peak_bytes_per_sec/1024/1024,3) AS "Peak Gen Rate (MB/s)",
        w.total_errors                          AS "ORA-01555 Errors"
    FROM config c, workload w;

    PROMPT 
    PROMPT ================================================================================
    PROMPT   DECISION MATRIX: CHOOSE YOUR STRATEGY
    PROMPT ================================================================================

    -- -----------------------------------------------------------------------------
    -- SECTION 2: OPTIMIZATION CHOICES
    -- Logic: 
    --   Choice A: Keep Retention fixed, Change Size.
    --   Choice B: Keep Size fixed, Change Retention.
    --   Choice C: Target specific SLAs (4h, 12h, 24h).
    -- -----------------------------------------------------------------------------
    COLUMN "Strategy"            FORMAT A35
    COLUMN "Target"              FORMAT A25
    COLUMN "Required Action"     FORMAT A40

    WITH 
        config AS (
            SELECT 
                (SELECT SUM(bytes) FROM dba_data_files df JOIN dba_tablespaces ts ON df.tablespace_name = ts.tablespace_name WHERE ts.contents = 'UNDO') AS total_bytes,
                (SELECT TO_NUMBER(value) FROM v$parameter WHERE name = 'undo_retention') AS curr_retention,
                (SELECT value FROM v$parameter WHERE name = 'db_block_size') AS blk_size
            FROM DUAL
        ),
        workload AS (
            SELECT 
                -- Peak Generation Rate (Bytes/Sec)
                MAX((undoblks * (SELECT blk_size FROM config)) / GREATEST((end_time - begin_time) * 86400, 1)) AS peak_bps
            FROM dba_hist_undostat
            WHERE begin_time >= TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM')
        )
    -- Option A: Adjust Size for Current Retention
    SELECT 
        'OPTION A: Prioritize Retention' AS "Strategy",
        'Keep ' || c.curr_retention || ' Seconds' AS "Target",
        'Resize UNDO to: ' || ROUND((w.peak_bps * c.curr_retention * 1.2) / 1073741824, 2) || ' GB' AS "Required Action"
    FROM config c, workload w
    UNION ALL
    -- Option B: Adjust Retention for Current Size
    SELECT 
        'OPTION B: Prioritize Storage',
        'Keep ' || ROUND(c.total_bytes / 1073741824, 2) || ' GB Size',
        'Set UNDO_RETENTION to: ' || ROUND(c.total_bytes / w.peak_bps, 0) || ' Secs'
    FROM config c, workload w
    UNION ALL
    -- Option C: Standard SLAs
    SELECT 
        'OPTION C: 4-Hour Standard',
        'Target 4 Hours',
        'Resize UNDO to: ' || ROUND((w.peak_bps * 14400 * 1.2) / 1073741824, 2) || ' GB'
    FROM config c, workload w
    UNION ALL
    SELECT 
        'OPTION C: 24-Hour (Flashback)',
        'Target 24 Hours',
        'Resize UNDO to: ' || ROUND((w.peak_bps * 86400 * 1.2) / 1073741824, 2) || ' GB'
    FROM config c, workload w;

    PROMPT 
    PROMPT * Note: Option A and C include a 20% Safety Buffer (x 1.2) to prevent autoextend lag.
    PROMPT * Option B shows the theoretical max duration your current disk can support at peak load.
    PROMPT ================================================================================
    SET FEEDBACK ON

Thursday, January 15, 2026

Powerquery

1. Best & Cleanest Method → Power Query in Excel (Recommended)

Steps:

  1. Open Excel → new blank workbook

  2. Put both lists into separate sheets:

    • Sheet1 = "PROD" Column A: schema Column B: table_name (first row = headers: Schema, TableName)
    • Sheet2 = "LOWER" Column A: schema Column B: table_name (headers: Schema, TableName)
  3. Create helper column with full name (very important!):

    In both sheets, add column C with formula:

    text
    =A2&"."&B2

    (copy down)

    Now you have:

    • PROD!C → full name like IDBS_APP.ORDER_HEADER
    • LOWER!C → same format
  4. Go to Data → Get Data → From Other Sources → Blank Query

  5. In Power Query Editor:

    powerquery
    let
        Prod = Excel.CurrentWorkbook(){[Name="PROD"]}[Content],
        Lower = Excel.CurrentWorkbook(){[Name="LOWER"]}[Content],
    
        #"Merged Queries" = Table.NestedJoin(Prod, {"C"}, Lower, {"C"}, "LOWER", JoinKind.LeftAnti),
        #"Expanded LOWER" = Table.ExpandTableColumn(#"Merged Queries", "LOWER", {}, {}),
    
        #"Removed Duplicates" = Table.Distinct(#"Expanded LOWER"),
        #"Sorted" = Table.Sort(#"Removed Duplicates",{{"C", Order.Ascending}})
    in
        #"Sorted"
  6. Click Close & Load → new sheet with only tables that exist in PROD but NOT in lower env

  7. (Optional) Do the opposite (extra in lower env) → change LeftAnti to RightAnti

This method is:

  • Clean
  • Repeatable
  • Doesn't break when you refresh data

2. Fast Classic Excel Method (VLOOKUP / XLOOKUP)

In a new sheet:

A1:D1 headers: PROD_Schema | PROD_Table | Status | Comment

Put PROD list in columns A:B

Then in C2 (status):

excel
=IF(ISNA(XLOOKUP(A2&"."&B2, LOWER!$A:$A&"."&LOWER!$B:$B, "Exists", "Missing")) ,"MISSING in lower env" ,"Exists in both")

Or simpler (classic style):

excel
=IF(ISNA(VLOOKUP(A2&"."&B2, LOWER!C:C, 1, FALSE)), "MISSING", "Exists")

Filter by "MISSING" → you have your report.

3. Super Fast Python One-liner Approach (if you can run Python)

Python
import pandas as pd

prod = pd.read_excel("tables_prod.xlsx", header=None, names=["schema","table"])
lower = pd.read_excel("tables_lower.xlsx", header=None, names=["schema","table"])

prod['full'] = prod['schema'] + '.' + prod['table']
lower['full'] = lower['schema'] + '.' + lower['table']

missing_in_lower = prod[~prod['full'].isin(lower['full'])]

# Save result
missing_in_lower[['schema','table']].to_excel("missing_in_lower.xlsx", index=False)
print(f"Found {len(missing_in_lower)} tables missing in lower env")

Just adjust file names/paths.

Summary – What I Would Do in Your Place

SituationRecommended choice
You want nice Excel + repeatablePower Query (method 1)
One-time, don't want to learn newXLOOKUP / VLOOKUP (method 2)
You already use Python / automatepandas script (method 3)
You want fastest possible (no Excel)Text files + comm or PowerShell Compare-Object

Most people in DBA/developer roles end up using Power Query for this kind of comparison because it's visual, powerful, and built into modern Excel.

Let me know which method you prefer and I can give you more detailed step-by-step (with screenshots description if needed).