Query Plan Display (qplano.sql)
Script
Section titled “Script”rem qplan.sqlremaccept query char prompt 'Query Name: 'remttitle 'Query Plan'remset lines 132col plan format a132remselect lpad( ' ', 2 * ( level - 1 ) ) || operation || ' ' || options || ' ' || object_owner || decode(object_owner,null, null,'.') || object_name || ' ' || object_type || decode(optimizer, null, null, ' optimizer: ')|| optimizer|| decode(cost, null, null, ' [cost=')||cost|| decode(cardinality, null, null, decode(cost, null, ' [card=', ' card='))||cardinality|| decode(cost, null, decode(cardinality, null, null, ']'), ']') plan , other from plan_table where statement_id = '&query'connect by prior id = parent_id and prior statement_id = statement_id start with id =0order by id;remrollback;undefine queryset lines 80
What This Script Does
Section titled “What This Script Does”This script displays formatted execution plans from Oracle’s PLAN_TABLE, presenting query execution strategies in a hierarchical tree structure with cost and cardinality information. It’s designed to help DBAs and developers understand how Oracle plans to execute their SQL queries.
SQL> @qplano.sqlQuery Name: MY_QUERY_PLAN
Query Plan--------------------------------------------------------------------------------SELECT STATEMENT [cost=150 card=100] HASH JOIN [cost=150 card=100] TABLE ACCESS FULL SCOTT.EMP [cost=3 card=14] TABLE ACCESS BY INDEX ROWID SCOTT.DEPT [cost=2 card=100] INDEX RANGE SCAN SCOTT.DEPT_IDX [cost=1 card=100]
Prerequisites
Section titled “Prerequisites”1. PLAN_TABLE Must Exist
Section titled “1. PLAN_TABLE Must Exist”-- Create PLAN_TABLE if it doesn't exist@$ORACLE_HOME/rdbms/admin/utlxplan.sql
2. Generate Execution Plan First
Section titled “2. Generate Execution Plan First”-- Example: Generate a plan for your queryEXPLAIN PLANSET STATEMENT_ID = 'MY_QUERY_PLAN'FORSELECT e.ename, d.dnameFROM emp e, dept dWHERE e.deptno = d.deptno;
Required Privileges
Section titled “Required Privileges”SELECT
privilege on PLAN_TABLE- Ability to run EXPLAIN PLAN (typically granted through CONNECT role)
Parameters
Section titled “Parameters”- Query Name: The STATEMENT_ID used when running EXPLAIN PLAN (case-sensitive)
Key Features
Section titled “Key Features”1. Hierarchical Display
Section titled “1. Hierarchical Display”- Indentation shows parent-child relationships
- Each level indented by 2 spaces
- Visual representation of execution order
2. Operation Details
Section titled “2. Operation Details”- OPERATION: The type of operation (e.g., TABLE ACCESS, INDEX SCAN)
- OPTIONS: Additional operation details (e.g., FULL, RANGE SCAN)
- OBJECT_OWNER.OBJECT_NAME: Schema and object being accessed
- OBJECT_TYPE: Type of object (TABLE, INDEX, etc.)
3. Cost Information
Section titled “3. Cost Information”- Cost: Estimated resource usage (lower is better)
- Cardinality: Estimated number of rows returned
- Optimizer: Optimizer mode used (if specified)
Understanding Execution Plans
Section titled “Understanding Execution Plans”Reading the Hierarchy
Section titled “Reading the Hierarchy”SELECT STATEMENT -- Top level (executed last) NESTED LOOPS -- Join method TABLE ACCESS FULL EMP -- First child (executed first) TABLE ACCESS BY INDEX ROWID DEPT -- Second child INDEX UNIQUE SCAN DEPT_PK -- Grandchild (executed before parent)
Common Operations
Section titled “Common Operations”-
Table Access Methods
TABLE ACCESS FULL
: Full table scanTABLE ACCESS BY INDEX ROWID
: Row lookup using ROWID from indexTABLE ACCESS CLUSTER
: Access clustered table
-
Index Operations
INDEX UNIQUE SCAN
: Single row lookupINDEX RANGE SCAN
: Multiple rows via indexINDEX FULL SCAN
: Read entire indexINDEX FAST FULL SCAN
: Multiblock read of entire index
-
Join Methods
NESTED LOOPS
: Best for small result setsHASH JOIN
: Best for large result sets with equi-joinsMERGE JOIN
: Best for pre-sorted dataCARTESIAN
: Usually indicates missing join condition
Performance Analysis Tips
Section titled “Performance Analysis Tips”1. Check for Full Table Scans
Section titled “1. Check for Full Table Scans”-- Look for TABLE ACCESS FULL on large tables-- Consider adding indexes if selective predicates exist
2. Evaluate Join Order
Section titled “2. Evaluate Join Order”-- Smaller tables should typically be accessed first-- Check cardinality estimates vs. actual row counts
3. Cost vs. Actual Performance
Section titled “3. Cost vs. Actual Performance”-- Cost is an estimate - always test with actual data-- Use SQL trace for real execution statistics
Common Use Cases
Section titled “Common Use Cases”1. Query Tuning
Section titled “1. Query Tuning”-- Compare plans before and after adding indexesEXPLAIN PLAN SET STATEMENT_ID = 'BEFORE_INDEX' FORSELECT * FROM large_table WHERE status = 'ACTIVE';
-- Create indexCREATE INDEX idx_status ON large_table(status);
EXPLAIN PLAN SET STATEMENT_ID = 'AFTER_INDEX' FORSELECT * FROM large_table WHERE status = 'ACTIVE';
-- Compare both plans@qplano.sql -- Run for BEFORE_INDEX@qplano.sql -- Run for AFTER_INDEX
2. Join Order Analysis
Section titled “2. Join Order Analysis”EXPLAIN PLAN SET STATEMENT_ID = 'JOIN_ANALYSIS' FORSELECT /*+ LEADING(small_table) */ *FROM small_table s, large_table lWHERE s.id = l.small_id;
@qplano.sql
3. Index Usage Verification
Section titled “3. Index Usage Verification”EXPLAIN PLAN SET STATEMENT_ID = 'INDEX_CHECK' FORSELECT * FROM employeesWHERE department_id = 50AND salary > 5000;
@qplano.sql-- Verify both conditions use indexes
Troubleshooting
Section titled “Troubleshooting”Issue: “no rows selected”
Section titled “Issue: “no rows selected””Cause: Statement ID doesn’t exist in PLAN_TABLE
-- Check available statement IDsSELECT DISTINCT statement_idFROM plan_tableORDER BY timestamp DESC;
Issue: Plan looks different than expected
Section titled “Issue: Plan looks different than expected”Cause: Statistics may be stale
-- Gather fresh statisticsEXEC DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT', tabname => 'EMP');
Issue: Missing cost/cardinality information
Section titled “Issue: Missing cost/cardinality information”Cause: Using RULE-based optimizer or no statistics
-- Check optimizer modeSHOW PARAMETER optimizer_mode
-- Ensure statistics existSELECT table_name, last_analyzedFROM user_tablesWHERE table_name = 'YOUR_TABLE';
Best Practices
Section titled “Best Practices”1. Use Meaningful Statement IDs
Section titled “1. Use Meaningful Statement IDs”-- Include timestamp or descriptionEXPLAIN PLANSET STATEMENT_ID = 'CUSTOMER_REPORT_20240115'FOR SELECT ...;
2. Clean Up PLAN_TABLE Regularly
Section titled “2. Clean Up PLAN_TABLE Regularly”-- Delete old execution plansDELETE FROM plan_tableWHERE timestamp < SYSDATE - 7;COMMIT;
3. Compare Multiple Plans
Section titled “3. Compare Multiple Plans”-- Use different statement IDs for variationsEXPLAIN PLAN SET STATEMENT_ID = 'WITH_HINT' FORSELECT /*+ INDEX(emp emp_idx) */ * FROM emp WHERE ...;
EXPLAIN PLAN SET STATEMENT_ID = 'WITHOUT_HINT' FORSELECT * FROM emp WHERE ...;
Advanced Usage
Section titled “Advanced Usage”Viewing Specific Plan Steps
Section titled “Viewing Specific Plan Steps”-- Query specific operations from a planSELECT operation, options, cost, cardinalityFROM plan_tableWHERE statement_id = 'MY_PLAN'AND operation = 'TABLE ACCESS'ORDER BY id;
Analyzing Parallel Execution
Section titled “Analyzing Parallel Execution”-- Look for parallel operations in OTHER columnSELECT id, operation, otherFROM plan_tableWHERE statement_id = 'PARALLEL_QUERY'AND other LIKE '%PARALLEL%';
Related Scripts
Section titled “Related Scripts”- Explain Plan for SQL ID - Generate plans for cached SQL
- Query Plan - Alternative plan display format
- SQL Plan Find - Find plans in shared pool
- Extended Plan - Display plans with runtime statistics
- The script includes a ROLLBACK to ensure no transaction is left open
- Line size is temporarily set to 132 for better formatting
- The hierarchical query uses CONNECT BY to build the tree structure
- Statement IDs are case-sensitive when querying PLAN_TABLE