This is an automated email from the ASF dual-hosted git repository.

kfaraz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 452b15b2662 perf: Interval tree for managing segment metadata in 
memory (#19138)
452b15b2662 is described below

commit 452b15b2662136459b2289d08b6eb89232d27c18
Author: pirvtech <[email protected]>
AuthorDate: Sat Aug 1 09:42:01 2026 -0700

    perf: Interval tree for managing segment metadata in memory (#19138)
    
    Segment metadata stored in memory of the Historicals, is used when looking 
up segments
    that match an interval for query and segment loading purposes. Currently 
this is a serial scan
    that goes through all segments metadata in ascending start time order to 
find matching segments.
    
    This changes introduces an Interval Tree as a more efficient way to store 
segment metadata in
    memory, to speed up searches for segments, and cut down search times from 
O(n) to O(logn).
    
    Changes:
    - Add IntervalTreeMap
    - Add SegmentTimelineConfig to enable fastIntervalSearch
    - Update SegmentManager and VersionedIntervalTimeline to use 
IntervalTreeMap when enabled
    - Add tests
---
 .../druid/msq/exec/MSQCompactionTaskRunTest.java   |   4 +
 .../druid/java/util/common/guava/Comparators.java  |  39 +
 .../org/apache/druid/timeline/IntervalTreeMap.java | 880 +++++++++++++++++++++
 .../druid/timeline/VersionedIntervalTimeline.java  | 125 ++-
 .../apache/druid/timeline/IntervalTreeMapTest.java | 550 +++++++++++++
 .../VersionedIntervalTimelineSpecificDataTest.java |  22 +-
 .../timeline/VersionedIntervalTimelineTest.java    |  20 +-
 .../VersionedIntervalTimelineTestBase.java         |   7 +-
 .../org/apache/druid/guice/StorageNodeModule.java  |   2 +
 .../segment/indexing/SegmentTimelineConfig.java    |  49 ++
 .../org/apache/druid/server/SegmentManager.java    |  22 +-
 .../org/apache/druid/sql/guice/SqlModuleTest.java  |   6 +
 12 files changed, 1693 insertions(+), 33 deletions(-)

diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQCompactionTaskRunTest.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQCompactionTaskRunTest.java
index fa99e238c46..0435f13d6a8 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQCompactionTaskRunTest.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQCompactionTaskRunTest.java
@@ -86,6 +86,7 @@ import org.apache.druid.segment.DataSegmentsWithSchemas;
 import org.apache.druid.segment.IndexSpec;
 import org.apache.druid.segment.QueryableIndexSegment;
 import org.apache.druid.segment.ReferenceCountedSegmentProvider;
+import org.apache.druid.segment.indexing.SegmentTimelineConfig;
 import org.apache.druid.segment.indexing.TuningConfig;
 import org.apache.druid.segment.loading.AcquireSegmentAction;
 import org.apache.druid.segment.loading.AcquireSegmentResult;
@@ -233,6 +234,8 @@ public class MSQCompactionTaskRunTest extends 
CompactionTaskRunBase
     ((InjectableValues.Std) 
objectMapper.getInjectableValues()).addValue(GroupingEngine.class, 
groupingEngine);
     ((InjectableValues.Std) 
objectMapper.getInjectableValues()).addValue(QueryToolChestWarehouse.class, 
null);
 
+    SegmentTimelineConfig segmentTimelineConfig = 
mock(SegmentTimelineConfig.class);
+
     Module modules = Modules.combine(
         new DruidGuiceExtensions(),
         new LifecycleModule(),
@@ -250,6 +253,7 @@ public class MSQCompactionTaskRunTest extends 
CompactionTaskRunBase
                         .toInstance(new 
ForwardingQueryProcessingPool(Execs.singleThreaded("Test-runner-processing-pool"))),
         binder -> 
binder.bind(ObjectMapper.class).annotatedWith(Json.class).toInstance(objectMapper),
         binder -> 
binder.bind(SegmentCacheManager.class).toInstance(segmentCacheManager),
+        binder -> 
binder.bind(SegmentTimelineConfig.class).toInstance(segmentTimelineConfig),
         binder -> 
binder.bind(VirtualStorageManager.class).toInstance(MSQTestBase.makeNilVirtualStorageManager()),
         binder -> binder.bind(GroupingEngine.class).toInstance(groupingEngine)
     );
diff --git 
a/processing/src/main/java/org/apache/druid/java/util/common/guava/Comparators.java
 
b/processing/src/main/java/org/apache/druid/java/util/common/guava/Comparators.java
index 618698c4ac4..04e4f7f0fc0 100644
--- 
a/processing/src/main/java/org/apache/druid/java/util/common/guava/Comparators.java
+++ 
b/processing/src/main/java/org/apache/druid/java/util/common/guava/Comparators.java
@@ -119,6 +119,34 @@ public class Comparators
     }
   };
 
+  private static final Comparator<Interval> INTERVAL_BY_START = new 
Comparator<>()
+  {
+    private final DateTimeComparator dateTimeComp = 
DateTimeComparator.getInstance();
+
+    @Override
+    public int compare(Interval lhs, Interval rhs)
+    {
+      if (lhs.getChronology().equals(rhs.getChronology())) {
+        return Long.compare(lhs.getStartMillis(), rhs.getStartMillis());
+      }
+      return dateTimeComp.compare(lhs.getStart(), rhs.getStart());
+    }
+  };
+
+  private static final Comparator<Interval> INTERVAL_BY_END = new 
Comparator<>()
+  {
+    private final DateTimeComparator dateTimeComp = 
DateTimeComparator.getInstance();
+
+    @Override
+    public int compare(Interval lhs, Interval rhs)
+    {
+      if (lhs.getChronology().equals(rhs.getChronology())) {
+        return Long.compare(lhs.getEndMillis(), rhs.getEndMillis());
+      }
+      return dateTimeComp.compare(lhs.getEnd(), rhs.getEnd());
+    }
+  };
+
   @Deprecated
   public static Comparator<Interval> intervals()
   {
@@ -135,4 +163,15 @@ public class Comparators
     return INTERVAL_BY_END_THEN_START;
   }
 
+  public static Comparator<Interval> intervalsByStart()
+  {
+    return INTERVAL_BY_START;
+  }
+
+  public static Comparator<Interval> intervalsByEnd()
+  {
+    return INTERVAL_BY_END;
+  }
+
+
 }
diff --git 
a/processing/src/main/java/org/apache/druid/timeline/IntervalTreeMap.java 
b/processing/src/main/java/org/apache/druid/timeline/IntervalTreeMap.java
new file mode 100644
index 00000000000..5a59966e8f0
--- /dev/null
+++ b/processing/src/main/java/org/apache/druid/timeline/IntervalTreeMap.java
@@ -0,0 +1,880 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.timeline;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Predicate;
+import org.joda.time.Interval;
+import org.joda.time.base.BaseInterval;
+
+import javax.validation.constraints.NotNull;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.function.BiConsumer;
+
+/**
+ * A variation of Interval Trees (https://en.wikipedia.org/wiki/Interval_tree)
+ * Custom optimizations for faster interval search and additional support for 
specific joda Interval comparator
+ * arithmetic used in the project. Additionally, it implements NavigableMap 
interface with relevant methods for
+ * traversal of the entries.
+ * <p>
+ * <p>
+ * Multiple different intervals can be added to the tree. It can then be 
searched to find all intervals matching a given
+ * interval. The user specifies the match condition, such as encompassing the 
given interval, overlapping, etc. The
+ * search can return multiple results as multiple intervals in the tree could 
match the criteria.
+ * <p>
+ * Using the tree, reduces the search time from O(N) iterating through all the 
intervals, to roughly O(log2(N)).
+ * Furthermore, a value can be associated with each interval, which is also 
returned in the search result.
+ *
+ * <p>
+ * The tree is a binary search tree sorted by interval start time. The 
intervals are stored as nodes in the tree.
+ * Additional state containing the minimum and maximum interval bounds of the 
entire subtree under a node is also
+ * stored in each node. This helps speed up the search for matching intervals 
by skipping unsuitable subtrees that will
+ * not contain a matching candidate interval.
+ * <p>
+ * To optimize the balancing cost w.r.t the operation time, the tree is not 
balanced on every modification operation.
+ * Rather, a configurable imbalance tolerance from the theoretical ideal 
height of log2(N) is allowed, breaching which
+ * triggers the rebalance.
+ * <p>
+ * Not thread safe.
+ * <p>
+ */
+public class IntervalTreeMap<T> extends AbstractMap<Interval, T> implements 
NavigableMap<Interval, T>
+{
+  // The compartor for comparing the interval start timnes
+  private final Comparator<Interval> startComparator;
+  // The comparator for comparing interval end times
+  private final Comparator<Interval> endComparator;
+
+  @VisibleForTesting
+  private Node<T> root;
+  private int size;
+
+  private int imbalanceTolerance;
+
+  private final EntrySet entrySet = new EntrySet();
+
+  public IntervalTreeMap()
+  {
+    this(Comparator.comparingLong(BaseInterval::getStartMillis),
+            Comparator.comparingLong(BaseInterval::getEndMillis));
+  }
+
+  public IntervalTreeMap(Comparator<Interval> startComparator, 
Comparator<Interval> endComparator)
+  {
+    this(startComparator, endComparator, 50);
+  }
+
+  public IntervalTreeMap(Comparator<Interval> startComparator, 
Comparator<Interval> endComparator, int imbalanceTolerance)
+  {
+    this.startComparator = startComparator;
+    this.endComparator = endComparator;
+    this.imbalanceTolerance = imbalanceTolerance;
+  }
+
+  /**
+   * Returns the allowed tolerance between the right and left branches of the 
tree before triggering a rebalance.
+   * The tolerance is expressed as a percentage deviation from ideal tree 
height.
+   * @return The imbalance tolerance
+   */
+  public int getImbalanceTolerance()
+  {
+    return imbalanceTolerance;
+  }
+
+  /**
+   * Set the allowed tolerance between the right and left branches.
+   * The setting will take effect on the next modification operation on the 
tree, such as an add or a delete.
+   * @param imbalanceTolerance The tolerance
+   */
+  public void setImbalanceTolerance(int imbalanceTolerance)
+  {
+    this.imbalanceTolerance = imbalanceTolerance;
+  }
+
+  static class Node<T> implements Map.Entry<Interval, T>
+  {
+    Interval interval;
+    T value;
+    int height;
+    // The full interval range of the subtree formed by this Node
+    Interval range;
+    Node<T> parent;
+    Node<T> left;
+    Node<T> right;
+
+    private static final String PRINT_FORMAT = "{%n"
+                                                + "%sinterval = %s%n"
+                                                + "%svalue = %s%n"
+                                                + "%sheight = %d%n"
+                                                + "%srange = %s%n"
+                                                + "%sleft = %s%n"
+                                                + "%sright = %s%n"
+                                                + "%s}";
+
+    private String print(int level)
+    {
+      String prefix = "\t".repeat(level);
+      String eprefix = "\t".repeat(level - 1);
+      return String.format(Locale.ENGLISH, PRINT_FORMAT,
+                              prefix, interval, prefix, value, prefix, height,
+                              prefix, range,
+                              prefix, (left != null) ? left.print(level + 1) : 
null,
+                              prefix, (right != null) ? right.print(level + 1) 
: null,
+                              eprefix
+                          );
+    }
+
+    @Override
+    public Interval getKey()
+    {
+      return interval;
+    }
+
+    @Override
+    public T getValue()
+    {
+      return value;
+    }
+
+    @Override
+    public T setValue(T value)
+    {
+      T oldValue = this.value;
+      this.value = value;
+      return oldValue;
+    }
+  }
+
+  @Override
+  public T put(Interval interval, T value)
+  {
+    //root = insert(root, interval, value);
+    T oldValue = insert(null, false, interval, value);
+    checkRebalance();
+    return oldValue;
+  }
+
+  private T insert(Node<T> parent, boolean left, Interval interval, T value)
+  {
+    // Passing parent so that when a new node is created, it can be added to 
parent, and we can still use return value
+    // for another purpose, namely returning the old value if the key already 
exists in the tree
+    Node<T> node;
+    if (parent == null) {
+      node = root;
+    } else if (left) {
+      node = parent.left;
+    } else {
+      node = parent.right;
+    }
+
+    if (node == null) {
+      node = new Node<>();
+      node.interval = interval;
+      node.value = value;
+      node.height = 0;
+      node.range = interval;
+      if (root == null) {
+        root = node;
+      } else if (left) {
+        setLeftNode(parent, node);
+      } else {
+        setRightNode(parent, node);
+      }
+      ++size;
+      return null;
+    }
+
+    T oldValue;
+
+    int cmp = compareInterval(interval, node.interval);
+
+    // If exact interval already exists, just replace the value and return
+    if (cmp == 0) {
+      oldValue = node.value;
+      node.value = value;
+      return oldValue;
+    }
+
+    if (cmp < 0) {
+    // Go to the left
+      oldValue = insert(node, true, interval, value);
+    } else {
+      // Go to the right
+      oldValue = insert(node, false, interval, value);
+    }
+    recomputeState(node);
+
+    //return node;
+    return oldValue;
+  }
+
+  @Override
+  public T get(Object key)
+  {
+    if (!Interval.class.isAssignableFrom(key.getClass())) {
+      throw new ClassCastException("key must be an instance of Interval");
+    }
+    Interval interval = (Interval) key;
+
+    T value = null;
+    Node<T> node = root;
+    while (node != null) {
+      int cmp = compareInterval(node.getKey(), interval);
+      if (cmp == 0) {
+        value = node.value;
+        break;
+      } else if (cmp > 0) {
+        node = node.left;
+      } else {
+        node = node.right;
+      }
+    }
+    return value;
+  }
+
+  private int compareInterval(Interval interval1, Interval interval2)
+  {
+    int cmp = startComparator.compare(interval1, interval2);
+    if (cmp == 0) {
+      return endComparator.compare(interval1, interval2);
+    }
+    return cmp;
+  }
+
+  public Map<Interval, T> findEncompassing(Interval interval)
+  {
+    return findMatching(i -> i.contains(interval));
+  }
+
+  public Map<Interval, T> findOverlapping(Interval interval)
+  {
+    return findMatching(i -> i.overlaps(interval));
+  }
+
+  /**
+   * Get all entries matching a given condition
+   * @param condition The match condition
+   *
+   * This condition should not only return true when a node matches the 
condition but also when a child node range
+   * matches. It is a convenience method for {@link 
#forEachMatching(Predicate, Predicate, BiConsumer)} and see the
+   * method's documentation for more information. It calls the method with 
rangeCondition set to be same as condition.
+   */
+  public Map<Interval, T> findMatching(Predicate<Interval> condition)
+  {
+    Map<Interval, T> result = new HashMap<>();
+    forEachMatching(condition, result::put);
+    return result;
+  }
+
+  /**
+   * Find entries matching a given condition by doing a full traversal.
+   * @param condition The match condition
+   *
+   * The method traverses through all the nodes of the tree looking for 
matches.
+   */
+  public Map<Interval, T> findMatchingFullTraversal(Predicate<Interval> 
condition)
+  {
+    Map<Interval, T> result = new HashMap<>();
+    forEachMatchingFullTraversal(condition, result::put);
+    return result;
+  }
+
+  /**
+   * Perform on action for matching nodes.
+   * @param condition The match condition
+   * @param action The action
+   *
+   * This condition should not only return true when a node matches the 
condition but also when the child node range
+   * matches. It is a convenience method for {@link 
#forEachMatching(Predicate, Predicate, BiConsumer)} and see the
+   * method's documentation for more information. It calls the method with 
rangeCondition set to be same as condition.
+   */
+  public void forEachMatching(Predicate<Interval> condition, 
BiConsumer<Interval, T> action)
+  {
+    forEachMatching(condition, condition, action);
+  }
+
+  /**
+   * Perform on action for matching nodes by doing a full traversal.
+   * @param condition The match condition
+   * @param action The action
+   *
+   * The method traverses through all the nodes of the tree looking for 
matches.
+   */
+  public void forEachMatchingFullTraversal(Predicate<Interval> condition, 
BiConsumer<Interval, T> action)
+  {
+    forEachMatching(condition, null, action);
+  }
+
+  /**
+   * Perform an action for matching nodes
+   * @param condition The condition to match for the node
+   * @param rangeCondition The condition to check a child node for, to 
determine whether to traverse the subtree
+   * @param action The action to perform
+   *
+   * The rangeCondition is applied on the interval range of the child node and 
only if the condition returns true is the
+   * child subtree traversed. Interval range is the min start time to max end 
time for all the nodes in the child
+   * subtree. This is a lookup speedup optimization. If rangeCondition is 
null, the check is skipped and all the
+   * children are traversed to find matches.
+   *
+   * In some cases such as finding nodes overlapping the given interval or 
encompassing the given interval, the same
+   * predicate can be used for condition and rangeCondition. In other 
situations a full traversal maybe needed and a
+   * null can be passed in for rangeCondition. There are helper methods for 
these.
+   */
+  public void forEachMatching(Predicate<Interval> condition, 
Predicate<Interval> rangeCondition, BiConsumer<Interval, T> action)
+  {
+    forEachMatching(root, condition, rangeCondition, action);
+  }
+
+  private void forEachMatching(Node<T> node, Predicate<Interval> condition, 
Predicate<Interval> rangeCondition, BiConsumer<Interval, T> action)
+  {
+
+    if (node == null) {
+      return;
+    }
+
+    // Process in-order
+
+    // Search left
+    if ((node.left != null) && ((rangeCondition == null) || 
rangeCondition.apply(node.left.range))) {
+      forEachMatching(node.left, condition, rangeCondition, action);
+    }
+
+    if (condition.apply(node.interval)) {
+      action.accept(node.interval, node.value);
+    }
+
+    // Search right
+    if (node.right != null && ((rangeCondition == null) || 
rangeCondition.apply(node.right.range))) {
+      forEachMatching(node.right, condition, rangeCondition, action);
+    }
+  }
+
+  @Override
+  public T remove(Object key)
+  {
+    return remove((Interval) key);
+  }
+
+  public T remove(Interval interval)
+  {
+    List<T> oldValue = new ArrayList<>(1);
+    root = removeNode(root, interval, oldValue);
+    if (root != null) {
+      root.parent = null;
+    }
+    checkRebalance();
+    return oldValue.size() == 1 ? oldValue.get(0) : null;
+  }
+
+  private Node<T> removeNode(Node<T> node, Interval interval, List<T> oldValue)
+  {
+    // When deleting a node, try to replace it with the right most leaf of the 
left sub-tree.
+    // If it is does not exist, i.e., the bottom most right node in the left 
subtree only has a left child and does not
+    // have a right child, this node becomes the replacement. Also, in this 
scenario, the left child (subtree) of this
+    // node is moved up to its parent as the parent's right child.
+    if (node == null) {
+      return null;
+    }
+
+    int cmp = compareInterval(interval, node.interval);
+
+    if (cmp == 0) {
+      // This is the node to delete
+      --size;
+      oldValue.add(node.value);
+      if ((node.left != null) && (node.right != null)) {
+        // Make the right bottom most child in the left subtree of the node, 
the new node at the current level
+        Node<T> left = node.left;
+        Node<T> newNode = unlinkRightLeaf(left);
+        // Make the current left and right children, the left and right 
children of the new node respectively.
+        // However, if the new node turns out to be the same as the left node, 
it means the left node did not have any
+        // right child. In this case, only set its right child to be the 
current node's right child.
+        if (left != newNode) {
+          // A right child exists
+          setLeftNode(newNode, left);
+        }
+        setRightNode(newNode, node.right);
+        recomputeState(newNode);
+        return newNode;
+      } else if (node.left != null) {
+        // Right node is null, make the left node the new node at current level
+        return node.left;
+      } else if (node.right != null) {
+        // Left node is null, make the right node the new node at current level
+        return node.right;
+      }
+      return null;
+    }
+
+    // Current node didn't match, search children
+    if (cmp < 0) {
+      Node<T> left = removeNode(node.left, interval, oldValue);
+      setLeftNode(node, left);
+    } else {
+      Node<T> right = removeNode(node.right, interval, oldValue);
+      setRightNode(node, right);
+    }
+
+    // Update our state as a modification may have happened somewhere in our 
subtree
+    recomputeState(node);
+
+    return node;
+  }
+
+  private Node<T> unlinkRightLeaf(Node<T> node)
+  {
+    if (node.right == null) {
+      return node;
+    } else {
+      Node<T> rnode = unlinkRightLeaf(node.right);
+      // If the right node has a left child, make it new right child
+      if (rnode == node.right) {
+        setRightNode(node, rnode.left);
+        rnode.left = null;
+      }
+      recomputeState(node);
+      return rnode;
+    }
+  }
+
+  private void inOrderTraverse(Node<T> node, List<Node<T>> nodes)
+  {
+    if (node == null) {
+      return;
+    }
+    inOrderTraverse(node.left, nodes);
+    nodes.add(node);
+    inOrderTraverse(node.right, nodes);
+  }
+
+  public void rebalance()
+  {
+    // In order traversal followed by repeated binary segmentation of the list
+    List<Node<T>> nodes = new ArrayList<>(size);
+    inOrderTraverse(root, nodes);
+    root = constructTree(nodes, 0, nodes.size());
+    root.parent = null;
+  }
+
+  private Node<T> constructTree(List<Node<T>> nodes, int start, int end)
+  {
+    if (start == end) {
+      return null;
+    }
+    int mid = (start + end - 1) / 2;
+    Node<T> node = nodes.get(mid);
+
+    Node<T> left = constructTree(nodes, start, mid);
+    setLeftNode(node, left);
+
+    Node<T> right = constructTree(nodes, mid + 1, end);
+    setRightNode(node, right);
+
+    recomputeState(node);
+    return node;
+  }
+
+  @Override
+  public Map.Entry<Interval, T> lowerEntry(Interval key)
+  {
+    Node<T> lnode = null;
+    Node<T> node = root;
+    while (node != null) {
+      // Since we want to return a smaller entry even when there is an exact 
match, go left in the equality case too
+      if (compareInterval(key, node.getKey()) <= 0) {
+        node = node.left;
+      } else {
+        lnode = node;
+        node = node.right;
+      }
+    }
+    return lnode;
+  }
+
+  @Override
+  public Interval lowerKey(Interval key)
+  {
+    Map.Entry<Interval, T> entry = lowerEntry(key);
+    return entry != null ? entry.getKey() : null;
+  }
+
+  @Override
+  public Map.Entry<Interval, T> floorEntry(Interval key)
+  {
+    Node<T> fnode = null;
+    Node<T> node = root;
+    while (node != null) {
+      int cmp = compareInterval(node.getKey(), key);
+      if (cmp == 0) {
+        fnode = node;
+        break;
+      } else if (cmp > 0) {
+        node = node.left;
+      } else {
+        fnode = node;
+        node = node.right;
+      }
+    }
+    return fnode;
+  }
+
+  @Override
+  public Interval floorKey(Interval key)
+  {
+    Map.Entry<Interval, T> entry = floorEntry(key);
+    return entry != null ? entry.getKey() : null;
+  }
+
+  @Override
+  public Map.Entry<Interval, T> ceilingEntry(Interval key)
+  {
+    Node<T> cnode = null;
+    Node<T> node = root;
+    while (node != null) {
+      int cmp = compareInterval(node.getKey(), key);
+      if (cmp == 0) {
+        cnode = node;
+        break;
+      } else if (cmp > 0) {
+        cnode = node;
+        node = node.left;
+      } else {
+        node = node.right;
+      }
+    }
+    return cnode;
+  }
+
+  @Override
+  public Interval ceilingKey(Interval key)
+  {
+    Entry<Interval, T> entry = ceilingEntry(key);
+    return entry != null ? entry.getKey() : null;
+  }
+
+  @Override
+  public Map.Entry<Interval, T> higherEntry(Interval key)
+  {
+    Node<T> hnode = null;
+    Node<T> node = root;
+    while (node != null) {
+      if (compareInterval(key, node.getKey()) < 0) {
+        hnode = node;
+        node = node.left;
+      } else {
+        node = node.right;
+      }
+    }
+    return hnode;
+  }
+
+  @Override
+  public Interval higherKey(Interval key)
+  {
+    Entry<Interval, T> entry = higherEntry(key);
+    return entry != null ? entry.getKey() : null;
+  }
+
+  @Override
+  public Map.Entry<Interval, T> firstEntry()
+  {
+    return firstEntry(root);
+  }
+
+  @Override
+  public Map.Entry<Interval, T> lastEntry()
+  {
+    if (root == null) {
+      return null;
+    }
+    Node<T> node = root;
+    while (node.right != null) {
+      node = node.right;
+    }
+    return node;
+  }
+
+  @Override
+  public Interval firstKey()
+  {
+    Map.Entry<Interval, T> entry = firstEntry();
+    return entry != null ? entry.getKey() : null;
+  }
+
+  @Override
+  public Interval lastKey()
+  {
+    Map.Entry<Interval, T> entry = lastEntry();
+    return entry != null ? entry.getKey() : null;
+  }
+
+  @Override
+  public Map.Entry<Interval, T> pollFirstEntry()
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Map.Entry<Interval, T> pollLastEntry()
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public NavigableMap<Interval, T> descendingMap()
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public NavigableSet<Interval> navigableKeySet()
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public NavigableSet<Interval> descendingKeySet()
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public NavigableMap<Interval, T> subMap(Interval fromKey, boolean 
fromInclusive, Interval toKey, boolean toInclusive)
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public NavigableMap<Interval, T> headMap(Interval toKey, boolean inclusive)
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public NavigableMap<Interval, T> tailMap(Interval fromKey, boolean inclusive)
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Comparator<? super Interval> comparator()
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public SortedMap<Interval, T> subMap(Interval fromKey, Interval toKey)
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public SortedMap<Interval, T> headMap(Interval toKey)
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public SortedMap<Interval, T> tailMap(Interval fromKey)
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  private void recomputeState(Node<T> node)
+  {
+    int lheight = (node.left != null) ? node.left.height : -1;
+    int rheight = (node.right != null) ? node.right.height : -1;
+    node.height = Math.max(lheight, rheight) + 1;
+    node.range = computeRange(node.interval, node.left, node.right);
+  }
+
+  @Override
+  public void clear()
+  {
+    root = null;
+    size = 0;
+  }
+
+  @Override
+  public @NotNull Set<Map.Entry<Interval, T>> entrySet()
+  {
+    return entrySet;
+  }
+
+  @Override
+  public int size()
+  {
+    return size;
+  }
+
+  @VisibleForTesting
+    // returns the number of edges from root to leaf along the longest path
+  int height()
+  {
+    return (root != null) ? root.height : -1;
+  }
+
+  class EntrySet extends AbstractSet<Map.Entry<Interval, T>>
+  {
+
+    // Currently this returns a distinct collection when iterating
+    @Override
+    public Iterator<Map.Entry<Interval, T>> iterator()
+    {
+      return new EntrySetIterator();
+    }
+
+    @Override
+    public int size()
+    {
+      return IntervalTreeMap.this.size;
+    }
+
+    class EntrySetIterator implements Iterator<Map.Entry<Interval, T>>
+    {
+
+      Node<T> current = firstEntry(IntervalTreeMap.this.root);
+
+      @Override
+      public boolean hasNext()
+      {
+        return (current != null);
+      }
+
+      @Override
+      public Entry<Interval, T> next()
+      {
+        Entry<Interval, T> entry = current;
+        if (entry == null) {
+          return entry;
+        }
+        // Move current to next node
+        if (current.right != null) {
+          current = firstEntry(current.right);
+        } else {
+          // No more right children, go up one level to the parent.
+          // However, if the current node is right child of parent, keep going 
up till you find a parent who is on the
+          // right side
+          Node<T> prev;
+          do {
+            prev = current;
+            current = current.parent;
+          } while ((current != null) && (current.right == prev));
+        }
+        return entry;
+      }
+    }
+
+  }
+
+  /**
+   * Perform a tree rebalance if the imbalance between the left and right 
sides of the tree has increased beyond a
+   * tolerated limit, as opposed to rebalancing all the time. This is to done 
to strike a balance between performance
+   * degradation arising from an imbalance tree and the processing overheard 
of rebalancing each time the contents of
+   * the tree changes.
+   *
+   * The limit is defined using a configurable tolerance percentage in excess 
of an ideal balanced tree height for the
+   * number of entries in the tree.
+   */
+  private void checkRebalance()
+  {
+    if (root != null) {
+      int ideal = (int) Math.floor(Math.log10(size + 1) / Math.log10(2));
+      double tolerance = ideal * imbalanceTolerance / 100.0;
+      int threshold = ideal + (int) tolerance;
+      if (root.height > threshold) {
+        rebalance();
+      }
+    }
+  }
+
+  private Node<T> firstEntry(Node<T> node)
+  {
+    if (node == null) {
+      return null;
+    }
+    while (node.left != null) {
+      node = node.left;
+    }
+    return node;
+  }
+
+  private void setLeftNode(Node<T> node, Node<T> left)
+  {
+    if (node.left != left) {
+      node.left = left;
+      if (left != null) {
+        left.parent = node;
+      }
+    }
+  }
+
+  private void setRightNode(Node<T> node, Node<T> right)
+  {
+    if (node.right != right) {
+      node.right = right;
+      if (right != null) {
+        right.parent = node;
+      }
+    }
+  }
+
+  /**
+   * @return The contents of the tree as a string
+   */
+  @Override
+  public String toString()
+  {
+    return (root != null) ? root.print(1) : "";
+  }
+
+  @SafeVarargs
+  private Interval computeRange(Interval interval, Node<T>... nodes)
+  {
+    // Find the intervals that have the minimum start and the maximum end
+    Interval min = interval;
+    Interval max = interval;
+    for (Node<T> node : nodes) {
+      if (node != null) {
+        if (startComparator.compare(node.range, min) < 0) {
+          min = node.range;
+        }
+        if (endComparator.compare(node.range, max) > 0) {
+          max = node.range;
+        }
+      }
+    }
+    // Return an interval with the min and max
+    return interval.withStart(min.getStart()).withEnd(max.getEnd());
+  }
+
+}
diff --git 
a/processing/src/main/java/org/apache/druid/timeline/VersionedIntervalTimeline.java
 
b/processing/src/main/java/org/apache/druid/timeline/VersionedIntervalTimeline.java
index a2f2699c41c..60c434df949 100644
--- 
a/processing/src/main/java/org/apache/druid/timeline/VersionedIntervalTimeline.java
+++ 
b/processing/src/main/java/org/apache/druid/timeline/VersionedIntervalTimeline.java
@@ -27,6 +27,7 @@ import com.google.errorprone.annotations.concurrent.GuardedBy;
 import org.apache.druid.java.util.common.DateTimes;
 import org.apache.druid.java.util.common.UOE;
 import org.apache.druid.java.util.common.guava.Comparators;
+import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.timeline.partition.PartitionChunk;
 import org.apache.druid.timeline.partition.PartitionHolder;
 import org.apache.druid.utils.CollectionUtils;
@@ -74,21 +75,20 @@ import java.util.stream.StreamSupport;
 public class VersionedIntervalTimeline<VersionType, ObjectType extends 
Overshadowable<ObjectType>>
     implements TimelineLookup<VersionType, ObjectType>
 {
+  private static final Logger logger = new 
Logger(VersionedIntervalTimeline.class);
   private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
 
   // Below timelines stores only *visible* timelineEntries
   // adjusted interval -> timelineEntry
-  private final NavigableMap<Interval, TimelineEntry> 
completePartitionsTimeline = new TreeMap<>(
-      Comparators.intervalsByStartThenEnd()
-  );
+  private final NavigableMap<Interval, TimelineEntry> 
completePartitionsTimeline;
   // IncompletePartitionsTimeline also includes completePartitionsTimeline
   // adjusted interval -> timelineEntry
   @VisibleForTesting
-  final NavigableMap<Interval, TimelineEntry> incompletePartitionsTimeline = 
new TreeMap<>(
-      Comparators.intervalsByStartThenEnd()
-  );
+  final NavigableMap<Interval, TimelineEntry> incompletePartitionsTimeline;
   // true interval -> version -> timelineEntry
   private final Map<Interval, TreeMap<VersionType, TimelineEntry>> 
allTimelineEntries = new HashMap<>();
+  // Only instantiated and used when fastIntervalSearch is enabled
+  private IntervalTreeMap<TreeMap<VersionType, TimelineEntry>> 
allTimeIntervals;
   private final AtomicInteger numObjects = new AtomicInteger();
 
   private final Comparator<? super VersionType> versionComparator;
@@ -96,15 +96,38 @@ public class VersionedIntervalTimeline<VersionType, 
ObjectType extends Overshado
   // Set this to true if the client needs to skip tombstones upon lookup (like 
the broker)
   private final boolean skipObjectsWithNoData;
 
+  // Set this to true to use an interval tree index for the segment intervals
+  private final boolean fastIntervalSearch;
+
   public VersionedIntervalTimeline(Comparator<? super VersionType> 
versionComparator)
   {
     this(versionComparator, false);
   }
 
   public VersionedIntervalTimeline(Comparator<? super VersionType> 
versionComparator, boolean skipObjectsWithNoData)
+  {
+    this(versionComparator, skipObjectsWithNoData, false);
+  }
+
+  /**
+   * Constructor
+   * @param versionComparator The version comparator
+   * @param skipObjectsWithNoData Skip tombstones during lookup
+   * @param fastIntervalSearch Use the faster segment retrieval index based on 
interval tree
+   */
+  public VersionedIntervalTimeline(Comparator<? super VersionType> 
versionComparator, boolean skipObjectsWithNoData, boolean fastIntervalSearch)
   {
     this.versionComparator = versionComparator;
     this.skipObjectsWithNoData = skipObjectsWithNoData;
+    this.fastIntervalSearch = fastIntervalSearch;
+    if (fastIntervalSearch) {
+      allTimeIntervals = new IntervalTreeMap<>(Comparators.intervalsByStart(), 
Comparators.intervalsByEnd());
+      this.completePartitionsTimeline = new 
IntervalTreeMap<>(Comparators.intervalsByStart(), Comparators.intervalsByEnd());
+      this.incompletePartitionsTimeline = new 
IntervalTreeMap<>(Comparators.intervalsByStart(), Comparators.intervalsByEnd());
+    } else {
+      this.completePartitionsTimeline = new 
TreeMap<>(Comparators.intervalsByStartThenEnd());
+      this.incompletePartitionsTimeline = new 
TreeMap<>(Comparators.intervalsByStartThenEnd());
+    }
   }
 
   public static <VersionType, ObjectType extends Overshadowable<ObjectType>> 
Iterable<ObjectType> getAllObjects(
@@ -210,6 +233,9 @@ public class VersionedIntervalTimeline<VersionType, 
ObjectType extends Overshado
           TreeMap<VersionType, TimelineEntry> versionEntry = new 
TreeMap<>(versionComparator);
           versionEntry.put(version, entry);
           allTimelineEntries.put(interval, versionEntry);
+          if (fastIntervalSearch) {
+            allTimeIntervals.put(interval, versionEntry);
+          }
           numObjects.incrementAndGet();
         } else {
           entry = exists.get(version);
@@ -269,6 +295,9 @@ public class VersionedIntervalTimeline<VersionType, 
ObjectType extends Overshado
         versionEntries.remove(version);
         if (versionEntries.isEmpty()) {
           allTimelineEntries.remove(interval);
+          if (fastIntervalSearch) {
+            allTimeIntervals.remove(interval);
+          }
         }
 
         remove(incompletePartitionsTimeline, interval, entry, true);
@@ -289,13 +318,40 @@ public class VersionedIntervalTimeline<VersionType, 
ObjectType extends Overshado
   {
     lock.readLock().lock();
     try {
-      for (Entry<Interval, TreeMap<VersionType, TimelineEntry>> entry : 
allTimelineEntries.entrySet()) {
-        if (entry.getKey().equals(interval) || 
entry.getKey().contains(interval)) {
-          TimelineEntry foundEntry = entry.getValue().get(version);
-          if (foundEntry != null) {
-            return foundEntry.getPartitionHolder().getChunk(partitionNum);
+
+      // Speed up search with an exact interval match lookup first
+      TreeMap<VersionType, TimelineEntry> versionEntries = 
allTimelineEntries.get(interval);
+      if (versionEntries != null) {
+        TimelineEntry foundEntry = versionEntries.get(version);
+        if (foundEntry != null) {
+          return foundEntry.getPartitionHolder().getChunk(partitionNum);
+        }
+      }
+
+      // If an exact interval match is not found search for an encapsulating 
interval
+
+      // If tree search is enabled use it else revert to checking all intervals
+      if (fastIntervalSearch) {
+        Map<Interval, TreeMap<VersionType, TimelineEntry>> possibleMatches = 
allTimeIntervals.findEncompassing(interval);
+        for (Entry<Interval, TreeMap<VersionType, TimelineEntry>> entry : 
possibleMatches.entrySet()) {
+          Interval possibleInterval = entry.getKey();
+          if (possibleInterval.contains(interval)) {
+            TimelineEntry foundEntry = entry.getValue().get(version);
+            if (foundEntry != null) {
+              return foundEntry.getPartitionHolder().getChunk(partitionNum);
+            }
+          }
+        }
+      } else {
+        for (Entry<Interval, TreeMap<VersionType, TimelineEntry>> entry : 
allTimelineEntries.entrySet()) {
+          if (entry.getKey().contains(interval)) {
+            TimelineEntry foundEntry = entry.getValue().get(version);
+            if (foundEntry != null) {
+              return foundEntry.getPartitionHolder().getChunk(partitionNum);
+            }
           }
         }
+
       }
 
       return null;
@@ -747,21 +803,38 @@ public class VersionedIntervalTimeline<VersionType, 
ObjectType extends Overshado
       timeline = completePartitionsTimeline;
     }
 
-    for (Entry<Interval, TimelineEntry> entry : timeline.entrySet()) {
-      Interval timelineInterval = entry.getKey();
-      TimelineEntry val = entry.getValue();
-
-      // exclude empty partition holders (i.e. tombstones) since they do not 
add value
-      // for higher level code...they have no data rows...
-      if ((!skipObjectsWithNoData || val.partitionHolder.hasData()) && 
timelineInterval.overlaps(interval)) {
-        retVal.add(
-            new TimelineObjectHolder<>(
-                timelineInterval,
-                val.getTrueInterval(),
-                val.getVersion(),
-                
PartitionHolder.copyWithOnlyVisibleChunks(val.getPartitionHolder())
-            )
-        );
+    if (fastIntervalSearch) {
+      IntervalTreeMap<TimelineEntry> tree = (IntervalTreeMap<TimelineEntry>) 
timeline;
+      tree.forEachMatching(timelineInterval -> 
timelineInterval.overlaps(interval),
+          (timelineInterval, val) -> {
+            if (!skipObjectsWithNoData || val.partitionHolder.hasData()) {
+              retVal.add(
+                      new TimelineObjectHolder<>(
+                              timelineInterval,
+                              val.getTrueInterval(),
+                              val.getVersion(),
+                              
PartitionHolder.copyWithOnlyVisibleChunks(val.getPartitionHolder())
+                      )
+              );
+            }
+          });
+    } else {
+      for (Entry<Interval, TimelineEntry> entry : timeline.entrySet()) {
+        Interval timelineInterval = entry.getKey();
+        TimelineEntry val = entry.getValue();
+
+        // exclude empty partition holders (i.e. tombstones) since they do not 
add value
+        // for higher level code...they have no data rows...
+        if ((!skipObjectsWithNoData || val.partitionHolder.hasData()) && 
timelineInterval.overlaps(interval)) {
+          retVal.add(
+                  new TimelineObjectHolder<>(
+                          timelineInterval,
+                          val.getTrueInterval(),
+                          val.getVersion(),
+                          
PartitionHolder.copyWithOnlyVisibleChunks(val.getPartitionHolder())
+                  )
+          );
+        }
       }
     }
 
diff --git 
a/processing/src/test/java/org/apache/druid/timeline/IntervalTreeMapTest.java 
b/processing/src/test/java/org/apache/druid/timeline/IntervalTreeMapTest.java
new file mode 100644
index 00000000000..d0eefc13ec7
--- /dev/null
+++ 
b/processing/src/test/java/org/apache/druid/timeline/IntervalTreeMapTest.java
@@ -0,0 +1,550 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.timeline;
+
+import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.java.util.common.Pair;
+import org.apache.druid.java.util.common.guava.Comparators;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.joda.time.Interval;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+public class IntervalTreeMapTest
+{
+
+  @Test
+  public void testSize()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    Assertions.assertEquals(6, tree.size(), "Size");
+  }
+
+  @Test
+  public void testPut()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    compareData(baseData, tree);
+  }
+
+  @Test
+  public void testReplace()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    Pair<Interval, String> entry = baseData.get(2);
+    Interval interval = entry.lhs;
+    String value = entry.rhs;
+    String newValue = value + "n";
+    String oldValue = tree.put(interval, newValue);
+    Assertions.assertEquals(oldValue, value, "Old value match");
+  }
+
+  @Test
+  public void testGet()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    baseData.forEach(
+        (Pair<Interval, String> item) -> {
+          Interval interval = item.lhs;
+          String evalue = item.rhs;
+          String value = tree.get(interval);
+          Assertions.assertEquals(evalue, value, "value");
+        }
+    );
+  }
+
+  @Test
+  public void testValues()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    Set<String> values = new LinkedHashSet<>(tree.values());
+    Set<String> bvalues = baseData.stream().map(entry -> 
entry.rhs).collect(Collectors.toCollection(LinkedHashSet::new));
+    Assertions.assertEquals(bvalues, values, "values");
+  }
+
+  @Test
+  public void testMatch()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    Map<Interval, String> entries = 
tree.findEncompassing(Intervals.of("2025-01-04T00:00:00/P1D"));
+
+    Assertions.assertEquals(1, entries.size());
+    Assertions.assertEquals("v5", 
entries.get(Intervals.of("2025-01-04T00:00:00/P1D")), "Match");
+  }
+
+  @Test
+  public void testNoMatch()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    Map<Interval, String> entries = 
tree.findEncompassing(Intervals.of("2025-01-07T00:00:00/P1D"));
+
+    Assertions.assertEquals(0, entries.size());
+  }
+
+  @Test
+  public void testOverlap()
+  {
+    IntervalTreeMap<String> tree = setupTree(overlapData);
+    Map<Interval, String> entries = 
tree.findEncompassing(Intervals.of("2025-01-02T09:00:00/PT1H"));
+
+    Assertions.assertEquals(2, entries.size());
+    Assertions.assertEquals("v4", 
entries.get(Intervals.of("2025-01-02T00:00:00/P1D")), "Day match");
+    Assertions.assertEquals("v7", 
entries.get(Intervals.of("2025-01-01T00:00:00/P1Y")), "Year match");
+  }
+
+  @Test
+  public void testSparseOverlap()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+    Map<Interval, String> entries = 
tree.findEncompassing(Intervals.of("2025-06-03T00:00:00/P1D"));
+
+    Assertions.assertEquals(4, entries.size());
+    Assertions.assertEquals("v1", 
entries.get(Intervals.of("2025-05-10T00:00:00/P1M")), "Match 1");
+    Assertions.assertEquals("v7", 
entries.get(Intervals.of("2025-06-03T00:00:00/P1D")), "Match 2");
+    Assertions.assertEquals("v13", 
entries.get(Intervals.of("2025-06-01T00:00:00/P1M")), "Match 3");
+    Assertions.assertEquals("v14", 
entries.get(Intervals.of("2025-01-01T00:00:00/P1Y")), "Match 4");
+  }
+
+  @Test
+  public void testRemove()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+    int size = tree.size();
+
+    // Remove node that does not exist
+    String intervalstr = "2025-03-11T00:00:00/P1M";
+    String oldValue = tree.remove(Intervals.of(intervalstr));
+    Assertions.assertEquals(size, tree.size(), "Size");
+    Assertions.assertNull(oldValue, "Old value");
+    List<Pair<Interval, String>> expectedData = new 
ArrayList<>(sparseOverlapData);
+    compareData(expectedData, tree);
+
+    // Remove leaf
+    intervalstr = "2025-06-01T00:00:00/P1M";
+    String value = tree.get(Intervals.of(intervalstr));
+    Assertions.assertNotNull(value, "Value");
+
+    oldValue = tree.remove(Intervals.of(intervalstr));
+    size--;
+    Assertions.assertEquals(size, tree.size(), "Size");
+    Assertions.assertEquals(value, oldValue, "Old value");
+    expectedData = new ArrayList<>(sparseOverlapData);
+    expectedData.remove(Pair.of(Intervals.of(intervalstr), value));
+    compareData(expectedData, tree);
+
+    // Remove node in penultimate level
+    intervalstr = "2025-09-04T00:00:00/P1D";
+    value = tree.get(Intervals.of(intervalstr));
+    Assertions.assertNotNull(value, "Value");
+
+    oldValue = tree.remove(Intervals.of(intervalstr));
+    size--;
+    Assertions.assertEquals(size, tree.size(), "Size");
+    Assertions.assertEquals(value, oldValue, "Old value");
+    expectedData = new ArrayList<>(expectedData);
+    expectedData.remove(Pair.of(Intervals.of(intervalstr), value));
+    compareData(expectedData, tree);
+
+    // Remove node at a higher level
+    intervalstr = "2025-07-12T00:00:00/P1D";
+    value = tree.get(Intervals.of(intervalstr));
+    Assertions.assertNotNull(value, "Value");
+
+    oldValue = tree.remove(Intervals.of(intervalstr));
+    size--;
+    Assertions.assertEquals(size, tree.size(), "Size");
+    Assertions.assertEquals(value, oldValue, "Old value");
+    expectedData = new ArrayList<>(expectedData);
+    expectedData.remove(Pair.of(Intervals.of(intervalstr), value));
+    compareData(expectedData, tree);
+  }
+
+  @Test
+  public void testRemoveRootAndMatch()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    tree.remove(Intervals.of("2025-01-03T00:00:00/P1D"));
+    Assertions.assertEquals(5, tree.size(), "Size");
+    Map<Interval, String> entries = 
tree.findEncompassing(Intervals.of("2025-01-04T00:00:00/P1D"));
+    Assertions.assertEquals(1, entries.size());
+    Assertions.assertEquals("v5", 
entries.get(Intervals.of("2025-01-04T00:00:00/P1D")), "Match");
+  }
+
+  @Test
+  public void testRemoveMultiple()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+    int isize = tree.size();
+    tree.remove(Intervals.of("2025-01-12T00:00:00/P1D"));
+    tree.remove(Intervals.of("2025-06-03T00:00:00/P1D"));
+    tree.remove(Intervals.of("2025-06-01T00:00:00/P1M"));
+    int csize = tree.size();
+    Assertions.assertEquals(3, isize - csize, "Size");
+  }
+
+  @Test
+  public void testClear()
+  {
+    IntervalTreeMap<String> tree = setupTree(baseData);
+    tree.clear();
+    Assertions.assertEquals(0, tree.size(), "Size");
+  }
+
+  @Test
+  public void testLargeLoadTree()
+  {
+    IntervalTreeMap<String> tree = new 
IntervalTreeMap<>(Comparators.intervalsByStart(), Comparators.intervalsByEnd());
+    List<Pair<Interval, String>> expectedData = new ArrayList<>();
+    Set<String> existingIntervals = new HashSet<>();
+    Random random = ThreadLocalRandom.current();
+    int total = 100000;
+    int count = 0;
+    while (count < total) {
+      int year = random.nextInt(26) + 2000;
+      int month = random.nextInt(12) + 1;
+      int day = random.nextInt(28) + 1;
+      int hour = random.nextInt(23) + 1;
+      String intervalstr = year + "-" + month + "-" + day + "T" + hour + 
":00:00/P" + ((count % 30) + 1) + "D";
+      if (!existingIntervals.contains(intervalstr)) {
+        Interval interval = Intervals.of(intervalstr);
+        String value = "v" + count;
+        tree.put(interval, value);
+        expectedData.add(Pair.of(interval, value));
+        existingIntervals.add(intervalstr);
+        ++count;
+      }
+    }
+    Assertions.assertEquals(total, tree.size(), "Size");
+    compareData(expectedData, tree);
+  }
+
+  private static final Logger log = new Logger(IntervalTreeMapTest.class);
+
+  @Disabled
+  @Test
+  public void testPerf()
+  {
+    IntervalTreeMap<String> tree = new 
IntervalTreeMap<>(Comparators.intervalsByStart(), Comparators.intervalsByEnd());
+    List<Pair<Interval, String>> expectedData = new ArrayList<>();
+    Map<Interval, String> mappedData = new HashMap<>();
+    Set<String> existingIntervals = new HashSet<>();
+    Random random = ThreadLocalRandom.current();
+    int total = 10000;
+    int count = 0;
+    while (count < total) {
+      int year = random.nextInt(26) + 2000;
+      int month = random.nextInt(12) + 1;
+      int day = random.nextInt(28) + 1;
+      int hour = random.nextInt(23) + 1;
+      String intervalstr = year + "-" + month + "-" + day + "T" + hour + 
":00:00/P" + ((count % 30) + 1) + "D";
+      if (!existingIntervals.contains(intervalstr)) {
+        Interval interval = Intervals.of(intervalstr);
+        String value = "v" + count;
+        tree.put(interval, value);
+        mappedData.put(interval, value);
+        expectedData.add(Pair.of(interval, value));
+        existingIntervals.add(intervalstr);
+        ++count;
+      }
+    }
+    long start = System.currentTimeMillis();
+    for (int i = 0; i < total; i++) {
+      Pair<Interval, String> pair = expectedData.get(i);
+      Interval interval = pair.lhs;
+      for (Map.Entry<Interval, String> entry : mappedData.entrySet()) {
+        if (entry.getKey().contains(interval)) {
+          break;
+        }
+      }
+    }
+    log.info("Seq find time %d", (System.currentTimeMillis() - start));
+    start = System.currentTimeMillis();
+    for (int i = 0; i < total; i++) {
+      Pair<Interval, String> pair = expectedData.get(i);
+      Interval interval = pair.lhs;
+      tree.findEncompassing(interval);
+    }
+    log.info("Tree find time %d", (System.currentTimeMillis() - start));
+  }
+
+  @Test
+  public void testAutoRebalance()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+    Assertions.assertEquals(4, tree.height(), "Height");
+    compareData(sparseOverlapData, tree);
+  }
+
+  @Test
+  public void testManualRebalance()
+  {
+    // Set a high threshold so auto-rebalance does not happen
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData, t -> 
t.setImbalanceTolerance(100));
+    Assertions.assertEquals(4, tree.height(), "Height");
+    compareData(sparseOverlapData, tree);
+    tree.rebalance();
+    Assertions.assertEquals(3, tree.height(), "Height");
+    compareData(sparseOverlapData, tree);
+  }
+
+  @Test
+  public void testIsEmpty()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+    Assertions.assertFalse(tree.isEmpty(), "Not Empty");
+    sparseOverlapData.forEach(t -> tree.remove(t.lhs));
+    Assertions.assertTrue(tree.isEmpty(), "Empty");
+  }
+
+  @Test
+  public void testFirstEntryAndKey()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+    Map.Entry<Interval, String> entry = tree.firstEntry();
+    Interval matchInterval = Intervals.of("2025-01-01T00:00:00/P1D");
+    Assertions.assertEquals(matchInterval, entry.getKey(), "Entry interval");
+    Assertions.assertEquals("v2", entry.getValue(), "Entry value");
+
+    Interval interval = tree.firstKey();
+    Assertions.assertEquals(matchInterval, interval, "Interval key");
+  }
+
+  @Test
+  public void testLastEntryAndKey()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+    Map.Entry<Interval, String> entry = tree.lastEntry();
+    Interval matchInterval = Intervals.of("2025-10-06T00:00:00/P1M");
+    Assertions.assertEquals(matchInterval, entry.getKey(), "Entry interval");
+    Assertions.assertEquals("v12", entry.getValue(), "Entry value");
+
+    Interval interval = tree.lastKey();
+    Assertions.assertEquals(matchInterval, interval, "Interval key");
+  }
+
+  @Test
+  public void testFloorKey()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+
+    // Only one smaller entry
+    Interval floor = tree.floorKey(Intervals.of("2025-01-11T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-01-01T00:00:00/P1Y"), floor, 
"Floor key 1");
+
+    // Entry with same start date but different end date
+    floor = tree.lowerKey(Intervals.of("2025-01-01T00:00:00/P1M"));
+    Assertions.assertEquals(Intervals.of("2025-01-01T00:00:00/P1D"), floor, 
"Lower key 2");
+
+    // Matching entry
+    floor = tree.floorKey(Intervals.of("2025-01-12T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-01-12T00:00:00/P1D"), floor, 
"Floor key 3");
+
+    // Random entry
+    floor = tree.floorKey(Intervals.of("2025-08-01T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-07-12T00:00:00/P1D"), floor, 
"Floor key 4");
+
+    // Last entry
+    floor = tree.floorKey(Intervals.of("2025-11-01T00:00:00/P1M"));
+    Assertions.assertEquals(Intervals.of("2025-10-06T00:00:00/P1M"), floor, 
"Floor key 5");
+
+    // No smaller entry
+    floor = tree.floorKey(Intervals.of("2024-12-31T00:00:00/P1D"));
+    Assertions.assertNull(floor, "Floor key 6");
+  }
+
+  @Test
+  public void testLowerKey()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+
+    // Only one smaller entry
+    Interval lower = tree.lowerKey(Intervals.of("2025-01-11T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-01-01T00:00:00/P1Y"), lower, 
"Lower key 1");
+
+    // Entry with same start date but different end date
+    lower = tree.lowerKey(Intervals.of("2025-01-01T00:00:00/P1M"));
+    Assertions.assertEquals(Intervals.of("2025-01-01T00:00:00/P1D"), lower, 
"Lower key 2");
+
+    // Matching entry
+    lower = tree.lowerKey(Intervals.of("2025-01-12T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-01-01T00:00:00/P1Y"), lower, 
"Lower key 3");
+
+    // Random entry
+    lower = tree.lowerKey(Intervals.of("2025-08-01T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-07-12T00:00:00/P1D"), lower, 
"Lower key 4");
+
+    // Last entry
+    lower = tree.lowerKey(Intervals.of("2025-11-01T00:00:00/P1M"));
+    Assertions.assertEquals(Intervals.of("2025-10-06T00:00:00/P1M"), lower, 
"Lower key 5");
+
+    // No smaller entry
+    lower = tree.lowerKey(Intervals.of("2024-12-31T00:00:00/P1D"));
+    Assertions.assertNull(lower, "Lower key 6");
+  }
+
+  @Test
+  public void testCeiinglKey()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+
+    // First entry
+    Interval ceiling = 
tree.ceilingKey(Intervals.of("2024-12-31T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-01-01T00:00:00/P1D"), ceiling, 
"Ceiling key 1");
+
+    // Entry with same start date but different end date
+    ceiling = tree.ceilingKey(Intervals.of("2025-02-01T00:00:00/PT6H"));
+    Assertions.assertEquals(Intervals.of("2025-02-01T00:00:00/P1D"), ceiling, 
"Ceiling key 2");
+
+    // Matching entry
+    ceiling = tree.ceilingKey(Intervals.of("2025-09-04T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-09-04T00:00:00/P1D"), ceiling, 
"Ceiling key 3");
+
+    // Random entry
+    ceiling = tree.ceilingKey(Intervals.of("2025-03-31T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-04-02T00:00:00/P1D"), ceiling, 
"Ceiling key 4");
+
+    // Only one greater entry
+    ceiling = tree.ceilingKey(Intervals.of("2025-09-28T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-10-06T00:00:00/P1M"), ceiling, 
"Ceiling key 5");
+
+    // No greater entry
+    ceiling = tree.ceilingKey(Intervals.of("2025-11-01T00:00:00/P1D"));
+    Assertions.assertNull(ceiling, "Ceiling key 6");
+  }
+
+  @Test
+  public void testHigherKey()
+  {
+    IntervalTreeMap<String> tree = setupTree(sparseOverlapData);
+
+    // First entry
+    Interval higher = tree.higherKey(Intervals.of("2024-12-31T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-01-01T00:00:00/P1D"), higher, 
"Higher key 1");
+
+    // Entry with same start date but different end date
+    higher = tree.ceilingKey(Intervals.of("2025-02-01T00:00:00/PT6H"));
+    Assertions.assertEquals(Intervals.of("2025-02-01T00:00:00/P1D"), higher, 
"Higher key 2");
+
+    // Matching entry
+    higher = tree.higherKey(Intervals.of("2025-09-04T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-10-06T00:00:00/P1M"), higher, 
"Higher key 3");
+
+    // Random entry
+    higher = tree.higherKey(Intervals.of("2025-03-31T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-04-02T00:00:00/P1D"), higher, 
"Higher key 4");
+
+    // Only one greater entry
+    higher = tree.higherKey(Intervals.of("2025-09-28T00:00:00/P1D"));
+    Assertions.assertEquals(Intervals.of("2025-10-06T00:00:00/P1M"), higher, 
"Higher key 5");
+
+    // No greater entry
+    higher = tree.higherKey(Intervals.of("2025-11-01T00:00:00/P1D"));
+    Assertions.assertNull(higher, "Higher key 6");
+  }
+
+  private void compareData(List<Pair<Interval, String>> inputData, 
IntervalTreeMap<String> tree)
+  {
+    //Iterator<Map.Entry<Interval, String>> iterator = tree.inOrderTraverse();
+    Iterator<Map.Entry<Interval, String>> iterator = 
tree.entrySet().iterator();
+
+    List<Pair<Interval, String>> expected = inputData.stream()
+            .sorted((p1, p2) -> 
Comparators.intervalsByStartThenEnd().compare(p1.lhs, p2.lhs))
+            .collect(Collectors.toList());
+
+    compareEntries(expected.iterator(), iterator);
+  }
+
+  private void compareEntries(Iterator<Pair<Interval, String>> expected, 
Iterator<Map.Entry<Interval, String>> actual)
+  {
+    while (actual.hasNext()) {
+      Assertions.assertTrue(expected.hasNext(), "Entry available");
+      Pair<Interval, String> expectedEntry = expected.next();
+      Map.Entry<Interval, String> actualEntry = actual.next();
+      Assertions.assertEquals(expectedEntry.lhs, actualEntry.getKey(), 
"Interval match");
+      Assertions.assertEquals(expectedEntry.rhs, actualEntry.getValue(), 
"Value match");
+    }
+    Assertions.assertFalse(expected.hasNext(), "No outstanding entries");
+  }
+
+  static List<Pair<Interval, String>> baseData = new ArrayList<>();
+  static List<Pair<Interval, String>> overlapData = new ArrayList<>();
+  static List<Pair<Interval, String>> sparseOverlapData = new ArrayList<>();
+
+  static {
+
+    baseData.add(Pair.of(Intervals.of("2025-01-03T00:00:00/P1D"), "v1"));
+    baseData.add(Pair.of(Intervals.of("2025-01-05T00:00:00/P1D"), "v2"));
+    baseData.add(Pair.of(Intervals.of("2025-01-01T00:00:00/P1D"), "v3"));
+    baseData.add(Pair.of(Intervals.of("2025-01-02T00:00:00/P1D"), "v4"));
+    baseData.add(Pair.of(Intervals.of("2025-01-04T00:00:00/P1D"), "v5"));
+    baseData.add(Pair.of(Intervals.of("2025-01-06T00:00:00/P1D"), "v6"));
+
+    overlapData.addAll(baseData);
+    overlapData.add(Pair.of(Intervals.of("2025-01-01T00:00:00/P1Y"), "v7"));
+
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-05-10T00:00:00/P1M"), 
"v1"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-01-01T00:00:00/P1D"), 
"v2"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-02-01T00:00:00/P1D"), 
"v3"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-01-12T00:00:00/P1D"), 
"v4"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-07-12T00:00:00/P1D"), 
"v5"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-02-01T00:00:00/P1M"), 
"v6"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-06-03T00:00:00/P1D"), 
"v7"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-08-09T00:00:00/P1D"), 
"v8"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-08-02T00:00:00/P1M"), 
"v9"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-09-04T00:00:00/P1D"), 
"v10"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-04-02T00:00:00/P1D"), 
"v11"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-10-06T00:00:00/P1M"), 
"v12"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-06-01T00:00:00/P1M"), 
"v13"));
+    sparseOverlapData.add(Pair.of(Intervals.of("2025-01-01T00:00:00/P1Y"), 
"v14"));
+
+  }
+
+  private IntervalTreeMap<String> setupTree(List<Pair<Interval, String>> 
inputData)
+  {
+    return setupTree(inputData, null);
+  }
+
+  private IntervalTreeMap<String> setupTree(List<Pair<Interval, String>> 
inputData, Consumer<IntervalTreeMap<String>> setupFunc)
+  {
+    IntervalTreeMap<String> tree = new 
IntervalTreeMap<>(Comparators.intervalsByStart(), Comparators.intervalsByEnd());
+    if (setupFunc != null) {
+      setupFunc.accept(tree);
+    }
+    for (Pair<Interval, String> entry : inputData) {
+      tree.put(entry.lhs, entry.rhs);
+    }
+    return tree;
+  }
+
+}
diff --git 
a/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineSpecificDataTest.java
 
b/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineSpecificDataTest.java
index a41eda2fc36..46d57f06bbd 100644
--- 
a/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineSpecificDataTest.java
+++ 
b/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineSpecificDataTest.java
@@ -32,20 +32,40 @@ import org.joda.time.Interval;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 
 /**
  * This test class is separated from {@link VersionedIntervalTimelineTest} 
because it populates specific data for tests
  * in {@link #setUp()}.
  */
+@RunWith(Parameterized.class)
 public class VersionedIntervalTimelineSpecificDataTest extends 
VersionedIntervalTimelineTestBase
 {
+  @Parameterized.Parameters
+  public static Collection<Boolean> parameters()
+  {
+    return Arrays.asList(
+            false,
+            true
+    );
+  }
+
+  public VersionedIntervalTimelineSpecificDataTest(boolean fastIntervalSearch)
+  {
+    this.fastIntervalSearch = fastIntervalSearch;
+  }
+
+  private final boolean fastIntervalSearch;
+
   @Before
   public void setUp()
   {
-    timeline = makeStringIntegerTimeline();
+    timeline = makeStringIntegerTimeline(fastIntervalSearch);
 
     add("2011-04-01/2011-04-03", "1", 2);
     add("2011-04-03/2011-04-06", "1", 3);
diff --git 
a/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineTest.java
 
b/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineTest.java
index bd2e61e20cb..ca332773e82 100644
--- 
a/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineTest.java
+++ 
b/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineTest.java
@@ -34,6 +34,8 @@ import org.joda.time.Interval;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -43,13 +45,29 @@ import java.util.Set;
 
 /**
  */
+@RunWith(Parameterized.class)
 public class VersionedIntervalTimelineTest extends 
VersionedIntervalTimelineTestBase
 {
+  @Parameterized.Parameters
+  public static Collection<Boolean> parameters()
+  {
+    return Arrays.asList(
+            false,
+            true
+    );
+  }
+
+  public VersionedIntervalTimelineTest(boolean fastIntervalSearch)
+  {
+    this.fastIntervalSearch = fastIntervalSearch;
+  }
+
+  private final boolean fastIntervalSearch;
 
   @Before
   public void setUp()
   {
-    timeline = makeStringIntegerTimeline();
+    timeline = makeStringIntegerTimeline(fastIntervalSearch);
   }
 
   @Test
diff --git 
a/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineTestBase.java
 
b/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineTestBase.java
index 55f5c8ddfa9..fa6986cf28a 100644
--- 
a/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineTestBase.java
+++ 
b/processing/src/test/java/org/apache/druid/timeline/VersionedIntervalTimelineTestBase.java
@@ -110,7 +110,12 @@ public class VersionedIntervalTimelineTestBase
 
   static VersionedIntervalTimeline<String, OvershadowableInteger> 
makeStringIntegerTimeline()
   {
-    return new VersionedIntervalTimeline<>(Ordering.natural());
+    return makeStringIntegerTimeline(false);
+  }
+
+  static VersionedIntervalTimeline<String, OvershadowableInteger> 
makeStringIntegerTimeline(boolean fastIntervalSearch)
+  {
+    return new VersionedIntervalTimeline<>(Ordering.natural(), false, 
fastIntervalSearch);
   }
 
   VersionedIntervalTimeline<String, OvershadowableInteger> timeline;
diff --git a/server/src/main/java/org/apache/druid/guice/StorageNodeModule.java 
b/server/src/main/java/org/apache/druid/guice/StorageNodeModule.java
index 8feeb2cc25b..8a36cb81f6e 100644
--- a/server/src/main/java/org/apache/druid/guice/StorageNodeModule.java
+++ b/server/src/main/java/org/apache/druid/guice/StorageNodeModule.java
@@ -34,6 +34,7 @@ import org.apache.druid.java.util.emitter.EmittingLogger;
 import org.apache.druid.query.DruidProcessingConfig;
 import org.apache.druid.segment.DefaultColumnFormatConfig;
 import org.apache.druid.segment.column.ColumnConfig;
+import org.apache.druid.segment.indexing.SegmentTimelineConfig;
 import org.apache.druid.segment.loading.SegmentCacheManager;
 import org.apache.druid.segment.loading.SegmentLoaderConfig;
 import org.apache.druid.segment.loading.SegmentLocalCacheManager;
@@ -65,6 +66,7 @@ public class StorageNodeModule implements Module
     JsonConfigProvider.bind(binder, "druid.server", DruidServerConfig.class);
     JsonConfigProvider.bind(binder, "druid.segmentCache", 
SegmentLoaderConfig.class);
     JsonConfigProvider.bind(binder, "druid.indexing.formats", 
DefaultColumnFormatConfig.class);
+    JsonConfigProvider.bind(binder, "druid.segment.timeline", 
SegmentTimelineConfig.class);
     bindLocationSelectorStrategy(binder);
     binder.bind(ServerTypeConfig.class).toProvider(Providers.of(null));
     
binder.bind(ColumnConfig.class).to(DruidProcessingConfig.class).in(LazySingleton.class);
diff --git 
a/server/src/main/java/org/apache/druid/segment/indexing/SegmentTimelineConfig.java
 
b/server/src/main/java/org/apache/druid/segment/indexing/SegmentTimelineConfig.java
new file mode 100644
index 00000000000..451af0df52f
--- /dev/null
+++ 
b/server/src/main/java/org/apache/druid/segment/indexing/SegmentTimelineConfig.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.segment.indexing;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+
+/**
+ * Configuration settings related to segment timeline management
+ */
+public class SegmentTimelineConfig
+{
+  @JsonProperty
+  private final boolean fastIntervalSearch;
+
+  @JsonCreator
+  public SegmentTimelineConfig(@JsonProperty("fastIntervalSearch") @Nullable 
Boolean fastIntervalSearch)
+  {
+    this.fastIntervalSearch = fastIntervalSearch != null && fastIntervalSearch;
+  }
+
+  /**
+   * Whether an indexing mechanism based on an interval tree for organizing 
segments in memory is being used, that
+   * leads to faster search
+   */
+  public boolean isFastIntervalSearch()
+  {
+    return fastIntervalSearch;
+  }
+}
diff --git a/server/src/main/java/org/apache/druid/server/SegmentManager.java 
b/server/src/main/java/org/apache/druid/server/SegmentManager.java
index 4bea68953ac..73ab935a084 100644
--- a/server/src/main/java/org/apache/druid/server/SegmentManager.java
+++ b/server/src/main/java/org/apache/druid/server/SegmentManager.java
@@ -36,6 +36,7 @@ import org.apache.druid.segment.Segment;
 import org.apache.druid.segment.SegmentLazyLoadFailCallback;
 import org.apache.druid.segment.SegmentMapFunction;
 import org.apache.druid.segment.SegmentReference;
+import org.apache.druid.segment.indexing.SegmentTimelineConfig;
 import org.apache.druid.segment.join.table.IndexedTable;
 import 
org.apache.druid.segment.join.table.ReferenceCountedIndexedTableProvider;
 import org.apache.druid.segment.loading.AcquireMode;
@@ -71,12 +72,21 @@ public class SegmentManager
 
   private final SegmentCacheManager cacheManager;
 
+  private final SegmentTimelineConfig segmentTimelineConfig;
+
   private final ConcurrentHashMap<String, DataSourceState> dataSources = new 
ConcurrentHashMap<>();
 
-  @Inject
   public SegmentManager(SegmentCacheManager cacheManager)
+  {
+    this(cacheManager, new SegmentTimelineConfig(false));
+  }
+
+
+  @Inject
+  public SegmentManager(SegmentCacheManager cacheManager, 
SegmentTimelineConfig segmentTimelineConfig)
   {
     this.cacheManager = cacheManager;
+    this.segmentTimelineConfig = segmentTimelineConfig;
   }
 
   @VisibleForTesting
@@ -339,7 +349,7 @@ public class SegmentManager
     dataSources.compute(
         dataSegment.getDataSource(),
         (k, v) -> {
-          final DataSourceState dataSourceState = v == null ? new 
DataSourceState() : v;
+          final DataSourceState dataSourceState = v == null ? new 
DataSourceState(segmentTimelineConfig) : v;
           final VersionedIntervalTimeline<String, DataSegment> loadedIntervals 
=
               dataSourceState.getTimeline();
           final PartitionChunk<DataSegment> entry = loadedIntervals.findChunk(
@@ -516,8 +526,7 @@ public class SegmentManager
    */
   public static class DataSourceState
   {
-    private final VersionedIntervalTimeline<String, DataSegment> timeline =
-        new VersionedIntervalTimeline<>(Ordering.natural());
+    private final VersionedIntervalTimeline<String, DataSegment> timeline;
 
     private final ConcurrentHashMap<SegmentId, 
ReferenceCountedIndexedTableProvider> tablesLookup = new ConcurrentHashMap<>();
     private long totalSegmentSize;
@@ -525,6 +534,11 @@ public class SegmentManager
     private long rowCount;
     private final SegmentRowCountDistribution segmentRowCountDistribution = 
new SegmentRowCountDistribution();
 
+    public DataSourceState(SegmentTimelineConfig segmentTimelineConfig)
+    {
+      timeline = new VersionedIntervalTimeline<>(Ordering.natural(), false, 
segmentTimelineConfig.isFastIntervalSearch());
+    }
+
     private void addSegment(DataSegment segment, long numOfRows)
     {
       totalSegmentSize += segment.getSize();
diff --git a/sql/src/test/java/org/apache/druid/sql/guice/SqlModuleTest.java 
b/sql/src/test/java/org/apache/druid/sql/guice/SqlModuleTest.java
index 8c86d913d67..e04032a6637 100644
--- a/sql/src/test/java/org/apache/druid/sql/guice/SqlModuleTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/guice/SqlModuleTest.java
@@ -53,6 +53,7 @@ import org.apache.druid.query.QueryToolChestWarehouse;
 import org.apache.druid.query.lookup.LookupExtractorFactoryContainerProvider;
 import org.apache.druid.rpc.indexing.NoopOverlordClient;
 import org.apache.druid.rpc.indexing.OverlordClient;
+import org.apache.druid.segment.indexing.SegmentTimelineConfig;
 import org.apache.druid.segment.join.JoinableFactory;
 import org.apache.druid.segment.loading.SegmentCacheManager;
 import org.apache.druid.segment.metadata.CentralizedDatasourceSchemaConfig;
@@ -118,6 +119,9 @@ public class SqlModuleTest
   @Mock
   private SegmentCacheManager segmentCacheManager;
 
+  @Mock
+  private SegmentTimelineConfig segmentTimelineConfig;
+
   @Mock
   private QueryRunnerFactoryConglomerate conglomerate;
 
@@ -140,6 +144,7 @@ public class SqlModuleTest
         lookupExtractorFactoryContainerProvider,
         joinableFactory,
         segmentCacheManager,
+        segmentTimelineConfig,
         httpClient
     );
   }
@@ -209,6 +214,7 @@ public class SqlModuleTest
               
binder.bind(LookupExtractorFactoryContainerProvider.class).toInstance(lookupExtractorFactoryContainerProvider);
               binder.bind(JoinableFactory.class).toInstance(joinableFactory);
               
binder.bind(SegmentCacheManager.class).toInstance(segmentCacheManager);
+              
binder.bind(SegmentTimelineConfig.class).toInstance(segmentTimelineConfig);
               
binder.bind(QuerySchedulerProvider.class).in(LazySingleton.class);
               binder.bind(QueryScheduler.class)
                     .toProvider(QuerySchedulerProvider.class)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to