How to convert TSV to CSV

Al Brown
Last updated: Jun 8, 2026

To convert a TSV file to CSV, 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:

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

Then write the result out with the CSV format:

1clickhouse local -q "SELECT * FROM file('events.tsv') INTO OUTFILE 'events.csv' TRUNCATE FORMAT CSVWithNames"
1"event_date","event_id","country","action","amount","qty"
2"2026-01-01",1,"GB","click",5,1
3"2026-01-02",2,"US","view",6.01,2
4"2026-01-03",3,"DE","signup",7.02,3
5"2026-01-04",4,"FR","purchase",8.03,4
6"2026-01-05",5,"IN","click",9.04,5

file('events.tsv') is read in place with no import step: ClickHouse picks TSVWithNames from the extension, takes column names from the first row, and infers types from the data. INTO OUTFILE ... FORMAT CSVWithNames writes those same rows back as comma-separated values with a header, quoting string fields and leaving numbers bare.

The types survive the round trip #

This is the difference between a real conversion and a find-and-replace of tabs with commas. ClickHouse parses the TSV into typed columns, so it knows amount is a float and event_date is a date. DESCRIBE the output file to see the schema it carried across:

1clickhouse local -q "DESCRIBE file('events.csv')"
1event_date	Nullable(Date)
2event_id	Nullable(Int64)
3country	Nullable(String)
4action	Nullable(String)
5amount	Nullable(Float64)
6qty	Nullable(Int64)

A textual tab-to-comma swap cannot do this, and it breaks the moment a field contains a comma: that field needs quoting in CSV, and ClickHouse quotes it for you. Both formats are described in what is a TSV file.

Options that matter #

This is the information you don't get from an online "TSV to CSV" converter, which uploads your file, gives you one fixed output, and caps the size.

CSVWithNames writes the header row. If the consumer expects a bare CSV with no header, use the plain CSV format instead:

1clickhouse local -q "SELECT * FROM file('events.tsv') INTO OUTFILE 'events_noheader.csv' TRUNCATE FORMAT CSV"
1"2026-01-01",1,"GB","click",5,1
2"2026-01-02",2,"US","view",6.01,2
3"2026-01-03",3,"DE","signup",7.02,3

If your input TSV has no header, read it with the plain TSV format and pass a schema, for example file('events.tsv', 'TSV', 'event_date Date, ...').

Because the source is a SQL table, you can also select, rename, cast, or filter columns in the same command instead of doing a second pass:

1clickhouse local -q "
2SELECT
3  event_date,
4  upper(country)        AS country_code,
5  amount::Decimal(10,2) AS amount
6FROM file('events.tsv')
7INTO OUTFILE 'events_clean.csv' TRUNCATE FORMAT CSVWithNames"
1"event_date","country_code","amount"
2"2026-01-01","GB",5
3"2026-01-02","US",6.01
4"2026-01-03","DE",7.02

For a semicolon rather than a comma at the destination, set format_csv_delimiter and keep everything else the same: ... FORMAT CSVWithNames SETTINGS format_csv_delimiter=';'.

One thing to know: this is a row-for-row conversion of flat columns. If your TSV holds a column of raw JSON or another nested structure, it stays as a string in the CSV. CSV has no nested types, so there is nothing lost beyond what CSV itself can't represent.

Did anything get dropped? #

Count both files. They match, which is the quick check that the conversion was clean:

1clickhouse local -q "SELECT count() FROM file('events.tsv')"   # 20
2clickhouse local -q "SELECT count() FROM file('events.csv')"   # 20

Reverse direction #

Going the other way is the same command with the formats swapped. See convert CSV to TSV. If you'll query the data repeatedly rather than hand it off, skip CSV and go to a columnar format with convert TSV to Parquet instead — it's smaller and far faster to scan.

How fast is it? #

On a 3,000,000-row, ~106 MB tab-separated file (events_large.tsv), the full TSV-to-CSV conversion runs in:

1clickhouse local -q "SELECT * FROM file('events_large.tsv') INTO OUTFILE 'events_large.csv' TRUNCATE FORMAT CSVWithNames"

~0.23 seconds, best of three with a warm OS page cache, on an Apple M4 Pro laptop (14 cores, 24 GB RAM). That parses every row and re-serialises it; there is no precomputed table. Because it streams, the file does not have to fit in memory, so the same command works on a multi-gigabyte TSV that an upload-based converter would reject.

Convert in Python with chDB #

If you're already in Python, chDB is the same ClickHouse engine in-process. The conversion is the identical SQL:

1import chdb
2
3chdb.query(
4    "SELECT * FROM file('events.tsv') "
5    "INTO OUTFILE 'events_chdb.csv' TRUNCATE FORMAT CSVWithNames"
6)
1"event_date","event_id","country","action","amount","qty"
2"2026-01-01",1,"GB","click",5,1
3"2026-01-02",2,"US","view",6.01,2
4"2026-01-03",3,"DE","signup",7.02,3

Install with pip install chdb. No server, no separate ClickHouse install.

Run it yourself #

The complete, runnable example lives in the ClickHouse examples repo: generate.sh to create the sample TSVs (including the ~106 MB file used for the timing), run.sh with every command on this page, run.py / run.ipynb for the chDB version, and expected_output.txt.

github.com/ClickHouse/examples/tree/main/local-analytics/convert-tsv-to-csv

clickhouse-local runs the exact same SQL unchanged across dozens of formats and remote sources, and against a ClickHouse server or ClickHouse Cloud when the data outgrows your machine. Related: query a TSV file with SQL and run SQL on a CSV file.

Share this resource

Subscribe to our newsletter

Stay informed on feature releases, product roadmap, support, and cloud offerings!
Loading form...