Oracle Database
02 / 03

Performance: Indexing, Query Plans & Hints

Oracle Database: Performance Tuning

Indexes

-- B-tree index (default)
CREATE INDEX idx_emp_dept ON employees(department_id);
CREATE INDEX idx_emp_name ON employees(last_name, first_name);  -- composite

-- Unique index
CREATE UNIQUE INDEX idx_emp_email ON employees(email);

-- Function-based index (for queries on expressions)
CREATE INDEX idx_upper_last ON employees(UPPER(last_name));
-- Now this uses the index:
SELECT * FROM employees WHERE UPPER(last_name) = 'KING';

-- Bitmap index (low-cardinality columns, data warehouse)
CREATE BITMAP INDEX idx_emp_job ON employees(job_id);

-- Partial index (only index certain rows)
CREATE INDEX idx_active_orders ON orders(order_date)
WHERE status = 'ACTIVE';

-- Index on partitioned table
CREATE INDEX idx_sales_date ON sales(sale_date) LOCAL;  -- local = per partition

-- Check index usage
SELECT index_name, table_name, uniqueness, status
FROM user_indexes
WHERE table_name = 'EMPLOYEES';

-- Drop index
DROP INDEX idx_emp_dept;

-- Rebuild fragmented index
ALTER INDEX idx_emp_dept REBUILD ONLINE;

Execution Plans & EXPLAIN PLAN

-- View execution plan
EXPLAIN PLAN FOR
SELECT e.last_name, d.department_name
FROM employees e JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 5000;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

-- Or use AUTOTRACE in SQL*Plus
SET AUTOTRACE ON;
SELECT * FROM employees WHERE department_id = 60;

-- From v$sql_plan (for already-executed queries)
SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(sql_id => 'abc123xyz'));

-- Key operations to look for:
-- TABLE ACCESS FULL   — no index used (bad for large tables)
-- INDEX RANGE SCAN    — good, uses index
-- INDEX UNIQUE SCAN   — best, exact match on unique index
-- HASH JOIN           — large table joins (good)
-- NESTED LOOPS        — small table joins (good when driving table is small)
-- SORT MERGE JOIN     — sorted inputs

-- Check statistics age (stale stats = bad plans)
SELECT table_name, last_analyzed, num_rows
FROM user_tables
ORDER BY last_analyzed;

-- Gather fresh stats
EXEC DBMS_STATS.GATHER_TABLE_STATS('HR', 'EMPLOYEES', cascade => TRUE);

Hints & Query Optimization

-- Hints: override optimizer decisions (use sparingly, as a last resort)

-- Force index use
SELECT /*+ INDEX(e idx_emp_dept) */ * FROM employees e WHERE department_id = 60;

-- Force full table scan (when FTS is actually faster for large result sets)
SELECT /*+ FULL(e) */ * FROM employees e WHERE salary > 1000;

-- Join order hints
SELECT /*+ LEADING(d e) USE_NL(e) */ *
FROM departments d JOIN employees e ON d.department_id = e.department_id;

-- Parallel query
SELECT /*+ PARALLEL(employees, 4) */ * FROM employees;

-- Result cache (cache query result in SGA)
SELECT /*+ RESULT_CACHE */ * FROM expensive_view;

-- Common performance tips
-- 1. Avoid functions on indexed columns in WHERE clause:
--    Bad:  WHERE TO_CHAR(hire_date, 'YYYY') = '2020'
--    Good: WHERE hire_date >= DATE '2020-01-01' AND hire_date < DATE '2021-01-01'

-- 2. Use bind variables (not literals) to share cursor cache
--    Bad:  WHERE id = 100
--    Good: WHERE id = :emp_id

-- 3. Use EXISTS instead of IN for correlated subqueries
SELECT * FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.department_id = d.department_id);

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free