Concatenated IndexesHow composite indexes really work, why column order matters, and when skip scan changes the story
Concatenated indexes, also called composite indexes, are easy to explain badly and surprisingly rich to explain well. The usual summary is “Oracle can use the index only when the leading column is present,” but that is only the starting point. To design them properly, you need to think about leading portions, equality versus range predicates, ordering requirements, skip scan eligibility, covering behavior, and whether one composite index can replace several single-column indexes in a given workload.
What a concatenated index actually is
A concatenated index is a single index built on more than one column, such as (department_id, job_id) or (customer_id, order_date, status). These are also known as composite indexes, and their usefulness depends heavily on whether queries can use a leading portion of the key. That phrase - leading portion - is the heart of composite-index design.
If an index is defined on (A, B, C), then the leading portions are (A), (A, B), and (A, B, C). This is why a query on B alone is fundamentally different from a query on A alone.
The leading-edge rule, explained properly
Oracle’s tuning guidance says a composite index can support an access path when the SQL statement uses a leading portion of the index. That is the basic rule behind range scans on composite keys. If the first column is missing, the optimizer usually cannot do a normal index range scan based on the composite ordering. This is why queries on A, A+B, or A+B+C behave differently from a query on B alone.
Classic range-scan friendly predicates
WHERE A = :1WHERE A = :1 AND B = :2WHERE A = :1 AND B = :2 AND C = :3WHERE A = :1 AND B > :2
Not leading by default
WHERE B = :1WHERE C = :1WHERE B = :1 AND C = :2WHERE C BETWEEN :1 AND :2
“Cannot use the index” is too strong in modern Oracle. A non-leading predicate often prevents a standard range scan, but the optimizer may still consider INDEX SKIP SCAN when the leading column has very few distinct values.
Why column order matters so much
Column order affects far more than whether a predicate is legal for a range scan. It also affects selectivity usage, sort elimination opportunities, the usefulness of the index for partially specified predicates, and whether a range condition cuts off the effective use of columns that come after it. Oracle’s index guidance emphasizes designing composite indexes around actual query patterns rather than abstract rules.
Equality first
Range later
Ordering-aware design
ORDER BY A, B, matching index order can reduce or remove sorting work.The advice “put the most selective column first” is not universally right. If your workload almost always filters on a less-selective column first and rarely on the more-selective one alone, query pattern can matter more than raw selectivity.
Index skip scan: the exception that keeps confusing people
Index skip scan is why composite-index discussions often become muddled. Oracle can sometimes use a composite index even when the leading column is not present, but only by logically probing multiple subtrees for the missing leading-column values. Oracle documents this access path as INDEX SKIP SCAN and describes it as most useful when the leading column has low cardinality. In other words: skip scan is real, but it is not a reason to ignore column order.
EXPLAIN PLAN FOR SELECT /*+ INDEX_SS(e idx_emp_dept_job) */ * FROM emp_test e WHERE job_id = 'ST_CLERK'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
If skip scan appears, treat it as an optimizer option that can sometimes rescue a non-leading predicate, not as proof that column order no longer matters.
Reading execution plans for concatenated indexes
Oracle plans tell you a lot once you know what to look for. A composite index may be used via INDEX RANGE SCAN, INDEX UNIQUE SCAN, INDEX FULL SCAN, or INDEX SKIP SCAN depending on the predicate shape and selectivity. The goal is not to memorize plan names blindly, but to tie each access path back to the SQL pattern that produced it.
| Plan operator | Typical meaning here | What it suggests |
|---|---|---|
INDEX RANGE SCAN | Oracle is traversing a leading portion of the index | Classic composite-index success case |
INDEX UNIQUE SCAN | All key columns of a unique index are specified | Most precise access pattern |
INDEX SKIP SCAN | Oracle is probing non-leading predicates through multiple leading-key branches | Possible rescue path, but workload-dependent |
INDEX FULL SCAN | Oracle may be using the index for ordering or covering needs | Useful in some report and sort-elimination cases |
EXPLAIN PLAN FOR SELECT * FROM emp_test WHERE department_id = 50; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); EXPLAIN PLAN FOR SELECT * FROM emp_test WHERE job_id = 'ST_CLERK'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
Covering behavior and sort elimination
Composite indexes are useful not just for filtering but also for returning rows in a useful order and, in some cases, avoiding table access when all required columns are present in the index. Oracle documents that concatenated indexes can help satisfy ORDER BY clauses efficiently when the ordering matches the index structure closely enough.
Sort elimination
Covering potential
CREATE INDEX idx_orders_cust_date_status ON orders(customer_id, order_date, status); EXPLAIN PLAN FOR SELECT customer_id, order_date, status FROM orders WHERE customer_id = 101 ORDER BY order_date, status; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
This is one reason blindly splitting a composite index into multiple single-column indexes can make a workload worse: you may lose both access efficiency and ordering benefit.
Design rules that hold up in real workloads
The best rules are those that survive contact with real applications. Oracle’s performance guidance consistently points toward designing indexes around actual predicate patterns and optimizer behavior rather than slogans.
Rule 1
Rule 2
Rule 3
ORDER BY and grouping needs, not just the WHERE clause.Rule 4
Put columns first because the workload needs them first, not because an isolated selectivity formula told you to.
Common mistakes and how to avoid them
Mistake 1
Mistake 2
Mistake 3
Too many overlapping composite indexes increase DML cost, maintenance overhead, and optimizer complexity. Composite indexes should simplify the workload, not create an index graveyard.
End-to-end demo: testing a composite index on department and job
This demo uses a safe and familiar sample pattern: build a larger test table from HR data, create a composite index on (department_id, job_id), gather stats, and compare plan shapes for leading-column, full-key, and non-leading-column predicates. The goal is not to invent fixed costs or elapsed times, because those depend on environment. The goal is to show real commands and the exact plan patterns you should verify.
Use exact commands, then verify with metadata and plans. Do not teach fake outputs where Oracle’s optimizer would legitimately vary by data volume and statistics.
Step 1: create the demo table and gather stats
CREATE TABLE emp_test AS SELECT employee_id, department_id, job_id, salary, last_name FROM hr.employees CROSS JOIN ( SELECT level AS n FROM dual CONNECT BY level <= 1000); BEGIN DBMS_STATS.GATHER_TABLE_STATS( ownname => USER, tabname => 'EMP_TEST', cascade => FALSE); END; /
Step 2: create the concatenated index and verify it exists
CREATE INDEX idx_emp_dept_job ON emp_test(department_id, job_id); SELECT index_name, index_type, status, visibility FROM user_indexes WHERE index_name = 'IDX_EMP_DEPT_JOB';
The success condition here is one visible row in USER_INDEXES with the expected index name and a usable status.
Step 3: test the leading-column predicate
EXPLAIN PLAN FOR SELECT * FROM emp_test WHERE department_id = 50; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
The plan you hope to see is an index access path such as INDEX RANGE SCAN on IDX_EMP_DEPT_JOB, because the leading column is present.
Step 4: test both indexed columns together
EXPLAIN PLAN FOR SELECT * FROM emp_test WHERE department_id = 50 AND job_id = 'ST_CLERK'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
This is the classic sweet spot. The plan should normally show the composite index used very efficiently because both leading columns are constrained.
Step 5: test the non-leading column alone
EXPLAIN PLAN FOR SELECT * FROM emp_test WHERE job_id = 'ST_CLERK'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
At this point the plan may show a full table scan, or it may show an INDEX SKIP SCAN if Oracle decides the leading column has few enough distinct values for that to be worthwhile. Either result is consistent with Oracle’s documented behavior; that is why the demo avoids pretending one outcome is guaranteed on every system.
Step 6: test skip scan explicitly
EXPLAIN PLAN FOR SELECT /*+ INDEX_SS(emp_test idx_emp_dept_job) */ * FROM emp_test WHERE job_id = 'ST_CLERK'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
The success condition here is not a specific cost. It is whether the plan now shows INDEX SKIP SCAN, which proves the optimizer can sometimes use the composite index even without the leading column predicate.
Step 7: test with actual execution statistics
SELECT /*+ gather_plan_statistics */ * FROM emp_test WHERE department_id = 50 AND job_id = 'ST_CLERK'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));
Success means the index exists, plans for leading-column predicates show normal composite-index access, the non-leading-only case demonstrates the leading-edge rule, and skip scan can be demonstrated honestly as a conditional exception rather than a guarantee.
Knowledge check
These questions are built to sharpen judgment, not just terminology. Submit answers and read the explanations carefully.
(A, B, C), which set correctly describes the leading portions?INDEX SKIP SCAN?ORDER BY as well as filtering?
No comments:
Post a Comment