Skip to main content

Column Families

Column families let you keep different kinds of data in one database, each in its own sorted key space, and still change them together. A Raft log keeps its entries in one column family and its metadata in another, and one write batch updates both atomically. If you know SQLite, a column family is like a separate table in one database file.

Each column family has its own memtables, its own SSTables, and its own options. All column families of a database share one WAL and one sequence of sequence numbers. That is what makes a batch across column families atomic.

When to use a column family​

Use a separate column family when a group of keys:

  • has a different write pattern, for example an append-only log beside small metadata rows;
  • needs different options, for example a larger memtable or compression;
  • must change atomically with other keys, but must not mix with them in the same key order.

Use key prefixes in one column family when the keys have the same pattern and the same options. A prefix costs nothing, and an iterator with an upper bound reads one prefix only.

Kommander uses both: the column families default, metadata, and shard0 to shard7, and inside each shard, a key prefix for each partition.

Create column families at open​

You create column families only when you open the database:

using SolariumDb db = SolariumDb.Open("./data",
new DbOptions { CreateMissingColumnFamilies = true },
columnFamilies: ["metadata", "log"]);

The rules:

  • The default column family always exists. Its name is ColumnFamily.DefaultName.
  • Every column family that exists already opens, even when columnFamilies does not name it.
  • A name in columnFamilies that does not exist is created when CreateMissingColumnFamilies is true. When it is false, the default, Open fails.
  • There is no call to create or drop a column family on an open database.

Use a column family​

ColumnFamily log = db.GetColumnFamily("log");

db.Put("0001"u8, "entry"u8, log);
byte[]? value = db.Get("0001"u8, log);

using Iterator iterator = db.NewIterator(log);

db.ColumnFamilies lists every column family, the default one included. db.DefaultColumnFamily is the default one. A method that takes no ColumnFamily uses the default column family.

Give each column family its own options​

DbOptions.DefaultColumnFamilyOptions applies to every column family that DbOptions.ColumnFamilyOptions does not name:

DbOptions options = new()
{
CreateMissingColumnFamilies = true,
DefaultColumnFamilyOptions = new ColumnFamilyOptions
{
WriteBufferSize = 16L * 1024 * 1024,
},
ColumnFamilyOptions = new Dictionary<string, ColumnFamilyOptions>
{
["log"] = new ColumnFamilyOptions
{
WriteBufferSize = 128L * 1024 * 1024,
MaxWriteBufferNumber = 3,
Level0FileNumCompactionTrigger = 8,
Level0SlowdownWritesTrigger = 28,
Level0StopWritesTrigger = 44,
},
["values"] = new ColumnFamilyOptions
{
Compression = CompressionType.Lz4,
},
},
};

using SolariumDb db = SolariumDb.Open("./data", options, ["log", "values"]);

Options are records with init properties. You set them each time you open the database. They are not stored in the directory, so you can change them between opens. Options lists each option with its default, and Tuning explains when to change it.

Atomic batches across column families​

ColumnFamily log = db.GetColumnFamily("log");
ColumnFamily metadata = db.GetColumnFamily("metadata");

db.Write(new WriteBatch()
.Put("0000000042"u8, entry, log)
.Put("last-index"u8, "0000000042"u8, metadata),
WriteOptions.Synced);

After a crash, the database holds both puts or neither of them. The shared WAL makes this possible: the batch is one WAL record.

What column families share​

Shared by all column familiesSeparate for each column family
The WAL filesThe memtables
The sequence numbersThe SSTables and their levels
The write stalls: one column family past a trigger slows the writes to all of themThe options
The flush thread and the compaction threadsThe flush point in the manifest

A column family with few writes can keep old WAL files alive, because its memtable still needs them. DbOptions.MaxTotalWalSize stops this: when the live WAL files are larger than this size, the column families that keep the oldest WAL file alive are flushed.