Data model, row keys, and encoding¶
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.