sql_perf_analyzer.sh


#!/bin/bash

# Filename: sql_perf_analyzer.sh

# Description: Intelligent SQL Performance Diagnostic Toolkit (Shell Wrapper)

# Author: ChatGPT Toolkit for Oracle


# === Setup Variables ===

SCRIPT_DIR=$(dirname "$0")

SQL_DIR="$SCRIPT_DIR/sql"

LOG_DIR="$SCRIPT_DIR/output/logs"

REPORT_DIR="$SCRIPT_DIR/output/reports"

DATE_TAG=$(date '+%Y%m%d_%H%M%S')

LOG_FILE="$LOG_DIR/sql_perf_analysis_$DATE_TAG.log"


mkdir -p "$LOG_DIR" "$REPORT_DIR"


# === Prompt Inputs ===

echo "Enter SQL_ID to diagnose: "

read SQL_ID


echo "Enter GOOD timeframe (start time YYYY-MM-DD HH24:MI): "

read GOOD_START


echo "Enter GOOD timeframe (end time YYYY-MM-DD HH24:MI): "

read GOOD_END


echo "Enter BAD timeframe (start time YYYY-MM-DD HH24:MI): "

read BAD_START


echo "Enter BAD timeframe (end time YYYY-MM-DD HH24:MI): "

read BAD_END


echo "Output format? (1=Text, 2=HTML, 3=Both): "

read FORMAT


# === Begin Logging ===

echo "[INFO] Starting SQL Performance Diagnostics..." | tee -a "$LOG_FILE"

echo "[INFO] SQL_ID: $SQL_ID" | tee -a "$LOG_FILE"

echo "[INFO] GOOD: $GOOD_START to $GOOD_END" | tee -a "$LOG_FILE"

echo "[INFO] BAD : $BAD_START to $BAD_END" | tee -a "$LOG_FILE"


# === Run Diagnostic SQL Scripts ===

for script in \

  check_plan_flips.sql \

  compare_plan_steps.sql \

  analyze_wait_events.sql \

  check_object_stats.sql \

  run_advisors.sql \

  suggest_baseline.sql

  do

  echo "[INFO] Running $script ..." | tee -a "$LOG_FILE"


  sqlplus -s / as sysdba <<EOF >> "$LOG_FILE"

  SET DEFINE ON

  DEFINE sql_id='$SQL_ID'

  DEFINE good_start="$GOOD_START"

  DEFINE good_end="$GOOD_END"

  DEFINE bad_start="$BAD_START"

  DEFINE bad_end="$BAD_END"

  SPOOL "$REPORT_DIR/${script%.sql}_$DATE_TAG.txt"

  @"$SQL_DIR/$script"

  SPOOL OFF

  EXIT

EOF


done


# === Wrap-up ===

echo "[INFO] Analysis Complete. Reports saved under $REPORT_DIR" | tee -a "$LOG_FILE"

echo "[INFO] Log saved to $LOG_FILE"


if [ "$FORMAT" = "2" ] || [ "$FORMAT" = "3" ]; then

  echo "[INFO] Generating HTML Summary..." | tee -a "$LOG_FILE"

  echo "<html><head><title>SQL Performance Report</title></head><body>" > "$REPORT_DIR/final_report_$DATE_TAG.html"

  for f in "$REPORT_DIR"/*_$DATE_TAG.txt; do

    echo "<h2>Report: $(basename "$f")</h2><pre>" >> "$REPORT_DIR/final_report_$DATE_TAG.html"

    cat "$f" >> "$REPORT_DIR/final_report_$DATE_TAG.html"

    echo "</pre><hr>" >> "$REPORT_DIR/final_report_$DATE_TAG.html"

  done

  echo "</body></html>" >> "$REPORT_DIR/final_report_$DATE_TAG.html"

fi


exit 0

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


-- Script: analyze_wait_events.sql

-- Purpose: Analyze wait events for a given SQL_ID during bad execution timeframe using ASH

-- RAC-aware, CDB/PDB-compliant


SET LINES 200 PAGES 1000 TRIMSPOOL ON

SET VERIFY OFF


COLUMN inst_id FORMAT 999

COLUMN event FORMAT A50

COLUMN wait_class FORMAT A20

COLUMN total_secs FORMAT 999999


PROMPT Analyzing wait events for SQL_ID: &sql_id during BAD timeframe...


SELECT ash.inst_id,

       ash.event,

       ash.wait_class,

       COUNT(*) * 10 AS total_secs

  FROM gv$active_session_history ash

 WHERE ash.sql_id = '&sql_id'

   AND ash.sample_time BETWEEN TO_DATE('&bad_start', 'YYYY-MM-DD HH24:MI')

                           AND TO_DATE('&bad_end', 'YYYY-MM-DD HH24:MI')

   AND ash.wait_class NOT IN ('Idle')

 GROUP BY ash.inst_id, ash.event, ash.wait_class

 ORDER BY total_secs DESC;


PROMPT Wait event analysis completed.

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

-- Script: compare_plan_steps.sql
-- Purpose: Compare execution plans from good and bad timeframes for a SQL_ID
-- RAC-aware, PDB-aware

SET LINES 200 PAGES 1000 TRIMSPOOL ON
SET VERIFY OFF

COLUMN inst_id FORMAT 999
COLUMN id FORMAT 999
COLUMN operation FORMAT A30
COLUMN options FORMAT A20
COLUMN object_name FORMAT A30
COLUMN object_type FORMAT A20
COLUMN cost FORMAT 999999
COLUMN cardinality FORMAT 9999999
COLUMN bytes FORMAT 9999999

PROMPT Fetching GOOD plan steps...
SELECT inst_id, id, plan_hash_value, operation, options,
       object_name, object_type, cost, cardinality, bytes
  FROM   gv$sql_plan
 WHERE  sql_id = '&sql_id'
   AND  plan_hash_value = (
          SELECT plan_hash_value FROM (
            SELECT plan_hash_value,
                   COUNT(*) AS freq
              FROM dba_hist_sqlstat h
              JOIN dba_hist_snapshot s
                ON h.snap_id = s.snap_id AND h.dbid = s.dbid
             WHERE h.sql_id = '&sql_id'
               AND s.begin_interval_time BETWEEN TO_DATE('&good_start','YYYY-MM-DD HH24:MI') AND TO_DATE('&good_end','YYYY-MM-DD HH24:MI')
             GROUP BY plan_hash_value
             ORDER BY freq DESC
          ) WHERE ROWNUM = 1
        );

PROMPT -------------------------------
PROMPT Fetching BAD plan steps...
SELECT inst_id, id, plan_hash_value, operation, options,
       object_name, object_type, cost, cardinality, bytes
  FROM   gv$sql_plan
 WHERE  sql_id = '&sql_id'
   AND  plan_hash_value = (
          SELECT plan_hash_value FROM (
            SELECT plan_hash_value,
                   COUNT(*) AS freq
              FROM dba_hist_sqlstat h
              JOIN dba_hist_snapshot s
                ON h.snap_id = s.snap_id AND h.dbid = s.dbid
             WHERE h.sql_id = '&sql_id'
               AND s.begin_interval_time BETWEEN TO_DATE('&bad_start','YYYY-MM-DD HH24:MI') AND TO_DATE('&bad_end','YYYY-MM-DD HH24:MI')
             GROUP BY plan_hash_value
             ORDER BY freq DESC
          ) WHERE ROWNUM = 1
        );

PROMPT Plan comparison completed.

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

-- Script: check_plan_flips.sql
-- Purpose: Detect execution plan changes (plan flips) for a given SQL_ID over time
-- RAC-aware and CDB/PDB compliant

SET VERIFY OFF
SET LINES 200 PAGES 1000 TRIMSPOOL ON
COLUMN inst FORMAT 999
COLUMN plan_hash_value FORMAT 9999999999
COLUMN first_seen FORMAT A20
COLUMN last_seen FORMAT A20

PROMPT Checking for plan changes for SQL_ID: &sql_id

SELECT s.inst_id AS inst,
       p.plan_hash_value,
       MIN(s.begin_interval_time) AS first_seen,
       MAX(s.end_interval_time) AS last_seen,
       COUNT(*) AS snap_count
  FROM   dba_hist_sqlstat p
  JOIN   dba_hist_snapshot s
         ON p.snap_id = s.snap_id AND p.dbid = s.dbid AND p.instance_number = s.instance_number
 WHERE  p.sql_id = '&sql_id'
 GROUP BY s.inst_id, p.plan_hash_value
 ORDER BY first_seen;

PROMPT Plan flip analysis completed.

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

-- Script: check_object_stats.sql
-- Purpose: Check stats, status, and staleness for tables, partitions, indexes used in a SQL_ID
-- RAC-aware, CDB/PDB-compliant

SET VERIFY OFF
SET LINES 200 PAGES 1000 TRIMSPOOL ON

PROMPT Identifying objects used in SQL_ID &sql_id ...

-- Create temporary table for object list (for reuse)
CREATE GLOBAL TEMPORARY TABLE sql_obj_list (
  object_owner VARCHAR2(128),
  object_name  VARCHAR2(128),
  object_type  VARCHAR2(128)
) ON COMMIT PRESERVE ROWS;

-- Extract objects from GV$SQL_PLAN
INSERT INTO sql_obj_list
SELECT DISTINCT object_owner, object_name, object_type
FROM   gv$sql_plan
WHERE  sql_id = '&sql_id'
  AND  object_name IS NOT NULL;

COMMIT;

-- Check table stats
PROMPT ---- Table Statistics and Status ----
SELECT t.owner,
       t.table_name,
       t.last_analyzed,
       s.stale_stats,
       s.num_rows,
       s.blocks,
       s.avg_row_len,
       s.global_stats,
       s.user_stats
FROM   sql_obj_list o
JOIN   dba_tables t ON t.owner = o.object_owner AND t.table_name = o.object_name
LEFT JOIN dba_tab_statistics s ON s.owner = t.owner AND s.table_name = t.table_name;

-- Check index stats
PROMPT ---- Index Statistics and Status ----
SELECT i.owner,
       i.index_name,
       i.status,
       i.last_analyzed,
       i.blevel,
       i.leaf_blocks,
       i.distinct_keys,
       i.clustering_factor
FROM   sql_obj_list o
JOIN   dba_indexes i ON i.table_owner = o.object_owner AND i.table_name = o.object_name;

-- Check partitions if any
PROMPT ---- Partition Stats (if any) ----
SELECT table_owner, table_name, partition_name, last_analyzed, num_rows, blocks
FROM   dba_tab_partitions
WHERE  table_owner IN (SELECT DISTINCT object_owner FROM sql_obj_list)
AND    table_name IN (SELECT DISTINCT object_name FROM sql_obj_list);

-- Cleanup
TRUNCATE TABLE sql_obj_list;
DROP TABLE sql_obj_list;

PROMPT Object statistics check completed.

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

-- Script: run_advisors.sql
-- Purpose: Run Access, Undo, and SQL Tuning Advisor for a given SQL_ID
-- RAC-aware, CDB/PDB-safe, must be run with SYS or tuning privileges

SET SERVEROUTPUT ON
SET VERIFY OFF
SET LINES 200 PAGES 1000

PROMPT Starting advisory diagnostics for SQL_ID: &sql_id

DECLARE
  l_sql_id        VARCHAR2(20) := '&sql_id';
  l_task_name     VARCHAR2(30);
  l_task_desc     VARCHAR2(100);
BEGIN
  -- SQL Tuning Advisor
  l_task_name := 'SQL_TUNE_' || l_sql_id;
  l_task_desc := 'Tuning SQL ' || l_sql_id;

  BEGIN
    dbms_sqltune.drop_tuning_task(l_task_name);
  EXCEPTION WHEN OTHERS THEN NULL;
  END;

  l_task_name := dbms_sqltune.create_tuning_task(
                   sql_id       => l_sql_id,
                   scope        => 'COMPREHENSIVE',
                   time_limit   => 1800,
                   task_name    => l_task_name,
                   description  => l_task_desc);

  dbms_sqltune.execute_tuning_task(task_name => l_task_name);

  dbms_output.put_line('Tuning task created and executed: ' || l_task_name);
  dbms_output.put_line('View with: SELECT * FROM TABLE(DBMS_SQLTUNE.REPORT_TUNING_TASK(''' || l_task_name || '''));');

EXCEPTION
  WHEN OTHERS THEN
    dbms_output.put_line('Error running tuning advisor: ' || SQLERRM);
END;
/

PROMPT --- Access Advisor Suggestion via SQL Tuning Set ---
DECLARE
  sts_name   VARCHAR2(30) := 'STS_' || '&sql_id';
  task_name  VARCHAR2(30) := 'ACC_ADV_' || '&sql_id';
BEGIN
  -- Clean up
  BEGIN dbms_sqltune.drop_sqlset(sts_name); EXCEPTION WHEN OTHERS THEN NULL; END;
  BEGIN dbms_advisor.delete_task(task_name); EXCEPTION WHEN OTHERS THEN NULL; END;

  -- Create SQL Tuning Set and load SQL
  dbms_sqltune.create_sqlset(sts_name);
  dbms_sqltune.capture_cursor_cache_sqlset(
    sqlset_name => sts_name,
    basic_filter => 'sql_id = ''' || '&sql_id' || '''',
    time_limit => 30);

  -- Run Access Advisor
  dbms_advisor.quick_tune(
    advisor_name => 'Access Advisor',
    task_name    => task_name,
    attr1        => sts_name);

  dbms_output.put_line('Access Advisor Task: ' || task_name);
  dbms_output.put_line('View: SELECT * FROM DBA_ADVISOR_RECOMMENDATIONS WHERE task_name = ''' || task_name || '''');

EXCEPTION
  WHEN OTHERS THEN
    dbms_output.put_line('Access Advisor Error: ' || SQLERRM);
END;
/

PROMPT --- Undo Advisory (if needed) ---
SELECT name, value, isdefault
  FROM v$parameter
 WHERE name IN ('undo_retention','undo_tablespace');

SELECT BEGIN_TIME, UNDOTSN, UNDOBLKS, TXNCOUNT, MAXQUERYLEN
  FROM   v$undostat
 WHERE  BEGIN_TIME BETWEEN TO_DATE('&bad_start','YYYY-MM-DD HH24:MI') AND TO_DATE('&bad_end','YYYY-MM-DD HH24:MI')
 ORDER BY BEGIN_TIME;

PROMPT Advisory diagnostics completed.

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

-- Script: suggest_baseline.sql
-- Purpose: Check for existing SQL Baselines for a SQL_ID and optionally create one for good plan
-- RAC-aware, CDB/PDB-safe

SET SERVEROUTPUT ON
SET VERIFY OFF
SET LINES 200 PAGES 1000

PROMPT Checking existing baselines for SQL_ID: &sql_id

SELECT sql_handle, plan_name, enabled, accepted, fixed
  FROM dba_sql_plan_baselines
 WHERE sql_text LIKE (SELECT sql_fulltext FROM gv$sql WHERE sql_id = '&sql_id' AND ROWNUM = 1);

PROMPT Attempting to capture good plan baseline...

DECLARE
  l_sql_id       VARCHAR2(20) := '&sql_id';
  l_phv          NUMBER;
  l_cnt          NUMBER;
BEGIN
  SELECT plan_hash_value INTO l_phv
    FROM (
           SELECT plan_hash_value, COUNT(*) freq
             FROM dba_hist_sqlstat h
             JOIN dba_hist_snapshot s
               ON h.snap_id = s.snap_id AND h.dbid = s.dbid
            WHERE h.sql_id = l_sql_id
              AND s.begin_interval_time BETWEEN TO_DATE('&good_start','YYYY-MM-DD HH24:MI')
                                           AND TO_DATE('&good_end','YYYY-MM-DD HH24:MI')
            GROUP BY plan_hash_value
            ORDER BY freq DESC
         ) WHERE ROWNUM = 1;

  SELECT COUNT(*) INTO l_cnt
    FROM dba_sql_plan_baselines
   WHERE sql_text LIKE (SELECT sql_fulltext FROM gv$sql WHERE sql_id = l_sql_id AND ROWNUM = 1)
     AND plan_hash_value = l_phv;

  IF l_cnt = 0 THEN
    DBMS_OUTPUT.put_line('No baseline found for best plan. Capturing now...');
    DBMS_SPM.load_plans_from_cursor_cache(sql_id => l_sql_id, plan_hash_value => l_phv);
  ELSE
    DBMS_OUTPUT.put_line('Baseline for best plan already exists.');
  END IF;

EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.put_line('Error creating baseline: ' || SQLERRM);
END;
/

PROMPT Baseline check completed.


Popular posts from this blog