Oracle Index Tuning Guide - Performance Optimization Best Practices
Oracle Index Tuning Guide
Section titled “Oracle Index Tuning Guide”Comprehensive guide to Oracle index analysis, optimization, and maintenance.
Index Usage Analysis
Section titled “Index Usage Analysis”Find Unused Indexes
Section titled “Find Unused Indexes”-- Enable index monitoring (per index)ALTER INDEX hr.emp_name_idx MONITORING USAGE;
-- Check if index was usedSELECT INDEX_NAME, TABLE_NAME, MONITORING, USED, START_MONITORING, END_MONITORINGFROM V$OBJECT_USAGEWHERE INDEX_NAME = 'EMP_NAME_IDX';
-- Disable monitoringALTER INDEX hr.emp_name_idx NOMONITORING USAGE;Index Usage from AWR (12c+)
Section titled “Index Usage from AWR (12c+)”-- Index usage statisticsSELECT owner, name AS index_name, total_access_count, total_exec_count, last_usedFROM DBA_INDEX_USAGEWHERE owner = 'HR'ORDER BY total_access_count DESC NULLS LAST;Index Analysis Queries
Section titled “Index Analysis Queries”Index Size and Statistics
Section titled “Index Size and Statistics”-- Index sizesSELECT owner, index_name, table_name, ROUND(bytes/1024/1024, 2) AS size_mb, num_rows, distinct_keys, clustering_factorFROM DBA_INDEXES iJOIN DBA_SEGMENTS s ON i.owner = s.owner AND i.index_name = s.segment_nameWHERE i.owner = 'HR'ORDER BY bytes DESC;Index Efficiency (Clustering Factor)
Section titled “Index Efficiency (Clustering Factor)”-- Compare clustering factor to table rowsSELECT i.index_name, i.table_name, i.num_rows AS index_rows, t.num_rows AS table_rows, i.clustering_factor, i.blevel, i.leaf_blocks, CASE WHEN i.clustering_factor < t.num_rows THEN 'Good' WHEN i.clustering_factor < t.blocks * 10 THEN 'Moderate' ELSE 'Poor' END AS efficiencyFROM DBA_INDEXES iJOIN DBA_TABLES t ON i.table_owner = t.owner AND i.table_name = t.table_nameWHERE i.owner = 'HR'ORDER BY i.clustering_factor DESC;Identifying Missing Indexes
Section titled “Identifying Missing Indexes”From SQL Tuning Advisor
Section titled “From SQL Tuning Advisor”-- Run SQL Tuning AdvisorDECLARE l_task_name VARCHAR2(30) := 'index_analysis';BEGIN l_task_name := DBMS_SQLTUNE.CREATE_TUNING_TASK( sql_id => '&sql_id', task_name => l_task_name ); DBMS_SQLTUNE.EXECUTE_TUNING_TASK(task_name => l_task_name);END;/
-- View recommendationsSELECT DBMS_SQLTUNE.REPORT_TUNING_TASK('index_analysis') FROM DUAL;From Execution Plans
Section titled “From Execution Plans”-- Find full table scans on large tablesSELECT sql_id, plan_hash_value, operation, object_owner, object_name, cardinality, costFROM V$SQL_PLANWHERE operation = 'TABLE ACCESS' AND options = 'FULL' AND cardinality > 10000ORDER BY cost DESC;Columns Without Indexes
Section titled “Columns Without Indexes”-- Find frequently filtered columns without indexesSELECT table_name, column_name, num_distinct, densityFROM DBA_TAB_COLUMNSWHERE owner = 'HR' AND num_distinct > 100 AND column_name NOT IN ( SELECT column_name FROM DBA_IND_COLUMNS WHERE table_owner = 'HR' AND table_name = c.table_name )ORDER BY num_distinct DESC;Index Types and When to Use
Section titled “Index Types and When to Use”B-Tree Index (Default)
Section titled “B-Tree Index (Default)”-- Best for: High cardinality, range queries, ORDER BYCREATE INDEX emp_salary_idx ON employees(salary);
-- Composite indexCREATE INDEX emp_dept_sal_idx ON employees(department_id, salary);Bitmap Index
Section titled “Bitmap Index”-- Best for: Low cardinality, data warehouse, AND/OR queriesCREATE BITMAP INDEX emp_status_idx ON employees(status);
-- Note: Avoid in OLTP due to lock contentionFunction-Based Index
Section titled “Function-Based Index”-- Index on expressionCREATE INDEX emp_upper_name_idx ON employees(UPPER(last_name));
-- Enable query to use indexSELECT * FROM employees WHERE UPPER(last_name) = 'SMITH';Invisible Index
Section titled “Invisible Index”-- Create without affecting optimizer (for testing)CREATE INDEX emp_test_idx ON employees(hire_date) INVISIBLE;
-- Test with hintSELECT /*+ INDEX(e emp_test_idx) */ * FROM employees e;
-- Make visible if beneficialALTER INDEX emp_test_idx VISIBLE;Index Maintenance
Section titled “Index Maintenance”Rebuild Indexes
Section titled “Rebuild Indexes”-- Online rebuild (recommended)ALTER INDEX hr.emp_idx REBUILD ONLINE;
-- Rebuild with new tablespaceALTER INDEX hr.emp_idx REBUILD TABLESPACE idx_ts ONLINE;
-- Rebuild with compressionALTER INDEX hr.emp_idx REBUILD COMPRESS ONLINE;
-- Coalesce instead of rebuild (less resource intensive)ALTER INDEX hr.emp_idx COALESCE;When to Rebuild
Section titled “When to Rebuild”-- Check index fragmentationSELECT index_name, blevel, leaf_blocks, del_lf_rows_len, ROUND(del_lf_rows_len / (leaf_blocks * 8192) * 100, 2) AS pct_deletedFROM INDEX_STATS;
-- Note: Run ANALYZE INDEX ... VALIDATE STRUCTURE firstANALYZE INDEX hr.emp_idx VALIDATE STRUCTURE;Index Statistics
Section titled “Index Statistics”-- Gather index statisticsEXEC DBMS_STATS.GATHER_INDEX_STATS('HR', 'EMP_IDX');
-- Gather all indexes for a tableEXEC DBMS_STATS.GATHER_TABLE_STATS('HR', 'EMPLOYEES', cascade => TRUE);Composite Index Strategy
Section titled “Composite Index Strategy”Column Order Matters
Section titled “Column Order Matters”-- For query: WHERE dept_id = 10 AND status = 'ACTIVE' AND salary > 50000
-- Best order: equality columns first, range column lastCREATE INDEX emp_comp_idx ON employees(department_id, status, salary);
-- This index can be used for:-- WHERE department_id = 10-- WHERE department_id = 10 AND status = 'ACTIVE'-- WHERE department_id = 10 AND status = 'ACTIVE' AND salary > 50000Index Skip Scan
Section titled “Index Skip Scan”-- Optimizer may skip leading column if cardinality is low-- For index on (gender, last_name)SELECT * FROM employees WHERE last_name = 'SMITH';-- May use index skip scan if gender has few distinct valuesIndex Compression
Section titled “Index Compression”-- Compress duplicate values in composite indexesCREATE INDEX emp_dept_job_idx ON employees(department_id, job_id) COMPRESS 1;
-- Advanced compression (12c+)CREATE INDEX emp_adv_idx ON employees(department_id) COMPRESS ADVANCED LOW;
-- Check compression ratioSELECT index_name, compression, prefix_lengthFROM DBA_INDEXESWHERE owner = 'HR';Parallel Index Operations
Section titled “Parallel Index Operations”-- Create index in parallelCREATE INDEX emp_idx ON employees(last_name) PARALLEL 4;
-- Rebuild in parallelALTER INDEX emp_idx REBUILD PARALLEL 4;
-- Reset to non-parallel for DMLALTER INDEX emp_idx NOPARALLEL;Index Hints
Section titled “Index Hints”-- Force specific indexSELECT /*+ INDEX(e emp_name_idx) */ * FROM employees e WHERE last_name = 'SMITH';
-- Force full table scan (compare performance)SELECT /*+ FULL(e) */ * FROM employees e WHERE last_name = 'SMITH';
-- Force index range scanSELECT /*+ INDEX_RS(e emp_idx) */ * FROM employees e;
-- Force index fast full scanSELECT /*+ INDEX_FFS(e emp_idx) */ * FROM employees e;Troubleshooting Index Issues
Section titled “Troubleshooting Index Issues”Index Not Being Used
Section titled “Index Not Being Used”-- Check execution planEXPLAIN PLAN FORSELECT * FROM employees WHERE last_name = 'SMITH';SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- Common reasons:-- 1. Statistics stale-- 2. Data type mismatch (implicit conversion)-- 3. Function on indexed column-- 4. Leading wildcard (LIKE '%SMITH')-- 5. NULL comparison (index doesn't include NULLs by default)-- 6. Optimizer chose full scan as more efficientCheck for Implicit Conversion
Section titled “Check for Implicit Conversion”-- This prevents index useSELECT * FROM employees WHERE employee_id = '100'; -- String to number
-- This uses indexSELECT * FROM employees WHERE employee_id = 100;Index on NULL Values
Section titled “Index on NULL Values”-- Standard B-tree doesn't index NULL-- Include constant to index NULLsCREATE INDEX emp_mgr_idx ON employees(manager_id, 0);
-- Now this can use the indexSELECT * FROM employees WHERE manager_id IS NULL;Performance Monitoring
Section titled “Performance Monitoring”-- Index wait eventsSELECT EVENT, TOTAL_WAITS, TIME_WAITED_MICRO/1000000 AS secsFROM V$SYSTEM_EVENTWHERE EVENT LIKE '%index%'ORDER BY TIME_WAITED_MICRO DESC;
-- Index I/O statisticsSELECT owner, object_name, statistic_name, valueFROM V$SEGMENT_STATISTICSWHERE object_type = 'INDEX' AND owner = 'HR' AND value > 0ORDER BY value DESC;