Ichsan Said created PHOENIX-8001:
------------------------------------

             Summary: Support specifying a subset of composite key columns for 
SALT_BUCKETS hash computation
                 Key: PHOENIX-8001
                 URL: https://issues.apache.org/jira/browse/PHOENIX-8001
             Project: Phoenix
          Issue Type: New Feature
          Components: core
    Affects Versions: 5.4.0
            Reporter: Ichsan Said


h2. Background: The Salt Bucket Trade-off

Salt buckets in Apache Phoenix transparently prepend a 1-byte hash to every 
HBase 
row key, distributing writes across N pre-split regions to prevent write 
hotspots. 
The hash is computed from the entire encoded composite primary key.

This design makes an implicit assumption: that writes should be distributed as 
randomly as possible across all buckets. While this is optimal for pure write 
throughput, it forces a fundamental trade-off that users currently cannot 
control.

h2. The Three Access Pattern Archetypes

Consider a table with PK (tenant_id, user_id, event_ts) and SALT_BUCKETS = 16.

*Archetype 1: Write-heavy, point-lookup reads (current behavior is optimal)*
Use case: High-volume event ingestion, reads are always by full PK.
With current salting: writes spread across 16 buckets, point lookups compute 
the exact salt byte. No problem here.

*Archetype 2: Write-heavy, range-scan reads on salt columns prefix (current 
behavior is suboptimal)*
Use case: IoT / time-series ingestion combined with queries like 
"show me all events for user X in the last hour".
With current salting: writes spread across 16 buckets, but range scans on 
(tenant_id, user_id) must fan out to all 16 buckets. Read amplification = 16x.
With SALT_BUCKETS_COLUMNS = 'tenant_id,user_id': writes still spread across 
buckets by salt columns value, and range scans on those salt columns hit 
exactly 1 bucket.

*Archetype 3: Moderate write, heavily skewed read on salt columns (new 
capability)*
Use case: Multi-tenant SaaS where one salt columns value drives 80% of traffic.
With current salting: writes spread, but all reads for the dominant salt 
columns 
value still fan out to all buckets. With SALT_BUCKETS_COLUMNS = 'tenant_id': 
reads for any salt columns value hit exactly 1 bucket, and load is spread 
across 
all buckets by salt columns value.

h2. Flexible Bucket Isolation

Beyond access pattern optimization, SALT_BUCKETS_COLUMNS enables a new class 
of operational capability: deterministic data placement by salt columns value.

With the current implementation, the bucket a salt columns value lands in is 
opaque. 
It is the result of hashing the entire composite key, which means operators 
cannot 
predict or reason about where data for a specific salt columns value resides 
without 
computing the hash manually.

When SALT_BUCKETS_COLUMNS is specified, the bucket assignment becomes 
deterministic 
and transparent. Given a known salt columns value, the destination bucket is 
predictable at schema design time. This unlocks several capabilities that are 
not possible today:

*Data isolation per salt columns value*
All data sharing the same salt columns value is guaranteed to reside in the 
same 
bucket, and therefore the same HBase region (at least initially, before region 
splits). 
This makes per-salt-columns-value operations feasible at the infrastructure 
level: 
selective cache eviction, backup and restore, compaction scheduling, and 
targeted 
bulk operations without touching unrelated data.

*Hotspot isolation*
When a small number of salt columns values drive disproportionate traffic, the 
operator can identify exactly which bucket is hot and apply targeted 
remediation 
such as region server rebalancing or dedicated region server assignment. 
Because 
bucket assignment is deterministic based on the salt columns value, tracing a 
hot 
bucket back to the responsible salt columns value is a simple hash computation. 
With full-key hashing, a single salt columns value is spread across many 
buckets 
depending on the other PK columns, so there is no direct mapping between a hot 
bucket and the salt columns value causing it.

*Compliance and security isolation*
In regulated environments where data from different salt columns value classes 
must not share the same physical storage boundary, deterministic bucket 
assignment 
provides a foundation for enforcing such guarantees at the schema level. While 
HBase does not natively enforce security at the region boundary, deterministic 
placement makes it tractable to build operational policies around it.

It is worth noting that this flexibility comes with a responsibility: if the 
chosen 
salt columns have very low cardinality relative to SALT_BUCKETS, some buckets 
may 
receive significantly more data than others. For example, using 
SALT_BUCKETS_COLUMNS = 'tenant_id' with only 3 distinct salt columns values and 
SALT_BUCKETS = 16 means 13 buckets will be empty. Users must size their salt 
columns 
cardinality against their bucket count to achieve the desired distribution 
properties.

h2. Root Cause

The current implementation in SaltingUtil.getSaltingByte() always hashes 
buf[1..size-1], the entire encoded PK, with no mechanism to specify 
which portion of the key should drive bucket assignment. This prevents users 
from making an informed trade-off between:

* Write distribution uniformity
* Scan locality for prefix-based queries
* Data co-location for salt columns value workloads
* Region-level data isolation by salt columns value

h2. Current Workarounds and Their Limitations

Users currently work around this limitation through:

1. *Manual key design* reordering PK columns to put the desired salt columns 
   first, then relying on non-salted distribution. This loses the ability to 
   use SALT_BUCKETS entirely and requires application-level key management.

2. *Accepting fan-out scans* building application logic that issues N parallel 
   scans (one per bucket) and merges results. This pushes infrastructure 
concerns 
   into application code and increases client-side complexity.

3. *Secondary indexes* creating an index on the salt columns to avoid fan-out. 
   This doubles write load and storage, and introduces index maintenance 
overhead.

None of these approaches are satisfactory for production workloads at scale.

h2. Proposed Solution

Add SALT_BUCKETS_COLUMNS as an optional CREATE TABLE property. When specified, 
only the listed PK columns are used as input to the salt byte hash. When 
omitted, 
behavior is identical to current (full row key hash), fully backward compatible.

{code:sql}
-- hash(tenant_id, user_id, event_ts): maximum write spread,
-- but range scans on salt columns must touch all 16 buckets
CREATE TABLE events (...) SALT_BUCKETS = 16;

-- hash(tenant_id, user_id): co-locate by salt columns value,
-- range scans on salt columns touch exactly 1 bucket
CREATE TABLE events (...) SALT_BUCKETS = 16, SALT_BUCKETS_COLUMNS = 
'tenant_id,user_id';

-- hash(tenant_id): co-locate by salt columns value,
-- all data for a salt columns value is in 1 bucket
CREATE TABLE events (...) SALT_BUCKETS = 16, SALT_BUCKETS_COLUMNS = 'tenant_id';
{code}

h2. Backward Compatibility

When SALT_BUCKETS_COLUMNS is not specified, SaltingUtil.getSaltingByte() is 
called 
with the full row key bytes, identical to current behavior. Existing tables are 
unaffected. New tables without SALT_BUCKETS_COLUMNS behave identically to today.

h2. Scale Impact

At production scale (10B+ rows, 100+ nodes, multi-tenant deployments):

* Eliminates up to 16x read amplification for salt columns scoped range scans
* Reduces cross-region RPC overhead for prefix queries proportional to 
SALT_BUCKETS
* Enables data isolation by salt columns value without schema redesign
* Improves cache hit rates on RegionServer block cache for salt columns value 
workloads
* Allows capacity planners to predict and control data distribution by salt 
columns value

h2. Guide Posts: Open Question for Committer Review

Based on our initial reading of the code, guide posts appear to store actual 
HBase 
row keys including the already-computed salt byte, and BaseResultIterators 
reads 
those stored bytes directly without re-hashing. If this understanding is 
correct, 
changing which columns drive the hash at write time would not affect statistics 
collection, guide post storage, or parallel scan boundary detection.

However, we recognize that the full interaction between the salt bucket 
algorithm 
and guide post mechanics, particularly around synthetic bucket boundary 
insertion 
in ScanRanges.intersectScan() and the splitPostfix computation in 
getParallelScans(), 
may have subtleties that are not apparent from reading the code alone. We would 
appreciate committer review of whether SALT_BUCKETS_COLUMNS introduces any edge 
cases in guide post-based scan planning that we may have missed.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to