Indexes On Small Tables Part I (One Of The Few) April 16, 2009

A common question I get asked is when is a table too small to benefit from being indexed.

If a table only has a few blocks for example, which could all be potentially read via a single multiblock read operation, surely there’s no benefit in indexing such a table (except perhaps to police an associated PK constraint). It must take at least 2 Logical I/O (LIO) operations to read data from the table via an index, at least one LIO to read an index block and at least one LIO to read the associated table block referenced by the ROWID in the index. If a Full Table Scan (FTS) can be effectively performed via a single multiblock read operation, hence reading the entire table with just one LIO , surely an index will always be a more expensive option and so ultimately useless with such small tables.

Well not necessarily …

The first thing to point out is that generally speaking, a Full Table Scan is a relatively expensive operation. Tables can be big, really really big, consisting of potentially many many 1,000s of data blocks, potentially requiring many 1,000s of multiblock read operations to be performed. Therefore, generally speaking, if we’re going to perform a relatively expensive FTS, we’re not going to be too concerned if we use an extra I/O or two, as we potentially have to perform 1,000s of I/Os anyways. A shortcut here or there is not going to generally make much of a difference one way or the other.

Note also that with a FTS being this relatively expensive operation, we’re not likely to generally speaking want to perform 1,000s of such FTS operations every minute within our databases. Generally speaking, a FTS is a much less common event than an Index Range Scan operation and so we wouldn’t take advantage of any possible short cuts here or there very often.

However, generally speaking, an index scan is a relatively inexpensive operation, potentially consisting of just a few LIO operations. We may have an index that has a blevel of say 2 (height of 3) and we may typically only want to select a row or two. That would therefore consist of just 3 LIOs of read the index related blocks (the index root block, an index branch block and an index leaf block) plus an I/O or two to read a row or two from the table. It’s potentially just a handful of blocks, just a few little LIOs but if we could somehow save an I/O or two in the process, this could in fact make a huge difference to the relative costs of the Index Range Scan.

Note also that with an Index Range Scan being this relatively inexpensive operation, we’re quite like to generally speaking want to perform lots and lots of such Index operations each and every minute in our databases. Generally speaking, an Index Range scan is a very very common event and so any short cut here or there can be extremely useful and significant and be taken advantage of frequently within the database.

So a FTS has a tendency to be relatively expensive and is not performed anywhere near as as frequently as Index Range Scan operations which have a tendency to be relatively inexpensive. Generally speaking of course.

But Oracle takes this generalisation very much to heart in how it goes about processing these operations.

The next point to make is that if a table has just a few rows and say consists of just the one data block below its High Water Mark (HWM), it doesn’t necessarily mean we only need just the one I/O operation to read the entire table. For example, how does Oracle know there’s just one block worth of data ? How does Oracle know where to actually physically locate this one block worth of data ? How does Oracle know that once its read this block, there aren’t any other data blocks of interest ?

The answer is that it can’t without referencing data dictionary objects and without accessing the table segment header where the extent map is located. Even for a tiny table with only a handful of rows that can reside in only the one table block, it therefore requires more than just the one consistent get operation to read data from the table via a FTS. However, as a FTS is usually a relatively expensive operation, these few little consistent reads here and there to determine the actual number of blocks in the table and the actual location of these blocks is generally going to be a relatively trivial overhead. Oracle though doesn’t differentiate between a small and a larger table when it comes to a FTS, so these extra few consistent reads can potentially be a significant overhead for FTS operations on smaller tables.

As an example, let’s create a little table and see what consistent gets are required to read it via a FTS …

Let’s begin by creating a small table that consists of just 100 little rows.

Note to read just this one row from this one block table, we have actually performed 4 consistent gets operations. Not 1 consistent get, but 4 consistent gets …

Let’s look at the actual type of consistent gets, by running the following statement in another session before and after executing the above SELECT statement (note SID 134 refers to the session SID that ran the above SELECT statement) :

Note that yes indeed, there were 4 consistent gets performed and that none of the consistent gets were the “cheaper” consistent gets examinations. Therefore, the 4 consistent gets used in performing the FTS of the one block table required 4 x 2 = 8 latches.

Now 4 consistent reads to perform a FTS isn’t too bad, even for this little table and 8 latches isn’t exactly a huge number.

However, as we’ll see next, an index on this tiny one block table can do so much better …

Share this:

Like this:

Related

“So a FTS has a tendency to be relatively expensive and is not performed anywhere near as as frequently as Index Range Scan operations which have a tendency to be relatively inexpensive. Generally speaking of course.

But Oracle takes this generalisation very much to heart in how it goes about processing these operations.”

Well, no it doesn’t, unless you tell it to. The default value of optimizer_index_cost_adj is 100, which (roughly speaking) instructs the optimizer to treat index scans as 100% as expensive as full table scans. So by default, Oracle does not assume one or the other access method is relatively more or less expensive. And you will observe that under this default parameter value, the optimizer will elect FTS more often than you might like.

I think you’ve missed the point somewhat. I’m not discussing how the CBO costs FTS vs an index scan, I’m discussing how a FTS might require more CR operations than an index scan, even on a table with just one data block.

The point I’m making is that there are “hidden” overheads when performing a FTS with Oracle having to visit the segment header which are not applicable to Index scans as I’ll discuss. So indeed, Oracle does note there are extra I/Os associated with performing a FTS that are not necessary associated with an Index Scan. Setting optimizer parameters doesn’t change the fact a FTS must visit the segment header.

Note I haven’t even created an index yet in my demo, that’s all to come. Hey who knows, perhaps Oracle might favour the index, even without setting any optimizer paramaters …

Perhaps it’s just your wording, but the O_I_C_A does not in fact instruct the CBO to treat index scans as being 100% as expensive as FTS, but that the cost of an I/O associated with an index scan access path is as expensive as the I/O of a FTS access path. Big difference.

Finally, you should not be setting the O_I_C_A parameter with currently supported versions of Oracle, you should be using system statistics which are generally more “accurate” than those values set by over keen DBAs, many of whom don’t even know what they’re really doing when they set the O_I_C_A parameter.

1) Although 4 blocks are listed, Oracle is in actual fact still only visiting the segment header and the actual data block containing all the rows of interest. It’s a quirk with the ASSM tablespace, although if you don’t follow the demo to the letter and say have multiple sessions inserting the 100 odd rows, then more than one data block will likely contain data and indeed more blocks will be visited and the CRs will increase accordingly. So although you’re using an ASSM tablespace, the point I’m making that a table containing all it’s rows within one data block requires 4 consistent gets is still valid. If you were to flush the buffer cache before running the select statement and trace the session, you should be able to see this in the trace file.

and

2) it’s not ideal because an index on this small, effectively one (or four) block table will actually be more efficient than the FTS as we shall see … ;)

The access to the data dictionary is serialized:one session at a time. All sessions must queue in order to access the data dictionary. Efforts are made to segment this data dictionary in order to allow multplite sessions access at the same time providing they access different regions of the data dictionary. Also Oracle try to use Mutex which are OS latches for they are faster. But all in all, if memory is 100k faster than disk, a full in-memory DB is far from being 100k faster. I re-call old figure of a ratio of 37, sound a bit weak but gives an hint of the scale we speak about.

1) The key difference between a FTS and an index scan is the visit to the segment header with a FTS (as I plan to discuss). The manner in which Oracle determines which block to first visit is similar in both cases, but with a FTS it must acccess the segment header while with the index scan it does not.

2) It doesn’t matter. A consistent get is a consistent get, whether or not it involves a physical I/O. Assuming it’s a heavily accessed small table and index, then both segments are likely to be cached. I wanted to generate PIOs however as it makes tracing the session turn up useful info.

3) Yes it does. Typically many small lookup type tables are accessed very frequently to determine a specific value in a join scenario. An index on a small table to be used in such join conditions can potentially save significant resources as we’ll see.

Discussions on the DD are beyond scope here as any DD overheads are somewhat similar between parsing/executing a FTS and Index scan execution plan.

The key point I will make though is that Oracle begins a FTS function call by accessing the segment header of the segment as it contains vital info required for the FTS operation.

However an index scan (but not a Fast Full Index Scan) begins by directly accessing the index root block (or single leaf block if it’s a level 0 index). there is nothing within the index segment header that’s required for an index scan, so the segment header is an “overhead” we can save and potentially taken advantage of.

Also, the type of consistent get can be “cheaper”, resulting in less block pinning and latch contention as we’ll see as well.

Something not mentionned here : a FTS will cycle its blocks only on a fraction of the LRU, 2% if I am correct. Thus if we have a lot of small different FTS, or a BIG FTS comes after your small FTS, all the blocks of your small FTS will be washed away from memory either by the Big FTS or my the sheer number of concurrent small FTS.
A index accesss on the contrary will have its blocks in more persistent locations of the SGA. Thus, on heavily loaded system can’t we say that it is better to have index access, even if they are marginaly costlier in order to guarantee better overall retention?

Not quite. A table is deemed “small” by Oracle if it’s 2% or less of the buffer cache in size. A small table is “cached” when read via a FTS and is not immediately loaded into the least recently used end of the LRU list as becomes a “large” table read via a FTS. Some bugs in some earlier releases though didn’t perform the touch count properly on cached small tables and as such they struggled to remain cached unless under serve load.

Therefore, a FTS of a larger table only has a very minimal impact on the buffer cache as such blocks age very quickly and don’t push other buffers out.

Whether something should remain cached depends entirely on how frequently it gets accessed and what it needs to push out in order to be cached.

The good thing about an index access is that the blocks can remain cached. The bad thing about an index access is that the blocks can remain cached !! Overall better retention is not necessarily a good thing if it means retaining and caching things that don’t get reused.

Meaning, if you have a really expensive and possibly inappropriate index access that loads the buffer cache with heaps of blocks that are not likely to be accessed again, pushing out other blocks that could have been reused, that’s not really a good thing. Yes the buffer cache touch count algorithm helps and protects things to same extent but a “bad” index access can still cause issues within the buffer cache.

A FTS of a large table doesn’t have the same risks, as such blocks don’t cause much damage. Therefore, an argument could be made the other way around and a FTS is actually the safer option with regard to the buffer cache.

Again, it all comes back to how often do the associated blocks get re-accessed in cache.

When a block is read from a “large” table via a FTS, the blocks are basically loaded into the LRU end of the LRU list, even though they’ve only just been loaded into the buffer cache. As such, the chances of them remaining in the buffer cache is minimal and will likely be immediately overwritten once processed.

However, when a block is read via an index, the blocks are loaded somewhere near the “middle” of the LRU list, not at the LRU end of the list. This means that the blocks have a way to go before being aged out and if subsequently accessed and the touch count increments sufficiently, have a reasonable chance of remaining cached in the buffer cache.

This is all designed to protect the buffer cache from the possible effects of a massive table being read into the buffer cache via a FTS. It by designed can only a minimal impact on the caching of other objects as the FTS will itself age out it’s own previously cached blocks.

The blocks from an index scan (or small table) on the other are given a reasonable chance of survival in the buffer cache, providing they are indeed subsequently accessed.

Very briefly, most of the pools basically behave the same as the default. There are a few slight differences, for example the Keep pool has the default behaviour of “cache” even for larger segments, but the basic LRU and touch based costings apply to all the pools.