Documentation
Querying and loading data
What works through an attached database, how to list tables, and how to move data in and out.
An attached DuckHouse database works like a local one for everyday reads and writes. Quack, the protocol underneath, is new, and a handful of things do not work through the attachment yet. Each has a one-line alternative, and this page covers all of them.
The short version
| Through the attachment | Works | Use instead |
|---|---|---|
SELECT from wh.my_table | Yes | |
INSERT INTO wh.my_table | Yes | |
| CREATE TABLE, CREATE TABLE AS, DROP TABLE | Yes | |
| Loading a local file into a remote table | Yes | |
| Exporting a remote table to a local file | Yes | |
| UPDATE and DELETE | Not yet | wh.query('UPDATE …') |
| SHOW TABLES, information_schema | Returns nothing | wh.query('SHOW ALL TABLES') |
Tables outside the main schema | Not yet | wh.query('SELECT … FROM stripe.invoices') |
| Joining two remote tables | Not yet | Put the whole join inside wh.query('…') |
| CREATE SCHEMA | Not yet | wh.query('CREATE SCHEMA x') |
wh.query() runs SQL on the server
Every attached database has a query() function. The SQL you give it runs inside your DuckHouse database rather than on your machine, and only the result comes back. Nothing on the list above applies there, so it is the answer to every row marked “not yet”.
-- List everything
FROM wh.query('SHOW ALL TABLES');
-- Describe one table
FROM wh.query('DESCRIBE events');
-- Change and remove rows
FROM wh.query('UPDATE events SET status = ''done'' WHERE id = 42');
FROM wh.query('DELETE FROM events WHERE created_at < now() - INTERVAL 90 DAY');Inside the string, write a single quote as two (''done''), and refer to tables by the names they have on the server: events, not wh.events.
It is also the fast way to join
A join written inside query() runs next to the data, and only the joined result crosses the network. For large tables that is the difference between moving a few rows and moving both tables.
FROM wh.query('
SELECT c.email, sum(i.amount_paid) / 100 AS paid
FROM stripe.customers c
JOIN stripe.invoices i ON i.customer = c.id
WHERE i.status = ''paid''
GROUP BY c.email
ORDER BY paid DESC
LIMIT 20
');Joining a remote table to a local one works normally, because only one side is remote: SELECT … FROM wh.events e JOIN my_local_table l USING (id).
Loading data
From a file on your machine
DuckDB reads the file locally and streams it up. Parquet, CSV and JSON all work.
CREATE TABLE wh.orders AS SELECT * FROM 'orders.parquet';
-- Add more later
INSERT INTO wh.orders SELECT * FROM 'orders_2026_09.csv';From a URL or a bucket
If the data is already online, have the server fetch it. Nothing passes through your machine, so this is the quickest way to load anything large.
FROM wh.query('
CREATE TABLE trips AS
SELECT * FROM read_parquet(''https://example.com/data/trips.parquet'')
');From Postgres, MySQL or Stripe, continuously
Connections keep tables in sync on a schedule, with no code to run.
Getting data out
-- To a local file
COPY (SELECT * FROM wh.orders WHERE year = 2026) TO 'orders_2026.parquet';
-- To a local table, for offline work
CREATE TABLE orders_local AS SELECT * FROM wh.orders;