Skip to content

Data model, row keys, and encoding

Documentation index

Map HBase concepts to Python

Concept Example Driver representation
Namespace default b"default"
Table users client.get_table(b"default", b"users")
Row key user:000042 bytes
Column family cf Created in the table schema
Qualifier name bytes, added when writing a cell
Cell value Alice bytes; encoding is your application's responsibility
Cell timestamp milliseconds since epoch Optional ts argument on Put.add_column

HBase sorts row keys lexicographically by bytes, groups columns into families, and can retain timestamped cell versions. See the Apache HBase data model. The Python result model has additional restrictions described below.

Design keys around the read pattern

For user-oriented event queries, a key such as user:000042:event:000001 groups one user's events into a contiguous range. Fixed-width numeric text preserves order for non-negative integers within the chosen width; ordinary text does not (b"10" < b"2"). Do not change key encoding after data has been written without a migration plan.

user_id = 42
event_id = 17
row_key = f"user:{user_id:06d}:event:{event_id:08d}".encode("ascii")
assert row_key == b"user:000042:event:00000017"

Choose separators, escaping, identifier ranges, and whether IDs can contain separators before ingestion. Salting/sharding a key may distribute writes, but it also makes range reads more complex; the driver does not choose or merge salted ranges for you.

Encode values deliberately

import json

text_value = "张三".encode("utf-8")
json_value = json.dumps({"name": "张三", "active": True}, ensure_ascii=False).encode("utf-8")
assert text_value.decode("utf-8") == "张三"
assert json.loads(json_value.decode("utf-8"))["active"] is True

JSON is an application format, not a server-side query engine. A BinaryComparator compares serialized bytes, not JSON fields or numeric values. Store fields you must filter independently as separate qualifiers.

Missing values and results

Row.rowkey is the key; Row.kv maps family -> qualifier -> bytes. row.get(family, qualifier) returns None when the column is absent. Never use a truthiness test to distinguish empty bytes from absence.

A Get with selected columns returns None if none of those columns match, even if the row has unrelated cells. Use a new Get for each purpose: table.exists(get) changes that Get into an existence-only request.

Counters are binary integers

Use Increment to initialize a counter from an absent cell. Do not initialize it with Put(..., b"0"): that is one byte of text, not the signed 8-byte integer representation used by HBase increments. table.increment(...) returns a Python integer for one result column. If you subsequently use Get, decode the stored value with int.from_bytes(value, "big", signed=True).

An increment or append is not idempotent. Retrying after a network timeout can apply it twice; a timeout does not prove the server rejected the request.

Versions and time ranges

Put.add_column(..., ts=...) accepts a millisecond timestamp. Retention depends on the server's column-family schema. The high-level Row model collapses multiple versions into one value per qualifier and does not expose the timestamp, so it is not a historical-cell API.

Do not use current Get/Scan time-range setters for correctness-sensitive time filtering: the RPC paths are incomplete. See limitations. For event queries, put the queryable time component in your application row key or another explicitly maintained access path.