Skip to content

Scanning, filters, and advanced writes

Documentation home · Runnable recipes · API reference

Examples on this page assume an open table with a cf family. They are contextual snippets, not a complete script. Use the cookbook for independent examples with setup, data, and assertions.

Design the scan boundary first

Row keys are ordered as bytes. The constructor Scan(start_row=b"", end_row=b"") includes both endpoints by default; empty bounds are unbounded. Make a half-open interval explicit:

from hbasedriver.operations.scan import Scan

scan = (
    Scan(start_row=b"user:001")
    .with_end_row(b"user:100", inclusive=False)
    .add_family(b"cf")
)
with table.scan(scan) as scanner:
    first_page = scanner.next_batch(20)
for row in first_page:
    print(row.rowkey, row.get(b"cf", b"name"))

next_batch(20) obtains up to 20 rows from that scanner. Exiting the context closes it; the example does not continue the entire scan.

For long reads, consume bounded chunks:

from hbasedriver.operations.scan import Scan

with table.scan(Scan().add_family(b"cf")) as scanner:
    while True:
        rows = scanner.next_batch(100)
        if not rows:
            break
        for row in rows:
            print(row.rowkey)  # Replace with your bounded processing step.

Avoid list(scanner) on an unbounded table. Iteration currently requests one row through next_batch(1); explicit chunks let you control application memory and request size. Neither Scan.set_limit() nor PageFilter should be treated as a reliable global cap in this driver.

Resume after the last key

table.scan_page(scan, page_size) opens/closes a scanner for one page and returns (rows, last_returned_key). Continue with:

from hbasedriver.operations.scan import Scan

# resume_key was returned by a previous nonempty page.
scan = (
    Scan()
    .with_start_row(resume_key, inclusive=False)
    .with_end_row(b"user:100", inclusive=False)
    .add_family(b"cf")
)
rows, next_key = table.scan_page(scan, page_size=20)

Do not pass include_start_row=False to the Scan constructor: that keyword is not accepted. Reapply the original filter and projection on every page. A nonempty page always has a resume key, even if no more rows remain; stop on empty rows. The pagination recipe demonstrates the complete loop.

Pagination across changing data is not snapshot-consistent. Re-creating a scanner per page trades scanner lifetime for additional setup overhead. Reverse scans, wide/partial rows, and split/move recovery need separate integration tests for your topology.

Choose the correct filter

Goal Filter Important distinction
Row-key prefix PrefixFilter(b"user:") Also use start/end keys to constrain the search
Select a qualifier QualifierFilter(CompareOperator.EQUAL, BinaryComparator(b"name")) Filters cells, not whole-row business predicates
Qualifier prefix ColumnPrefixFilter(b"metric:") Matches qualifier names
Row must have a matching column SingleColumnValueFilter(...) Set missing-column behavior explicitly
Combine conditions FilterList(operator=..., filters=[...]) Nested enum: FilterList.Operator
Return row keys with reduced cells FirstKeyOnlyFilter() / KeyOnlyFilter() Inspect transformed result shape before decoding values

Current Scan.add_column() has a protobuf serialization bug for bytes qualifiers. To select a single qualifier in a family, use this alternative:

from hbasedriver.filter import BinaryComparator, CompareOperator, QualifierFilter
from hbasedriver.operations.scan import Scan

scan = (
    Scan(start_row=b"user:")
    .with_end_row(b"user;", inclusive=False)
    .add_family(b"cf")
    .set_filter(QualifierFilter(CompareOperator.EQUAL, BinaryComparator(b"name")))
)
with table.scan(scan) as scanner:
    selected = scanner.next_batch(20)

For whole-row predicates, use SingleColumnValueFilter and include the tested family in the Scan. Its filter_if_missing and latest_version_only options are constructor arguments, not setter methods. The filter recipe shows a working AND combination.

Filter comparisons are byte comparisons unless you deliberately choose a different comparator/encoding. Text b"100" does not sort like integer 100 relative to b"20". See data model.

Time ranges, versions, and scan settings

Do not use Scan.set_time_range() to enforce a query window: its fields are not serialized. Get's finite time range has a separate protobuf-assignment failure. Also, Row does not preserve multiple versions or cell timestamps.

For time-oriented queries, plan an application row-key scheme and test its byte ordering; do not claim a client-side timestamp filter over Row when timestamps are unavailable.

Java-style setters such as setCaching, setBatch, setMaxResultSize, setReversed, and setAllowPartialResults exist. They do not imply full Java Client behavior. In particular:

  • caching is not the number of rows in the final result;
  • batch refers to cell/row-fragment handling, not BatchPut;
  • partial and reverse scans need workload-specific validation;
  • a method that accepts a setting may not carry it through every execution path.

Pick the correct write abstraction

Requirement API Boundary
Write one row Put Bytes supplied by the application
Submit several rows conveniently BatchPut / table.batch() Sequential calls; partial success possible
Defer writes in memory BufferedMutator Not durable, no combined RPC guarantee
Atomically increase one counter Increment Signed 8-byte numeric storage, retries can double-count
Compare then write CheckAndPut Same-row atomic condition, not a full lock service
Append bytes Append Replays duplicate the suffix
Put/Delete together on one row RowMutations Not a cross-row transaction

BufferedMutatorParams() takes no keyword arguments. Assign write_buffer_size (bytes), write_buffer_periodic_flush (milliseconds), or max_key_value_size after construction. Use BufferedMutator(table, params) with a caller-owned Table and explicit flush() at an application checkpoint.

A successful flush means that invocation returned without error; it does not make the client buffer a recovery log. A failed flush can remove unsent work from the buffer. Preserve input/checkpoints and reconcile server state before retrying. See configuration, limitations, and the buffering recipe.

Lifecycle and concurrency

Use nested Client → Table → Scanner/Mutator contexts, closing inner resources first. This is the intended usage pattern, not a guarantee that every low-level socket is reclaimed. Table handles share some Client internals, and closing a Table can affect shared state.

Do not assume thread-safe sharing, full automatic reconnection, or correct cache isolation for arbitrary multi-table scans. Use conservative bounded sessions, test your concurrency pattern, and monitor sockets and memory. See configuration for details.