Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Art of indexing_in_o8i
1. Paper #135 / Page 1
The Art of Indexing in Oracle8i
Venkat S. Devraj
IMMEDIENT (formerly Raymond James Consulting)
Introduction
In most applications, OLTP or DSS, indexes are one of the most ardently used objects. In spite of this, they are
highly under-estimated. Creating an index is considered to be a simple and precise science. In my experience, I
have noted quite a few sites creating indexes with great vigor and finally ending up with one-too-many and not
knowing which ones deserve to stay and which ones can be eliminated without an adverse impact on queries. And
of course, there are other sites that tend to seriously under-exploit them, worrying about the possible detrimental
impact on inserts and deletes. Furthermore, the rich and varied indexing options offered by Oracle8i adds fuel to
the fire. Be it new full-fledged index types or new index-access paths, indexing is emerging as an art, demanding
a rock-solid understanding of both concepts and implementation from developers and DBAs.
Objectives
The objective of this paper is two-fold :
· To introduce the various index types and access-paths available in Oracle8i and shed more technical light on
them;
· Present specific examples of index-usage at my client-sites to foster better understanding.
Hopefully, this paper will help an attendee in determining specifically what type of index to create during which
situation; when an index should be re-built and what kind of access-path should be determined for major DML
statements, using those indexes. I have intentionally breezed through and at times, even avoided standard “run-of-the-
mill” commentary about indexes and instead focussed on material not easily available in the manuals. The
intention of this paper is not to replace the Oracle documentation-set, but to act as a source of reference for real-world
implementation of indexes.
Index-types in Oracle8i
Oracle8i continues to offer the following major index-types :
· B-Tree indexes
· Bit-mapped indexes
In addition, there are the following variations of the B-Tree index (sub-types) :
· Partitioned indexes
· Reverse-key indexes
· Index-organized tables (IOTs)
· Cluster indexes
· Function-based indexes (Oracle8i only)
2. Paper #135 / Page 2
· Application domain indexes (Oracle8i only)
Furthermore, you might find the following index-related terms useful :
· Local and global indexes
· Pre-fixed and non-prefixed indexes
· Abstract data-type indexes
Last but not the least, the following index access-paths are provided :
· Regular Index/Table lookups : Uses the ROWID from the index to locate a row in the table.
· Index-only lookups : If all the necessary columns are available in the index itself, the table-lookup is avoided.
· Index range scans : A range of index keys are scanned to locate the desired key-value.
· Fast Full Index scans (FFIs) : FFIs allow reading of multiple index blocks to expedite full index scans. Since
an index is usually smaller than a table, an index full-scan is a much faster alternative to a full table scan, if
the index includes all required columns. The init.ora parameter db_file_multiblock_read_count determines the
number of index blocks read in one I/O operation (remember, many operating systems have a 64K upper limit
on I/O).
· Parallel Query Option (PQO) “enriched” paths : In Oracle8/8i, PQO is available for all the above access-paths.
This paper assumes prior knowledge of certain key concepts. Before we start, let’s take a quick peek at those
concepts :
· Constraints and index-creation : Declaring primary-key and unique constraints automatically creates an index
on the concerned columns. The USING INDEX clause of the CREATE and ALTER TABLE commands
allows more control over this index-creation process.
· Rule and Cost based optimization : Rule based optimization is still supported in Oracle8i. Oracle8i sites can
use either RULE, COST or CHOOSE as a value for the OPTIMIZER_MODE init.ora parameter. This setting
is of paramount importance in the way Oracle8i uses indexes. For example, the rule-based optimizer merges
indexes if they are non-unique and the predicates used are equality predicates. This is because range-predicates
with multiple indexes generally require additional processing that would impede performance.
Under the cost-based optimizer (CBO), this would not necessarily happen if the cost of doing the merge were
higher than the cost of using multiple non-unique indexes. In addition, certain new index sub-types and access
paths are available only while using CBO. For example, partitioned indexes and fast full index scans (FFIs).
With changes in the CBO in almost every new release, never assume that your DML statements are using a
certain index. Always run an EXPLAIN PLAN on every important DML statement to ensure usage of the
right indexes.
· Analyzing of tables and indexes : CBO requires all tables and indexes to be analyzed periodically, especially
after periods of high DML activity. These statistics are used by CBO to evaluate the “cost” of all available
access-paths and the path with the lowest cost is selected.
3. · Usage of hints to force index-selection : Under both rule or cost based optimization, certain hints may be
provided to DML statements to specifically select a certain index. This functionality is provided to override
the optimizer, in case the access-path selected by it is sub-optimal. Syntax for hints are very similar to
comments, except for the addition of a plus (+) symbol. If a hint is provided incorrectly, the optimizer ignores
the hint.
Example : SELECT /*+ INDEX(table_a, index_a) */ id, name FROM table_a;
Index-related hints supported by Oracle8i include INDEX, INDEX_DESC, INDEX_COMBINE,
INDEX_FFS and PARALLEL_INDEX for retrieving rows from indexes in an ascending order, descending
order, combining bit-mapped indexes using boolean logic, prompting the optimizer to use the Fast Full Index
(FFI) scan in lieu of a full-table scan and inducing a parallel-scan of an index (using PQO) respectively.
B-Tree Indexes : Everybody’s most favorite index!
This is one of the earliest indexing options available in Oracle. And we are all absolutely familiar with this index-type,
right ? OK, let’s have a quick show-of-hands : how many of us here today, understand or remember a B-Tree
structure ? Believe me, good usage of these “basic” indexes requires a good understanding of how B-Trees
indexes operate. Implementation of B-Trees are a little unique in every application in terms of it’s height and
order. However, the basic behavior is the same. Let’s take a quick practical look at how B-Trees indexes have
been implemented in Oracle8.
M
G S
A B C
D E F
H I J K
L
N O P
Q R
T U V
Root/branch
nodes
Leaf nodes
Fig. 1a : A simplistic B-tree implementation (height : 2 and order : 2) [Branch/leaf nodes read to locate “R” are displayed
Paper #135 / Page 3
in bold]
B-Trees are an excellent way of looking up pointers to large information with minimal number of disk-reads. In a
B*Tree, all information is either a branch/root node or a leaf-node. The final information (ROWIDs and keys) are
always stored in a leaf (-node). Upto 16 columns can comprise the key in Oracle7.x, whereas in Oracle8.x, it is 32
columns. Furthermore, a key cannot be greater than 1/3rd the size of the database block (violating this restriction
would result in the ORA-01450 error). A leaf may contain multiple physical rows. The leaves are always present
4. only at the last “level”. A B-Tree is a “balanced tree”, because every leaf is equi-distant from the root-node. It
takes the same time to access any row in any given leaf. Fig. 1a is a poor man’s picture of a B-Tree
implementation. Anyhow, it clearly indicates the root, the branches and the leaves. Every tree has a certain order
and height. The order is the number of “branches” for every node of the tree. The height is the number of levels
the tree contains. Ideally the height has to be minimal, since the number of disk-reads required to get information
is proportionate to the height of the tree. For example in fig. 1a, in order to get “R”, two disk-reads are required
(“M” at the root-node and “S” at the second-level node). Thus, here a minimum of three reads would be required
to locate a key (two reads at the root/branch level and the third at the leaf-node level). It is to be borne in mind,
that each read may be a logical read or a physical read, depending on how the index is laid out. If a single read
operation places the required number of blocks in memory, subsequent reads would be performed off memory,
else they would need to go to disk. The height is inversely proportionate to the order. The higher the order, the
lower the height (termed as a “flat” tree). An important objective in any B-Tree implementation is to increase the
branches and reduce the height, thereby ensuring speedy access to the leaves.
A major challenge with a B-Tree implementation occurs during INSERTs, DELETEs and UPDATEs. During
every INSERT, the key has to be inserted appropriately within the right leaf. Every key has one and only one
place within the tree. If the leaf is already full and cannot accommodate any additional keys, then the leaf has to
be split into two. Now, there are two options available :
1. if the new key value is the biggest (of all the keys existing in the old leaf block), then the total keys are split in
a 99:1 ratio, wherein only the new key is placed in the new leaf block, whereas the rest of the (old) keys are
retained within the old leaf block (including all deleted keys).
2. if the new key value is not the biggest, then the total keys are split in a 50:50 ratio, wherein each of the leaf
Paper #135 / Page 4
blocks (old and new) would absorb half the keys of the original leaf.
This split has to be propagated upwards to the parent node by means of a new entry to point to the new leaves. If
the parent node happens to be full, then the node has to be split into two and it’s parent has to be notified of the
split. God forbid, if that parent is full, as well! Thus, a split may be propagated all the way up to the root-node,
itself. Remember, each split is costly involving physical disk I/O. Additionally, prior to splitting, Oracle has to
seek and find an empty block to accommodate the split. The following steps are taken to do so :
1. Seek in the index free-list for a free block. More than one free-list can be defined for an index (via the
CREATE/ALTER INDEX commands). Free-lists for indexes do not aid Oracle in finding an available block
for accommodating the new key-value being inserted. This is because a key-value cannot be randomly placed
in the first “free” leaf-block that’s available within the index. The value has to be placed within a specific
leaf-block after appropriate sorting. Free-lists in indexes are used only during block splitting. Each free-list
comprises of a linked-list of “empty” blocks. When more than one free-list is defined for an index, the free-list
allotted to the process is initially scanned for a free block. If none are found, the master free-list is
checked.
2. If no free blocks are found, Oracle tries to allocate another extent. If there is no free space in the tablespace,
Oracle gives an error (ORA-01654).
3. If a free block is found via either of the above steps, the high watermark for the index is increased.
4. Use the free block found to perform the split.
Part of the challenge in creating B-Tree indexes is to avoid splits at runtime or rather, to induce splits during
index-creation (“pre-splitting”) and take the performance-hit at that point in time, and not during run-time inserts.
These splits may not be restricted merely to inserts, they may occur during updates, as well.
5. Let’s take a look at what goes on within Oracle during an indexed-key UPDATE. Index updates differ vastly from
table updates. In a table update, data is changed within the data-block (assuming there is sufficient space in the
block for this change to take place). However in an index, when a key is changed, it’s position within the tree has
to change. Remember, a key has one and only one position within a B-Tree. So when it changes, the old entry has
to be deleted and a new one has to be created at a new leaf. Chances are, the old entry may never be re-used.
Because Oracle would reuse a key-entry slot only when the newly inserted key is exactly the same as an old
(deleted) one, including data-type, length and case. And what are the chances of that happening ? Most
applications at my client-sites use a sequence to generate NUMBER keys (especially primary keys). Unless they
are using the RECYCLE option, the sequences do not generate the same number twice. So, the deleted space
within the index remains unused. This is the reason why during large-scale DELETEs and UPDATEs, the table-size
Paper #135 / Page 5
shrinks or at least remains constant, whereas the indexes grow continuously.
From the above explanation of B-Trees, the following dos and don’ts emerge :
· Avoid indexing columns, that are subject to high UPDATEs.
· Avoid indexing multiple columns in tables, that are subject to high DELETEs. If possible, only index the
primary-key and/or column(s) determining the deletes on such tables. If indexing multiple columns is
unavoidable, then consider partitioning the tables based on those columns and do a TRUNCATE on each such
partition (instead of a DELETE). TRUNCATE simulates a drop and re-create of the table and index by re-setting
the high-water mark, when used with the DROP STORAGE clause.
· Avoid creating b-tree indexes for columns with low selectivity (less uniqueness). Low selectivity leads to
dense leaf-blocks, causing large-scale index scans. The higher the uniqueness, the better the performance,
since range-scans would reduce and maybe even be replaced with an unique scan.
· Null values are never stored in single-column indexes,. In the case of multi-column indexes, the value is
stored only if one of the columns is not null. Bear this in mind while constructing IS NULL or IS NOT NULL
clauses for DML statements. An IS NULL would never induce an index-scan, whereas an unrestrained IS
NOT NULL would cause a full index range-scan.
You have heard these advises before, but now the precise logic behind these words of wisdom is apparent.
The importance of PCTFREE
For a B-Tree index, PCTFREE determines the extent of a leaf-split. In other words, PCTFREE indicates the
amount of free-space within a block for “future updates”. However in the case of an index (unlike a table), these
updates do not make sense, since an update anyway forces a delete and subsequent insert.
In the case of indexes, PCTFREE plays a role mostly at the time of index-creation. A non-zero value specifies the
block-splitting ratio. If PCTFREE is set to 20, during index-creation, upto 80% of the leaf may contain key-information.
However the remaining 20% should be available for future inserts of key-information into that leaf-block.
This ensures that during run-time inserts, there is minimal overhead in terms of incurring leaf-block splits.
Even though a higher PCTFREE may cause index-creation time to increase, it prevents the performance-hit from
being deferred to actual usage time. So, the end-user, who is waiting for a row to be inserted does not incur the
performance penalty associated with a leaf-block split.
6. Based on this information, the following points emerge :
· PCTFREE for an index is used primarily during creation. It is ignored during actual usage.
· Specify a higher PCTFREE for OLTP applications, if the table is a popular one, incurring a lot of DML
Paper #135 / Page 6
changes (via interactive user-screens);
· Specify a lower PCTFREE, if the index-creation time is critical. This will pack in more rows per leaf-block,
thereby avoiding the need for further splitting at creation-time. This is of paramount significance to shops,
that have “24x7” availability requirements. Index-creation in most cases, requires considerable downtime
(especially if the table is a multi-million row table). Lesser the amount of index-creation time, smaller can be
the maintenance-window. I have seen this tiny, often unnoticed parameter save around 20% of index-creation
time. At a high-availability site, an index on a table containing around 11 million rows took me about 80
minutes to build using PCTFREE of 30 and a parallel degree of 4. The same index on the same table, with
13.5 million rows took me around 90 minutes to create with a PCTFREE of 0 (without any hardware/software
enhancements). Nologging (minimal redo) was on during both creations.
· For any column where the values are constantly increasing, it is probably a good idea to set a very low
PCTFREE (even, zero). This is because only the rightmost leaf-block will always be inserted into. This will
make the tree grow towards the right. The leftmost leaves would remain static. So, there is no sense in leaving
any part of those blocks empty with a non-zero PCTFREE.
Partitioned indexes
Partitioned indexes are indexes, which have been horizontally sub-divided based on a specific range of key-values.
Partitioning allows logically dependent structures to be physically independent. This allows maintenance
operations to be performed on specific partition, while the others are still available for access. Thus, a specific
partition may be offline and unavailable, while the other partitions are online and available for access. An index-partition
may be re-built or recovered with minimal effect on applications. Each index-partition may have
different storage characteristics and may be created in different tablespaces. Partitioned indexes may be created
even for non-partitioned tables.
Partitioned index-related terms
Global and Local indexes : When an index is partitioned in a similar fashion as the table, in terms of ranges of
partition-keys, then it is referred to as a local index. In other words, the local index shares the key-ranges with the
parent table. It would store the keys of only it’s corresponding table-partition. A global index does not have a one-to-
one correspondence with any table-partition in terms of partition-key ranges. A global index would store rows
from multiple table-partitions. A local index is also referred to as an equi-partitioned index; whereas a global
index is also referred to as a non equi-partitioned index.
For example, a table may be partitioned into 52 segments based on the week-of-year. However, the index might
be partitioned into 12 segments based on month-of-year. Such an index would be termed a global index. If the
index were partitioned into 52 segments, it would be referred to as a local index.
A bit-mapped index on a partitioned table has to be a local index.
7. Prefixed and non-prefixed indexes : When an index of a partitioned table contains the partition key as the leading
column, it is termed as a prefixed index. A non-prefixed index does not have partition key columns as the leading
keys.
Local pre-fixed indexes should be created, whenever possible. Since they have a one-to-one correspondence with
the partitioned table, administration is easier. Global pre-fixed indexes theoretically may have a slight
performance edge at times, since the overhead of accessing large number of indexes is avoided. However
partition pruning (where the partition key-value specified in the WHERE clause helps Oracle in determining
which partition might contain the required results and accesses only that partition) helps in super-charging local
partition access. Even in situations where partition pruning is not possible, parallel access of multiple local
indexes via PQO may provide higher performance. It is generally more expensive to scan a non-prefixed index,
compared to a pre-fixed index, since as mentioned earlier, in the case of a pre-fixed index, the optimizer can use
the partition key-value to scan just the necessary index partitions (rather than all) for a predicate referring to the
indexed columns.
Oracle8.x does not support global non-prefixed indexes, since they do not have any practical utility. A global
index may be split into local indexes or multiple local indexes may be merged into a global one.
Considering partitioned indexes
Let me illustrate this “partitioning scenario” with an example. One of my clients is a company providing free e-mail
via the web. Their users receive a lot of e-mails. When a user deletes a message, the mail-entry is “deleted”
from the main-messages table and moved to a garbage-bin. This is achieved by setting the garbage-flag to ‘1’ for
that row in the main table. Only when the user specifically deletes it from his garbage-bin, the mail is permanently
deleted. Many users set up their account such that once a week, their garbage-bin is automatically purged. So
every week-end, this huge batch-job was run to delete all rows from the main-table where the garbage-flag = ‘1’.
Since the garbage-flag column was being used in the WHERE clause of the DELETE, it was part of a regular B-Tree
index (a bit-mapped index was not used even though the cardinality was low, because the column was
subject to high updates and another key column was being used as the index-prefix). However, every time it was
updated to ‘1’, it’s index-entry would be deleted and a new-one would be created. Furthermore, when the record
would finally be deleted, the index-entry would continue to take up space. Thus the table would not significantly
grow in size (due to e-mails being deleted and inserted all the time). However the index was growing at an
incredible rate, taking up large amounts of tablespace area. Finally this problem was solved by replacing the
design with a “partition-aware” design.
Consider partitioning indexes :
· for intense OLTP applications, dealing with a large number of rows. The flexibility in administrating
Paper #135 / Page 7
relatively small segments is a great boon.
· for high-availability (24x7) environments, where the down-time associated with an index-rebuild cannot be
tolerated
· for better overall performance. Whenever possible, Oracle8/8i parallelizes scans of partitioned indexes. Also
partition elimination allows specific partitions to be eliminated from searches based on the key-ranges. This
vastly expedites index-scans. Finally, partitioning allows an index to be spread out over multiple physical
tablespaces/data-files, thereby reducing the occurrence of I/O hot-spots.
8. Do not partition indexes on key-values prone to constant updates, especially if such updates cause partition
migration, where a row has to be moved from one partition to another.
Lastly, one frequently question that pops up at client-sites is, how many partitions can be realistically created
without any performance degradation ? Theory apart (where thousands and thousands of partitions can be
created), the answer to this question depends on various factors such as system resources (CPUs, memory, I/O
channels across which the partitions are created, etc.) and varies from site to site. A lot also depends on
applications and data. If the data lends itself to a “clean” partitioning structure, based on a popular (frequently
used in most queries) key-column(s), then partitioning across a large number of partitions would be more
effective, since partition pruning is possible in most situations. However if such a popular key column is not
available, then utilizing a large number of partitions may cause performance degradation, since multiple partitions
would need to be frequently visited to retrieve the required values. As such, it is always essential to ensure that
you select a popular key column as the partitioning key. If such a column does not currently exist, evaluate
introducing it explicitly (for instance, by denormalizing the table). Additionally, always ensure that there is
sufficient scope for parallelizing partitioned operations (such as multiple CPUs, data spread across multiple
disks/controllers, etc.). I have seen partitioning to be very effective in certain situations, when the number of
partitions do not exceed a few hundreds. After that, the law of diminishing returns comes into play, as system
resources get saturated and performance begins to stink.
Reverse-key indexes
A reverse-key index is primarily a b-tree index, where the bytes of key-values are stored in a reversed order. By
reversing the keys, index entries are spread more evenly throughout the b-tree. This is very useful in an OPS
(Oracle Parallel Server) environment, where contention for the same leaf-blocks might cause inter-instance
“pinging” to occur, thereby affecting performance adversely due to excessive disk I/O.
Paper #135 / Page 8
Examples of a reversed key are as follows :
ADAMSON NOSMADA
1558 8551
Dates in a reverse-key index are stored by reversing the seven bytes contained in a DATE data-type.
Considering reverse-key indexes
In late 1996, at a client-site in Bombay, India, our team was implementing a Custodial Services system. We faced
a major performance-impediment with the share-certificates table; especially when the clients would acquire a
large number of share-certificates with sequential numbering. We had created the right index on the table and had
ensured that the index was indeed, being used. However, even a pure index-access would take unusually long : at
least 15 seconds more than what we expected (based on our experiments with other similar tables). Finally we
figured out that storing those sequential certificate numbers caused the index to be lop-sided. In other words,
some of the leaves would be dense (heavily populated), whereas some of them would be sparse, based on the
9. series of share-certificates we obtained. Most DML activity would focus on these dense leaves, thereby creating
“hot-spots” on the index. By hot-spots, I mean index-activity would be much more for these few leaf-blocks,
compared to the others. Since there would usually be only one disk-controller accessing those blocks, all activity
would have to be serialized, thereby adversely affecting response-time and throughput.
In such a situation, reverse-key indexes would have been a boon. However we were using Oracle7 v7.3 at that
time, where the feature was still not available. We lived with the situation for a while. Finally, we circumvented
the situation by reversing the values within the application. We had two columns in the table, a column that would
store the share-certificate number as it was and a primary-key column, which would store the same share-certificate
in a reversed order. We wrote a stored function, which would accept the actual share-certificate as an
input parameter and return the reversed number, which would subsequently be inserted in the table as the primary
key. This gave us a relatively “well-balanced” index.
The same concept applies to any value that increases in spurts : sequential numbers, date and time columns, etc.
Increasing in spurts indicates a scenario where the value monotonously increases, then there is not much activity
for a while, thereby creating “gaps” in the values; then the value constantly increases again. Such columns would
be prime candidates for using the Oracle8/8i reverse-key indexes. Also, in fast-growing tables where indexes are
built on sequential values (such as a primary-key index), and large-scale deletes frequently occur, the index may
begin to “slide”. A sliding index is one that continuously grows towards the right (newer rows being inserted),
while the left-portion is constantly being deleted (older rows being purged). The left-portions of the tree
comprises of nulls or empty leaf-nodes, whereas the tree is expanding to the right, thus appearing to “slide”.
Reversing the index may halt the right-oriented growth (due to constantly increasing key-values) and make the
growth more balanced by placing the reversed key-values in all directions (left and right). Additionally,
partitioning the index (on a “rolling window” scheme) and truncating older partitions (rather than deleting) would
help in eliminating the large number of empty leaf-blocks.
One question might come to your mind : how did we manage to figure out that our index was indeed lop-sided ?
We used an “ALTER SESSION SET EVENTS” command to understand the index-structure better. This
command is mentioned in more detail in Appendix A.
Some caveats to bear in mind while using reverse-key indexes :
· Reverse key indexes are not effective when used in range scans
· Reverse key indexes are prone to all the restrictions of regular b-tree indexes, such as avoiding excessive
Paper #135 / Page 9
DML activity, periodic re-building, etc. (since they are merely a variation of B-Trees).
Index-organized tables (IOTs)
Index-organized tables are tables, where the rows are stored in a B-Tree structure, based on the primary-key
value. A primary key definition is mandatory for an IOT. In Oracle8 v8.0.x, IOTs do not have rowids (this
restriction is removed in 8i via “logical rowids” [explained below]). An index entry is expected to be terse to
remain effective. Since a full-fledged row may be too big to retain index-effectiveness, Oracle8/8i provides the
PCTTHRESHOLD clause to indicate a percentage of the total block-size. All non-primary key columns that cause
a row to grow beyond the size of this percentage are moved to an “overflow” tablespace.
10. Considering index-organized tables
A short while ago, I was part of a team, building a large data-warehouse for a major investment company in
Oracle8/8i. In the data-warehouse, we had a bunch of code-tables, acting as dimension tables. In the first iteration
of the physical design, we built all those code-tables as regular tables with two columns : the code-id and the
description. We had a primary key index on the code-id. We also cached some of these tables and their indexes in
the Oracle SGA.
However with more familiarity with Oracle8i, we observed that these code-tables were prime candidates for using
the index-organized feature. Since there was only one index on each of these tables, we could use the primary-key
to structure the rows in these tables in a B-Tree format. In the second iteration of the physical design, we re-defined
most of our code-tables as index-organized tables. This change brought us space-savings to the tune of
35%. From around 800 MB for both regular-tables and the primary key indexes, the space-usage has dropped
down to approximately 520 MB for the index-organized tables. These space-savings affect not only disk-usage,
but also memory usage (since many of the tables were cached). That was true space-savings for us, since memory
is always at a premium! Soon after we could consider caching additional information within the SGA, without
having to expand it.
Needless to add, this space-saving was obtained due to absence of the physical rowids and any indexes on the
index-organized tables. This example illustrates that tables such as code-tables or certain master tables, not prone
to frequent changes are ideal for using the index-organized feature of Oracle8/8i. This useful option brings
substantial space-savings to the table, especially when going from a regular table with an index comprising of
most of the columns in the table (built that way to exploit the “index-only lookup” access-path) to an index-organized
Paper #135 / Page 10
table.
Some caveats with IOTs in Oracle8 versions (prior to 8i):
· The table structure cannot be altered
· Non-primary key indexes cannot be created
· Absence of rowids might cause problems while migrating Oracle7 applications to Oracle8, if the application
DML explicitly depends on rowids
Note that all the above restrictions have been eliminated in Oracle8i, allowing IOT usage to be more practical.
The new “logical rowids” contain the primary key of the referenced row, instead of a physical location within the
database. This means that if a row is moved to a new location, the logical rowid would still contain a valid
reference to the row and would not need to be updated. To retain the speed of conventional rowids, logical rowids
also contain a physical location hint, which can be used to optimize performance. If an index is created on a field
other than the primary key (note that IOTs support secondary indexes in 8i), Oracle8i will create a logical index
using logical rowids to reference the table rows.
One caveat that does remain even in 8i is row-chaining, which is inevitable with large-sized rows.
11. Bit-mapped indexes
The biggest difference between a b-tree index and a bit-mapped index is that the former encourages indexing on
columns with high selectivity, whereas the latter encourages low selectivity. This is primarily due to inherent
structural differences. A bit-mapped index creates a binary bit-map stream, consisting of 1s and 0s for every
distinct key-value. It uses distinct bits (of every byte) to create these binary steams. Due to application of bit-level
binary arithmetic, large-numbers of rows can be processed at high speeds. Also, if a table has multiple low-cardinality
columns, then bit-map indexes may be created on each of those columns. Multiple indexes in turn will
be merged using bit-level boolean logic resulting in very fast index-access times. In the case of compound
indexes, a bit-mapped index can have upto a maximum of 14 columns in Oracle7.x and upto 30 columns in
Oracle8/8i (always 2 less than B*Tree indexes). Bit-mapped indexes are available only with CBO.
Bit-mapped indexes are stored in compressed form, thereby causing huge space-savings. Oracle uses a proprietary
compression algorithm, capable of handling even high-cardinality columns (this is useful in scenarios such as a
star-schema, where the dimension tables have very high cardinality). On a data-warehouse at a client-site, a B-tree
index on a column with 2 distinct values on a table with 12 million rows took up around 184Mb, whereas a bit-mapped
index on the same column now takes up a mere 3Mb – a direct saving of more than 98% disk-space!
The star-transformation access-path (introduced in Oracle8) uses bit-mapped indexes to merge results from
multiple dimension tables with a large fact table to produce extremely fast query results, without having to
perform an expensive Cartesian product as in a regular star query.
An example of a good candidate for a bit-mapped index would be the “marital status” column on the customer
table in a data-mart for the Sales department. The marital status column would have a domain consisting of the
following 4 values : single, married, divorced, widowed. The logical view of a bit-mapped index created on this
column is represented in table 2a :
Paper #135 / Page 11
'RPDLQ 9DOXH
&XVWRPHU /DVW
1DPH
6LQJOH 0DUULHG 'LYRUFHG :LGRZHG %LWPDS
SDWWHUQ
6PLWK
:KLWH
%URZQ
%ODFN
Table 2a : column “Bit-map pattern” provides the logical index-key pattern [remember a bit may either be on (1) or off (0) ]
From a physical standpoint, each bit-map stream consists of a header portion and a body, wherein the header
comprises of the key-value and the beginning and ending rowids for a range of rows having that key-value. The
body comprises of the actual bit-map stream for the same range of rows. The size of the body is guaranteed to be
50% or less of the initialization parameter DB_BLOCK_SIZE. Thus, a bitmap may be stored as smaller pieces to
adhere to this rule. The physical structure is depicted in figure 2b.
12. Paper #135 / Page 12
Header body
{Key-value} {begin ROWID} {end ROWID} {bit-map stream}
In Oracle7 :
Key-value Begin ROWID End ROWID Bit-map stream
‘Salaried’ 12345211.3415.5642 12345211.3415.5822 0101010000111
In Oracle8/8i :
Key-value Begin ROWID End ROWID Bit-map stream
‘Salaried’ AAAAgmAADAAAAE8AAB AAAAgoAADAAAAFVAAC 01110111011
Figure 2b: Physical view of a bit-mapped index
A few things to consider while creating a bit-map index :
· Is the table often queried by this column ? If not, is an index really needed on the column ?
· How many rows are present in the table ? A small table (a few thousand rows) does not warrant a bit-mapped
index.
· Does the column have low cardinality ? If not, is the degree of uniqueness high ? Degree of uniqueness may
be depicted as “number of unique values / total number of rows in the table * 100”.
· Is the column prone to frequent DML (non-batch, OLTP DML) ? If so, it’s probably a good idea to leave it
alone or maybe use it as a secondary column to create a regular B-tree index (the leading column should have
high selectivity).
· Are there multiple columns in the table, which are good candidates for bit-mapped indexes ? If so, would such
columns be frequently referred in the WHERE clause of queries ? If so, there are high chances of the bit-mapped
indexes on these columns being merged by CBO to provide high query throughput.
· Is the data in the table subject to frequent changes simultaneously by multiple users ? If so, a bit-mapped
index may not be convenient. Bit-mapped indexes restrict concurrency, due to the entire bit-map being locked
during a single DML operation affecting any row in the range of rows represented by the bit-map. This could
potentially cause hundreds of rows to be locked by a single DML statement, intending to affect a single row.
This is not an issue during parallel DML operations affecting partitioned tables, since index-maintenance
during such parallel DML occurs at the partition-level. As such, a single index partition is not subjected to
parallelism, thus leading to concurrency problems.
Function-based indexing
This new feature introduced in Oracle8i allows functions to be indexed and used in SQL statements. Earlier
Oracle releases expressly disabled index-usage when functions were utilized on the key column. However now,
SQL or user-defined functions can be indexed. For instance, a query such as the following would benefit
tremendously from function-based indexes :
SQL SELECT * FROM names WHERE INITCAP(name) LIKE ‘Bill%’;
13. Paper #135 / Page 13
An index can be created on a function as follows :
CREATE SQL INDEX upper_nm_idx01 ON names(INITCAP(name))
TABLESPACE tblspc_nm STORAGE(storage_clause);
Now, applications can issue case-insensitive queries such as the following :
SQL SELECT * FROM name WHERE name LIKE ‘Bill%’;
Function-based indexes are also physically stored in a B-tree structure. Please refer to the Oracle8i documentation
(Oracle8i Concepts) set for further explanation of function-based indexing.
Application domain indexes
Application domain indexes are “extended” indexes specific to a certain application domain. These are indexes
created on non-conventional data-types (domains) such as spatial data and multimedia data-types such as images
and audio/video streams, spreadsheets and documents, etc. Here, an encapsulated set of code-routines referred to
as an indextype define the index access and implementation rules for efficient and speedy retrieval of complex
domains. A data-cartridge is custom-built (via the Oracle8i Data-cartridge Interface) to define the application
that controls the domain and also defines the inherent structure of the domain index. In other words, all operations
performed on the index such as range scans, equality checks, etc. need to be explicitly defined. The index can be
stored inside the database as an index-organized table or externally, as a file. As of Oracle8i v8.1.5, a domain
index can have only a single column. This column can be a regular scalar data-type or a user-defined attribute of
an object or a LOB. Information on domain indexes (meta-data) cannot be found in conventional index-related
views such as DBA_INDEXES, USER_INDEXES, etc. The data-cartridge needs to utilize a specific index meta-data
management strategy and nominate a pre-defined table to hold the meta-data on domain indexes of a certain
indextype. Furthermore, wrapper views can be created on top of the meta-data table, if necessary, for additional
encapsulation. The Oracle8i documentation set (Oracle8i Data Cartridge Developer's Guide) discusses
application domain indexes in detail.
Abstract Data-types their Indexes
Peek into your abstract data-types with a magnifying glass to identify the scalar data-types, which need indexing.
Once the abstract type has been broken down into scalars, all the above mentioned indexing rules apply to them.
Whenever possible, ensure that all your DML specifies the absolute path to these scalar types (else the optimizer
may not use the right index). By absolute path, I mean “person.name.last_name” and not just “person” even
“person.name”.
Considering an index-rebuild
This is the million-dollar question that many DBAs and developers face : when should one think about re-building
an application’s indexes ? I have noted that in some organizations, there is a fixed schedule : every 3 months,
indexes are re-created on tables, undergoing major DML activity. However some of these frequent index-rebuilds
might be unnecessary or even indexes of less frequently used tables might need to be re-built on a more regular
14. basis. How does one determine whether they need to be re-built or not ? How does one get actual numbers
indicating that an index is a candidate for re-building ? By using the command ANALYZE
INDEX…VALIDATE STRUCTURE. There is another old command called VALIDATE INDEX. This is still
available in Oracle8/8i (mainly for backward compatibility). Either of these commands populate the
INDEX_STATS view with meaningful information, that helps in determining whether an index should be re-built
or not. Please refer to figure 3a for a description of the structure of INDEX_STATS view.
As is apparent from the structure, DBAs and developers can use the INDEX_STATS view to glean some useful
information. The statistics in INDEX_STATS are different from the ones in DBA_INDEXES. The statistics in
DBA_INDEXES are created as a result of using the COMPUTE STATISTICS or ESTIMATE STATISTICS
options of the ANALYZE command and are used by the cost-based optimizer (CBO). The statistics in
INDEX_STATS are created by the VALIDATE STRUCTURE option of the ANALYZE command. Another
important difference is that the ANALYZE STRUCTURE…DELETE STATISTICS command erases statistics in
DBA_INDEXES; however the statistics in INDEX_STATS are not erased. They get erased only when the index
is dropped or get re-computed every time the ANALYZE…VALIDATE STRUCTURE command is run. The
INDEX_STATS view only contains information about one index at any given point in time. Don’t worry about
old statistics in the INDEX_STATS misleading CBO. CBO does not use the statistics in INDEX_STATS.
Paper #135 / Page 14
SQL desc sys.index_stats
Name Null? Type My comments
------------------------------- -------- ---- --------------
HEIGHT NUMBER Height of tree
BLOCKS NUMBER
NAME VARCHAR2(30)
PARTITION_NAME VARCHAR2(30)
LF_ROWS NUMBER Leaf rows
LF_BLKS NUMBER Leaf blocks
LF_ROWS_LEN NUMBER
LF_BLK_LEN NUMBER
BR_ROWS NUMBER Branch rows
BR_BLKS NUMBER Branch blocks
BR_ROWS_LEN NUMBER
BR_BLK_LEN NUMBER
DEL_LF_ROWS NUMBER Deleted leaves
DEL_LF_ROWS_LEN NUMBER
DISTINCT_KEYS NUMBER
MOST_REPEATED_KEY NUMBER
BTREE_SPACE NUMBER
USED_SPACE NUMBER
PCT_USED NUMBER
ROWS_PER_KEY NUMBER = 1 for UNIQUE keys
BLKS_GETS_PER_ACCESS NUMBER
Figure 3a : description of the INDEX_STATS view in an Oracle8/8i database
15. Paper #135 / Page 15
COLUMN name FORMAT a25 -- increase if your index-names are 25 chars
COLUMN “Total leaf rows” FORMAT 999,999,999,999
COLUMN “Deleted leaf rows” FORMAT 999,999,999,999
SELECT name, lf_rows “Total leaf rows”, del_lf_rows “Deleted leaf rows”
FROM SYS.index_stats
WHERE name = UPPER(‘enter_index_name’);
Figure 3b : Query to determine whether index is to be re-built (to be run after ANALYZE INDEX…VALIDATE STRUCTURE
as a DBA user)
The query listed in figure 3b provides the total leaf rows and the deleted leaf rows for a given index. If the deleted
leaf rows seem relatively high compared to the total leaf rows, then the index should be re-built. I use 20% as a
general rule-of-thumb. If the deleted leaf rows are 20% or more of the total leaf rows, I rebuild the index
(provided Management waves the green flag!). Also , the command provided in appendix A (ALTER SESSION
SET EVENTS…) provides additional information to help in deciding whether an index should be rebuilt.
The REBUILD option
Starting with Oracle7 7.3, you can use the REBUILD option of the ALTER INDEX command to rebuild indexes.
You can even use this option to reverse a regular B-Tree index and vice-versa. However a caveat with REBUILD
is, it does not detect and eliminate index corruption, since it bases the rebuild on the existing index (which might
be corrupt already). (This holds true even in Oracle8i.)
ANALYZE TABLE …VALIDATE STRUCTURE CASCADE is the best mechanism to detect index-corruption.
This command matches each row in the table against all indexes and each row in every index against the table. If
there is any corruption found, an error message is returned. Subsequently the index may be dropped and re-built
using the regular DROP and CREATE INDEX commands, rather than the REBUILD option of the ALTER
INDEX command.
Conclusion
By selecting an approach that considers the tips outlined in this paper, you can not only determine when to rebuild
an index, but also how should the new index be re-built – as a reverse-key index or as a partitioned index or even
converting a table to an index-organized table.
Even though Oracle8i has vastly increased the number of indexing options available, B-Tree indexes continue to
offer the best solution in most cases. As long as they are well-maintained, they provide acceptable performance
under diverse conditions. However, certain situations, explained above, prove to be ideal for deviating from the
B-Tree approach and instead using another index type such as bit-mapped, or even variations of the B-Tree such
as reverse-key, function-based, partitioned or index-organized tables. With the versatile index offerings of
Oracle8i, there has never been a better time to put on that new thinking cap and look at the process of indexing as
an art, not a science!
16. Paper #135 / Page 16
Appendix A
Command to dump an index tree structure
The command “ALTER SESSION SET EVENTS ‘IMMEDIATE TRACE NAME TREEDUMP LEVEL
level_number’;” dumps the structure of an index b-tree. It gives useful information such as number of levels in
the index and number of rows per level. Such information helps the DBA to decide whether an index needs to be
re-built and if so, whether it should be reversed.
The level_number mentioned in the command above needs to be replaced with an actual number. In
Oracle8/8i, this level-number is the “object_id” of the index in SYS.DBA_OBJECTS.
Running the command gives a trace-file in the “user_dump_dest” directory. The trace-file name usually is of the
form : “ora_SID_NAME_PID#.trc” , where SID_NAME is the ORACLE_SID and PID# is the process-id
number. Immediately after running this command, you can go to the user_dump_dest directory and run the
following command : “ls –lt” to pick up the latest trace file. A sample trace-file is provided in fig. 4a. The last
lines of the dump-file contain the meat of the output (between “begin tree dump” and “end tree dump”). The trace
file size is directly proportional to the size of the index.
The file-contents can be interpreted by looking at the number of levels : the branches and the leaves per branch. In
the sample file provided in fig. 4a, there is only one branch (0x40002b) and 4 leaves (0x4003f0, 0x4003f1,
0x400b0f and 0x400b10), since the dump was produced for a small index with just 1,819 rows. The rows
contained in each leaf are 503, 418, 498 and 400 respectively. This indicates that all the leaves are well-balanced
(in terms of rows per leaf). So, it is a good idea to leave this index alone. However if the rows in some leaves
seem excessively high compared to other leaves, then probably those leaves are hot-spots and could lead to I/O
bottlenecks.
Generally, spurts of sequential values or large delete-jobs cause such uneven distribution of keys. If the culprit is
merely a huge batch delete job, then probably the situation can be set right by partitioning the table and truncating
the partition. If partitioning is a not a feasible solution, then re-building the index (without reversing) after such
batch-deletes would help. Alternatively, if the imbalance is caused by spurts of sequential values being inserted in
the table, then reversing the key(s) would help.
17. Paper #135 / Page 17
Dump file /opt/app/oracle/admin/dv01/udump/ora_dv01_4231.trc
Oracle8 Enterprise Edition Release 8.1.5.0.0 – Production
PL/SQL Release 8.1.5.0.0 – Production
ORACLE_HOME = /opt/app/oracle/product/8.1.5
System name: sample
Node name: sample
Release: 4.0
Version: 3.0
Machine: 3446A
Instance name: dv01
Redo thread mounted by this instance: 1
Oracle process number: 7
Unix process pid: 4231, image: oracledv01
Wed Jul 14 11:52:11 1999
*** SESSION ID:(8.69) 1999.07.14.11.52.11.000
----- begin tree dump
branch: 0x40002b 4194347 (0: nrow: 4, level: 1)
leaf: 0x4003f0 4195312 (-1: nrow: 503 rrow: 503)
leaf: 0x4003f1 4195313 (0: nrow: 418 rrow: 417)
leaf: 0x400b0f 4197135 (1: nrow: 498 rrow: 498)
leaf: 0x400b10 4197136 (2: nrow: 400 rrow: 400)
----- end tree dump
Figure 4a : Sample trace-file (output of the ALTER SESSION…SET EVENTS command described in appendix A)
Warning : Please use all commands and scripts provided in this paper at your own risk. I have used all the commands, provided herein,
multiple times on Oracle8 v8.0.x and v8.1.x databases without any kind of adverse effect. However neither I nor my company, Immedient
(formerly Raymond James Consulting), warrant that this document is error-free and/or assume any risk associated with it’s usage . Please
test all scripts and commands on a development database prior to usage on a production database.
References
· Oracle8i Server Documentation
· Oracle 24x7 Tips and Techniques; Venkat S. Devraj; Oracle Press (Osborne/McGraw-Hill); 1999