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.
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
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
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
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
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
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.
- 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.
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.
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.
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.
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.
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.
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.
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 .
| Option | What it does | Example |
|---|---|---|
| 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.
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.
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".
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.
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).
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.
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.
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.
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.
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.
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.
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 .
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.
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.
.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 .
cat new_customers.csv | duckdb shop.duckdb -c "COPY customers FROM '/dev/stdin' (FORMAT csv, HEADER true)"