Monday, January 19, 2026

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).

Sunday, December 7, 2025

3 Ways to Handle Oracle Object ID Drift

Handling Oracle Object ID Drift: A Comprehensive Guide

If you maintain a metadata table (e.g., DB_OBJECT_ID_CAPTURED) that tracks Oracle Object IDs, you know the struggle. Every time partition maintenance occurs—splits, merges, truncates—the DATA_OBJECT_ID changes. If your downstream jobs rely on these static IDs, they fail.

Keeping this table in sync manually is error-prone. You need a way to filter out logical "Composite" partitions and track only the physical segments that hold data.

Here are the three industry-standard solutions to solve this, ranked from the best architectural fix to the best maintenance script.


Solution 1: The Architectural Fix (The View)

Best for: Systems where historical ID tracking is not required. The Concept: Instead of fighting to keep a physical table in sync, replace it with a View. The View passes the query directly to the live system dictionary (ALL_OBJECTS), ensuring your IDs are always 100% real-time.

SQL
-- 1. Backup the old table
ALTER TABLE db_object_id_captured RENAME TO db_object_id_captured_old;

-- 2. Create the View (Replaces the Table)
CREATE OR REPLACE VIEW db_object_id_captured AS
SELECT 
    279 AS PUR_ID, -- NOTE: Hardcoded Group ID (Adjust as needed)
    object_id, 
    data_object_id, 
    object_name AS table_name, 
    subobject_name AS partition_name, 
    object_type,
    owner
FROM all_objects 
WHERE owner = 'YOUR_SCHEMA'
  AND object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
  -- This filter ensures we only see physical data segments (Composite = NO)
  AND data_object_id IS NOT NULL; 

Solution 2: The Real-Time Fix (The Trigger)

Best for: Applications that require a physical table but need instant updates. The Concept: A DDL Trigger fires the millisecond a CREATE or ALTER command finishes, instantly updating your capture table.

SQL
CREATE OR REPLACE TRIGGER trg_sync_object_ids
AFTER DDL ON SCHEMA
DECLARE
    v_obj_name VARCHAR2(128);
    v_obj_type VARCHAR2(128);
BEGIN
    v_obj_name := ora_dict_obj_name;
    v_obj_type := ora_dict_obj_type;

    IF v_obj_type IN ('TABLE','INDEX','TABLE PARTITION','TABLE SUBPARTITION') THEN
        MERGE INTO db_object_id_captured target
        USING (
            SELECT object_id, data_object_id, object_name, subobject_name, object_type
            FROM all_objects
            WHERE object_name = v_obj_name AND owner = ora_dict_obj_owner
        ) source
        ON (target.table_name = source.object_name 
            AND NVL(target.partition_name, '###') = NVL(source.subobject_name, '###'))
        WHEN MATCHED THEN
            UPDATE SET target.object_id = source.object_id, 
                       target.data_object_id = source.data_object_id
        WHEN NOT MATCHED THEN
            INSERT (pur_id, table_name, partition_name, object_type, object_id, data_object_id)
            VALUES (279, source.object_name, source.subobject_name, source.object_type, source.object_id, source.data_object_id);
    END IF;
END;
/

Solution 3: The Batch Fix (The Master Script)

Best for: Controlled environments where you want to review drift before applying changes. The Concept: A robust PL/SQL block with two modes: Preview (Health Check) and Execute (Apply). It handles the complex logic of matching Partitions vs. Subpartitions and excluding Composite parents.

SQL
SET SERVEROUTPUT ON;

DECLARE
    -- CONFIGURATION
    v_pur_id          NUMBER       := 279;             
    v_schema_owner    VARCHAR2(50) := 'YOUR_SCHEMA';   
    v_table_filter    VARCHAR2(50) := NULL; -- Set NULL for ALL tables
    
    -- MODE: FALSE = Preview (Health Check), TRUE = Execute (Apply Changes)
    v_apply_changes   BOOLEAN      := FALSE;           
    v_rows_merged     NUMBER       := 0;
BEGIN
    DBMS_OUTPUT.PUT_LINE('MODE: ' || CASE WHEN v_apply_changes THEN 'EXECUTE' ELSE 'PREVIEW' END);

    IF NOT v_apply_changes THEN
        -- PREVIEW LOGIC
        FOR r IN (
            WITH captured_db AS (
                SELECT table_name, partition_name, object_id, data_object_id
                FROM db_object_id_captured
                WHERE pur_id = v_pur_id
                  AND (v_table_filter IS NULL OR table_name = v_table_filter)
            ),
            live_db AS (
                SELECT o.object_name, o.subobject_name, o.object_type, o.object_id, o.data_object_id
                FROM all_objects o
                WHERE o.owner = v_schema_owner
                  AND o.object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
                  AND o.data_object_id IS NOT NULL -- Filters out Composite Partitions
                  AND o.object_name IN (SELECT DISTINCT table_name FROM captured_db)
            )
            SELECT 
                NVL(live.object_name, cap.table_name) AS t_name,
                NVL(live.subobject_name, cap.partition_name) AS p_name,
                CASE 
                    WHEN live.object_id IS NULL THEN 'ORPHAN (Safe to Delete)'
                    WHEN cap.object_id IS NULL THEN 'NEW (Will Insert)'
                    WHEN live.object_id != cap.object_id OR 
                         NVL(live.data_object_id,-1) != NVL(cap.data_object_id,-1) THEN 'DRIFT (Will Update)'
                    ELSE 'SYNCED'
                END AS status,
                cap.object_id as old_id, live.object_id as new_id
            FROM live_db live
            FULL OUTER JOIN captured_db cap
              ON live.object_name = cap.table_name
              AND NVL(live.subobject_name, '###') = NVL(cap.partition_name, '###')
            WHERE (live.object_id IS NULL) OR (cap.object_id IS NULL) 
               OR (live.object_id != cap.object_id) 
               OR (NVL(live.data_object_id,-1) != NVL(cap.data_object_id,-1))
            ORDER BY 1, 2
        ) LOOP
            DBMS_OUTPUT.PUT_LINE('[' || r.status || '] ' || r.t_name || ' : ' || r.p_name || 
                                 ' (Old: ' || r.old_id || ' -> New: ' || r.new_id || ')');
        END LOOP;
    ELSE
        -- EXECUTE LOGIC (MERGE)
        MERGE INTO db_object_id_captured target
        USING (
            SELECT v_pur_id AS pur_id, o.object_id, o.data_object_id, o.object_name, o.subobject_name, o.object_type
            FROM all_objects o
            WHERE o.owner = v_schema_owner
              AND o.object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
              AND o.data_object_id IS NOT NULL
              AND (v_table_filter IS NULL OR o.object_name = v_table_filter)
              AND o.object_name IN (SELECT DISTINCT table_name FROM db_object_id_captured WHERE pur_id = v_pur_id)
        ) source
        ON (
            target.pur_id          = source.pur_id
            AND target.table_name  = source.object_name
            AND NVL(target.partition_name, '###') = NVL(source.subobject_name, '###')
            AND target.object_type = source.object_type
        )
        WHEN MATCHED THEN
            UPDATE SET target.object_id = source.object_id, 
                       target.data_object_id = source.data_object_id
            WHERE DECODE(target.object_id, source.object_id, 0, 1) = 1 
               OR DECODE(target.data_object_id, source.data_object_id, 0, 1) = 1
        WHEN NOT MATCHED THEN
            INSERT (pur_id, table_name, partition_name, object_type, object_id, data_object_id)
            VALUES (source.pur_id, source.object_name, source.subobject_name, source.object_type, source.object_id, source.data_object_id);
            
        v_rows_merged := SQL%ROWCOUNT;
        COMMIT;
        DBMS_OUTPUT.PUT_LINE('SUCCESS. Rows Synced: ' || v_rows_merged);
    END IF;
END;
/

Appendix: Manual Verification Cheat Sheet

If you need to verify a specific table manually, run these 3 statements in order.

1. Check Live Data (Source)

SQL
SELECT OBJECT_ID, DATA_OBJECT_ID, OBJECT_NAME, SUBOBJECT_NAME
FROM ALL_OBJECTS
WHERE OWNER = 'YOUR_SCHEMA_NAME'
  AND OBJECT_NAME = 'YOUR_TABLE_NAME'
  AND DATA_OBJECT_ID IS NOT NULL -- Filters Composite Partitions
ORDER BY OBJECT_NAME, SUBOBJECT_NAME;

2. Check Captured Data (Target)

SQL
SELECT * FROM DB_OBJECT_ID_CAPTURED 
WHERE PUR_ID = 279 AND TABLE_NAME = 'YOUR_TABLE_NAME';

3. Run the Safe Update

SQL
UPDATE DB_OBJECT_ID_CAPTURED
SET OBJECT_ID      = <NEW_OBJECT_ID>,
    DATA_OBJECT_ID = <NEW_DATA_OBJECT_ID>
WHERE PUR_ID = 279
  AND TABLE_NAME = 'YOUR_TABLE_NAME'
  AND NVL(PARTITION_NAME, '###') = NVL('<PARTITION_NAME>', '###')
  AND OBJECT_ID = <OLD_OBJECT_ID>; -- Safety Check

all_objects

 

Here are the 3 individual, standardized SQL statements.

These use the "Best Practice" logic we established:

  1. Composite Exclusion: Uses DATA_OBJECT_ID IS NOT NULL (Much faster/cleaner than joining ALL_TAB_PARTITIONS).

  2. Subpartition Support: Automatically includes TABLE SUBPARTITION.

  3. Safety: The Update statement includes the Old Values and Object Type to ensure you never update the wrong row.

1. The "Live Data" Query (Source)

Run this to get the New values from the database.

  • Note: This replaces your complex UNION ALL query. It automatically filters out logical "Parent" partitions (Composite=YES) by ensuring DATA_OBJECT_ID exists.

SELECT 
    OBJECT_ID, 
    DATA_OBJECT_ID, 
    OBJECT_NAME       AS TABLE_NAME,
    SUBOBJECT_NAME    AS PARTITION_NAME, -- Holds Partition or Subpartition Name
    OBJECT_TYPE
FROM ALL_OBJECTS
WHERE OWNER = 'YOUR_SCHEMA_NAME'       -- <1-- Change Owner
  AND OBJECT_NAME = 'YOUR_TABLE_NAME'  -- <2-- Change Table Name
  AND OBJECT_TYPE IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
  AND DATA_OBJECT_ID IS NOT NULL       -- <3-- This ensures COMPOSITE = 'NO'
ORDER BY 
    OBJECT_NAME, 
    OBJECT_TYPE, 
    SUBOBJECT_NAME;


2. The "Captured Data" Query (Target)

Run this to see what you currently have stored (the Old values).

SELECT * FROM DB_OBJECT_ID_CAPTURED 
WHERE PUR_ID = 279 
  AND TABLE_NAME = 'YOUR_TABLE_NAME'
ORDER BY 
    TABLE_NAME, 
    OBJECT_TYPE, 
    PARTITION_NAME;

3. The "Fix" Statement (Update)

Use the values from Statement 1 (Live) to update Statement 2 (Target).

  • Replace the placeholders <...> with the actual numbers/names.


UPDATE DB_OBJECT_ID_CAPTURED
SET OBJECT_ID      = <NEW_OBJECT_ID_FROM_STEP_1>,
    DATA_OBJECT_ID = <NEW_DATA_OBJECT_ID_FROM_STEP_1>
WHERE PUR_ID = 279
  AND TABLE_NAME = 'YOUR_TABLE_NAME'
  
  -- 1. IDENTIFY THE PARTITION (Handle NULLs for non-partitioned tables)
  AND NVL(PARTITION_NAME, '###') = NVL('<PARTITION_NAME_FROM_STEP_1>', '###')
  
  -- 2. LOCK THE TYPE (Ensures you don't mix up Partition vs Subpartition)
  AND OBJECT_TYPE = '<OBJECT_TYPE_FROM_STEP_1>' 
  -- 3. SAFETY CHECK (Only update if it matches the OLD values from Step 2)
  AND OBJECT_ID = <OLD_OBJECT_ID_FROM_STEP_2>
  AND DATA_OBJECT_ID = <OLD_DATA_OBJECT_ID_FROM_STEP_2>;

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

Master Script:

/*

This is the most seamless approach. We will wrap the logic in a standard PL/SQL block.

How this works:

  1. Run it as is: It acts exactly like your "Health Check" query (Preview Mode). It prints the changes to the DBMS_OUTPUT window.

  2. Change one word: Change v_apply_changes to TRUE and run it again. It executes the Merge.

This guarantees that what you see in the report is exactly what gets applied.

The "All-in-One" Script

*/
SET SERVEROUTPUT ON;

DECLARE
    -- =============================================================
    -- 1. CONFIGURATION SECTION
    -- =============================================================
    v_pur_id         NUMBER       := 279;             -- Your Group ID
    v_schema_owner   VARCHAR2(50) := 'YOUR_SCHEMA';   -- Your Schema
    
    -- OPTIONAL FILTERS (Leave NULL to ignore)
    v_table_filter   VARCHAR2(50) := NULL;            -- e.g. 'MY_BIG_TABLE'
    v_part_filter    VARCHAR2(50) := NULL;            -- e.g. 'P_2025_JAN'
    
    -- ACTION: FALSE = Preview, TRUE = Execute
    v_apply_changes  BOOLEAN      := FALSE;           
    -- =============================================================

    v_rows_merged    NUMBER := 0;
BEGIN
    DBMS_OUTPUT.PUT_LINE('--------------------------------------------------');
    DBMS_OUTPUT.PUT_LINE('MODE:      ' || CASE WHEN v_apply_changes THEN 'EXECUTE' ELSE 'PREVIEW' END);
    DBMS_OUTPUT.PUT_LINE('TABLE:     ' || NVL(v_table_filter, 'ALL'));
    DBMS_OUTPUT.PUT_LINE('PARTITION: ' || NVL(v_part_filter, 'ALL'));
    DBMS_OUTPUT.PUT_LINE('--------------------------------------------------');

    -- =============================================================
    -- PART A: PREVIEW MODE
    -- =============================================================
    IF NOT v_apply_changes THEN
        FOR r IN (
            WITH captured_db AS (
                SELECT table_name, partition_name, object_id, data_object_id
                FROM db_object_id_captured
                WHERE pur_id = v_pur_id
                  AND (v_table_filter IS NULL OR table_name = v_table_filter)
                  AND (v_part_filter IS NULL OR partition_name = v_part_filter)
            ),
            live_db AS (
                SELECT o.object_name, o.subobject_name, o.object_type, o.object_id, o.data_object_id
                FROM all_objects o
                WHERE o.owner = v_schema_owner
                  AND o.object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
                  AND o.data_object_id IS NOT NULL -- COMPOSITE=NO Check
                  
                  -- Filters
                  AND (v_table_filter IS NULL OR o.object_name = v_table_filter)
                  AND (v_part_filter IS NULL OR o.subobject_name = v_part_filter)

                  -- Safety Join
                  AND o.object_name IN (SELECT DISTINCT table_name FROM captured_db)
            )
            SELECT 
                NVL(live.object_name, cap.table_name) AS t_name,
                NVL(live.subobject_name, cap.partition_name) AS p_name,
                CASE 
                    WHEN live.object_id IS NULL THEN 'ORPHAN (Safe to Delete)'
                    WHEN cap.object_id IS NULL THEN 'NEW (Will Insert)'
                    WHEN live.object_id != cap.object_id OR 
                         NVL(live.data_object_id,-1) != NVL(cap.data_object_id,-1) THEN 'DRIFT (Will Update)'
                    ELSE 'SYNCED'
                END AS status,
                cap.object_id as old_id, live.object_id as new_id
            FROM live_db live
            FULL OUTER JOIN captured_db cap
              ON live.object_name = cap.table_name
              AND NVL(live.subobject_name, '###') = NVL(cap.partition_name, '###')
            WHERE (live.object_id IS NULL) OR (cap.object_id IS NULL) 
               OR (live.object_id != cap.object_id) 
               OR (NVL(live.data_object_id,-1) != NVL(cap.data_object_id,-1))
            ORDER BY 1, 2
        ) LOOP
            DBMS_OUTPUT.PUT_LINE('[' || r.status || '] ' || r.t_name || ' : ' || r.p_name || 
                                 ' (Old: ' || r.old_id || ' -> New: ' || r.new_id || ')');
        END LOOP;
        
        DBMS_OUTPUT.PUT_LINE('--------------------------------------------------');

    -- =============================================================
    -- PART B: EXECUTE MODE
    -- =============================================================
    ELSE
        MERGE INTO db_object_id_captured target
        USING (
            SELECT 
                v_pur_id AS pur_id,
                o.object_id, 
                o.data_object_id, 
                o.object_name, 
                o.subobject_name, 
                o.object_type
            FROM all_objects o
            WHERE o.owner = v_schema_owner
              AND o.object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
              AND o.data_object_id IS NOT NULL 
              
              -- Apply Filters
              AND (v_table_filter IS NULL OR o.object_name = v_table_filter)
              AND (v_part_filter IS NULL OR o.subobject_name = v_part_filter)

              AND o.object_name IN (
                  SELECT DISTINCT table_name FROM db_object_id_captured WHERE pur_id = v_pur_id
              )
        ) source
        ON (
            target.pur_id          = source.pur_id
            AND target.table_name  = source.object_name
            AND NVL(target.partition_name, '###') = NVL(source.subobject_name, '###')
            AND target.object_type = source.object_type
        )
        WHEN MATCHED THEN
            UPDATE SET 
                target.object_id      = source.object_id,
                target.data_object_id = source.data_object_id,
                target.last_updated   = SYSDATE
            WHERE DECODE(target.object_id, source.object_id, 0, 1) = 1 
               OR DECODE(target.data_object_id, source.data_object_id, 0, 1) = 1
        WHEN NOT MATCHED THEN
            INSERT (pur_id, table_name, partition_name, object_type, object_id, data_object_id, last_updated)
            VALUES (source.pur_id, source.object_name, source.subobject_name, source.object_type, source.object_id, source.data_object_id, SYSDATE);
            
        v_rows_merged := SQL%ROWCOUNT;
        COMMIT;
        DBMS_OUTPUT.PUT_LINE('SUCCESS. Rows Synced: ' || v_rows_merged);
    END IF;
END;
/

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

Script 2

/* Here is the updated Robust Health Check script.

I have decoupled the filter logic. Now, the captured_db block is completely independent, allowing you to manually type whatever WHERE clause you need (single ID, multiple IDs, date ranges, etc.).

The script is smart enough to use your manual list to filter the live_db query automatically, keeping performance high.

*/


WITH 
-- =================================================================
-- 1. YOUR MANUAL FILTER (Edit this section)
-- =================================================================
captured_db AS (
    SELECT 
        table_name, 
        partition_name, 
        object_type, 
        object_id, 
        data_object_id
    FROM db_object_id_captured
    WHERE pur_id = 279  -- <--- MANUALLY EDIT YOUR CONDITION HERE
    -- Examples: 
    -- WHERE pur_id IN (279, 280)
    -- WHERE table_name LIKE 'STG_%'
),

-- =================================================================
-- 2. LIVE DB FETCH (Automatically looks at tables from step 1)
-- =================================================================
live_db AS (
    SELECT 
        o.object_name, 
        o.subobject_name, 
        o.object_type, 
        o.object_id, 
        o.data_object_id
    FROM all_objects o
    WHERE o.owner = 'YOUR_SCHEMA_NAME' -- <--- UPDATE SCHEMA OWNER
      AND o.object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
      AND o.data_object_id IS NOT NULL -- Matches "COMPOSITE='NO'" (Physical segments only)
      
      -- OPTIMIZATION: Only looks for tables found in your manual list above
      AND o.object_name IN (SELECT DISTINCT table_name FROM captured_db)
)

-- =================================================================
-- 3. THE COMPARISON REPORT (Full Health Check)
-- =================================================================
SELECT 
    NVL(live.object_name, cap.table_name) AS TABLE_NAME,
    NVL(live.subobject_name, cap.partition_name) AS PARTITION_NAME,
    
    CASE 
        WHEN live.object_id IS NULL THEN 'ORPHAN (In Table, Deleted from DB)'
        WHEN cap.object_id IS NULL THEN 'NEW (In DB, Missing in Table)'
        WHEN live.object_id != cap.object_id OR 
             NVL(live.data_object_id, -1) != NVL(cap.data_object_id, -1) 
             THEN 'DRIFT (IDs Changed)'
        ELSE 'SYNCED'
    END AS STATUS,

    cap.object_id AS OLD_ID,
    live.object_id AS NEW_ID
    
FROM live_db live
FULL OUTER JOIN captured_db cap
  ON live.object_name = cap.table_name
  AND NVL(live.subobject_name, '###') = NVL(cap.partition_name, '###')
  AND live.object_type = cap.object_type

-- Optional: Filter to show only issues
WHERE (live.object_id IS NULL) 
   OR (cap.object_id IS NULL) 
   OR (live.object_id != cap.object_id)
   OR (NVL(live.data_object_id, -1) != NVL(cap.data_object_id, -1))

ORDER BY 1, 2;

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

Script 3:
======

Part 1: The Preview Report (ReadOnly)

Run this query first. It joins your table with the live database using the COMPOSITE='NO' logic (Physical Segments only) and tells you the status of every object.

SQL

WITH config AS (
    SELECT 279 AS filter_pur_id, 'YOUR_SCHEMA_NAME' AS filter_owner FROM dual
),
live_data AS (
    -- Equivalent to your "Composite = NO" query, but faster
    SELECT object_name, subobject_name, object_type, object_id, data_object_id
    FROM all_objects, config
    WHERE owner = config.filter_owner
      AND object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
      AND data_object_id IS NOT NULL -- This filters out COMPOSITE partitions
),
captured_data AS (
    SELECT table_name, partition_name, object_type, object_id, data_object_id
    FROM db_object_id_captured, config
    WHERE pur_id = config.filter_pur_id
)
SELECT 
    NVL(live.object_name, cap.table_name) AS TABLE_NAME,
    NVL(live.subobject_name, cap.partition_name) AS PARTITION_NAME,
    
    -- LIVE VALUES
    live.object_id AS LIVE_OBJ_ID,
    live.data_object_id AS LIVE_DATA_ID,
    
    -- CAPTURED VALUES
    cap.object_id AS DB_CAPTURED_OBJ_ID,
    cap.data_object_id AS DB_CAPTURED_DATA_ID,
    
    -- STATUS CALCULATION
    CASE 
        WHEN cap.object_id IS NULL THEN 'NEW (Insert Needed)'
        WHEN live.object_id IS NULL THEN 'ORPHAN (Delete Needed)'
        WHEN live.object_id != cap.object_id OR 
             NVL(live.data_object_id, -1) != NVL(cap.data_object_id, -1) 
             THEN 'NEEDS TO BE UPDATED'
        ELSE 'THEY ARE IN SYNC'
    END AS STATUS
FROM live_data live
FULL OUTER JOIN captured_data cap
  ON live.object_name = cap.table_name
  AND NVL(live.subobject_name, '###') = NVL(cap.partition_name, '###')
  AND live.object_type = cap.object_type
WHERE 
    -- Filter to show only specific table if needed, or leave commented for all
    -- live.object_name = 'YOUR_TABLE_NAME' AND 
    (live.object_id != cap.object_id OR 
     NVL(live.data_object_id, -1) != NVL(cap.data_object_id, -1) OR 
     cap.object_id IS NULL OR live.object_id IS NULL)
ORDER BY 1, 2;


====

This is the "Best of the Best" approach. It strictly follows your logic (validating COMPOSITE='NO') and gives you exactly what you asked for:

  1. Part 1: A clean Report (Preview) showing the comparison side-by-side.

  2. Part 2: A generator that writes the SQL statements for you. It produces the exact UPDATE scripts with the specific WHERE clauses you requested.


Part 1: The Preview Report (ReadOnly)

Run this query first. It joins your table with the live database using the COMPOSITE='NO' logic (Physical Segments only) and tells you the status of every object.

SQL
WITH config AS (
    SELECT 279 AS filter_pur_id, 'YOUR_SCHEMA_NAME' AS filter_owner FROM dual
),
live_data AS (
    -- Equivalent to your "Composite = NO" query, but faster
    SELECT object_name, subobject_name, object_type, object_id, data_object_id
    FROM all_objects, config
    WHERE owner = config.filter_owner
      AND object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
      AND data_object_id IS NOT NULL -- This filters out COMPOSITE partitions
),
captured_data AS (
    SELECT table_name, partition_name, object_type, object_id, data_object_id
    FROM db_object_id_captured, config
    WHERE pur_id = config.filter_pur_id
)
SELECT 
    NVL(live.object_name, cap.table_name) AS TABLE_NAME,
    NVL(live.subobject_name, cap.partition_name) AS PARTITION_NAME,
    
    -- LIVE VALUES
    live.object_id AS LIVE_OBJ_ID,
    live.data_object_id AS LIVE_DATA_ID,
    
    -- CAPTURED VALUES
    cap.object_id AS DB_CAPTURED_OBJ_ID,
    cap.data_object_id AS DB_CAPTURED_DATA_ID,
    
    -- STATUS CALCULATION
    CASE 
        WHEN cap.object_id IS NULL THEN 'NEW (Insert Needed)'
        WHEN live.object_id IS NULL THEN 'ORPHAN (Delete Needed)'
        WHEN live.object_id != cap.object_id OR 
             NVL(live.data_object_id, -1) != NVL(cap.data_object_id, -1) 
             THEN 'NEEDS TO BE UPDATED'
        ELSE 'THEY ARE IN SYNC'
    END AS STATUS
FROM live_data live
FULL OUTER JOIN captured_data cap
  ON live.object_name = cap.table_name
  AND NVL(live.subobject_name, '###') = NVL(cap.partition_name, '###')
  AND live.object_type = cap.object_type
WHERE 
    -- Filter to show only specific table if needed, or leave commented for all
    -- live.object_name = 'YOUR_TABLE_NAME' AND 
    (live.object_id != cap.object_id OR 
     NVL(live.data_object_id, -1) != NVL(cap.data_object_id, -1) OR 
     cap.object_id IS NULL OR live.object_id IS NULL)
ORDER BY 1, 2;

Part 2: The SQL Generator (The "Fix" Script)

This PL/SQL block does not execute changes. Instead, it prints the specific UPDATE statements you asked for to the "Dbms Output" window. You can then copy-paste them and run them manually.

  • It checks COMPOSITE='NO' logic.

  • It includes the Old Values in the WHERE clause as requested.


    SET SERVEROUTPUT ON SIZE 100000;
    DECLARE
        v_pur_id       NUMBER       := 279;
        v_schema_owner VARCHAR2(50) := 'YOUR_SCHEMA_NAME';
    BEGIN
        DBMS_OUTPUT.PUT_LINE('-- GENERATED UPDATE STATEMENTS (INCLUDES SUBPARTITIONS) --');
        
        FOR r IN (
            SELECT 
                live.object_name,
                live.subobject_name,
                live.object_type,
                -- New Values (From Live DB)
                live.object_id AS new_obj_id,
                live.data_object_id AS new_data_id,
                -- Old Values (From Your Table)
                cap.object_id AS old_obj_id,
                cap.data_object_id AS old_data_id
            FROM all_objects live
            JOIN db_object_id_captured cap
              ON live.object_name = cap.table_name
              -- Matches Partition OR Subpartition Name automatically
              AND NVL(live.subobject_name, '###') = NVL(cap.partition_name, '###')
              AND live.object_type = cap.object_type
            WHERE live.owner = v_schema_owner
              AND cap.pur_id = v_pur_id
              AND live.data_object_id IS NOT NULL -- Composite=NO Logic
              AND live.object_type IN ('TABLE', 'TABLE PARTITION', 'TABLE SUBPARTITION')
              
              -- CRITICAL: Only generate updates if IDs drifted
              AND (live.object_id != cap.object_id OR 
                   NVL(live.data_object_id, -1) != NVL(cap.data_object_id, -1))
            ORDER BY live.object_name, live.subobject_name
        ) LOOP
            -- 1. HEADER
            DBMS_OUTPUT.PUT_LINE('UPDATE DB_OBJECT_ID_CAPTURED');
            DBMS_OUTPUT.PUT_LINE('SET OBJECT_ID = ' || r.new_obj_id || 
                                 ', DATA_OBJECT_ID = ' || r.new_data_id);
            
            -- 2. IDENTIFIERS (The "Where" Clause)
            DBMS_OUTPUT.PUT_LINE('WHERE PUR_ID = ' || v_pur_id);
            DBMS_OUTPUT.PUT_LINE('  AND TABLE_NAME = ''' || r.object_name || '''');
            
            -- Extended Logic: Explicitly handle Partitions vs Subpartitions in the WHERE clause
            IF r.subobject_name IS NOT NULL THEN
                 DBMS_OUTPUT.PUT_LINE('  AND PARTITION_NAME = ''' || r.subobject_name || '''');
            ELSE
                 DBMS_OUTPUT.PUT_LINE('  AND PARTITION_NAME IS NULL');
            END IF;
            -- [NEW] Explicitly lock the Object Type (Table vs Partition vs Subpartition)
            DBMS_OUTPUT.PUT_LINE('  AND OBJECT_TYPE = ''' || r.object_type || '''');
            -- 3. SAFETY CHECK (Old Values)
            DBMS_OUTPUT.PUT_LINE('  AND OBJECT_ID = ' || r.old_obj_id);
            
            IF r.old_data_id IS NOT NULL THEN
                DBMS_OUTPUT.PUT_LINE('  AND DATA_OBJECT_ID = ' || r.old_data_id || ';');
            ELSE
                DBMS_OUTPUT.PUT_LINE('  AND DATA_OBJECT_ID IS NULL;');
            END IF;
            DBMS_OUTPUT.PUT_LINE('------------------------------------------');
        END LOOP;
    END;
    /