More about HBase
Pre-read: https://medium.com/@shahidmsj27/introduction-to-hbase-a5ff69db3763
Let’s continue to learn more about Hbase, this time we’ll understand how read & write works internally understanding the nuances of each component. We’ll then move on to learn about compactions and certain basic data modelling practices.
Now let’s dive a little deep as to how reads and writes work in Hbase.
How a Write (Put) Works in HBase:
This process ensures durability and consistency.
- Client Request:
- The client application initiates a
Putoperation, specifying theRowKey,ColumnFamily:ColumnQualifier, value (as bytes), and optionally aTimestamp.
2. Client Locates Region:
- The HBase client library maintains an internal cache that maps
RowKeyranges to the specific RegionServers that host them. - It checks its cache to find which RegionServer is responsible for the target
RowKey. - If the information isn’t in the cache, or if the cache entry is stale (e.g., due to a Region split or move), the client queries the special
hbase:metatable. The location of thehbase:metatable is found via ZooKeeper.
[!Question] How does ZK handle hbase:meta when Hmaster is the one responsible for it? - Hmaster does the entire heavy lifting and just informs about it to the ZK. Now whenever a client asks ZK about hbase:meta, it redirects the client to the hbase:meta location from where the client reads.
3. Client Contacts RegionServer:
- Once the client knows the correct RegionServer, it sends the
Putrequest directly to that RegionServer.
4. RegionServer Receives Request:
- The RegionServer receives the
Putrequest and directs it to the appropriateHRegionobject it hosts internally (based on itsRowKeyrange map).
5. Write Ahead Log (WAL) Write:
- Write Ahead Log (WAL) is a file attached to every Region Server inside the distributed environment. The WAL stores the new data that hasn’t been persisted or committed to the permanent storage. It is used in case of failure to recover the data sets.
- Crucial Step for Durability: Before writing data to memory, the
HRegionfirst writes thePutoperation (the changes) to the Write Ahead Log (WAL). The WAL is a sequential log file stored on HDFS. - This ensures that even if the RegionServer crashes before the data is persisted to an HFile, the WAL can be replayed during recovery to reconstruct the MemStore’s contents, preventing data loss.
6. MemStore Write:
- After successfully writing to the WAL, the
HRegionthen writes the data into its in-memory buffer called the MemStore. Each Column Family within a Region has its own MemStore. HRegion: Java object or class instance that aRegionServercreates and manages in its memory to physically handle that specific conceptual Region
7. Client Acknowledgment:
- Once the data is successfully written to both the WAL and the MemStore, the RegionServer sends an acknowledgment back to the client, indicating the
Putoperation was successful. At this point, the data is durable (due to the WAL) and immediately available for reads.
8. MemStore Flush (Asynchronous Background Process):
- The data in the MemStore is temporary. When a MemStore for a Column Family reaches a certain size threshold (e.g., 128MB by default) or after a configurable time interval, the RegionServer flushes its contents.
- This flush operation writes the MemStore’s sorted contents as a new, immutable HFile to HDFS.
9. HFile on HDFS (Persistent Record):
- The newly created HFile is now the persistent record of the data on HDFS. Old WAL entries related to the flushed data can then be removed.
- Over time, multiple HFiles accumulate for a Column Family in a Region. HBase runs Compactions (minor and major) in the background to merge these HFiles into larger, more optimal ones, and to remove old versions and deleted data.
How a Read (Get) Works in HBase:
This process prioritizes low-latency access to the latest data.
- Client Request:
- The client application initiates a
Getoperation, specifying theRowKey, and optionallyColumnFamiliesandColumnQualifiers, andTimestamp(for a specific version).
2. Client Locates Region:
- Similar to a write, the client first checks its internal cache for the
RowKeyto RegionServer mapping. - If not found or stale, it queries the
hbase:metatable via ZooKeeper to locate the correct RegionServer.
3. Client Contacts RegionServer:
- The client sends the
Getrequest directly to the responsible RegionServer.
4. RegionServer Receives Request:
- The RegionServer receives the
Getrequest and identifies the appropriateHRegionobject it hosts internally.
5. Data Retrieval Path (Prioritizing Latest Data):
- The
HRegionperforms a sequential lookup to find the requested data, starting from the most volatile (newest) data to the most persistent (oldest):
a. MemStore: It first checks the MemStore for the relevant Column Families. Any data recently written but not yet flushed will be found here.
b. BlockCache: It then checks the BlockCache (an in-memory read cache maintained by the RegionServer). Frequently accessed data blocks from HFiles are stored here.
c. HFiles: If the data is not found in the MemStore or BlockCache, the RegionServer reads the necessary data from the HFiles on HDFS. It consults multiple HFiles per Column Family (and handles any overlapping versions or delete markers) to reconstruct the final, correct version of the requested Cell.
6. Filter and Merge:
- If multiple versions exist or delete markers are present (due to compactions not yet running), the RegionServer filters out older versions based on the request (e.g., get latest, get all versions within a timestamp range) and removes any logically deleted cells.
- It merges data from different sources (MemStore, BlockCache, HFiles) to present a unified view of the row.
7. Return to Client:
- The RegionServer sends the retrieved data (the requested Cells) back to the client.
One thing to note here is that while returning the data during a get call, it will get the data from Hfiles and write to blockcache (depends on the table setting) before returning to the client to reduce subsequent overheads.
What are Compactions?
Compactions help us in optimising our reads and writes. It generally combines small HFiles into a bigger HFile. One single big Hfile improves on the efficiency of IO and makes it easier for RegionServer to query faster.
Dictated by some but not all configurations as mentioned below:
- hbase.hstore.compaction.min: This is the minimum number of HFiles in a Store
- hbase.hstore.compaction.max : This is the maximum number of HFiles that can be included in a minor compaction operation
- hbase.hstore.majorcompaction: default 7 days
We have two types of compaction:
- Minor Compaction
- Major compaction
Minor Compaction:
- Merges small Hfiles within a column family into a larger one.
- Happen relatively often (every few minutes/hours)
- Low overhead
Major Compaction:
- Merges all Hfiles for a given column family within a region into a single large Hfile. This also clean up deleted and expired cells.
- Expensive operation, hence scheduled for off peak hours.
Data Modelling Practices
The core of efficient HBase design revolves around the RowKey and Column Family choices.
Before anything we need to understand the concept of KeyValue. HBase stored each and every data in a KeyValue format. What do we mean by that?
Every data that is ever stored in a cell in HBase is stored as a key(combination of meta info like cf, cq, rowkey and timestamp) and a value that is stored into the cell. So this means that every data in every cell that is ever stored will all have the “locator” info as key and corresponding value as, value.
How to design Rowkeys:
- The goal is to store data in such a way that related rows are near each other, since they’re lexicographically sorted.
- Also they should be designed such that unrelated rows could be partitioned effectively to avoid hotspotting.
- We would want our rowkeys to be short but we’ll have to bear in mind that rowkeys are the most important aspect required for data access for both Get and Scan. So we can take some leeway here.
How to design Column Families:
- HBase starts experiencing performance issues as soon as we go beyond 2–3 column families. So a rule of thumb is to make do with one column family if you can in your schemas.
According to the Hbase docs
Only introduce a second and third column family in the case where data access is usually column scoped; i.e. you query one column family or the other but usually not both at the one time.
Now why is this the case? Let’s dive a bit deep into it.
Generally we try to model or create multiple column families when we want to access them separately. Let’s take an example
In the above example, we can see we have two column families and column qualifiers inside each of those column families. We’d want to model this sort of design only when we are sure that PersonInfo and Sales are queried separately and not together. Because it’d add to the latency since each of those column families are stored on two separate Hfiles.
Get Shahid Jamal’s stories in your inbox
Join Medium for free to get updates from this writer.
A question can arise that in that case why would we add another column family in the first place? Why can’t we work with just adding another column qualifier or another rowkey?
Both are reasonable questions. But we’ll see when to pivot according to our needs
- Use different row keys when data is a truly separate entity that you query by a different key or independently. We can design our row key as
001::PersonInfo,001::Sales, but that poses another challenge that we'll discuss in the section where we define the column family names. But to summarise, we're not optimising our data in cells fully by designing row keys like that and in hindsight this might either create hotspotting or inefficient reads. - Using different column qualifiers would definitely work but then we’d be getting all the data whenever we query using the row key.
- But another question, we can specifically request for a column qualifier anyway, so what’s stopping me from putting it all in one cf and multiple cq?
- Here an important database concept comes to play. Even though we’ve only requested for the column qualifier, all the data are stored physically together on the same data block, querying a qualifier will have to fetch the entire block, so if the data is large, there will be performance issues. - Try to keep column family name as short as possible. For e.g. if the column family is something like data, replace that with ‘d’. Preferred is one character. Ref: https://hbase.apache.org/book.html#keysize.cf
- Why is this needed? As we understood from the KeyValue concept, every data that we store in HBase is a combination of it’s rowkey, cf, cq and timestamp. So if the names of cf and cq are bigger, it’ll take up more space, meaning it’ll use more memory, compactions will be slower and memstore taking up more space for individual data meaning it’ll be able to keep lesser data, meaning less reads from memstore, meaning slower reads and hence performance issues.
How to design Column Qualifier:
- This aspect of HBase gives us a lot of flexibility. Unlike cq, these are not physical files but rather logical data inside a physical HFile or Column Family. So in reality this could be pretty flexible from our end.
- Given that, we should still keep the name of cq small and concise so as to cater for the keyValue concept.
How to prevent Hotspotting?
The first and foremost and the most important part is to design a good rowkey.
We can do that by doing the following:
- Salting
- Hashing
- Pre-Splitting Tables
- RegionServer grouping
1. Salting
Adding a small prefix to rowkeys to spread new writes evenly across different regions.
Pros:
- Prevents hot spotting by distributing data more evenly across regions/shards
- Improves write performance by avoiding sequential key patterns
Cons:
- Makes range scans inefficient since related data is scattered
- Complicates queries that need to retrieve data in original order
- Adds overhead to key generation and lookup operations
Use When:
- Point lookups — When you know the exact key and can calculate the salt
- User-specific range queries —
getUserData(userId, startDate, endDate)where salt is based on userId - Partition-aware scans — Queries that can work within a single salt bucket
- High-volume writes — When you need to distribute insert load
Don’t use when:
- Global range scans — Queries across all users for a date range
- Lexicographic ordering — When you need data in original key order
- Unknown key patterns — When you can’t predict the salt prefix
2. Hashing
Hash the original data and use that output as the row key.
Pros:
- Provides excellent data distribution across partitions
- Fast lookups with O(1) average time complexity
Cons:
- Completely destroys data locality and ordering
- Range queries become impossible or very expensive
- Difficult to perform partial key lookups
- Rehashing during cluster resize can be complex and costly
Use when:
- Exact key lookups — Perfect for key-value access patterns
- Uniform random access — When queries are evenly distributed
- High-throughput point queries — Maximum performance for single-key operations
- Load balancing — When even distribution is critical
Don’t use when:
- Any range queries — Completely destroys locality
- Prefix matching — No ordering preserved
- Analytical queries — Scanning patterns become impossible
3. Pre-Splitting Tables
When creating a table, define “split points” (specific RowKeys) that tell HBase to create multiple Regions right from the start.
Pros:
- Avoids the overhead of dynamic region splits during heavy write loads
- Provides predictable performance from the start
- Prevents initial hotspotting on new tables
- Allows for better capacity planning and resource allocation
Cons:
- Requires good understanding of data distribution patterns upfront
- Can lead to empty regions if splits are poorly chosen
- May waste resources if data doesn’t grow as expected
- Difficult to optimize without knowing future access patterns
Use When:
- Predictable data patterns — When you know the key distribution upfront
- Bulk data loads — Large initial data imports
- Sequential key patterns — Timestamp-based or counter-based keys
- Performance-critical applications — When split overhead is unacceptable
Don’t use when:
- Unknown data patterns — Risk of uneven splits
- Sparse data — May create empty regions
- Dynamic workloads — When access patterns change over time
4. RegionServer Grouping
Label groups of RegionServers (e.g., "fast_group," "batch_group"). Then, assign specific tables to run only on servers within a chosen group.
Pros:
- Improves data locality for related tables/column families
- Reduces network overhead for operations spanning multiple regions
- Better resource utilization and cache efficiency
- Can improve performance for applications with predictable access patterns
Cons:
- Increases complexity of cluster management
- Can create new hotspots if grouping is poorly designed
- Makes load balancing more challenging
- Risk of cascading failures if a group becomes unavailable
- Harder to scale individual tables independently
Here I tried to cover as much as I could about the absolute basics of Hbase. Hope this articles makes the fundamentals clear for anyone who wants to start reading about Hbase.






