To query a JSON Lines file, use 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:
curl https://clickhouse.com/cli | sh # install clickhousectl
clickhousectl local use latest # download ClickHouse and put it on your PATH
Then query the file directly:
clickhouse local -q "SELECT * FROM file('events.jsonl') LIMIT 5"
2026-01-01 00:00:00 1 GB click 5 1
2026-01-01 01:00:00 2 US view 6.01 2
2026-01-01 02:00:00 3 DE purchase 7.02 3
2026-01-01 03:00:00 4 FR refund 8.03 4
2026-01-01 04:00:00 5 IN click 9.04 5
The .jsonl extension maps to ClickHouse's JSONEachRow format, which reads each line's keys as column names and infers the types from the values. The file is queried in place with no import step.
Prefer Python? See How to read a JSONL file in Python and query it with SQL for the same queries against a pandas DataFrame.
Each line is a self-describing JSON object, so you never write CREATE TABLE. DESCRIBE prints the column names taken from the keys and the types ClickHouse inferred from the values:
clickhouse local -q "DESCRIBE file('events.jsonl')"
event_time Nullable(DateTime)
user_id Nullable(Int64)
country Nullable(String)
event_type Nullable(String)
revenue Nullable(Float64)
quantity Nullable(Int64)
The keys gave the names; the values gave the types. Columns are Nullable because inference allows for missing keys: if a line omits a field, that row reads back as null instead of failing the parse.
Filter, group, and aggregate directly on the file
Because the file is a table, the full SQL surface works on it (WHERE, GROUP BY, aggregate functions, window functions, joins) with no load step in between:
clickhouse local -q "
SELECT
country,
count() AS events,
round(sum(revenue), 2) AS revenue,
round(avg(quantity), 2) AS avg_qty
FROM file('events.jsonl')
GROUP BY country
ORDER BY revenue DESC"
US 4 60.4 3.5
GB 4 56.36 2.5
AU 3 48.33 2
IN 3 45.3 2.67
FR 3 42.27 3.33
DE 3 39.24 4
This is the everyday workflow: open a terminal, run one query against the file, read the answer.
JSONL, NDJSON, and JSON Lines are the same format
These three names describe the same thing: one JSON object per line, newline-delimited. JSONL is the common file extension, NDJSON ("newline-delimited JSON") is the spec name, and "JSON Lines" is the human-readable label. ClickHouse reads all of them with JSONEachRow, and the .ndjson extension maps to it exactly like .jsonl:
clickhouse local -q "SELECT count() FROM file('events.ndjson')"
So if your tool emits .ndjson, query it the same way. (See what is NDJSON for the format background, and the NDJSON and JSONL versions of this guide, which are the same recipe under their own names.)
This is the one place JSON Lines trips people up versus a plain .json file. A .json file usually holds a single JSON value: one big array or one object spanning many lines. JSON Lines is the opposite: many independent objects, one per line, and no enclosing brackets or commas between them. That difference is why JSON Lines streams and appends cleanly (you can tail -f it or cat two files together) while a single JSON array does not. If your data is actually one array or one nested document, see run SQL on a JSON file and query nested JSON with SQL instead.
The .jsonl and .ndjson extensions are auto-detected. If your file ends in something else (.txt, .log, no extension at all) ClickHouse can't guess, so name the format as the second argument to file():
clickhouse local -q "SELECT count() FROM file('events.txt', JSONEachRow)"
JSONEachRow will also parse a file that is one top-level JSON array, treating each element as a row. So an API response saved as [{...},{...}] queries the same way, no reshaping first:
clickhouse local -q "SELECT * FROM file('array.json', JSONEachRow)"
One format reads both the line-delimited form and the single-array form, which covers most JSON you'll be handed.
You don't need to unzip anything first. clickhouse local detects the .gz extension and decompresses on the fly, so a .jsonl.gz file is queried exactly like a .jsonl:
clickhouse local -q "SELECT country, count() FROM file('events.jsonl.gz') GROUP BY country ORDER BY country"
AU 3
DE 3
FR 3
GB 4
IN 3
US 4
The same applies to .jsonl.zst, .jsonl.xz, and other supported codecs. The compression is inferred from the file name, which matters for JSON Lines specifically because it is verbose text and compresses well, so log dumps usually arrive gzipped.
Small files are instant in anything. The difference shows up at scale. On a 3,000,000-row, ~360 MB JSON Lines file (events_large.jsonl), a full GROUP BY country with sum and avg over every row completes in:
clickhouse local -q "
SELECT country, count() AS events, round(sum(revenue), 2) AS revenue, round(avg(quantity), 3) AS avg_qty
FROM file('events_large.jsonl')
GROUP BY country
ORDER BY revenue DESC"
~0.52 seconds, best of three with a warm OS page cache, on an Apple M4 Pro laptop (14 cores, 24 GB RAM; clickhouse local 26.6.1.117). That includes parsing the JSON text from scratch on every run; there's no cached table. JSON Lines is heavier to parse than a columnar format, so if you'll query the same data repeatedly, convert it once to Parquet with convert JSONL to Parquet and the scans get faster again.
clickhouse local runs the same SQL unchanged across dozens of file formats and remote sources.
The query you just ran on a laptop file is the same SQL you would run on a ClickHouse server, or in ClickHouse Cloud. Nothing about SELECT ... WHERE ... GROUP BY changes. You swap file('events.jsonl') for a table name and the rest stays put. You prototype against a file on your machine and ship the identical logic to production.
The complete, runnable example lives here. It has generate.sh (builds the demo file, a gzipped copy, and the ~360 MB perf file), run.sh (every command above), and expected_output.txt:
github.com/ClickHouse/examples → local-analytics/clickhouse-local-json-lines
git clone https://github.com/ClickHouse/examples
cd examples/local-analytics/clickhouse-local-json-lines
./generate.sh && ./run.sh