csvql

Read-only SQL on CSV files, for AI agents

Query CSV files where they already live. About as fast as DuckDB and polars on raw CSV, with 3 to 11× less memory. MCP-native, confined to a directory you choose. One static Zig binary. Your data never leaves your machine.

Why csvql

A database is something you load your data into. csvql is a query you run on the data where it already lives.

Read-only

SELECT only. No INSERT/UPDATE/DELETE/DROP exists in the parser. It cannot modify your data.

Sandboxed by a flag

--root <dir> confines every file the process can open. Paths outside it, including ../ traversal, fail with PathOutsideAllowedRoot. Give an agent a directory, not a filesystem.

MCP-native

Ships as an MCP server, wired up by csvql install. Results are capped at about 100 rows / 12 KB, so a careless query can't flood the context or pull a table out one call at a time.

3 to 11× less memory

A GROUP BY on an 8 GB file: 74 MB for csvql, 367 for DuckDB, 1,754 for polars. Counting 5 million distinct values in 11 GB: 235 MB against 2,639 for DuckDB. Fits on the small VM next to your data.

Zero ingest, zero network

Queries the raw CSV in place via mmap. No import step, no native store to build first. No cloud, no telemetry, air-gappable, with --audit logging.

One static binary

Written in Zig. No runtime, no dependencies. macOS, Linux, Windows. Also on npm, PyPI and Homebrew.

Same speed band, a fraction of the memory

Same GROUP BY department on raw CSV, one process per engine, Apple M2 Pro, 16 GB. Peak memory and wall time:

FilecsvqlDuckDBpolars 2.0
0.69 GB74 MB / 0.2s254 MB / 0.8s1151 MB / 0.6s
1.89 GB74 MB / 0.4s253 MB / 1.2s2107 MB / 1.1s
3.80 GB75 MB / 0.7s275 MB / 2.1s2666 MB / 9.8s
8.14 GB74 MB / 2.4s367 MB / 5.5s1754 MB / 26.7s

Memory is flat because an aggregate holds an accumulator, not a dataset. Speed is in the same band as DuckDB and polars: ahead on scans, filters, small files and joins against raw CSV, level with polars on COUNT(*) and top-N, behind DuckDB where it has to hold a lot of state.

Where DuckDB wins, on an 11.15 GB CSV with 75 million rows and a column of 5 million distinct values (DuckDB 1.4.2, best-of-3):

QuerycsvqlDuckDBMemory
COUNT(*) WHERE country='JP'1.95s4.34s53 MB vs 268 MB
COUNT(DISTINCT user_id) (5M)4.8s5.7s235 MB vs 2,639 MB
SELECT DISTINCT user_id (5M)9.0s7.0s262 MB vs 2,724 MB
GROUP BY user_id (5M groups)16.2s8.3s1,502 MB vs 2,894 MB

On high-cardinality grouping DuckDB is about 2× ahead, at roughly twice the memory. MEDIAN is a 5× loss. If memory is free, DuckDB is an excellent default. If you are running next to the data on a small box, or handing a directory-confined tool to an agent, the memory column is the reason csvql exists.

Every number has a reproduce script. Full tables, including joins, quoted CSV, wide files and the losses: BENCHMARKS.md. The run that produced the second table also found a real bug: Five ways to find distinct values in a 10 GB CSV.

Giving an agent a directory, not a filesystem

--root confines every file the process can open, so an agent with shell access to csvql still cannot read outside the directory you named.

# allowed
csvql --root /data "SELECT COUNT(*) FROM '/data/events.csv'"
2000000

# blocked, including ../ traversal
csvql --root /data "SELECT COUNT(*) FROM '/etc/passwd'"
execution error: error.PathOutsideAllowedRoot

Combined with SELECT-only execution, no network calls and --audit logging, that is the whole trust story: the tool cannot modify your data, cannot phone home, and cannot read outside its root.

Token cost: five questions about a taxi dataset cost about 640 tokens over MCP, tool schema included, and that does not change with file size. Pasting the 417 MB file would cost about 230 million tokens, so pasting was never an option. An agent that already shells out to duckdb -csv uses slightly fewer tokens than csvql over MCP, because MCP returns JSON. The case for csvql is the paragraph above, not the token count.

Install

# Homebrew (macOS / Linux)
brew install melihbirim/csvql/csvql

# Query a CSV
csvql "SELECT cab_type, COUNT(*) FROM 'trips.csv' GROUP BY cab_type"

# Wire it into Claude (Code + Desktop), no manual config
csvql install

Prebuilt binaries for macOS, Linux and Windows, and one-click .mcpb bundles for Claude Desktop, on the releases page. Also: npm i csvql-query, pip install csvql-query.