---
title: "How to read a RowBinary file"
date: "2026-06-15"
author: "Al Brown"
category: "Engineering"
excerpt: "Read a ClickHouse RowBinary file from your terminal with one command, using clickhouse local. RowBinaryWithNamesAndTypes is self-describing; plain RowBinary needs an explicit structure."
---

# How to read a RowBinary file

To read a RowBinary file, use [`clickhouse local`](/resources/engineering/what-is-clickhouse-local). It runs SQL directly on files from the command line, with no server to install. It's part of ClickHouse, so the same query scales to billions of rows when you outgrow your laptop.

Install it with [`clickhousectl`](https://clickhouse.com/docs/install):


```bash
curl https://clickhouse.com/cli | sh   # install clickhousectl
clickhousectl local use latest         # download ClickHouse and put it on your PATH
```

Then point it at the file and name the format:


```bash
clickhouse local -q "SELECT * FROM file('events.rowbinary', 'RowBinaryWithNamesAndTypes') LIMIT 10"
```

```response
   ┌──────────event_time─┬─user_id─┬─country─┬─event_type─┬─revenue─┬─quantity─┐
1. │ 2026-01-01 00:00:00 │       1 │ GB      │ click      │       5 │        1 │
2. │ 2026-01-01 00:02:17 │       2 │ US      │ view       │    6.01 │        2 │
3. │ 2026-01-01 00:04:34 │       3 │ DE      │ purchase   │    7.02 │        3 │
   └─────────────────────┴─────────┴─────────┴────────────┴─────────┴──────────┘
```

`RowBinaryWithNamesAndTypes` embeds column names and types in a header, so the file reads itself with no schema declaration and no import step. Plain `RowBinary` carries only raw values and needs an explicit structure; both variants are covered below.

## See the schema without declaring one

`RowBinary` is a ClickHouse-native format, so it shows up most often when something already in the ClickHouse world wrote it: an `INSERT ... FORMAT RowBinary`, a client library, or a `SELECT ... FORMAT RowBinaryWithNamesAndTypes` dump. The `WithNamesAndTypes` variant prefixes the data with a header listing every column name and type. That header is the schema, so `DESCRIBE` reads it back without a `CREATE TABLE`:


```bash
clickhouse local -q "DESCRIBE file('events.rowbinary', 'RowBinaryWithNamesAndTypes') FORMAT PrettyCompact"
```

```response
   ┌─name───────┬─type─────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
1. │ event_time │ DateTime │              │                    │         │                  │                │
2. │ user_id    │ UInt32   │              │                    │         │                  │                │
3. │ country    │ String   │              │                    │         │                  │                │
4. │ event_type │ String   │              │                    │         │                  │                │
5. │ revenue    │ Float64  │              │                    │         │                  │                │
6. │ quantity   │ UInt8    │              │                    │         │                  │                │
   └────────────┴──────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘
```

Names and exact types come straight from the embedded header. `DateTime` stays `DateTime`, `UInt32` stays `UInt32`. Unlike text formats, nothing is inferred or guessed, and nothing comes back `Nullable` unless the writer declared it so.

## Filter, aggregate, and group by

The file is a table, so the full SQL surface applies (`WHERE`, `GROUP BY`, aggregates, joins, window functions) with no load step in between:


```bash
clickhouse local -q "
SELECT country, count() AS events, round(sum(revenue),2) AS revenue, round(avg(quantity),2) AS avg_qty
FROM file('events.rowbinary', 'RowBinaryWithNamesAndTypes')
GROUP BY country
ORDER BY revenue DESC
FORMAT PrettyCompact"
```

```response
   ┌─country─┬─events─┬─revenue─┬─avg_qty─┐
1. │ US      │      4 │    60.4 │     3.5 │
2. │ GB      │      4 │   56.36 │     2.5 │
3. │ AU      │      3 │   48.33 │       2 │
4. │ IN      │      3 │    45.3 │    2.67 │
5. │ FR      │      3 │   42.27 │    3.33 │
6. │ DE      │      3 │   39.24 │       4 │
   └─────────┴────────┴─────────┴─────────┘
```

## Plain RowBinary has no header

This is the part that trips people up. Plain `RowBinary` is just the column values, packed end to end with nothing describing them. There is no header to read, so schema inference is impossible by design and the command fails fast rather than guessing:


```bash
clickhouse local -q "SELECT * FROM file('events_plain.rowbinary', 'RowBinary') LIMIT 1"
```

```response
Code: 36. DB::Exception: RowBinary file format doesn't support schema inference.
You must specify the structure manually. (BAD_ARGUMENTS)
```

The fix is to pass the structure yourself, as the third argument to `file()`. The column order and types must match exactly what was written, byte for byte:


```bash
clickhouse local -q "
SELECT * FROM file('events_plain.rowbinary', 'RowBinary',
  'event_time DateTime, user_id UInt32, country String, event_type String, revenue Float64, quantity UInt8')
LIMIT 3 FORMAT PrettyCompact"
```

```response
   ┌──────────event_time─┬─user_id─┬─country─┬─event_type─┬─revenue─┬─quantity─┐
1. │ 2026-01-01 00:00:00 │       1 │ GB      │ click      │       5 │        1 │
2. │ 2026-01-01 00:02:17 │       2 │ US      │ view       │    6.01 │        2 │
3. │ 2026-01-01 00:04:34 │       3 │ DE      │ purchase   │    7.02 │        3 │
   └─────────────────────┴─────────┴─────────┴────────────┴─────────┴──────────┘
```

Same rows, same result. The only difference is that you supplied the schema instead of the file.

## Why the self-describing variant is worth the bytes

A plain `RowBinary` reader trusts whatever structure you type. Get a type wrong and there's no header to contradict you, so ClickHouse reinterprets the bytes and hands back garbage rather than an error. The `WithNamesAndTypes` header closes that gap: if the structure you pass disagrees with the embedded one, the read is rejected up front.


```bash
clickhouse local -q "
SELECT * FROM file('events.rowbinary', 'RowBinaryWithNamesAndTypes',
  'event_time DateTime, user_id String, country String, event_type String, revenue Float64, quantity UInt8')
LIMIT 1"
```

```response
Code: 117. DB::Exception: Type of 'user_id' must be String, not UInt32:
(while reading header) ... (INCORRECT_DATA)
```

The header caught the mismatch (`user_id` is `UInt32`, not `String`) before reading a single data row. If you control the writer, prefer `RowBinaryWithNamesAndTypes`. It costs a few bytes of header and buys you a file that reads itself and validates on the way in. Reach for plain `RowBinary` only for the leanest machine-to-machine pipes where both ends already agree on the schema.

## How fast is it?

`RowBinary` is a fixed-layout binary format, so there is no text to parse and no types to infer per row. On a 3,000,000-row file (`events_large.rowbinary`, ~80 MB, generated by the example folder below), a full `GROUP BY country` with `sum` and `avg` over every row runs in:


```bash
clickhouse local -q "
SELECT country, count(), round(sum(revenue),2), round(avg(quantity),3)
FROM file('events_large.rowbinary','RowBinaryWithNamesAndTypes')
GROUP BY country ORDER BY 2 DESC
FORMAT Null"
```

**~0.26 seconds**, best of three with the file warm in the OS page cache, on an Apple M4 Pro laptop (14 cores, 24 GB RAM; clickhouse local 26.6.1.117). `RowBinary` is row-oriented, so this reads every column off disk even when a query touches only a few. For repeated analytical scans, convert it once to a columnar format like [Parquet](/resources/engineering/how-to-query-parquet-file) and read that instead.

## The same SQL scales unchanged

The query you just ran on a laptop file is the same SQL you would run against a ClickHouse server or [ClickHouse Cloud](https://clickhouse.com/cloud). Swap `file('events.rowbinary', 'RowBinaryWithNamesAndTypes')` for a table name and the `SELECT ... GROUP BY` stays put. `RowBinary` is in fact the wire format the ClickHouse client and many drivers use to ship rows, so reading it locally and querying a server are the same machinery, end to end. There is no separate local dialect to unlearn.

## Run it yourself

The complete, runnable example lives here. It has `generate.sh` (builds the self-describing file, a plain-`RowBinary` copy, and the 3M-row perf file), `run.sh` (every command above), and `expected_output.txt`:

**[github.com/ClickHouse/examples → local-analytics/clickhouse-local-rowbinary](https://github.com/ClickHouse/examples/tree/main/local-analytics/clickhouse-local-rowbinary)**


```bash
git clone https://github.com/ClickHouse/examples
cd examples/local-analytics/clickhouse-local-rowbinary
./generate.sh && ./run.sh
```

## Related

- [What is clickhouse-local?](/resources/engineering/what-is-clickhouse-local)
- [Read a ClickHouse Native file](/resources/engineering/read-clickhouse-native-file)
- [How to query a Parquet file from the command line](/resources/engineering/how-to-query-parquet-file)
