Writes must be bulk, not per-row — per-column insert overhead and async inserts
Every INSERT touches every column file once, so per-row inserts on a 30-column table pay 30x the per-column overhead and the engine drowns; the only sustainable write pattern is large batches.
Vectorized execution wants thousands of values per call. That same rule applies to writes — they have to arrive in batches, not one row at a time, or the per-column overhead crushes the engine.
Scene 05
Writes must be bulk, not per-row
- Watch
- Try it
- Predict
- Capture
1000 rows/sec arrive at the engine. Watch the client-side buffer fill to 1000 rows, then flush as one batched INSERT — the engine pays a single 30-column fan-out for the whole batch, not 30 fan-outs per row.
Highlighted lines are the ones running in the diagram right now.
def insertPerRow(rows):for row in rows: # N iterationsfor col in table.columns: # 30 columnsf = open(part_dir / f'{col}.bin')f.write(codec.encode(row[col]))f.sync()f.close()registerPart(part_dir) # 1 part per row# cost = N rows * 30 columns = 30N file ops
def insertBulk(batch):part_dir = newPartDir()for col in table.columns: # 30 columns, ONCEf = open(part_dir / f'{col}.bin')for row in batch: # N rows, hot loopf.write(codec.encode(row[col]))f.sync()f.close()registerPart(part_dir) # 1 part per batch# cost = 30 file ops, regardless of batch size
def onInsert(query, rows):if not cfg.async_insert:return insertBulk(rows) # synchronous pathbuf = buffers[shape(query)] # one buf per query shapebuf.append(rows)if (buf.bytes >= async_insert_max_data_size # ~1 MBor buf.age_ms >= async_insert_busy_timeout_ms # ~200 msor buf.queries >= async_insert_max_query_number):insertBulk(buf.drain()) # one fan-out per FLUSHif cfg.wait_for_async_insert:return waitForFlush(buf) # durable, back-pressuresreturn Ack() # fire-and-forget
Where this sits in Build a columnar OLAP store (ClickHouse / Druid style)
Scene 05 of 13, in the Write side act — Bulk inserts → immutable parts → background merge → too-many-parts cliff.. Every INSERT touches every column file; per-row inserts on a 30-column table pay 30× the per-column overhead. Batches or async_insert.
Up next. If every batch becomes its own self-contained file on disk, we need a name and a shape for that unit — because everything operational from here on out is about how many of these units a query has to touch.
All 13 scenes in Build a columnar OLAP store (ClickHouse / Druid style) · Every curriculum