Skip to content

Indexing | Computer Science - Wyatt's Notes

B-Tree of order mm. A balanced search tree where each internal node has between m/2\lceil m/2 \rceil and MM children. All leaves are at the same depth.

Properties:

  • Root: at least 2 children (unless it is a leaf).
  • Internal node with kk children has k1k - 1 keys.
  • All leaves at the same depth hh.
  • Height: h=O(logmn)h = O(\log_m n).

Operations:

  • Search: O(logmn)O(\log_m n). At each node, binary search the keys and follow the appropriate child pointer.
  • Insert: Find the correct leaf. If the leaf has room, insert. If full, split the leaf and propagate the median key up. May cascade splits to the root.
  • Delete: Find the key. If in an internal node, replace with predecessor/successor and delete from the leaf. If a node underflows (fewer than m/21\lceil m/2 \rceil - 1 keys), redistribute from a sibling or merge with a sibling.

B+ Tree. Variant where:

  • All data records are stored only in leaf nodes (internal nodes store only keys for routing).
  • Leaf nodes are linked in a linked list for efficient range queries.

This makes B+ trees the standard for database indexing. The linked leaf list allows range scans in O(logmn+k)O(\log_m n + k) where kk is the number of records in the range.

B+ Tree insert pseudocode:

def bplus_insert(root, key, value):
leaf = find_leaf(root, key)
insert_into_leaf(leaf, key, value)
if len(leaf.keys) > MAX_KEYS:
new_leaf, median = split_leaf(leaf)
insert_into_parent(leaf.parent, median, new_leaf)
Worked Example 5.1: B+ Tree Insertion

Insert keys 10, 20, 5, 15, 25, 30 into a B+ tree with maximum 2 keys per leaf node and maximum 2 Keys per internal node (order m=3m = 3).

Insert 10. Leaf: [10][10]

Insert 20. Leaf: [10,20][10, 20]

Insert 5. Leaf would be [5,10,20][5, 10, 20] — overflow (max 2 keys). Split: left leaf [5][5]Right Leaf [10,20][10, 20]. Push median (10) to new internal node.

Internal: [10]
├── Leaf: [5]
└── Leaf: [10, 20]

Insert 15. Goes to right leaf. Leaf would be [10,15,20][10, 15, 20] — overflow. Split: [10,15][10, 15] and [20][20]. Push 15 up.

Internal: [10, 15]
├── Leaf: [5]
├── Leaf: [10, 15]
└── Leaf: [20]

Insert 25. Goes to rightmost leaf. Leaf: [20,25][20, 25].

Insert 30. Leaf would be [20,25,30][20, 25, 30] — overflow. Split: [20,25][20, 25] and [30][30]. Push 25 up. Internal would be [10,15,25][10, 15, 25] — overflow (max 2 keys). Split internal: push 15 to new root.

Root: [15]
├── Internal: [10]
│ ├── Leaf: [5]
│ └── Leaf: [10, 15]
└── Internal: [25]
├── Leaf: [20, 25]
└── Leaf: [30]
Worked Example 5.2: B+ Tree Deletion

Starting from the tree in Example 5.1, delete key 25.

Delete 25. Found in rightmost leaf of the right internal node. Leaf becomes [30][30] (1 key, Minimum is 2/2=1\lceil 2/2 \rceil = 1). No underflow. Update internal node: key 25 changes to 30 (to maintain correct routing).

Root: [15]
├── Internal: [10]
│ ├── Leaf: [5]
│ └── Leaf: [10, 15]
└── Internal: [30]
├── Leaf: [20]
└── Leaf: [30]

Now delete 15. Leaf [10,15][10, 15] becomes [10][10]. No underflow. Internal node key 15 changes to 10. But wait — the internal node [10][10] would need to distinguish between leaves [5][5] and [10][10]. Since the left child contains keys <10\lt 10 and the right child contains keys 10\geq 10This Is still correct.

Now delete 10 from the left subtree”s right leaf. Leaf becomes empty — underflow.

Redistribution: Sibling leaf [5][5] has 1 key (at minimum). Cannot redistribute. Merge: Merge empty leaf with [5][5]. The merged leaf is [5][5]. The internal node [10][10] now has Only one child (the merged leaf), so it underflows.

Since the internal node is a child of the root, and the root has two children, we can merge: remove The internal node and promote its remaining child to be a direct child of the root.

Root: [30]
├── Leaf: [5]
├── Leaf: [10] (originally contained 10 before deletion)
└── Leaf: [30]

Wait — after deleting 10, the leaf [10][10] should become [][] And merging with [5][5] gives [5][5]. Let me retrace. The point is that deletion can trigger cascading merges up the tree.

Static hashing. A hash function h:K0,,B1h : K \to \\{0, \ldots, B - 1\\} maps keys to BB buckets. Average lookup: O(1)O(1) under uniform hashing. No support for range queries.

Collision handling. When two keys hash to the same bucket, the DBMS must resolve the collision:

  1. Separate chaining: Each bucket contains a linked list of entries. Lookup requires traversing the chain. Average chain length under uniform hashing is n/Bn / B.
  2. Open addressing (linear probing): If bucket h(k)h(k) is full, try h(k)+1h(k)+1, h(k)+2h(k)+2Etc. (mod BB). Prone to primary clustering: consecutive occupied slots increase the average probe length.
  3. Open addressing (quadratic probing): Try h(k)+12,h(k)+22,h(k)+32h(k) + 1^2, h(k) + 2^2, h(k) + 3^2Etc. Reduces clustering but may not probe all buckets.
  4. Open addressing (double hashing): Use a second hash function h2h_2: probe sequence is h(k)+ih2(k)h(k) + i \cdot h_2(k) for i=0,1,2,i = 0, 1, 2, \ldots. Minimises clustering.

Extendible hashing. Dynamically grows the number of buckets. Uses a global depth dd and Per-bucket local depth dd'.

  • Hash key to dd bits.
  • If the bucket is full and its local depth equals the global depth, double the directory (global depth increases by 1) and split the bucket.
  • If the bucket’s local depth is less than the global depth, split the bucket without doubling the directory.

Linear hashing. Gradually grows one bucket at a time using a pointer to the next bucket to split. Simpler than extendible hashing but may have slightly higher overflow probability.

Comparison:

PropertyB+ TreeHash Index
Point queryO(logmn)O(\log_m n)O(1)O(1) average
Range queryO(logmn+k)O(\log_m n + k)Not supported
Insert/DeleteO(logmn)O(\log_m n)O(1)O(1) average
SpaceNodes may be half-emptyMay have overflow chains
OrderMaintains key orderNo ordering

A bitmap index creates one bitmap per distinct value of an attribute. For a table with nn rows And attribute AA with values v1,,vk\\{v_1, \ldots, v_k\\}Store kk bitmaps of nn bits each, where Bitmap ii has a 1 in position jj if row jj has A=viA = v_i.

Use case: Low-cardinality columns (gender, status, country). Bitmap indexes support fast bitwise AND/OR for multi-criteria queries.

Bitwise operations for query evaluation:

QueryBitmap operation
A=v1A = v_1 AND B=v2B = v_2\mathrm{bitmap_}{A,v_1} AND \mathrm{bitmap_}{B,v_2}
A=v1A = v_1 OR A=v2A = v_2\mathrm{bitmap_}{A,v_1} OR \mathrm{bitmap_}{A,v_2}
Av1A \neq v_1NOT \mathrm{bitmap_}{A,v_1}

Compression. For columns with many distinct values, run-length encoding (WAH or BBC) compresses Bitmaps effectively while still supporting bitwise operations.

The cost model estimates the number of disk I/O operations for a query plan. We assume the buffer Pool has BB pages and each disk page access costs one I/O.

Selection cost estimation:

Access methodCost (I/Os)
Full table scannR/B\lceil n_R / B \rceil (or nRn_R if BB pages available)
B+ tree equality searchlogf(nR)\log_f(n_R) leaf + 1 data page
B+ tree range searchlogf(nR)\log_f(n_R) leaf + rangepages\lvert\mathrm{range} pages\rvert
Hash equality search1 (ideal)

Where ff is the fanout (average number of children per internal node).

Join cost estimation (from Section 7):

AlgorithmCost
Block nested-loopnR+nR/(B2)nSn_R + \lceil n_R/(B-2)\rceil \cdot n_S
Sort-merge2nRlogB1(nR)+2nSlogB1(nS)+nR+nS2n_R \log_{B-1}(n_R) + 2n_S \log_{B-1}(n_S) + n_R + n_S
Hash join3(nR+nS)3(n_R + n_S) (if build relation fits in BB pages)
Worked Example 5.3: Comparing Access Methods

Scenario: Student table with 50000 tuples, 2500 pages (20 tuples per page). Buffer pool has 52 Pages. Attribute ID has a B+ tree index of height 3 (fanout 100). Query: SELECT * FROM Student WHERE ID = 12345.

Method 1: Full table scan. Cost = 2500 I/Os.

Method 2: B+ tree index on ID. Follow root \to internal \to internal \to leaf: 3 I/Os For the index traversal. Then 1 I/O for the data page. Total: 4 I/Os.

The B+ tree index is 625×625\times faster for a single equality lookup. However, for a query selecting 50% of the table, a full scan (2500 I/Os) would be faster than 25000 individual B+ tree lookups (25000×4=10000025000 \times 4 = 100000 I/Os). This is why the optimiser chooses between index and scan based on Selectivity estimates.

Indexes are the lookup tables that make database queries fast, like the index at the back of a textbook that tells you which page discusses a topic. Without an index, every query scans every row (a full table scan), which is like reading an entire book to find one fact. B-tree indexes organize data in a balanced tree structure that allows logarithmic lookup, the way a phone book is organized alphabetically so you can find any name in about 20 steps regardless of whether the book has 1,000 or 1,000,000 entries.

Assuming more indexes always improve performance: Indexes speed up reads but slow down writes (each insert/update must update indexes). Over-indexing degrades write performance. Choose indexes wisely.

Confusing clustered with non-clustered indexes: A clustered index determines the physical order of data (only one per table). Non-clustered indexes create separate structures pointing to the data. Don’t assume they’re the same.

Forgetting that indexes don’t help with all queries: Indexes are most useful for equality and range queries on indexed columns. Queries that scan most of the table won’t benefit from indexes.