Oracle Low Cardinality Index Guide - Bitmap vs B-Tree Index Selection
Oracle Low Cardinality Index Guide
Section titled “Oracle Low Cardinality Index Guide”Understanding when and how to index columns with low cardinality (few distinct values).
What Is Cardinality?
Section titled “What Is Cardinality?”Cardinality = Number of distinct values in a column
| Cardinality Level | Distinct Values | Example Columns |
|---|---|---|
| Very Low | 2-10 | gender, status, yes/no flags |
| Low | 10-100 | country, category, department |
| Medium | 100-1000 | city, product_type |
| High | 1000+ | customer_id, email, SSN |
-- Check column cardinalitySELECT COLUMN_NAME, NUM_DISTINCT, NUM_NULLS, DENSITYFROM DBA_TAB_COLUMNSWHERE OWNER = 'HR' AND TABLE_NAME = 'EMPLOYEES'ORDER BY NUM_DISTINCT;Index Options for Low Cardinality
Section titled “Index Options for Low Cardinality”Option 1: Bitmap Index
Section titled “Option 1: Bitmap Index”Best for: Data warehouses, low-DML environments
-- Create bitmap indexCREATE BITMAP INDEX emp_status_bix ON employees(status);
-- Advantages:-- - Compact storage-- - Excellent for AND/OR/NOT queries-- - Fast COUNT operations-- - Good for multiple low-cardinality predicatesOption 2: B-Tree Index (with considerations)
Section titled “Option 2: B-Tree Index (with considerations)”Best for: OLTP with concurrent DML
-- Create B-tree indexCREATE INDEX emp_status_idx ON employees(status);
-- May not be used by optimizer if selectivity is poor-- Oracle might choose full table scan insteadOption 3: Composite Index
Section titled “Option 3: Composite Index”Combine with high-cardinality column
-- Status + employee_id (more selective together)CREATE INDEX emp_status_empid_idx ON employees(status, employee_id);
-- Useful for queries like:-- WHERE status = 'ACTIVE' AND employee_id BETWEEN 1000 AND 2000Option 4: No Index
Section titled “Option 4: No Index”When table is small or accessed infrequently
-- Sometimes no index is best-- Full table scan might be faster for:-- - Small tables (< 1000 rows)-- - Queries returning > 10-20% of rows-- - Columns with very low selectivityBitmap Index Details
Section titled “Bitmap Index Details”When to Use Bitmap Indexes
Section titled “When to Use Bitmap Indexes”-- Perfect for: Multiple AND/OR conditionsSELECT COUNT(*)FROM salesWHERE region = 'WEST' AND product_category = 'ELECTRONICS' AND sale_type = 'RETAIL';
-- Each bitmap index is combined efficiently-- Uses bitmap AND/OR operations in memoryWhen NOT to Use Bitmap Indexes
Section titled “When NOT to Use Bitmap Indexes”-- Avoid in OLTP with concurrent DML-- Bitmap indexes lock entire bitmap segment-- One UPDATE can block many other sessions
-- Check for bitmap lock contentionSELECT EVENT, TOTAL_WAITSFROM V$SYSTEM_EVENTWHERE EVENT LIKE '%bitmap%';Bitmap Index Internals
Section titled “Bitmap Index Internals”-- Check bitmap index detailsSELECT INDEX_NAME, INDEX_TYPE, BLEVEL, LEAF_BLOCKS, DISTINCT_KEYSFROM DBA_INDEXESWHERE INDEX_TYPE LIKE 'BITMAP%' AND OWNER = 'HR';B-Tree on Low Cardinality
Section titled “B-Tree on Low Cardinality”Why B-Tree May Not Be Used
Section titled “Why B-Tree May Not Be Used”-- Optimizer considers:-- 1. Selectivity (distinct values / total rows)-- 2. Clustering factor-- 3. Cost of index scan vs full table scan
-- Example: status has 3 values, table has 1M rows-- Selectivity = 3/1000000 = 0.000003 (very poor)-- Each index entry points to ~333K rows-- Full table scan likely faster than index scanMaking B-Tree More Effective
Section titled “Making B-Tree More Effective”-- Add more selective columnsCREATE INDEX emp_status_dept_idx ON employees(status, department_id, employee_id);
-- Use index compressionCREATE INDEX emp_status_idx ON employees(status) COMPRESS 1;
-- Create partial index (function-based)CREATE INDEX emp_active_idx ON employees( CASE WHEN status = 'ACTIVE' THEN employee_id END);Performance Comparison
Section titled “Performance Comparison”Test Query Performance
Section titled “Test Query Performance”-- Enable timing and execution planSET TIMING ONSET AUTOTRACE ON
-- Test with no indexSELECT /*+ FULL(e) */ COUNT(*) FROM employees e WHERE status = 'ACTIVE';
-- Test with B-tree indexSELECT /*+ INDEX(e emp_status_idx) */ COUNT(*) FROM employees e WHERE status = 'ACTIVE';
-- Test with bitmap indexSELECT /*+ INDEX(e emp_status_bix) */ COUNT(*) FROM employees e WHERE status = 'ACTIVE';Analyze Index Usage
Section titled “Analyze Index Usage”-- Check if optimizer uses the indexEXPLAIN PLAN FORSELECT * FROM employees WHERE status = 'ACTIVE';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- Look for:-- TABLE ACCESS FULL = index not used-- INDEX RANGE SCAN = B-tree index used-- BITMAP INDEX SINGLE VALUE = bitmap index usedDecision Matrix
Section titled “Decision Matrix”| Scenario | Recommendation |
|---|---|
| Data warehouse, read-heavy | Bitmap index |
| OLTP, concurrent DML | Composite B-tree or no index |
| Reports with multiple filters | Bitmap indexes on each column |
| Very small table (< 1000 rows) | No index |
| Query returns > 20% of rows | Full table scan |
| Combination of low + high cardinality | Composite B-tree |
| Single value queries (flag = ‘Y’) | Consider function-based |
Practical Examples
Section titled “Practical Examples”Example 1: Status Flag
Section titled “Example 1: Status Flag”-- Table: orders, 10M rows-- Column: is_shipped (Y/N)
-- Poor choice: B-tree indexCREATE INDEX ord_shipped_idx ON orders(is_shipped);-- Index scan + table access for 5M rows = slow
-- Better: Bitmap for reportingCREATE BITMAP INDEX ord_shipped_bix ON orders(is_shipped);-- Good for: SELECT COUNT(*) WHERE is_shipped = 'N'
-- Alternative: Partial index for specific valueCREATE INDEX ord_not_shipped_idx ON orders(order_id)WHERE is_shipped = 'N';-- Only indexes unshipped ordersExample 2: Category Filtering
Section titled “Example 2: Category Filtering”-- Table: products, 500K rows-- Column: category (20 distinct values)
-- For data warehouse:CREATE BITMAP INDEX prod_cat_bix ON products(category);
-- For OLTP with concurrent access:CREATE INDEX prod_cat_name_idx ON products(category, product_name);-- Useful for: WHERE category = 'X' ORDER BY product_nameExample 3: Combined Low-Cardinality Columns
Section titled “Example 3: Combined Low-Cardinality Columns”-- Multiple low-cardinality columns-- status (3 values), region (10 values), priority (5 values)
-- Bitmap indexes excel hereCREATE BITMAP INDEX ord_status_bix ON orders(status);CREATE BITMAP INDEX ord_region_bix ON orders(region);CREATE BITMAP INDEX ord_priority_bix ON orders(priority);
-- Query combines them efficientlySELECT * FROM ordersWHERE status = 'OPEN' AND region IN ('NORTH', 'SOUTH') AND priority = 'HIGH';-- Uses BITMAP AND/OR operationsMonitoring and Maintenance
Section titled “Monitoring and Maintenance”-- Check bitmap index size vs B-treeSELECT INDEX_NAME, INDEX_TYPE, LEAF_BLOCKS, BYTES/1024/1024 AS size_mbFROM DBA_INDEXES iJOIN DBA_SEGMENTS s ON i.INDEX_NAME = s.SEGMENT_NAMEWHERE i.TABLE_NAME = 'ORDERS'ORDER BY INDEX_TYPE;
-- Bitmap index fragmentation-- Rebuild after heavy DMLALTER INDEX ord_status_bix REBUILD;