MotherDuck Labs · experiment
Postgres vs MotherDuck
Is the same analytics query really faster on MotherDuck than on Postgres? This experiment runs the exact same SQL against both engines, live in your browser — so you can measure the gap yourself.
Under the hood it's a full scan of ~3.9M order-items joined to orders, through the same pg driver — only the connection host changes. Hit Run comparison and watch Postgres grind while MotherDuck has already drawn its chart.
The query — run verbatim against both engines
SELECT
s.plan_tier,
date_trunc('month', o.ordered_at) AS month,
SUM(oi.line_total) AS revenue,
COUNT(DISTINCT o.order_id) AS orders
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id
JOIN shops s ON s.shop_id = o.shop_id
WHERE o.status = 'paid'
GROUP BY 1, 2
ORDER BY 1, 2Fair comparison. Both engines run the identical SQL, and the Postgres side is indexed on its primary and foreign keys — orders(order_id, shop_id, status, ordered_at), order_items(order_id, shop_id, product_id), and every table’s PK — so it’s row-store vs columnar, not Postgres without indexes (seed script).
Comparable compute. Postgres runs on a standard managed instance — 0.5 vCPU · 4 GB RAM · 10 GB storage, single node. MotherDuck runs on a single Standard Duckling — its default, production-grade compute tier. Roughly standard compute on both sides — the “after” isn’t an oversized warehouse.
About the dataset
A synthetic multi-shop commerce platform — shops (tenants) on plan tiers, their catalog, and ~3.9M order line-items. The revenue query above is a full scan of order_items joined up to orders and shops — exactly the kind of analytical aggregate that row-store Postgres labors over and a columnar engine eats for breakfast.
| Table | Kind | Rows | What it is |
|---|---|---|---|
| shops | dimension | 500 | tenants, each on a plan tier |
| categories | dimension | 12 | product categories |
| products | dimension | 50,000 | catalog across all shops |
| customers | dimension | 500,000 | buyers |
| orders | fact | 2,000,000 | one row per placed order |
| order_items | fact | 3,938,272 | line items — the heavy grain |
How the connection works
Both engines are reached through the same Node pg driver. MotherDuck speaks the Postgres wire protocol, so “switching to MotherDuck” is just a different host + credentials — no DuckDB native extension, no SQL rewrite, no driver change. That’s why this runs fine in a serverless function.
new Pool({
connectionString: POSTGRES_URL,
ssl: { rejectUnauthorized: false },
})new Pool({
host: "pg.us-east-1-aws.motherduck.com",
port: 5432,
user: "motherduck", // any non-empty user
password: MOTHERDUCK_TOKEN, // the token is the credential
database: "multishop_commerce",
ssl: { rejectUnauthorized: false },
})Defined once in lib/db.ts — the ?source= param picks which pool answers. Same query text either way.
Three ways to query MotherDuck from JS/TS
This demo uses the Postgres wire path because it drops into an existing Postgres app with no new dependencies and runs in a serverless function. When you want the full native engine or browser-side compute, reach for one of the other two.
pgMotherDuck's Postgres-protocol endpoint, via the standard node-postgres driver.
import { Pool } from "pg";
const pool = new Pool({
host: "pg.us-east-1-aws.motherduck.com",
user: "motherduck", // any non-empty user
password: MOTHERDUCK_TOKEN, // token is the credential
database: "multishop_commerce",
ssl: { rejectUnauthorized: false },
});
const { rows } = await pool.query("SELECT 1");- +Zero new deps if you already use Postgres
- +Pure JS — runs in any serverless / Node runtime
- +Drop-in for an existing PG app: just swap the host
Trade-off · Goes through the Postgres-protocol surface — a subset of DuckDB SQL and PG type coercion; no local-file ATTACH.
MotherDuck docs ↗@duckdb/node-apiThe native DuckDB engine in-process, connected to MotherDuck with an md: string.
import duckdb from "@duckdb/node-api";
const instance = await duckdb.DuckDBInstance.create(
`md:multishop_commerce?motherduck_token=${MOTHERDUCK_TOKEN}`
);
const connection = await instance.connect();
const result = await connection.run("SELECT 1");- +Full native DuckDB SQL + extensions
- +ATTACH local files / Parquet alongside MotherDuck
- +Arrow-native results; hybrid local+cloud execution
Trade-off · Native addon — platform-specific binary, larger bundle, heavier cold starts; not edge-runtime compatible.
MotherDuck docs ↗@motherduck/wasm-clientDuckDB-Wasm in the browser — query MotherDuck straight from the client.
import { MDConnection } from "@motherduck/wasm-client";
const connection = MDConnection.create({
mdToken: READ_SCALING_TOKEN, // reaches the browser!
});
await connection.isInitialized();
const result = await connection.evaluateQuery("SELECT 1");- +Queries run in the browser — no server round-trip
- +Hybrid execution: local Wasm + cloud compute
- +Great for interactive dashboards & per-user drill-downs
Trade-off · The token reaches the client — use a read-scaling / short-lived token, never your main one. Plus Wasm bundle + browser memory limits.
MotherDuck docs ↗All three authenticate with the same MotherDuck access token. For the Wasm path, mint a read-scaling token server-side so your primary token never ships to the browser.