Skip to Content
We are live but in Staging 🎉
TablesQuickstart

Quickstart

Five minutes from here you’ll have a real table, a few rows in it, and a working SQL query. Engine setup isn’t a step — every bucket gets an auto-enabled table engine on CreateBucket, so you go straight to CreateTable.

Prerequisites

  • dodil CLI installed and dodil login done — CLI Basics
  • A bucket — we’ll use kb-prod:
    dodil k3 bucket create kb-prod -d "Tables quickstart"

1. Create a table

We’ll model a small events table with a composite primary key and one JSON column for arbitrary payload:

dodil k3 table create events --bucket kb-prod \ --columns 'id:long:PK,user_id:string:PK,occurred_at:timestamp,event_type:string,payload:json' \ --partition-columns event_type

What that creates:

ColumnTypeNotes
idlongPrimary key (composite, alongside user_id)
user_idstringPrimary key (composite)
occurred_attimestampMicroseconds-since-epoch UTC
event_typestringPartition column — rows physically grouped by this
payloadjsonArbitrary JSON — queryable with DuckDB JSON operators

Column types: string · int · long · short · float · double · boolean · date · timestamp · binary · json. Full reference in SQL Compatibility.

Verify the table is ACTIVE:

dodil k3 table describe events --bucket kb-prod -o json

2. Insert rows

Bulk insert via the structured shortcut:

dodil k3 table insert events --bucket kb-prod \ --rows '[ {"id": 1, "user_id": "u-101", "occurred_at": 1716840000000000, "event_type": "click", "payload": {"page": "/pricing"}}, {"id": 2, "user_id": "u-101", "occurred_at": 1716840060000000, "event_type": "click", "payload": {"page": "/signup"}}, {"id": 3, "user_id": "u-102", "occurred_at": 1716840120000000, "event_type": "purchase", "payload": {"sku": "A-12", "amount": 49.99}} ]'

The response carries rows_written — counts of rows that landed in the table’s write log (the compactor will fold them into Delta on the next drain). For PK tables, writes go through the log and then into Delta; for no-PK tables, writes append directly to Delta.

Same INSERT via raw SQL through Execute:

dodil k3 table query --bucket kb-prod --sql " INSERT INTO events (id, user_id, occurred_at, event_type, payload) VALUES (1, 'u-101', TIMESTAMP '2026-05-27 10:00:00', 'click', '{\"page\":\"/pricing\"}'), (2, 'u-101', TIMESTAMP '2026-05-27 10:01:00', 'click', '{\"page\":\"/signup\"}'), (3, 'u-102', TIMESTAMP '2026-05-27 10:02:00', 'purchase', '{\"sku\":\"A-12\",\"amount\":49.99}') "

3. Query — eventual freshness (default)

dodil k3 table query --bucket kb-prod --sql " SELECT event_type, COUNT(*) AS n FROM events GROUP BY event_type ORDER BY n DESC "

This is the default — Freshness=EVENTUAL — scans Delta only. Fast, OLAP-optimized. The downside: rows you just inserted may not be visible until the next compaction folds them into Delta.

4. Query — strong freshness (read-your-writes)

When you need to see writes immediately:

dodil k3 table query --bucket kb-prod --sql " SELECT * FROM events WHERE user_id = 'u-101' ORDER BY occurred_at " --freshness strong

STRONG merges the write log with Delta on the read path. Pays a small cost vs EVENTUAL; use it sparingly on hot paths, freely for “did my write land?” checks.

Three freshness modes total: EVENTUAL (default, Delta-only), STRONG (write log + Delta), OLTP_ONLY (write log only — niche, mostly for point-lookups during heavy backlog). See SQL Compatibility.

5. Query a JSON column

JSON columns are first-class — DuckDB’s native JSON operators work directly:

dodil k3 table query --bucket kb-prod --sql " SELECT user_id, payload->>'sku' AS sku, (payload->>'amount')::double AS amount FROM events WHERE event_type = 'purchase' "

->> extracts a JSON field as text; cast to a typed value as needed. Other DuckDB JSON ops also work — ->, json_extract, json_each, etc.

6. Update + merge (the PK paths)

Update a single row via PK:

dodil k3 table update events --bucket kb-prod \ --predicate "id = 1 AND user_id = 'u-101'" \ --updates '{"event_type": "click_pricing"}'

Upsert a batch via MERGE:

dodil k3 table merge events --bucket kb-prod \ --match-columns id,user_id \ --rows '[ {"id": 1, "user_id": "u-101", "occurred_at": 1716840000000000, "event_type": "click_pricing", "payload": {"page": "/pricing", "variant": "B"}}, {"id": 4, "user_id": "u-103", "occurred_at": 1716840180000000, "event_type": "signup", "payload": {"plan": "pro"}} ]'

MERGE upserts — existing rows (by match_columns) are updated, new rows are inserted. The CLI shape mirrors the API.

For the full SQL shapes that drive these (UPDATE ... WHERE pk = X vs WHERE non_pk = X, MERGE INTO ... USING ... WHEN MATCHED ...), see SQL Compatibility.

7. Maintenance (optional)

After heavy writes, two quick housekeeping commands:

# Force the write log to drain into Delta now (otherwise it drains automatically) dodil k3 table compact events --bucket kb-prod # Compact small Delta files into larger ones (improves analytical query speed) dodil k3 table optimize events --bucket kb-prod

You usually don’t need to run these manually — K3 maintains the log → Delta drain in the background. They’re for after large bulk writes / nightly batches.

What you just did

StepOperationSurface
1CreateTable (manual mode)Tables · admin
2InsertData · structured shortcut
3Query (eventual)Data · structured shortcut
4Query (strong)Data — with read-your-writes
5Query (JSON ops)Data — DuckDB-native JSON
6Update + MergeData
7Compact + OptimizeMaintenance

Cleanup

dodil k3 table delete events --bucket kb-prod dodil k3 bucket delete kb-prod # only if you created it for this quickstart

Next steps