© 2005 Martin Rinehart
To a database, an index serves the same purpose as a card file serves in a library. Just as a library will have indexes on title, author and subject, a database table will also have multiple indexes. There will usually be two to three indexes for each table, and there may be more. Consider this table.
| ... | 10005432 | Jones | Thomas | 147 North Maple St. | SomeTown | AState | 54321 | 800 555 1212 |
| ... |
The most heavily used index is one based on the record ID. Each record in, for example, a customer table has an ID assigned (typically, an integer number) that is unique to the customer. Find that ID and you find the customer's other data. Insist that your telemarketers ask the customer for their ID number and you are commiting the worst sin in business: customer abuse.
Most tables also have indexes on at least one natural order field. Customer files will be indexed on last name. These will not be unique (unless you have very few customers) so other criteria, such as postal code, will also need to be indexed. You want your telemarketers asking friendly questions, such as, "What is your ZIP code, Mr. Smith?" (That's the U. S. postal code.) And then, "Thank you. Do you still live at ... ?" (At this point the telemarketer should be looking at the data from the customer table, showing Smith's name, address and other data. If your system is first rate it has also retrieved the last few orders placed by Smith, which it gets almost instantly because the sales table is indexed on customer IDs, among others.)
The classic data structure used for indexes is the B-tree (where the "B" stands for balanced, or for its inventor Rudolph Bayer, or for Bayer's employer, Boeing). B-trees (and various improved forms, such as B+trees and B*trees) are the form chosen to index most Relational Database Management Systems (RDBMSs) such as Oracle, DB2 and MySQL. They excel when the amount of data is more than can fit in memory and disk accesses will prove the limiting speed factor. With a B-tree you can find one record in a billion with just 2 or 3 disk reads.
![]() |
![]() |
| Courtesy Wikipedia definitions of "tree" and "pyramid." | |
A better analogy would be pyramid. A B-tree is a stack of nodes. Let's look at an example using 4-item nodes. (Real nodes might hold 64k items or some other rather large number. They work the same, regardless of number.) In my book Java Database Development I start with an algorithm for building a pyramid from blocks. It went this way.
|
With the second block, Rule 3B applies:
|
With a third block, rule 3A applies:
| ||||||||
Now you can see the pattern. Rule 3B applies, then rule 3A applies once for every level in the pyramid:
|
|
|
||||||||||||||||||||||||||||||||||||||||||
You'll see that this also models the construction of B-tree pyramids.
| pointer | value | pointer | value | pointer | value | pointer | value | pointer |
The number of pointers is called the node's order. Since a node starts and ends with a pointer, its order is one more than its number of values, which I call a node's size.
Within each node, the values are stored in ascending sequence. The pointer to the left of the first value points to the node containing values less than the first. The pointer to the right of the first value points to a node containing the values greater than the first, but less than the second. At the bottom level the pointers are null (point to node -1).
| # 1 | -1 | -1 | -1 | -1 | -1 |
Add value "Sally":
| # 1 | -1 | Sally | -1 | -1 | -1 | -1 |
Add value "Joan":
| # 1 | -1 | Joan | -1 | Sally | -1 | -1 | -1 |
Note that you had to move "Sally" when you entered "Joan". Next add "Alice" and "Zoe":
| # 1 | -1 | Alice | -1 | Joan | -1 | Sally | -1 | Zoe | -1 |
Now your node is filled. Assume you next add "Thelma" to your B-tree. This begins the process of growing the pyramid. The goal is to place the middle value on top of two nodes holding the lower and higher values, this way:
|
|||||
|
|
||||
First you decide which value will be the middle value after the addition of the new value. In this case, "Sally" is the middle value. Then you follow a three step procedure:
| # 1 | -1 | Alice | -1 | Joan | -1 | -1 | -1 |
| # 2 | -1 | Thelma | -1 | Zoe | -1 | -1 | -1 |
| # 3 | 1 | Sally | 2 | -1 | -1 | -1 |
And now we can interrupt this section to talk about the Find operation.
That's almost all there is to it. What about the case where you are not inserting, but are searching for a value? The procedure is the same but you stop when your comparison returns an exact match. (For some requirements you may want to return the number of exact matches, so a bit more searching is needed. Remember that the first "Smith" in a node at level N may have more "Smith"s both to its left and to its right at level N-1 and right on down to the ground level.)
What about the case where you are inserting and you find an exact match? The typical rule is that you insert after the last exact match (maintaining the original order) but your application may call for something else.
Now that we know how to find things, we can return to inserting, which is much more interesting.
|
|||||||
|
|
||||||
Our next insertion is "Barbara" who also goes into the node left of Sally, but that node is full. What now? Again, find the new middle value: "Arlene". We'll need another ground-level node to hold the higher values, "Barbara" and "Joan". We'll leave just the two smaller values, "Alice" and "Ann" in the original ground node and we'll pop the middle value, "Arlene", up into the root node. When we're done it will look like this:
|
|||||||||
|
|
||||||||
What do we do if the root node is full? Actually, we've been there already. Our original ground node was also the root node (as it was the only node). You split the root node into two and push up the middle value into a new node that then becomes the root. Here's the full algorithm.
Assume that we are inserting value V into a B-tree. Ground level is the number of pointers followed from the root to the bottom level.
The basic operation of the B-tree (remember that it may stand for balanced tree) has all nodes from 50% to 100% full. Deletions may cause a drop below 50%. If that would happen you do one of two things.
So in the above discussion replace "half" with a tuning parameter that you can test at, for instance, 40%, 35%, and so on, running your mix of finds, inserts and deletes to see what gives the best overall performance.
So the value field is really a value/record pointer pair. That's a pointer to the physical record.
For names there is another consideration. Should we have a fixed length value, or should we have a pointer into a table of variable-length values? You really have to program both and test for best performance with your application. Let the database administrator have a nice set of performance tuning tools.
For a more capable B-tree, see my article, "The B++tree."