Most databases speak one wire format and one bulk-load format. Every query in and out of ClickHouse — INSERT, SELECT, even the results printed to your terminal — passes through a pluggable format, and ClickHouse supports dozens of them. Picking the right one for the right job is a real performance decision, not just a convenience.
The formats you actually reach for
- JSONEachRow — one JSON object per line, no surrounding array or commas. The default choice for application-side inserts, because almost every language can produce it without a ClickHouse-specific library.
- CSV / TSV — plain delimited text. Universally producible, but the slowest to parse and the worst at preserving types (everything round-trips through text).
- Native — ClickHouse's own binary, columnar wire format. This is what
clickhouse-clientand server-to-server replication use by default, and it's the fastest option because data is already shaped the way the engine stores it — no row-to-column transposition needed on the way in. - RowBinary — binary, but row-oriented: values in declaration order, no field names, no delimiters. Faster than CSV to parse, more compact, but you must get column order exactly right since there's nothing self-describing about it.
- Parquet / Arrow — columnar formats from the wider data-lake ecosystem. ClickHouse reads and writes both natively, which makes it easy to sit next to Spark, Pandas, or a data lake on S3 without a separate conversion step.
Using a format
Every format is invoked the same way: a FORMAT clause on the query.
-- clickhouse-client
INSERT INTO events FORMAT JSONEachRow
{"user_id": 102, "event": "click", "ts": "2026-08-05 10:01:00"}
{"user_id": 204, "event": "view", "ts": "2026-08-05 10:02:00"}
SELECT * FROM events LIMIT 5 FORMAT Pretty;The same thing over the HTTP interface (port 8123, covered in Docker & Setup) is just a POST body:
curl 'http://localhost:8123/?query=INSERT+INTO+events+FORMAT+JSONEachRow' \
--data-binary '
{"user_id": 102, "event": "click", "ts": "2026-08-05 10:01:00"}
{"user_id": 204, "event": "view", "ts": "2026-08-05 10:02:00"}
'Reading files directly, without an INSERT
Table functions like file(), s3(), and url() let a format double as a way to query external data as if it were a table, no loading step required:
SELECT count()
FROM s3('https://bucket.s3.amazonaws.com/events/*.parquet', 'Parquet');
INSERT INTO events
SELECT * FROM file('/data/backfill.csv', 'CSV');These table functions are the query-time counterpart to the integration engines (S3, MySQL, PostgreSQL, URL, File) — a table function reads once for a single query; an engine defines a permanent table you can query repeatedly.
Native or Parquet over CSV or JSONEachRow whenever the source can produce them — parsing text and inferring/validating types row by row is frequently the actual bottleneck in a large one-time load, not disk or network.RowBinary has no field names and no self-description — if your INSERT column list and the binary layout don't match exactly, you silently get garbage values in the wrong columns rather than an error. Reserve it for pipelines where both ends are code you control.