Iterators
An iterator is an ordered cursor over a database (or a column family within one). RocksDB stores keys in sorted byte order, which makes iterators the natural primitive for range scans, prefix scans, and reverse scans.
This page covers the C# API. For the engine-level guarantees (snapshot semantics, prefix_extractor, total-order seek, tailing) read the upstream Iterator wiki and the Prefix Seek page.
Lifecycle
using var it = db.NewIterator();
it.SeekToFirst();
while (it.Valid())
{
var k = it.StringKey();
var v = it.StringValue();
// …
it.Next();
}
An iterator holds a native handle and a snapshot of the database state at construction time. Always Dispose it (or wrap it in using) — never leak iterators or you'll keep obsolete SST files alive on disk.
Seeking
| Method | Behaviour |
|---|---|
SeekToFirst() |
Position at the smallest key. |
SeekToLast() |
Position at the largest key. |
Seek(key) |
First key >= key. |
SeekForPrev(key) |
Last key <= key. |
Next() |
Move forward one key. |
Prev() |
Move backward one key. |
Valid() |
Iterator is positioned on a valid entry. |
using var it = db.NewIterator();
it.Seek("user:"); // first user
while (it.Valid() && it.StringKey().StartsWith("user:"))
{
Console.WriteLine(it.StringValue());
it.Next();
}
Reverse iteration
SeekToLast + Prev walks keys in descending order.
using var it = db.NewIterator();
for (it.SeekToLast(); it.Valid(); it.Prev())
{
Console.WriteLine($"{it.StringKey()} = {it.StringValue()}");
}
Range scans with bounds
The most efficient way to scope an iterator is to set iterate_lower_bound and/or iterate_upper_bound on ReadOptions. RocksDB uses them to skip whole SST files at seek time.
var ro = new ReadOptions()
.SetIterateLowerBound("user:")
.SetIterateUpperBound("user;"); // ';' is one byte past ':'
using var it = db.NewIterator(readOptions: ro);
for (it.SeekToFirst(); it.Valid(); it.Next())
{
// … only user:* keys
}
Span access (zero allocation)
On net6+, GetKeySpan() / GetValueSpan() give you a ReadOnlySpan<byte> over the native buffer, with no allocation. The span is only valid until the next Seek / Next / Prev.
using var it = db.NewIterator();
for (it.SeekToFirst(); it.Valid(); it.Next())
{
ReadOnlySpan<byte> key = it.GetKeySpan();
ReadOnlySpan<byte> value = it.GetValueSpan();
if (key.SequenceEqual("counter"u8))
{
long counter = BitConverter.ToInt64(value);
// …
}
}
You can also pass an ISpanDeserializer<T> to it.Key<T>(...) and it.Value<T>(...).
Pinning the snapshot
By default an iterator sees the database state at the moment it was created — concurrent writes are invisible. To pin a specific older state, pass a ReadOptions with a Snapshot set:
using var snap = db.CreateSnapshot();
var ro = new ReadOptions().SetSnapshot(snap);
using var it = db.NewIterator(readOptions: ro);
// …
See the Snapshots guide for the full picture.
Iterating a column family
Pass the ColumnFamilyHandle:
using var it = db.NewIterator(cf: users);
for (it.SeekToFirst(); it.Valid(); it.Next()) { /* … */ }
Common pitfalls
Long-lived iterators block compaction
RocksDB cannot delete SST files that an open iterator might still need. Keep iterators short-lived in long-running systems, or you'll see disk usage and rocksdb.estimate-pending-compaction-bytes grow.
Total-order seek vs. prefix seek
If you've set a prefix_extractor on the column family, plain Seek only scans within a prefix bucket. To iterate across prefixes set ReadOptions.SetTotalOrderSeek(true). Read the Prefix Seek wiki page for the trade-offs.
Tailing iterators
ReadOptions.SetTailing(true) gives you an iterator that follows newly-written keys as long as it's valid. Useful for fan-out / log-consumer patterns. See the Iterator wiki.
Worked example: paged scan
public static IEnumerable<(string Key, string Value)> PagedScan(
RocksDb db,
string startKey,
int pageSize)
{
using var it = db.NewIterator();
it.Seek(startKey);
int n = 0;
while (it.Valid() && n < pageSize)
{
yield return (it.StringKey(), it.StringValue());
it.Next();
n++;
}
}