tutorials/duckdb/import-data

DuckDB Import: Load CSV, Parquet, Excel and DataFrames into Tables

There are two ways to get data into a DuckDB table. Import a CSV or JSON file into an existing table visually in 1bench, or write a CREATE TABLE AS, COPY FROM or INSERT INTO statement that loads CSV, Parquet, JSON, Excel, DataFrames and other databases.

Updated Nov 2, 2026Tested against DuckDB 1.4.5, 1.5.6 and 2.0.0

Quick answer

In 1bench
Right-click the table in the sidebar, choose Import Data, pick a CSV, JSON, NDJSON or SQL file and click Import.
In SQL
sql
CREATE TABLE new_customers AS FROM read_csv('new_customers.csv');

DuckDB detects the delimiter, header and column types. For a table that already exists, use COPY customers FROM 'new_customers.csv' instead.

1bench
No SQL needed

Import visually in 1bench

1bench loads a CSV, JSON or NDJSON file into an existing DuckDB table, or runs a .sql file, from a right-click menu. The whole import runs in one transaction, so a bad row leaves the table as it was.

Prerequisite: download 1bench. Free 7-day trial, no sign-up.

  1. 1

    Open your DuckDB file

    Connect to your .duckdb file in 1bench and expand it in the sidebar. The file must not be opened read-only, or Import Data is greyed out.

  2. 2

    Choose Import Data

    Right-click the target table and choose Import Data. If the table is already open, the Import button at the bottom of the window does the same.

  3. 3

    Pick the file

    Click Browse and select the file. The Format is set from the extension: CSV, JSON, NDJSON (.ndjson or .jsonl) or SQL.

  4. 4

    Clear the table first (optional)

    Tick Clear target table before importing to delete the current rows and replace them with the file. Leave it unticked to append.

  5. 5

    Import

    Click Import. A progress bar counts the rows, and Stop import cancels with no changes made. When it finishes, a message reports how many rows were imported.

Good to know
  • The target table must already exist; 1bench does not create one from a CSV or JSON file. Create it first with CREATE TABLE in the Query panel, or import a .sql file that contains the CREATE TABLE statement.
  • CSV files must be comma-separated with a header row. Header names are matched to column names, so the column order does not matter and columns missing from the file get their default value. A header that is not a column fails with a Binder Error, and rows with the wrong number of fields are skipped and counted in the result message.
  • An empty unquoted CSV field is imported as NULL. A quoted empty string stays an empty string in text columns.
  • There are no delimiter, skip-rows or type options in the dialog. For semicolon or tab files, Parquet, Excel or anything else below, run the SQL from the Query panel's SQL editor.
  • SQL files run as written, inside one transaction, and their own BEGIN and COMMIT lines are skipped. DuckDB has no way to switch foreign keys off, so the Disable foreign key constraints box does not help here: create and fill parent tables first.
The manual way

Import with SQL

DuckDB has three ways to load a file into a table. makes a new table from a reader function such as , appends a file to a table that already exists, and appends the result of any query, so you can filter, rename or convert columns on the way in.

sql
CREATE TABLE new_customers AS FROM read_csv('new_customers.csv');

The examples use the 1bench demo shop (, , ). They load data into tables; to run SQL on a file without loading it, see query CSV and Parquet files in place. Every statement below works the same on DuckDB 1.4, 1.5 and 2.0 unless a section says otherwise.

Import a CSV into a new table

sniffs the delimiter, quote character, header row and column types, so most files need no options. is DuckDB shorthand for , and a bare file name works as a table too.

sql
CREATE TABLE new_customers AS SELECT * FROM 'new_customers.csv';
DESCRIBE new_customers;

Running it a second time fails with "Catalog Error: Table with name "new_customers" already exists!". Use to rebuild it, or to keep the first load. A table made this way has no primary key or NOT NULL constraints; to keep those, create the table first and use the next section.

Example: set every column type yourself instead of trusting the guess.

sql
CREATE TABLE new_customers AS
FROM read_csv('new_customers.csv', columns = {
  'id': 'BIGINT', 'name': 'VARCHAR', 'email': 'VARCHAR', 'country': 'VARCHAR',
  'plan': 'VARCHAR', 'signup_date': 'DATE', 'lifetime_value_cents': 'DECIMAL(12,2)'
});

Import a CSV into an existing table

appends a file to a table and checks the table's constraints as it goes. The file's columns must line up with the table's columns by position.

sql
COPY customers FROM 'new_customers.csv';

When the file has fewer columns than the table, the sniffer gives up with "Error when sniffing file" and the hint "It does not match the number of columns found by the sniffer: 5". List the columns the file does have, and the rest get their default. That only works when the missing columns have one: leaving out the primary key fails with "Constraint Error: NOT NULL constraint failed: products.id". For anything more, switch to , which lets you shape the rows first.

sql
COPY products (id, sku, name, category, price_cents) FROM 'products_no_active.csv';

INSERT INTO customers
SELECT * FROM read_csv('new_customers.csv')
WHERE plan IS NOT NULL;

is the short form of the second statement. Each one runs as a single statement, so one row that breaks a constraint, such as a duplicate id, rolls back the whole file.

Match columns by name with INSERT BY NAME

A plain matches columns by position and fails with "Binder Error: table "products" has 6 columns but 5 values were supplied" when the counts differ. matches on column names instead, in any order, and fills the columns you leave out with their default or NULL.

Example: load products from a semicolon file that has no id or active column, numbering the new rows from 101.

sql
INSERT INTO products BY NAME
SELECT 100 + row_number() OVER () AS id, *
FROM read_csv('new_products.csv', delim = ';', nullstr = 'N/A');

The missing column takes its default of true, and in the file becomes NULL. A name in the file that the table does not have fails with "Binder Error: Table "products" does not have a column with name "title"", so rename it in the .

CSV options

Options go inside as , or inside the parentheses after with a space instead of the .

OptionWhat it doesExample
First row holds the column names
Column separator or
Column names for a file without a header
Force the type of some columns
Read every column as text
Text that means NULL
Lines to skip before the header or data
Format for DATE columns
Format for TIMESTAMP columns
Drop rows that fail to parse or cast
Rows sniffed for types (-1 means all)

Example: load a point-of-sale export that starts with two comment lines, has no header and writes dates day first.

sql
INSERT INTO sales
SELECT * FROM read_csv('pos_sales.csv',
  skip = 2, header = false, timestampformat = '%d/%m/%Y %H:%M');

A value that does not fit a type you set stops the load with "Conversion Error: CSV Error on Line: 4" and prints the line. Add to drop such rows instead. It only skips values that fail inside the reader, so set the type with first: a column the sniffer read as VARCHAR passes through, and the insert then fails with "Could not convert string 'abc' to INT32".

Example: load a supplier file and drop the row whose price is not a number.

sql
INSERT INTO products BY NAME
SELECT 200 + row_number() OVER () AS id, *
FROM read_csv('supplier_products.csv', delim = ';', nullstr = 'N/A',
  types = {'price_cents': 'INTEGER'}, ignore_errors = true);

Import multiple CSV files

A glob loads every matching file in one statement. Without , DuckDB takes the columns from the first file, so files that gained a column later fail with "Binder Error: table "sales" has 8 columns but 7 values were supplied".

sql
INSERT INTO sales BY NAME
SELECT * FROM read_csv('monthly/*.csv', union_by_name = true);

Add to keep a column with the source file of each row, which helps when one month's file turns out to be wrong.

sql
CREATE TABLE monthly_raw AS
SELECT * FROM read_csv('monthly/*.csv', union_by_name = true, filename = true);

Import Parquet and JSON

Parquet stores column types, so it loads with no options and no guessing. JSON works the same way, and accepts both a JSON array and newline-delimited JSON (NDJSON or JSONL).

sql
CREATE TABLE archived_customers AS FROM read_parquet('customers.parquet');
CREATE TABLE country_totals AS FROM read_json('by_country.json');

COPY customers FROM 'customers.ndjson' (FORMAT json);
INSERT INTO customers BY NAME SELECT * FROM read_json('customers.ndjson');

JSON records with a missing key load NULL for that column. is the safer choice for JSON, since object keys have no fixed order.

Import an Excel file

comes from the excel extension. DuckDB installs and loads it on first use when it can reach the internet; otherwise run first.

sql
CREATE TABLE price_list AS FROM read_xlsx('products.xlsx', sheet = 'items');
CREATE TABLE price_list AS FROM 'products.xlsx';

Excel stores every number as a float, so integer columns arrive as DOUBLE. Cast them in the , or load into an existing table, which casts for you. reads part of a sheet, and reads every cell as text.

sql
INSERT INTO products BY NAME
SELECT id + 500 AS id, sku, name, category, price_cents
FROM read_xlsx('products.xlsx');

Import a pandas or Polars DataFrame

In Python, DuckDB reads a DataFrame by its variable name, so a pandas or Polars frame works like a table in SQL. No copy to CSV is needed.

python
import duckdb
import pandas as pd

con = duckdb.connect("shop.duckdb")
leads = pd.DataFrame({
    "id": [9201], "name": ["Cara Diaz"],
    "email": ["[email protected]"], "plan": ["pro"],
})

con.sql("CREATE TABLE lead_list AS SELECT * FROM leads")
con.sql("INSERT INTO customers BY NAME SELECT * FROM leads")

Polars frames work the same way. When the frame lives somewhere the name lookup cannot see, such as a dict or another module, register it under a name first.

python
import polars as pl

frame = pl.DataFrame({"id": [9301], "name": ["Dev Patel"], "email": ["[email protected]"]})
con.register("new_rows", frame)
con.sql("INSERT INTO customers BY NAME FROM new_rows")

A pandas column of mixed values (object dtype) becomes VARCHAR. For rows built in Python rather than a frame, takes a list of tuples. In the 2.0 Python package, relations no longer have , so check results with .

Import from SQLite, PostgreSQL or MySQL

opens another database inside DuckDB, and importing a table is then a . The sqlite, postgres and mysql extensions load on first use.

sql
ATTACH 'app.sqlite' AS app (TYPE sqlite, READ_ONLY);
CREATE TABLE app_users AS FROM app.users;

ATTACH 'host=localhost port=5432 dbname=saas_app user=demo' AS pg (TYPE postgres, READ_ONLY);
CREATE TABLE pg_orders AS FROM pg.public.orders;

ATTACH 'host=localhost port=3306 user=demo database=saas_app' AS my (TYPE mysql, READ_ONLY);
CREATE TABLE my_users AS FROM my.users;

Types carry over from PostgreSQL and MySQL ( arrives as , as ). SQLite dates are text, so cast them in the . To load only some rows, filter in the .

Example: copy only delivered orders, then append shipped ones later.

sql
CREATE TABLE pg_recent AS
SELECT id, status, total_amount, created_at FROM pg.orders WHERE status = 'delivered';

INSERT INTO pg_recent
SELECT id, status, total_amount, created_at FROM pg.orders WHERE status = 'shipped';

copies everything at once, but it rebuilds each table with its original defaults and indexes, so it is fragile across engines. From SQLite it fails with "Catalog Error: Scalar Function with name datetime does not exist!" on a column, and from a PostgreSQL database with a pgvector index it fails on "unknown index type". Copy table by table instead.

Import from another DuckDB file

Attach the second file and copy with , or copy the whole file with .

sql
ATTACH 'shop.duckdb' AS shop (READ_ONLY);
ATTACH 'archive.duckdb' AS archive;
CREATE TABLE archive.web_sales AS FROM shop.sales WHERE channel = 'web';

COPY FROM DATABASE shop TO archive;

On DuckDB 1.4 and 1.5, fails on tables with foreign keys with "Constraint Error: Violates foreign key constraint because key "id: 2" does not exist in the referenced table". DuckDB 2.0 copies them in the right order. On the older versions, copy the schema first and fill the parent tables before the child.

sql
COPY FROM DATABASE shop TO archive (SCHEMA);
INSERT INTO archive.customers FROM shop.customers;
INSERT INTO archive.products FROM shop.products;
INSERT INTO archive.sales FROM shop.sales;

From the CLI

The DuckDB shell's loads a file into a table and creates the table when it does not exist. Extra flags are passed to the reader.

text
.import new_customers.csv new_customers
.import new_products.csv new_products --delim ';'

This changed in 1.5. In 1.4, is the old SQLite-style command that uses the shell's own separators, so with default settings it creates a single VARCHAR column named after the whole header line. On 1.4, use instead.

To pipe data in from another program, read from .

bash
cat new_customers.csv | duckdb shop.duckdb -c "COPY customers FROM '/dev/stdin' (FORMAT csv, HEADER true)"

Work with DuckDB in one window

1bench is a desktop GUI for DuckDB. Browse tables, filter rows, run SQL and export what you see, with the SQL still there when you want it.

Open DuckDB in 1bench

Frequently asked questions

How do I import a .sql file into DuckDB?
In the DuckDB shell, .read dump.sql runs every statement in the file, and from your own shell duckdb shop.duckdb < dump.sql does the same. Explicit BEGIN and COMMIT lines in the file are fine. For a folder written by EXPORT DATABASE, use IMPORT DATABASE 'shop_export' instead, which runs its schema.sql and load.sql for you.
Why does DuckDB read my CSV column as VARCHAR?
The sniffer types a column from a sample of rows (20,480 by default), and one value that does not parse, such as abc in a number column, makes the whole column VARCHAR. Adding ignore_errors = true alone does not fix it, because nothing fails once the column is text. Set the type with types = {'price_cents': 'INTEGER'} and then ignore_errors drops the bad rows, or use sample_size = -1 when the odd value sits beyond the sample.
How do I import rows that already exist without a duplicate key error?
A plain INSERT or COPY into a table with a primary key fails with "Duplicate key "id: 9001" violates primary key constraint" and loads nothing. INSERT OR IGNORE INTO skips rows whose key exists, INSERT OR REPLACE INTO overwrites them, and INSERT INTO ... ON CONFLICT (id) DO UPDATE SET plan = excluded.plan updates only the columns you name.
Can a DuckDB file made by version 2.0 be imported into 1.5?
Not directly. DuckDB 1.5 refuses to open or ATTACH a file written by 2.0 with "Trying to read a database file with version number 69, but we can only read versions between 64 and 68". Run EXPORT DATABASE 'shop_export' (FORMAT parquet) in 2.0, then IMPORT DATABASE 'shop_export' in 1.5, which rebuilds the tables and data in the older format.