Unlocking RocksDB Column Families: Logical Separation, Tuning, and Practical Trade‑offs
RocksDB Column Families let you keep multiple logical datasets in one engine while tuning each family’s compression, cache, and memtable settings. This guide shows how to create, use, and monitor CFs, plus the trade‑offs you should watch for.
31 Jul 2026, 20:33 UTC

Why Column Families Matter
When a single RocksDB instance needs to serve multiple logical datasets—think a multi‑tenant application or a service that stores logs, metrics, and user profiles in one engine—Column Families (CFs) are the tool that keeps them distinct while still sharing the same underlying storage engine.
CFs let you:
- Assign different compression or block‑size settings to each dataset.
- Provide independent snapshot isolation across families.
- Keep a single DB handle while targeting specific namespaces.
Creating and Using Column Families
Below is a minimal C++ example that demonstrates:
- Opening a RocksDB instance with a default CF and two custom CFs.
- Writing distinct key/value pairs into each family.
- Reading them back to confirm isolation.
- Enabling statistics to observe per‑family metrics.
Compile with:
g++ -std=c++17 -lrocksdb -o cf_example cf_example.cpp
Run with:
./cf_example /tmp/rocksdb_cf_demo
Here is the source:
#include <rocksdb/db.h>
#include <rocksdb/options.h>
#include <rocksdb/slice.h>
#include <rocksdb/utilities/options_util.h>
#include <iostream>
using namespace rocksdb;
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "Usage: <executable> <db_path>\n";
return 1;
}
std::string db_path = argv[1];
// 1. Define options for the default family and custom families.
DBOptions db_opts;
db_opts.create_if_missing = true;
db_opts.create_missing_column_families = true;
db_opts.statistics = rocksdb::CreateDBStatistics();
// Custom options for CF "metrics".
ColumnFamilyOptions metrics_cf_opts;
metrics_cf_opts.compression = kSnappyCompression;
metrics_cf_opts.write_buffer_size = 64 * 1024 * 1024; // 64 MB
// Custom options for CF "logs".
ColumnFamilyOptions logs_cf_opts;
logs_cf_opts.compression = kNoCompression;
logs_cf_opts.write_buffer_size = 128 * 1024 * 1024; // 128 MB
// 2. Prepare CF descriptors.
std::vector<ColumnFamilyDescriptor> cf_descriptors;
cf_descriptors.emplace_back(kDefaultColumnFamilyName, ColumnFamilyOptions()); // default
cf_descriptors.emplace_back("metrics", metrics_cf_opts);
cf_descriptors.emplace_back("logs", logs_cf_opts);
// 3. Open the DB.
DB* db = nullptr;
std::vector<ColumnFamilyHandle*> cf_handles;
Status s = DB::Open(db_opts, db_path, cf_descriptors, &cf_handles, &db);
if (!s.ok()) {
std::cerr << "Open failed: " << s.ToString() << "\n";
return 1;
}
// 4. Write to each family.
WriteOptions write_opts;
s = db->Put(write_opts, cf_handles[1], "cpu", "23%" ); // metrics
s &= db->Put(write_opts, cf_handles[2], "app_start", "2026-10-13T17:00:00Z"); // logs
// 5. Read back.
std::string value;
s = db->Get(ReadOptions(), cf_handles[1], "cpu", &value);
std::cout << "metrics: cpu=" << value << "\n";
s = db->Get(ReadOptions(), cf_handles[2], "app_start", &value);
std::cout << "logs: app_start=" << value << "\n";
// 6. Print per‑family statistics.
std::string stats = db->GetProperty("rocksdb.stats");
std::cout << "\nRocksDB statistics:\n" << stats << "\n";
// 7. Clean up.
for (auto* h : cf_handles) db->DestroyColumnFamilyHandle(h);
delete db;
return 0;
}
Key Takeaways from the Example
- CF handles are required for every read/write; the default family is
cf_handles[0]. - Per‑family options (compression, write buffer size) are set via
ColumnFamilyOptions. - Statistics expose metrics that are aggregated across families, but you can filter by prefix (e.g.,
rocksdb.metrics.block-cache-usage).
Trade‑offs and Practical Limits
While CFs provide great logical separation, they introduce subtle costs:
- Metadata overhead: Each family adds a small amount of file metadata and an entry in the
column_family_namestable. - Write amplification: All writes go through the same WAL. If you have many small writes across families, the WAL can become a bottleneck.
- Memory footprint: Each family has its own memtable and block cache slices. If you allocate too much per family, you may exhaust RAM or starve other families.
To mitigate, monitor rocksdb.memtable-usage for each family and tune write_buffer_size and max_write_buffer_number accordingly. Also consider using rocksdb.statistics to log per‑family write amplification.
When to Use Column Families
- Multi‑tenant services where each tenant’s data can live in its own CF, allowing per‑tenant compression or cache settings.
- Applications that need to keep hot data (e.g., metrics) separate from cold data (e.g., logs) without spinning up multiple DB instances.
- Systems that require atomic snapshots across several logical namespaces.
Actionable Checklist
- Define a
ColumnFamilyDescriptorfor each logical group. - Set per‑family
ColumnFamilyOptions(compression, block size, write buffer). - Open the DB with
create_missing_column_families = trueto auto‑create missing families. - Always store the returned
ColumnFamilyHandleand use it for all operations. - Enable
DBOptions::statisticsand periodically inspect metrics likerocksdb.memtable-usageandrocksdb.write-amplification. - When scaling, consider dropping unused families with
DB::DropColumnFamilyand re‑balancing write buffers.
By following these steps, you can harness RocksDB’s column families to keep data logically isolated, finely tuned, and efficiently managed—all within a single database handle.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.