HBase Tutorial: Learn NoSQL Database from Scratch (2026)
HBase is the Hadoop-native wide-column NoSQL database modeled after Google Bigtable. I have used it to store billions of rows of event data, time-series metrics, and real-time counters. Unlike HDFS which excels at batch reads, HBase provides random, low-latency access to individual rows and row ranges. It sits on top of HDFS for storage but provides a completely different access pattern: key-based lookups and scans.
This tutorial covers the design decisions that make or break an HBase deployment: row key design, column family topology, region server architecture, and the compaction process.
Row Key Design: The Most Important Decision
Row key design is the single most impactful decision in HBase. Rows are stored in sorted order by key, so adjacent keys are physically colocated. A well-designed row key enables efficient scans; a poor one causes hotspotting where all writes hit one region server. The classic pattern is salting: prefix the natural key with a hash prefix (e.g., 00-99) to distribute writes.
Time-series keys are tricky. Using timestamp as the leading component creates a hot region. Use a hash of the entity ID as the prefix instead.
byte[] salt = Bytes.toBytes(String.format("%02d", Math.abs(md5Hash % 100)));
byte[] rowKey = Bytes.add(salt, userKey, Bytes.toBytes(timestamp));
Put put = new Put(rowKey);
put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("event"), Bytes.toBytes(eventJson));
table.put(put);
Column Families and Schema Design
Column families group related columns with shared storage properties — compression, block size, TTL. All columns in a family are stored together in HFiles. I recommend at most 2-3 column families per table. Each family has its own memstore and flushing behavior.
Qualifiers are dynamic in HBase — you can create new columns on the fly. However, the qualifier name is stored with every cell, so long names waste storage.
create 'events', {
NAME => 'm', VERSIONS => 1,
TTL => 86400000, COMPRESSION => 'LZO'
}, {
NAME => 'r', VERSIONS => 3,
TTL => 259200000, COMPRESSION => 'SNAPPY'
}
Region Servers and Splitting
HBase tables are split into regions, each served by one RegionServer. When a region grows beyond the split size (default 10 GB), it splits into two. During split, the parent region is unavailable for writes. Pre-splitting tables before loading data avoids this bottleneck.
RegionServer memory is the main constraint. I configure 40% of heap to block cache for read-heavy workloads, 40% to memstore for write-heavy.
create 'user_sessions', 's', 'm',
{SPLITS => (1..100).map { |i| format("%02d_", i - 1) }}
echo "status 'detailed'" | hbase shell
MemStore, HFile, and Flushing
Writes go to the MemStore (in-memory sorted buffer) and the Write-Ahead Log (WAL) on HDFS. When the MemStore reaches its flush size (128 MB default), it is written to disk as an HFile. Compaction merges small HFiles into larger files.
Blocked memstore flushing is the most common operational issue: if compaction cannot keep up, writes are blocked.
# Manage compaction:
compact 'user_sessions'
# Monitor flush pressure:
echo "status 'detailed'" | hbase shell | grep "flushQueueSize"
Filters and Scan Optimization
HBase scans iterate over rows in key order. Server-side filters — SingleColumnValueFilter, PrefixFilter — push predicate evaluation to the RegionServer. Set scan cache to 100-500 rows to batch results without excessive RPC.
Filters combined with startRow and stopRow achieve the best performance.
Scan scan = new Scan();
scan.setStartRow(Bytes.toBytes("00_"));
scan.setStopRow(Bytes.toBytes("01_"));
scan.setCaching(500);
SingleColumnValueFilter filter = new SingleColumnValueFilter(
Bytes.toBytes("m"), Bytes.toBytes("status"),
CompareOperator.EQUAL, Bytes.toBytes("ACTIVE"));
scan.setFilter(filter);
ResultScanner scanner = table.getScanner(scan);
Bulk Loading and Data Ingestion
HBase bulk load writes HFiles directly to HDFS without going through the write path. The process has two steps: generate HFiles from your data using HFileOutputFormat2, then load them with LoadIncrementalHFiles. This avoids all MemStore overhead and compaction.
I use bulk loading for initial data migrations and daily batch imports.
# Step 1: Generate HFiles
hadoop jar hbase-server-*.jar hfilebulkload /input/my_export /output/hfiles events cf
# Step 2: Complete bulk load
hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles /output/hfiles events
Frequently Asked Questions
When should I use HBase instead of Cassandra?
HBase is right when you need strong consistency, Hadoop integration, and HDFS-based storage. Choose Cassandra for multi-datacenter deployments with eventual consistency.
How does HBase handle row key hotspotting?
Use salting — prefix the natural key with a hash value distributed across 00-99 to spread writes across regions.
What happens when a RegionServer fails?
The Master detects the failure via ZooKeeper heartbeats, assigns the dead server regions to available RegionServers, and replays the WAL on HDFS.
How many versions should I keep per cell?
Keep 1-3 versions in most cases. Each version stores a full cell value, so retention above 3 significantly increases storage.
Originally published on Ayodhyyya. Last updated June 1, 2026.