LazySort How-To Guide
![]()
LazySort is designed to retrieve elements by index from an unsorted dataset as if the dataset were fully sorted, without sorting the entire dataset or consuming excessive memory.
It is ideal when sorting the entire dataset is impractical, but specific indexed elements (such as the median or a specific percentile) need to be retrieved. Additionally, it can determine the index of an element in the sorted representation of the dataset.
It is ideal when sorting the entire dataset is impractical, but specific indexed elements (such as the median or a specific percentile) need to be retrieved. Additionally, it can determine the index of an element in the sorted representation of the dataset.
Overview
The core algorithm consists of two phases:
- Analysis & Clustering: Analyzes the raw dataset and partitions it into indexed clusters.
- In-Memory Sort & Retrieval: Loads and sorts only the single cluster containing the requested element, then returns the target item.
Installation
Using pip:
pip install lazysort
Using uv:
uv add lazysort
Source code available at gitlab.com/daemn/lazysort
Usage
Demo Walkthrough
To demonstrate functionality, a 10 GB test file containing 10 million random 1024-byte items was generated.
Dataset Generator Script:
Dataset Generator Script:
File Sequence Reader
To access elements from the raw binary file, a simple SequenceReader wrapper was implemented:
LazySort Execution
We configured LazySort with work_items_limit=100_000. This limits in-memory sorting overhead to ~100 MB of data, plus ~400 KB for cluster metadata.
Each cluster is tracked by integer metadata and two boundary elements (minimum and maximum). The maximum number of clusters is determined by:
max_clusters = (total_items // work_items_limit) * 2 (For 10,000,000 items with a limit of 100,000, 200 clusters are created).
Each cluster is tracked by integer metadata and two boundary elements (minimum and maximum). The maximum number of clusters is determined by:
max_clusters = (total_items // work_items_limit) * 2 (For 10,000,000 items with a limit of 100,000, 200 clusters are created).
Performance & Memory Footprint
The chart below illustrates process memory consumption (resident memory) alongside disk read operations across execution phases:
Phase I: Initial Access (get 0 & get 1,000)
Initialization: Instantiating LazySort does not trigger dataset analysis. Memory starts at ~20 MB (Python interpreter and script overhead).
First Query (get 0): Dataset analysis runs on-demand. The algorithm identifies the cluster containing index 0: {"idx": 0, "size": 38752}.
In-Memory Sort: Since 38,752 <= 100,000, the cluster (39 MB) is loaded into memory and sorted. Total RAM usage reaches ~60 MB (~20 MB Python + ~39 MB data). Item 0 is returned.
Cache Hit (get 1,000): Index 1,000 resides in the already loaded cluster {"idx": 0, "size": 38752}. No disk I/O is required, returning the element instantly.
First Query (get 0): Dataset analysis runs on-demand. The algorithm identifies the cluster containing index 0: {"idx": 0, "size": 38752}.
In-Memory Sort: Since 38,752 <= 100,000, the cluster (39 MB) is loaded into memory and sorted. Total RAM usage reaches ~60 MB (~20 MB Python + ~39 MB data). Item 0 is returned.
Cache Hit (get 1,000): Index 1,000 resides in the already loaded cluster {"idx": 0, "size": 38752}. No disk I/O is required, returning the element instantly.
Phase II: Partitioning Large Clusters (get 9,500,000)
Query get 9,500,000: The target cluster {"idx": 9454757, "size": 179435} exceeds the 100,000-item limit.
Re-clustering: LazySort re-analyzes and splits this cluster into two smaller sub-clusters: {"idx": 9454757, "size": 96520} and {"idx": 9551277, "size": 82915}.
Target Load: The first sub-cluster (96,520 items / ~97 MB) contains index 9,500,000. It is loaded and sorted, bringing total memory usage to ~125 MB.
Re-clustering: LazySort re-analyzes and splits this cluster into two smaller sub-clusters: {"idx": 9454757, "size": 96520} and {"idx": 9551277, "size": 82915}.
Target Load: The first sub-cluster (96,520 items / ~97 MB) contains index 9,500,000. It is loaded and sorted, bringing total memory usage to ~125 MB.
Phase III: High-Index Retrieval (get 9,999,999)
Query get 9,999,999: The target cluster {"idx": 9910300, "size": 89700} is within the memory limit. It is loaded and sorted, using ~120 MB RAM.
Phase IV: Re-loading Previous Cluster (get 1,000)
Query get 1,000: Cluster {"idx": 0, "size": 38752} is loaded and sorted again as in Phase I, using ~60 MB RAM.

Comments
Post a Comment