Indexes

Indexes

In dbzero, an index is a specialized collection that stores other objects in a key-value format. Its primary purpose is to provide fast, range-based queries and sorted access to your data. This allows you to efficiently organize and retrieve objects based on criteria like priority, date, or any other comparable value.

Indexes are created using the dbzero.index() factory function.

💡

The core idea is to replicate the behavior of database indexes directly on your Python objects, avoiding the need for a separate database system for many use cases.

Basic Operations

Creating and Populating an Index

You can create an empty index and add items to it. It's usually kept as an attribute of another object.

@db0.memo
class Task:
    def __init__(self, description):
        self.description = description
 
# Create a new index
my_index = db0.index()
 
# Add items using index.add(key, value)
# The value must be a dbzero-managed object.
task1 = Task(description="Finish docs")
task2 = Task(description="Write tests")
 
my_index.add(1, task1) # key=1 (priority)
my_index.add(2, task2) # key=2 (priority)

The added value must be a dbzero.memo object. The same object can be added multiple times with different keys. An index can also store objects with None as their key.

Removing Elements

To remove an item, you must provide both the key and the value that were originally added.

# Remove the item with key=1
my_index.remove(1, task1)

Removing items with a None key works the same way:

null_key_obj = Task(description="Low priority")
my_index.add(None, null_key_obj)
# ...
my_index.remove(None, null_key_obj)

Supported keys

To achieve the best performance and efficiency, keys added to an index are converted to their native representations. The list of supported keys at the moment is limited to the following Python types:

  • int (-2^63 to 2^63-1 range)
  • decimal.Decimal
  • datetime.date
  • datetime.datetime
  • datetime.time

An index can store values only under a single type of key - mixing different key types is not supported. A type for index key is set upon first insertion and cannot be changed. The only exception is a None key, which can be used in combination with any other type to indicate "null" key.

# This is ok:
my_index.add(1, Task(description="Task priority 1"))
my_index.add(2, Task(description="Task priority 2"))
my_index.add(3, Task(description="Task priority 3"))
my_index.add(None, Task(description="Task no priority"))
# This raises an error:
my_index.add(datetime.date(2026, 1, 1), Task(description="Task with date")) # Exception

Querying and Sorting

The real power of indexes comes from querying and sorting capabilities.

Sorting with index.sort()

The sort() method takes a query object (like the result of a dbzero.find()) and returns its elements sorted according to their keys in the index.

# Setup: Add Task objects with priorities to an index
priority_index = db0.index()
tasks = [Task(priority=p) for p in [99, 66, 55, 88, 44]]
for t in tasks:
    priority_index.add(t.priority, t)
 
# Assign tag "project-alpha" to 3 tasks
db0.tags(*tasks[:3]).add("project-alpha")
 
# Find all tasks with "project-alpha" and sort them by priority
all_tasks = db0.find("project-alpha")
sorted_tasks = priority_index.sort(all_tasks)
 
# sorted_tasks will be in ascending order of priority: [55, 66, 99]
print([t.priority for t in sorted_tasks])

Sorting Order

By default, sort() orders items in ascending order. None values are placed at the end.

You can customize the sorting behavior with these arguments:

  • desc=True: Sorts in descending order.
  • null_first=True: Places None keys at the beginning of the result (default for descending sort).
  • null_first=False: Places None keys at the end (default for ascending sort).
# Example data with keys: [666, None, 555, 888, None]
tasks = db0.find("all-tasks")
 
# Descending order (None keys are first by default)
result_desc = priority_index.sort(tasks, desc=True)
# -> [None, None, 888, 666, 555]
result_desc = priority_index.sort(tasks, desc=True, null_first=False)
# -> [888, 666, 555, None, None]
 
# Ascending order (None keys are last by default)
result_null_first = priority_index.sort(tasks)
# -> [555, 666, 888, None, None]
result_null_first = priority_index.sort(tasks, null_first=True)
# -> [None, None, 555, 666, 888]

index.sort() is fast because it doesn't need to re-sort the data each time. It uses the pre-ordered structure of the index to quickly arrange items in the given results.

Multi-level Sorting

You can achieve multi-level sorting by chaining sort() calls. The last sort() call determines the primary sort order.

all_tasks = db0.find("all-tasks")
# First, order by date
tasks_sorted_by_date = date_index.sort(all_tasks)
# Then, order by priority - the primary order
tasks_sorted_by_priority = priority_index.sort(tasks_sorted_by_date)

Range Queries with index.select()

The select() method retrieves all objects whose keys fall within a specified range.

index.select(min_key, max_key)

The range is inclusive by default. You can use None to specify an unbounded range.

# Setup: index with keys from 0 to 9
numeric_index = db0.index()
for i in range(10):
    numeric_index.add(i, MyObject(i))
 
# Select items with keys between 2 and 5 (inclusive)
results = numeric_index.select(low=2, high=5)
# Result set: {2, 3, 4, 5}
 
# Select all items with keys >= 7 (high-unbounded)
results = numeric_index.select(low=7, high=None)
# Result set: {7, 8, 9}
 
# Select all items with keys <= 3 (low-unbounded)
results = numeric_index.select(low=None, high=3)
# Result set: {0, 1, 2, 3}
 
# Select all items in the index
all_items = numeric_index.select(None, None)
# or just:
all_items = numeric_index.select()

The keys can be numbers, dates, or datetimes. Strings are currently not supported as keys in dbzero indexes. For string-based indexing, consider using dbzero.dict instead.

from datetime import datetime, timedelta
 
date_index = db0.index()
# ... populate with datetime keys ...
 
# Get items from the last 24 hours
one_day_ago = datetime.now() - timedelta(days=1)
recent_items = date_index.select(one_day_ago, None)

Advanced Queries

Combining select and find

You can combine the results of index.select() with other queries using dbzero.find(). This allows for powerful, multi-faceted filtering. dbzero.find() performs an intersection (AND logic) of the results from all provided queries.

# Find objects with 'tag1' AND a priority between 500 and 800
query_result = db0.find(
    "tag1",
    priority_index.select(500, 800)
)
 
# Find objects that are in both index ranges
query_result = db0.find(
    priority_index.select(100, 200),
    date_index.select(start_date, end_date)
)

Finding Specific Objects in an Index

To check for existence of specific objects in the index you can use dbzero.find() to intersect items from the index with a list of one or more objects.

# Check if a specific object is in a range
if db0.find(index.select(100, 200), obj):
    print("Object found in range!")
 
# Check a list of specific objects
if db0.find(index.select(), [obj1, obj3, obj5]):
    print("At least one object is in the index!")