Granules and the sparse primary index
Rows are grouped into 8192-row blocks called granules, and the primary index keeps one entry per granule — small enough to fit the whole table's index in RAM — so binary search picks the granule range in microseconds.
If rows inside a part are sorted, the engine doesn't need an index entry per row — it can keep one entry per group of 8192 rows and still find a range fast. That gives us an index small enough to keep entirely in memory.
Scene 09
Granules and the sparse primary index
- Watch
- Try it
- Predict
- Capture
A WHERE service='api' AND ts > T query enters. The primary.idx (in RAM) is binary-searched in O(log G) ≈ 7 hops; granules 42..47 survive and the rest stay dim. Watch the four stages.
Highlighted lines are the ones running in the diagram right now.
def build_primary_index(part):g = cfg.index_granularity # rows per granuleprimary_idx = []for start in range(0, part.rows, g):granule = part.rows_sorted[start : start + g]primary_idx.append(granule[0].sort_key)write_file('primary.idx', primary_idx)# finer g → more entries, fatter index in RAM# coarser g → fewer entries, but fatter granule reads
def plan_with_index(query):lo, hi = bisect_range(primary_idx, query.predicate_range,)surviving = list(range(lo, hi + 1))for gid in surviving:block = mark_file.locate(gid)rows = decompress_and_read(block)emit(vectorized_filter(rows, query))# other granules: not a byte of .bin is read
def primary_idx_bytes(part):g = cfg.index_granularityentries = part.rows // g # N_rows / granule_sizereturn entries * sizeof(sort_key_tuple)def rows_pulled_per_match(part):# one surviving granule still loads g rows;# vectorized filter discards the non-matches.return cfg.index_granularity
Where this sits in Build a columnar OLAP store (ClickHouse / Druid style)
Scene 09 of 13, in the Read side act — Sort key, sparse primary index, skip indexes — narrowing what to scan.. Rows are grouped into 8192-row granules; one index entry per granule keeps the whole table's index in RAM, binary-searched in microseconds.
Up next. The sparse index narrows a query to a granule range using the sort-key prefix. For predicates on OTHER columns — not in the sort key — we need a second kind of index that can prove 'this granule definitely doesn't contain X'.
All 13 scenes in Build a columnar OLAP store (ClickHouse / Druid style) · Every curriculum