PL/SQL Binary Search

Binary search is a deterministic (for same input, same execution of code, and same output) search algorithm for ordered data structures. It assumes a sorted array, and maintains a bounded search interval defined by lower and upper indices.

At each iteration the midpoint is calculated and compared with the target value. Based on the comparison, exactly half of the remaining interval is eliminated from the search.

The invariant maintained is that, if the target exists, it lies within the current interval. The loop terminates when the interval collapses to zero width.

Because the search space is halved on each iteration, the worst-case complexity is O(log n) comparisons. For large datasets this is faster than linear search, with predictable and bounded execution time.

The precondition is ordering. If the data is not sorted, the algorithm is invalid. In practice this means either maintaining sorted state or paying the cost of sorting once before repeated searches.

While hash-based structures can provide constant-time lookup for exact matches, binary search is more versatile. It supports:

  • Lower and upper bound queries
  • Rank calculation
  • Predecessor and successor lookup
  • Insertion point determination
  • Range queries

These properties make it a useful primitive in algorithmic libraries and in-memory collection processing where deterministic behaviour and order semantics are required.

However, implementing it correctly, especially when handling duplicate values, requires careful attention to loop invariants and boundary conditions.

This post describes a clean implementation of binary_search_rightmost which returns the position of the rightmost occurrence of a target value in a sorted numeric array.


Click on the following link view the PL/SQL source code.

UTIL_NUMERIC Package
PL/SQL Package: UTIL_NUMERIC


The package contains the following binary search functions:


UTIL_NUMERIC Functions
Function Name Arguments Description
binary_search_leftmost p_target: Number to find

,p_list: String of comma separated numbers to be searched

,p_exact_match: Boolean. TRUE - return 0 if match not found.
Search list of numbers for a target value using a Binary Chop (Logarithmic) Search. Returns leftmost position of duplicate values.
binary_search_rightmost p_target: Number to find

,p_list: String of comma separated numbers to be searched

,p_exact_match: Boolean. TRUE - return 0 if match not found.
Search list of numbers for a target value using a Binary Chop (Logarithmic) Search. Returns rightmost position of duplicate values.
binary_search_nearest p_target: Number to find

,p_list: String of comma separated numbers to be searched
Returns position of value in array nearest to the target value.
binary_search_predecessor p_target: Number to find

,p_list: String of comma separated numbers to be searched
Returns position of nearest lower value.
binary_search_successor p_target: Number to find

,p_list: String of comma separated numbers to be searched
Returns position of nearest higher value.
binary_search_range p_range_from: Start number to search

,p_range_to: End number to search

,p_list: String of comma separated numbers to be searched
Returns count of numbers in range.
binary_search_rank p_target: Number to find

,p_list: String of comma separated numbers to be searched
Returns count of elements preceding target.


Implementing binary_search_rightmost in PL/SQL

Problem Definition

Given:

  • A sorted array of numbers in ascending order
  • A target value t

We want to:
  • Return the position of the rightmost element equal to t
  • Return 0 if the value is not found, if an exact match is required
  • Return the position where the value should be inserted is an exact match is not required


Example

Array: 1,1,2,2,2,3,4,4,4,4,5,6,8 Searching for 4 should return the position 10 (the rightmost, or last 4 in the array).


Why Rightmost Is Non-Trivial

A standard binary search returns the position of an occurrence of the target. But when duplicates exist, we must modify the algorith to:

  • Continue narrowing the search interval even after a match
  • Preserve the invariant that the target may still exist to the right

This requires a slightly different loop condition than the textbook "stop when equal."


Core Algorithm

The key invariant maintained during the loop:

  • The target value, if present, lies within the half-open interval [l ,r]

Where l = left bound of search, r = right bound.

  FUNCTION binary_search_rightmost_array(
    p_target      IN NUMBER,
    p_array       IN t_number_array,
    p_exact_match IN BOOLEAN DEFAULT TRUE
  ) RETURN NUMBER
  IS 
    l PLS_INTEGER; -- Left bound of array to search
    r PLS_INTEGER; -- Right bound of array to search
    n PLS_INTEGER; -- For VARRAY count = last element of array
    t PLS_INTEGER; -- Search target value
    m PLS_INTEGER; -- Mid position of range [l,r]
    p PLS_INTEGER; -- Position of target
    v_debug_module applog.program_name%TYPE := 'UTIL_NUMERIC.BINARY_SEARCH_RIGHTMOST_ARRAY';
    v_debug_msg applog.message%TYPE;
    v_debug_mode VARCHAR2(1) := 'X';
    e_null_value EXCEPTION;
    e_unsorted_array EXCEPTION;
  BEGIN
    IF array_contains_null(p_array) THEN 
      RAISE e_null_value;
    END IF;
    
    IF NOT is_sorted_array(p_array) THEN 
      RAISE e_unsorted_array;
    END IF;
    
    t := p_target;
    n := p_array.LAST;
    l := 1;
    r := n;
    p := 0;
                            
    -- Find position of target t in array
    WHILE l < r LOOP
      m := l + floor((r -l) / 2);
      IF p_array(m) > t THEN 
        r := m;
      ELSE
        l := m+1;
      END IF;
    END LOOP;
              

At the loop exit:

  • l points to the first element greater than the target.
  • Therefore the rightmost occurrence of the target is at l - 1


Resolution Logic

    IF n > 0 AND p_array(n) = p_target THEN 
      p := n; -- target found at last position of array
    ELSIF l > 1 AND p_array(l-1) = p_target THEN 
      p := l - 1; --target found at 1 position before last left bound searched is the rightmost target
    ELSE 
      IF p_exact_match THEN 
        p := 0; -- target not found, exact match required
      ELSE 
        p := l; -- insertion point for missing value is at last leftmost bound searched
      END IF;
    END IF;
    RETURN p;  
              

This ensures:

  • Correct rightmost match, when target = last value in array
  • Deterministic insertion behaviour where target not found
  • Proper termination of loop - No infinite loops!


Why This Works

The loop maintains [l ,r] as the region where the target may exist.
Each iteration reduces the interval.

  • If a(m) > target the target must lie in [l, m]
  • Otherwise, it must lie in [m+1, r]

This guarantees:
    Strict interval shrinkage
  • Termination of loop
  • No boundary oscillation


Edge Cases Handled

  • Empty arrays
  • Single element arrays
  • All array values same
  • Target rightmost at last position in array
  • Target < minimum value in array
  • Target > maximum value in array

Additional Validation:
  • Arrays must be pre-sorted into ascending order
  • NULL values are not allowed


Complexity

  • Time: O(log n)
  • Space: O(1)

Even with 32767 elements (the VARRAY limit) binary search performs at most 15 iterations.


Why Not Just Use SQL?

For table-based data, SQL is superior. In most production systems, if the data resides in database tables, SQL is the correct tool for filtering, ranking, and aggregation. Oracle's optimizer, indexing mechanisms, and analytic functions are highly efficient.

  SELECT MAX(pos)
  FROM (
    SELECT sal,
            ROW_NUMBER() OVER (ORDER BY sal) AS pos
    FROM emp
  )
  WHERE sal = :target;
              

However, the UTIL_NUMERIC package operates on:

  • In-memory PL/SQL collections
  • Deterministic numeric utilities (given same input, same output)
  • Reusable library components

The goal is algorithmic correctness, and structural clarity.


When Would You Use binary_search_* Instead of SQL?

There are cases where using the PL/SQL binary_search_* functions are appropriate.

  • When operating on in-memory PL/SQL collections (e.g. VARRAY or nested tables).
  • When deterministic behaviour is required independent of SQL execution plans.
  • When working inside libraries that operate on arrays rather than tables.
  • When insertion points, predecessor/successor logic, or rank semantics are required as part of an algorithm.
  • When building reusable components that must not depend on table structures.

In these situations, a correctly implemented binary search provides predictable O(log n) behaviour, and clean insertion semantics without requiring expensive context switches between SQL and PL/SQL.



Previous Blog Next Blog