Skip to content
Back to Blog

DuckDB vs SQL Server vs PostgreSQL for Analytics: SQL Server Standard's Ceilings, What They Cost, and What a Lakehouse Does Instead

Prateek SinghOctober 21, 202627 min read135 views
DuckDB vs SQL Server vs PostgreSQL for Analytics: SQL Server Standard's Ceilings, What They Cost, and What a Lakehouse Does Instead

SQL Server ships a real analytical engine and then fences Standard edition off from it: batch mode capped at 2 threads, a 32 GB columnstore cache, no batch mode on rowstore, no SIMD, no adaptive joins, all Enterprise at 3.83x the price per core. On one Apple M4 Max, DuckDB 2.0 ran six TPC-H-style queries over 60 million rows 18x to 47x faster than PostgreSQL 18, and DuckDB held to 2 threads still beat PostgreSQL on 8 workers by 3.7x to 8.6x. Where PostgreSQL's row store runs out, how a DuckDB lakehouse starts, what MotherDuck costs, and where DuckDB sits in AWS.

SQL Server has had a real analytical engine since 2012. Columnstore indexes, batch-mode execution, segment elimination, aggregate pushdown: the pieces that make a warehouse a warehouse are in the product, and Microsoft's engineers built them well. What this post is about is the fence around them. Microsoft's own edition page for SQL Server 2025 says, in a footnote under "Scalability and performance", that the degree of parallelism for batch-mode operations is limited to 2 for Standard edition and 1 for Express. On a 16-core Standard server, the query you bought columnstore for runs on two threads. The edition that lifts the limit costs 3.83x per core. That is the first half of this post. The second half is what the alternatives do: PostgreSQL measured honestly, DuckDB measured on the same rows, how a lakehouse starts on one laptop and grows to a team on S3, what MotherDuck charges, and where DuckDB sits in AWS.

batch-mode threads, SQL Server Standard
2
per query, on any core count; Express gets 1, Enterprise gets them all
Enterprise over Standard, per core
3.83x
$15,123 against $3,945 per 2-core pack, Microsoft list
DuckDB 2.0 ahead of PostgreSQL 18
18x to 47x
six queries, 60,000,000 rows, one Apple M4 Max, all 16 threads against 8 workers
DuckDB held to 2 threads, still ahead
3.7x to 8.6x
the Standard-edition ceiling, applied to DuckDB, against PostgreSQL on 8 workers

Edition limits and prices from Microsoft's pages, October 2026. The measurement: TPC-H scale factor 10, PostgreSQL 18.6 and the DuckDB 2.0.0 pre-release on the same Mac, warm, best of three after a warm-up, 20 October 2026.

One thing before the numbers. SQL Server does not run on this Mac: its 2025 Linux release notes support containers only on x86-64 hosts and say Rosetta 2 and QEMU are "neither tested nor supported". So the edition limits and prices below are Microsoft's own, the PostgreSQL comparison was measured on the Mac, and the SQL Server comparison was measured a day later on a second machine, an x86 Linux laptop, in the section "SQL Server, measured" added on 21 October. Two machines, so the two measured sections are each one engine against DuckDB on the same box, never SQL Server against PostgreSQL.

What an analytical engine needs, in five lines

  • Columnar storage, so a query that touches 4 of 16 columns reads a quarter of the bytes, and those bytes compress 5x to 10x because a column is all one type.
  • Batch, or vectorised, execution: a thousand values through an operator at once instead of one row through a chain of function calls. This is where the 10x to 100x over a row engine comes from.
  • Every core, for every scan, join and aggregate, without a knob.
  • Out-of-core hashing and sorting, so a join larger than memory spills and finishes instead of failing.
  • Open formats, so the data is readable by the next engine without an export.

Hold those five against the three systems.

SQL Server: the engine is there, the edition decides who gets it

Since SQL Server 2016 SP1, columnstore indexes are in every edition, Express included. The brochure line is true. The footnotes are where the analytical engine lives, and they are worth reading as a list, because together they describe a product that is one thing in Enterprise and a different thing in Standard:

SQL Server 2025: what the edition decides before your query runs from Microsoft's "Editions and supported features of SQL Server 2025", scale limits and footnotes, October 2026 batch-mode threads per query, 16-core server columnstore cache, per instance Express (free) 1 of 16 Express (free) 352 MB Standard 2 of 16 Standard 32 GB Enterprise 16 of 16 Enterprise no cap, whatever the box has Enterprise-only as well: batch mode on rowstore, adaptive joins, memory-grant and DOP feedback, aggregate and string-predicate pushdown, AVX2 and AVX-512 SIMD, star-join optimisation, online columnstore rebuild.
From Microsoft's edition page for SQL Server 2025, scale limits and the footnotes under "Scalability and performance" and "Intelligent query processing", as published on 19 October 2026. Enterprise's cache bar is drawn full because the page says "unlimited memory".

The degree-of-parallelism cap is the one that matters most, because it does not show up as an error. A Standard-edition instance on 32 cores (the 2025 limit; 24 before) will happily run your OLTP on all of them. Point a columnstore aggregate at it and the batch-mode operators get two threads. The plan looks fine. The server looks idle. Adding cores does nothing. The second cap is memory: the columnstore segment cache, the pool the compressed segments live in, is 32 GB per instance on Standard, inside a 256 GB buffer pool. Past that, the warehouse comes off disk.

Then the list of things that are simply not in Standard. Batch mode on rowstore, the 2019 feature that gave ordinary B-tree tables vectorised execution, is Enterprise only. So are adaptive joins, memory-grant feedback, degree-of-parallelism feedback, aggregate pushdown and string-predicate pushdown into the columnstore scan, the SIMD paths (AVX2 and SSE4 since 2016, AVX-512 with a trace flag in 2022 and 2025), star-join optimisation, parallel query processing over partitioned tables, global batch aggregation, and online columnstore rebuilds. Every one of those is a performance feature, not a compliance feature. Every one is in the table under "Enterprise: Yes, Standard: No".

SQL Server 2025EnterpriseStandardExpress
Cores per instanceall the OS offerslesser of 4 sockets or 32 cores (24 before 2025)lesser of 1 socket or 4 cores
Buffer poolall the OS offers256 GB1,410 MB
Columnstore segment cacheno cap32 GB352 MB
Batch-mode degree of parallelismall cores21
Batch mode on rowstoreyesnono
Adaptive joins, memory-grant feedback, DOP feedbackyesnono
Aggregate pushdown, string-predicate pushdown, SIMD (AVX2, AVX-512)yesnono
Star-join optimisation, parallel query on partitioned tables, global batch aggregationyesnono
Online columnstore index rebuildyesnono
Database size524 PB524 PB50 GB
List price, perpetual$15,123 per 2 cores$3,945 per 2 coresfree
List price, subscription$5,434 per 2 cores per year$1,418 per 2 cores per yearfree
Every row is from the same Microsoft page and Microsoft's SQL Server 2025 pricing datasheet (open no-level estimated retail, USD). Core licensing has a 4-core minimum per server; Enterprise under server + CAL is capped at 20 cores and not sold to new agreements.

I want to be exact about what I am and am not saying. I am not saying the engine is slow; batch mode over a clustered columnstore on Enterprise is a serious warehouse engine, and the ordered columnstore work in 2022 and 2025 is good engineering. I am saying the edition page is a price list for the engine's own features, and that a Standard customer who reads "columnstore: yes" has bought the storage format and not the execution model. That is a choice Microsoft is entitled to make. It is also a choice a buyer is entitled to see laid out plainly, which the footnotes do not do.

What the fence costs

Microsoft's SQL Server 2025 pricing datasheet lists Enterprise at $15,123 and Standard at $3,945 per 2-core pack, open no-level estimated retail, with a 4-core minimum per server; subscription is $5,434 and $1,418 per 2-core pack per year; Azure Arc pay-as-you-go is $274 and $73 per core per month. The ratio is 3.83x everywhere. For a 16-core box that is $120,984 against $31,560 perpetual, or $43,472 against $11,344 a year. The gap is the price of the analytical engine you already installed.

One 16-core analytics server: the licence line, per year Microsoft open no-level list, 2-core packs; MotherDuck list; hardware, people and cloud usage not counted on any row $0k$10k$20k$30k$40k SQL Server Enterprise, subscription $43,472 8 packs x $5,434 SQL Server Standard, subscription $11,344 8 packs x $1,418; 2 threads MotherDuck Business $3,000 $250/month platform, plus usage PostgreSQL 18 $0 licence PostgreSQL licence DuckDB 2.0 $0 licence MIT licence Perpetual list for the same box: Standard $31,560, Enterprise $120,984. Azure Arc pay-as-you-go: Standard $73, Enterprise $274 per core per month.
List prices only, from Microsoft's datasheet and MotherDuck's pricing page. Nothing here counts hardware, people, support, Software Assurance, or MotherDuck and AWS usage; it counts what the licence line says before any of that.

There is a third path Microsoft now offers, and it is telling: Mirroring in Microsoft Fabric is in every edition of 2025, Express included. It continuously replicates your SQL Server tables into Fabric's OneLake as Delta, where Fabric's own engines query them. Read that as the vendor's answer to the question this post asks. The analytical copy leaves the box, the Standard licence stays what it is, and a Fabric capacity, billed per capacity unit per hour whether or not anyone is querying, appears on a second invoice. For a lot of Standard customers that will be the right move. It is still two bills for one dataset.

SQL Server, measured: the Enterprise engine, the rowstore most shops run, and the two-thread line

Added 21 October. A day after this went up, the same six queries ran on an x86 Linux laptop with SQL Server 2025 installed: an Intel i9-13900HK with 31 GB, SQL Server 2025 RTM-CU9 in its Enterprise Developer edition, which is the Enterprise engine licensed for development, and DuckDB 2.0's pre-release build, both capped at 16 GB, same TPC-H scale factor 10 generated on that box. SQL Server ran three ways: plain rowstore heaps, which is what most Standard-edition reporting databases actually are; a clustered columnstore index on every table at full parallelism, which is SQL Server's best case; and the same columnstore with OPTION (MAXDOP 2) on every query, which is the Standard edition batch-mode ceiling applied to the Enterprise engine. One footnote first: SQL Server on this hybrid chip reports 12 schedulers while the OS has 20 logical CPUs, so its full-parallel runs used 12 and DuckDB used 20. The 2-thread rows are the equal-footing comparison.

Six queries, 60,000,000 rows, one x86 laptop: SQL Server 2025 against DuckDB 2.0 TPC-H SF10 · Intel i9-13900HK, 20 logical CPUs, 31 GB · 16 GB to each engine · warm, best of 3 ms on a log scale · SQL Server 2025 Enterprise Developer edition, 12 schedulers DuckDB 2.0.0 pre-release build · 21 October 2026 SQL Server, rowstore heaps SQL Server, clustered columnstore columnstore, MAXDOP 2 DuckDB 2.0, 20 threads DuckDB 2.0, 2 threads 10 ms100 ms1 s TPC-H Q1: scan 60M rows, 4 sums, 3 avgs 1.26 s 154 ms · 8.2x vs rowstore 149 ms 78 ms · 2.0x vs columnstore 350 ms TPC-H Q6: filtered sum, one year 587 ms 120 ms · 4.9x vs rowstore 49 ms 23 ms · 5.2x vs columnstore 93 ms TPC-H Q3: 3-way join, top 10 513 ms 193 ms · 2.7x vs rowstore 351 ms 76 ms · 2.5x vs columnstore 209 ms TPC-H Q5: 6-way join by region 487 ms 197 ms · 2.5x vs rowstore 330 ms 88 ms · 2.2x vs columnstore 215 ms 15M orders: distinct customers by month 597 ms 522 ms · 1.1x vs rowstore 1.70 s 190 ms · 2.8x vs columnstore 1.17 s 60M x 2M join, top 20 brand+type 932 ms 554 ms · 1.7x vs rowstore 1.28 s 536 ms · 1.0x vs columnstore 1.66 s
An x86 Linux laptop: Intel i9-13900HK (14 cores, 20 logical CPUs), 31 GB, Linux Mint 22.3, 21 October 2026. SQL Server 2025 RTM-CU9 (17.0.5005.3) Enterprise Developer edition on the host, max server memory 16 GB, MAXDOP 0; it reports 12 schedulers on this hybrid chip. Rowstore = heaps loaded by BULK INSERT, no indexes; columnstore = the same tables under a clustered columnstore index, UPDATE STATISTICS run, plan check 80 operators in batch mode and 2 in row mode. DuckDB 2.0.0 pre-release build 2.0.0.dev2610192346, memory_limit 16 GB, native file at 20 and at 2 threads. Four warm runs, best of the last three; SQL Server timed server-side around SELECT INTO a temp table, DuckDB around execute and fetchall. Same TPC-H SF10 generated on that box, row counts identical.

Three things are true at once in that chart. First, SQL Server's columnstore engine is good. In batch mode on 12 schedulers it is within 1.03x of DuckDB on the big join (554 against 536 milliseconds), within 2x to 2.8x on four more, and 5x behind only on the one-year filtered sum. Anyone who tells you SQL Server cannot do analytics has not built a clustered columnstore index. Second, the rowstore is where most of the gap lives: the heaps are 1.1x to 8.2x slower than SQL Server's own columnstore on the same data and 1.7x to 25x slower than DuckDB, and they take 10.96 GB on disk against 4.25 GB for the columnstore and 2.83 GB for DuckDB's file. The single biggest analytical win available to a Standard customer is the columnstore index they already own. Third, the two-thread line. Hold the Enterprise engine to MAXDOP 2 and the scan-heavy queries barely notice, because aggregate pushdown does the work inside the scan: Q1 goes from 154 to 149 milliseconds and Q6 from 120 to 49. The joins and the distinct count do notice: Q3 1.8x slower, Q5 1.7x, the month-by-month distinct customers 3.3x, the big join 2.3x. Against DuckDB held to the same two threads it is a split, three queries each. And that is the Enterprise engine at two threads. Standard edition does not have the pushdowns or the SIMD paths that kept Q1 and Q6 fast, so a real Standard instance lands at or below that line, not on it.

Query, same 60,000,000-row lineitemSQL Server rowstoreSQL Server columnstoreColumnstore, MAXDOP 2DuckDB 2.0, 20 threadsDuckDB 2.0, 2 threads
TPC-H Q1: scan 60M rows, 4 sums, 3 avgs1,256 ms154 ms149 ms78.3 ms (2.0x)349.6 ms
TPC-H Q6: filtered sum, one year587 ms120 ms49 ms23.3 ms (5.2x)93.3 ms
TPC-H Q3: 3-way join, top 10513 ms193 ms351 ms75.7 ms (2.5x)209.2 ms
TPC-H Q5: 6-way join by region487 ms197 ms330 ms87.7 ms (2.2x)214.6 ms
15M orders: distinct customers by month597 ms522 ms1,698 ms189.6 ms (2.8x)1169.3 ms
60M x 2M join, top 20 brand+type932 ms554 ms1,284 ms535.8 ms (1.0x)1655.8 ms
On disk10,961 MB4,249 MB4,249 MB2.83 GB2.83 GB
LoadBULK INSERT, 166 s+ 87 s to build32 s dbgen
Milliseconds, same runs as the chart; the bracket is SQL Server columnstore divided by DuckDB on 20 threads. On-disk from sys.dm_db_partition_stats per schema and the DuckDB file size. The MAXDOP 2 column is the Enterprise engine throttled to two threads, which is an upper bound for Standard edition: Standard also lacks aggregate pushdown, string-predicate pushdown and SIMD, so a real Standard instance would land at or below that column.

Put the price back in. The step from the MAXDOP 2 line to the full-parallel line is the step from Standard to Enterprise, 3.83x per core, and on this box it buys 1.7x to 3.3x on the join-shaped queries and nothing on the scans. DuckDB's full line, which is ahead of SQL Server's full line on all six, costs nothing and runs on all twenty threads without asking which edition you bought.

PostgreSQL: the honest middle, measured

PostgreSQL is the system I would pick for almost any application database, and 18.6 is current with 19 at release candidate 1. It is also, in core, a row store with a tuple-at-a-time executor, and no amount of affection changes what that costs on a 60,000,000-row aggregate. The defaults tell you what it expects to do: 2 parallel workers per gather, 4 MB of work_mem, 128 MB of shared buffers. PostgreSQL 19 turns JIT off by default, with the note that its costing "has been determined to be unreliable" and that sites doing many large analytical queries should enable it by hand.

So I measured it with the knobs turned, not the defaults. A fresh PostgreSQL 18.6 cluster on this Mac: 8 GB of shared buffers, 256 MB of work_mem, 8 workers per gather, JIT on, heap tables after VACUUM ANALYZE, warm cache. The data is TPC-H at scale factor 10, generated by DuckDB's tpch extension: a 59,986,052-row lineitem, 15,000,000 orders, 2,000,000 parts. The same six queries ran on DuckDB 2.0.0, the pre-release build of the version that ships tomorrow, three ways: its native file on all 16 threads, the same file held to 2 threads (the SQL Server Standard ceiling, applied to DuckDB for the sake of the argument), and the tables as ZSTD Parquet files read through views, which is how a lakehouse would hold them.

Six queries, 60,000,000 rows, one Mac: PostgreSQL 18.6 against DuckDB 2.0 TPC-H SF10 · Apple M4 Max, 16 cores, 64 GB · warm, best of 3 after a warm-up · seconds, log scale the label on each bar is PostgreSQL's time divided by that bar's PostgreSQL 18.6, 8 workers DuckDB 2.0, 2 threads DuckDB 2.0 on Parquet, 16 threads DuckDB 2.0 native, 16 threads 10 ms100 ms1 s10 s TPC-H Q1: scan 60M rows, 4 sums, 3 avgs 2.89 s 338 ms · 8.6x 163 ms · 17.7x 62 ms · 46.6x TPC-H Q6: filtered sum, one year 464 ms 70 ms · 6.6x 81 ms · 5.7x 14 ms · 33.1x TPC-H Q3: 3-way join, top 10 922 ms 147 ms · 6.3x 149 ms · 6.2x 37 ms · 24.9x TPC-H Q5: 6-way join by region 748 ms 161 ms · 4.6x 186 ms · 4.0x 41 ms · 18.2x 15M orders: distinct customers by month 2.56 s 693 ms · 3.7x 112 ms · 22.8x 87 ms · 29.4x 60M x 2M join, top 20 brand+type 4.62 s 888 ms · 5.2x 298 ms · 15.5x 188 ms · 24.5x
TPC-H scale factor 10 generated by DuckDB's tpch extension: lineitem 59,986,052 rows, orders 15,000,000, part 2,000,000. PostgreSQL 18.6, fresh Homebrew cluster, shared_buffers 8 GB, work_mem 256 MB, 8 workers per gather, JIT on, heap tables after VACUUM ANALYZE, no indexes. DuckDB 2.0.0, the pre-release build published to PyPI on 19 October (2.0.0.dev2610192346; the 2.0.0 release is scheduled for 21 October), on Python 3.13: native file opened read-only, and the same tables as ZSTD Parquet behind views. Four runs each, warm, best of the last three. Not a TPC benchmark result; the queries are Q1, Q3, Q5 and Q6 as written plus two of mine.

Read the amber row first. DuckDB 2.0 on 16 threads is 18x to 47x ahead of PostgreSQL on 8 workers: TPC-H Q1, the full scan with eight aggregates, is 62 milliseconds against 2.89 seconds; the 60,000,000-by-2,000,000 join is 188 milliseconds against 4.62 seconds. Now read the grey row. DuckDB held to two threads, which is one quarter of the workers PostgreSQL was given, is still 3.7x to 8.6x faster on every query. That is the vectorised-batch argument in one line: the execution model is worth more than the core count. And the Parquet row, the open-format version with no DuckDB file at all, lands within 1.3x to 5.8x of the native file and still 4x to 23x ahead of the heap.

Query, 60,000,000-row lineitemPostgreSQL 18.6, 8 workersDuckDB 2.0, 2 threadsDuckDB 2.0 on Parquet, 16 threadsDuckDB 2.0 native, 16 threads
TPC-H Q1: scan 60M rows, 4 sums, 3 avgs2.892 s0.338 s (8.6x)0.163 s (17.7x)0.062 s (46.6x)
TPC-H Q6: filtered sum, one year0.464 s0.070 s (6.6x)0.081 s (5.7x)0.014 s (33.1x)
TPC-H Q3: 3-way join, top 100.922 s0.147 s (6.3x)0.149 s (6.2x)0.037 s (24.9x)
TPC-H Q5: 6-way join by region0.748 s0.161 s (4.6x)0.186 s (4.0x)0.041 s (18.2x)
15M orders: distinct customers by month2.556 s0.693 s (3.7x)0.112 s (22.8x)0.087 s (29.4x)
60M x 2M join, top 20 brand+type4.615 s0.888 s (5.2x)0.298 s (15.5x)0.188 s (24.5x)
On disk12 GB heap (lineitem 7.9 GB)2.81 GB, one file2.36 GB, 8 Parquet files2.81 GB, one file
Seconds, same runs as the chart; the ratio in brackets is PostgreSQL's time divided by that column's. On-disk sizes from pg_database_size, the DuckDB file and the Parquet directory.

The last line of that table is the quiet one. PostgreSQL's heap holds this dataset in 12 GB; DuckDB 2.0's file holds it in 2.81 GB; the Parquet files hold it in 2.36 GB. Columnar compression is not a nicety. It is four to five times less to read, cache and pay for.

None of this is a complaint about PostgreSQL. It was asked to do a thing it does not claim to be for, with settings most installs never get, and it finished every query. Where it wins is everything else: thousands of connections, row locks, replication, extensions, thirty years of tooling. The question is only whether to run the warehouse on the heap, and the market has already answered:

PostgreSQL 18What it isWhat that means for analytics
StorageRow heap, 8 KB pages; TOAST for large values; no columnar storage in coreOne analytical query over 60,000,000 rows reads every column of every row it touches; the heap here was 12 GB for 2.4 GB of Parquet
ExecutionTuple-at-a-time iterator; JIT optional, and off by default from 19No vectorised batches; the sum of 60,000,000 numerics is 60,000,000 function calls
ParallelismParallel query since 9.6; 2 workers per gather by default, 4 MB work_mem by defaultStock settings use 3 of 16 cores; I gave it 8 workers and 256 MB work_mem and it still trailed DuckDB on 2 threads
ConcurrencyProcess per connection, MVCC, real multi-user ACIDIts strength: thousands of writers, row locks, replication, 30 years of tooling
Extensionspg_duckdb 1.0 (DuckDB inside the Postgres process), pg_mooncake, TimescaleDB columnar, Citus columnar (unmaintained)Each connection gets its own DuckDB; the extension must be on the primary and every replica; tables stay row-oriented unless you also change storage
Where the market wentSnowflake bought Crunchy Data (June 2025), Databricks bought Neon (May 2025), Microsoft's Azure HorizonDB and Azure Flexible Server ship pg_duckdb in previewEveryone wants Postgres as the front door; nobody is running the warehouse on the heap
Defaults from the PostgreSQL 18 resource-configuration page; the JIT change from the PostgreSQL 19 release notes (RC1, 15 October 2026); pg_duckdb from MotherDuck's 1.0 announcement; the acquisitions as reported by TechTarget.

pg_duckdb 1.0 is the interesting one, and I would try it before anything else if the data must stay in Postgres: DuckDB's engine runs inside the Postgres process, reads Postgres tables with parallel scans, and reads Parquet, Iceberg and Delta from S3 in the same query. MotherDuck's own numbers put the gain at up to about 4x against fully indexed Postgres tables, more where indexes cannot help, and the announcement is careful to say it will not turn PostgreSQL into a full warehouse: the tables stay row-oriented, each connection gets its own DuckDB, and the extension must be installed on the primary and every replica. That is the structural limit of the extension route. You can bolt a better executor onto Postgres. The heap is still the heap.

DuckDB: the engine without the ladder

DuckDB is an in-process analytical database: a columnar-vectorised engine that links into Python, R, Java, Node, Rust, Go or a 40 MB command-line binary, under the MIT licence, with a foundation holding the IP. It uses every core by default, spills to disk when a hash table or sort outgrows memory, reads and writes Parquet, CSV and JSON by URL, and through extensions reads and writes Iceberg and Delta and attaches to PostgreSQL, MySQL and SQLite. That extension mechanism is open to anyone; gpudb, my own DuckDB community extension, uses it to run SQL on the Apple silicon GPU. There is no edition. The 2-thread query and the 16-thread query are the same download. 2.0.0 is on the calendar for tomorrow, 21 October; the numbers above are from its pre-release build of 19 October, and against 1.5.6 on the same queries it was faster on five of the six, Q1 by 2x. 1.4.x stays the long-term-support line.

Its limits are real and worth stating in the same breath. A native DuckDB file has one read-write process at a time; many threads inside that process, many read-only processes alongside it, but one writer, and the client-server Quack protocol that lifts this is in beta until 2.0. There is no row-level security, no authentication, no replication, no HA, because there is no server. It is not an application database and does not want to be. It is the thing you point at a few billion rows in files and get an answer from in the time it takes PostgreSQL to plan.

How a lakehouse starts with DuckDB, and where all the features are

The honest answer to "how do I start a lakehouse" used to be "hire a platform team". DuckLake changes the answer to "run ATTACH". It is a lakehouse format from the DuckDB people with one idea at its centre: the data is Parquet on object storage, exactly like Iceberg and Delta, but the metadata lives in an ordinary SQL database instead of a tree of JSON and Avro manifest files. Planning a query becomes a SQL query against the catalog. Concurrency becomes the catalog database's transactions. DuckLake 1.0 shipped on 13 April 2026, "production-ready with guaranteed backward compatibility".

Step one is a laptop and a file:

-- step 1: a lakehouse on a laptop. DuckDB 1.5.2 or later.
INSTALL ducklake;
ATTACH 'ducklake:sales.ducklake' AS lake;      -- catalog in a local DuckDB file
USE lake;                                       -- data goes to sales.ducklake.files/

CREATE TABLE orders AS FROM read_parquet('exports/orders_*.parquet');
INSERT INTO orders FROM read_csv('today.csv');  -- 10 rows or fewer stay inlined in the catalog

FROM lake.snapshots();                          -- every commit is a snapshot
SELECT count(*) FROM orders AT (VERSION => 1);  -- time travel, one clause

That is already a lakehouse: Parquet files in a folder, snapshots, time travel, schema evolution, and inserts under ten rows kept in the catalog so a trickle of writes does not become ten thousand tiny files. Step two is a team, and it changes two words:

-- step 2: the same lake for a team. Catalog in PostgreSQL, data in S3.
CREATE SECRET (TYPE s3, PROVIDER credential_chain);          -- IAM role, profile or env

ATTACH 'ducklake:postgres:host=lake-db.internal dbname=ducklake' AS lake
    (DATA_PATH 's3://acme-lake/sales/');

-- any number of DuckDB processes attach the same way: the laptop, a Lambda, a Glue job.
-- PostgreSQL's transactions make the commits ACID across all of them.
CREATE TABLE lake.events (ts TIMESTAMP, user_id BIGINT, kind VARCHAR, amount DOUBLE)
    WITH (partitioned_by = ['year(ts)', 'month(ts)']);
INSERT INTO lake.events FROM read_parquet('s3://acme-raw/events/2026/10/*.parquet');
CHECKPOINT;                                                  -- flush inlined rows to Parquet

With the catalog in PostgreSQL and the data path on S3, every DuckDB process that can reach both is a full read-write client of the same lake: the analyst's laptop, the nightly Glue job, the Lambda behind an API, MotherDuck. PostgreSQL is doing the one thing it is perfect for, small transactional writes to a few metadata tables, and DuckDB is doing the scans. The features, as of 1.0:

DuckLake featureWhat it doesSince
Snapshots and time travelEvery commit is a snapshot; FROM lake.snapshots() lists them; SELECT ... FROM t AT (VERSION => 3) reads one1.0
Catalog in a SQL databasePostgreSQL, SQLite, DuckDB or MySQL hold all metadata; planning is a SQL query, not a walk through JSON manifests1.0
Data as Parquet on object storageS3, GCS, Azure or local disk; the files are readable by anything that reads Parquet and are compatible with Iceberg1.0
Many writersSeveral DuckDB instances share one lake by coordinating through the PostgreSQL catalog; ACID through the catalog's transactions1.0
Data inliningSmall inserts, updates and deletes stay in the catalog (10-row threshold by default) and are flushed to Parquet on CHECKPOINT; no small-file problem1.0
Partitioning and sorted tablesBucket partitioning with murmur3 (Iceberg-compatible) for high-cardinality keys; tables sorted by columns or expressions on insert, flush or compaction1.0
Deletion vectorsStored as Puffin files for Iceberg v3 compatibility; marked experimental in 1.01.0, experimental
TypesGEOMETRY and VARIANT added1.0
Iceberg interopDuckDB's iceberg extension reads and writes Iceberg v3 through REST catalogs; DuckLake's Parquet and deletion vectors are laid out to matchDuckDB 1.5
Stability1.0 is "production-ready with guaranteed backward compatibility"; 2.0 candidates are branching, roles and incremental materialised views, not soonspec promise
From the DuckLake 1.0 announcement (13 April 2026) and the DuckLake documentation. The reference implementation is the ducklake extension in DuckDB 1.5.2 and later.

Where it stands against Iceberg: Iceberg has the ecosystem (Spark, Trino, Flink, Snowflake, Athena all write it) and DuckLake has the simplicity (no catalog service, no manifest rewrites, one database transaction per commit). The two are converging on the file layer, and DuckDB itself reads and writes Iceberg v3 through REST catalogs, so the choice is about the catalog, not the data. If the account already lives in Glue and Athena, Iceberg through S3 Tables. If the team is DuckDB and Postgres, DuckLake.

MotherDuck: the same engine as a service

MotherDuck is DuckDB run for you, with a twist that matters for cost: a local DuckDB can attach to it and run one query across local files, S3 and MotherDuck tables, deciding per operator where the work goes. The pricing is the opposite shape to a per-core licence:

MotherDuckPriceWhat it includesWho it is for
Lite$010 GB storage, 10 Compute Unit hours of Pulse compute a montha laptop that never sleeps
Business$250 per organisation per month, plus usagestorage $0.04 per GB-month; Pulse $0.60 per CU-hour metered per query; Standard $2.40, Jumbo $4.80, Mega $12, Giga $24 per hour billed by the seconda team, a warehouse, dashboards
Enterprisecustomcustomcontracts, SSO, support
RegionsAWS onlyus-east-1 and us-west-2 (Tier 1), eu-central-1 and eu-west-1 (Tier 2), ap-northeast-1 and ap-southeast-2 (Tier 3); an organisation lives in one region, chosen at creationput it next to your S3 bucket
From MotherDuck's pricing page and its cloud-regions page, read on 20 October 2026. Prices change; the shape (a free tier, a platform fee, compute by the second) is the point.

The Lite tier is a real tier: 10 GB of storage and 10 compute-unit hours a month for nothing, enough for a side project or a proof. The Business tier is $250 a month plus compute by the second and storage at $0.04 per GB-month, which is $40 a month for a terabyte. Compare the licence line again: that platform fee for a year is $3,000, under a tenth of a 16-core Standard subscription, and the compute it buys is not capped at two threads. It is also AWS-only and single-region per organisation, so pick the region where the S3 bucket is.

Where DuckDB fits in AWS

The short version is that AWS already built the storage and the catalogs, and DuckDB is a client of both. The long version:

Where DuckDB sits in AWS: one engine, five places to run it, one copy of the data storage and catalog on the left are AWS services; every box on the right is the same DuckDB reading them Amazon S3 Parquet, CSV, JSON; httpfs S3 Tables / Glue Data Catalog Iceberg REST; read and write RDS PostgreSQL DuckLake catalog, many writers Laptop or CI duckdb CLI, Python, R, Java AWS Lambda 10 GB RAM, 6 vCPU per call AWS Glue 6.0 job AWS: $0.0103 vs Spark $0.0205 EC2 / ECS, one big box all cores, all RAM, no cluster MotherDuck us-east-1, us-west-2, eu, ap Amazon Athena $5 per TB scanned Redshift Serverless from 4 RPU, $1.50 an hour the AWS-native comparators same S3, same Iceberg tables, a different bill shape
DuckDB's Iceberg catalog documentation; AWS's Glue 6.0 walkthrough of 6 October 2026; the Redshift Serverless 4-RPU announcement of May 2026; Athena pricing. Lambda limits are the service's published maximums.
AWS pieceHow DuckDB uses itWhere it fits
Amazon S3httpfs reads and writes Parquet, CSV and JSON by URL; CREATE SECRET (TYPE s3, PROVIDER credential_chain) picks up the instance role, the profile or the environmentthe floor everything else stands on
S3 TablesATTACH '<table-bucket-arn>' AS lake (TYPE iceberg, ENDPOINT_TYPE s3_tables); CREATE TABLE and INSERT commit Iceberg snapshots through the REST endpointthe AWS-managed Iceberg catalog, no servers
AWS Glue Data CatalogATTACH '<account-id>' AS glue (TYPE iceberg, ENDPOINT_TYPE glue) for one view of every Iceberg table in the account; writes need an S3 Tables federated catalog or an explicit locationwhen Athena, EMR and Spark already share the catalog
AWS Glue 6.0 jobsDuckDB as one Python process in a glueetl job; AWS's own October 2026 walkthrough: 8.4 million rows, 56 s and $0.0103 a run against Spark's 117 s and $0.0205 on the same 2 DPUscheduled ETL without a cluster
AWS LambdaDuckDB in a layer; up to 10 GB of memory and about 6 vCPUs per invocation, so one function handles a gigabyte-scale Parquet question in seconds and nothing when idleper-request analytics, APIs, event handlers
EC2 or ECSOne large instance, all cores and all memory to one process; a u- or r-class box out-scans most small warehouses and bills by the hourthe nightly batch, the heavy model
MotherDuckThe same DuckDB as a service in six AWS regions; a local DuckDB can query MotherDuck tables and S3 in one statementwhen the team needs a shared warehouse and nobody wants to run one
Athena and Redshift ServerlessAthena at $5 per TB scanned; Redshift Serverless from 4 RPU at $1.50 an hour, billed by the second with a 60-second minimumthe same S3 and Iceberg tables, priced per scan or per hour of capacity
Same sources as the diagram. The Glue figures are AWS's for AWS's workload (8.4 million rows, 12 Parquet files, 2 DPU); AWS itself says they scale with data size, query shape and Region.

The AWS Glue number deserves a sentence because it is AWS's, not DuckDB's. The Big Data Blog's walkthrough of 6 October 2026 runs DuckDB 1.5.1 inside a Glue 6.0 job as one Python process, reads Parquet from S3 with the job's IAM role, and writes Iceberg snapshots into S3 Tables through the REST endpoint; on the same 2 DPU, DuckDB finished in 56 seconds for $0.0103 a run and Spark in 117 seconds for $0.0205. AWS's point, made in its own voice, is that a single-process engine is now a sensible default for a class of ETL that used to mean a cluster. The syntax for the two catalogs:

-- step 3: the AWS-native catalogs, read and write from the same DuckDB.
INSTALL iceberg; INSTALL httpfs;
CREATE SECRET (TYPE s3, PROVIDER credential_chain);

-- Amazon S3 Tables: a table bucket is an Iceberg REST catalog
ATTACH 'arn:aws:s3tables:us-east-1:123456789012:bucket/acme-tables' AS s3t
    (TYPE iceberg, ENDPOINT_TYPE s3_tables);
CREATE TABLE s3t.analytics.daily_revenue AS
    SELECT o_orderdate, sum(o_totalprice) FROM lake.orders GROUP BY 1;   -- commits an Iceberg snapshot

-- AWS Glue Data Catalog: one view of every Iceberg table in the account
ATTACH '123456789012' AS glue (TYPE iceberg, ENDPOINT_TYPE glue);
SELECT * FROM glue.finance.ledger WHERE posted >= DATE '2026-10-01';

Athena and Redshift Serverless belong in the picture as the AWS-native comparators, because they read the same S3 and the same Iceberg tables. Athena bills $5 per terabyte scanned, which is a bargain for a query a day and an invoice for a dashboard that refreshes every minute. Redshift Serverless starts at 4 RPU for $1.50 an hour in us-east-1, billed by the second with a 60-second minimum, and is the right answer when the question is a shared warehouse with hundreds of users. DuckDB is the right answer when the question is a job, a function, a notebook or a team of twelve, and the bill should be the compute you used.

Which one, then

IfThen
You already run SQL Server Standard and reports are slowColumnstore is in your edition; use it. Then read the batch-mode DOP footnote before buying hardware: more cores will not help a 2-thread query. Price Enterprise honestly, or move the analytical copy out
Analytics on data that lives in PostgreSQLTry pg_duckdb on a replica first. If the heap is the problem (it was here), export to Parquet nightly and query that with DuckDB; keep PostgreSQL for what it is good at
One analyst, one laptop, files up to the size of the diskDuckDB, nothing else. Read the Parquet where it is; out-of-core handles the larger-than-RAM joins
A team, S3 full of Parquet, no warehouse yetDuckLake with the catalog in RDS PostgreSQL and data in S3, or S3 Tables if the account already lives in Glue; MotherDuck when you want the service
Thousands of concurrent dashboard users, row-level security, HANot DuckDB's native file. MotherDuck, a Postgres front with DuckDB behind it, or a warehouse built for that
Microsoft shop, regulated, must stay on SQL ServerEnterprise is the analytical edition and always was; budget 3.83x per core, or mirror to Fabric and accept the second bill
My reading of the numbers above, not a vendor's. Where two rows both fit, the cheaper one to leave is the better first move.

The point I would leave a SQL Server Standard customer with is not "switch". It is that the edition page is the benchmark you were never shown, and it is public. Two threads for batch mode, 32 GB for the columnstore cache, no SIMD, no batch mode on rowstore, and a 3.83x step to the edition that has them: that is the analytical performance of the product you are licensed for, in Microsoft's words. Measure your own reports against that before buying cores. Then build the clustered columnstore index you already own, which on the Linux box was worth up to 8x by itself, and run the six queries above on DuckDB over a Parquet export of the same tables. On the Mac the row engine with eight workers lost to the columnar engine with two, every time; on the Linux box the Enterprise engine at full parallelism lost to DuckDB on all six, and the file was a quarter the size of the heap. The engine has not been the expensive part of analytics for a while now. The ladder has.

FAQ: SQL Server, PostgreSQL and DuckDB for analytics

Is SQL Server Standard edition good for analytics?

It has columnstore indexes and batch mode, but Microsoft's 2025 edition page caps batch-mode operations at a degree of parallelism of 2 and the columnstore segment cache at 32 GB per instance, and keeps batch mode on rowstore, adaptive joins, aggregate pushdown and SIMD for Enterprise. On a 16-core Standard server an analytical query runs on 2 threads.

How much more does SQL Server Enterprise cost than Standard?

3.83x per core at Microsoft's open no-level list: $15,123 against $3,945 per 2-core pack perpetual, or $5,434 against $1,418 per 2-core pack per year on subscription. A 16-core server is $120,984 against $31,560 perpetual.

Is SQL Server columnstore faster than DuckDB?

Measured on one x86 Linux laptop on 21 October 2026, TPC-H scale factor 10: SQL Server 2025 Enterprise Developer with clustered columnstore indexes in batch mode was within 1.03x to 2.8x of DuckDB 2.0 on five of six queries and 5x behind on the sixth, with SQL Server on 12 schedulers and DuckDB on 20 threads. SQL Server's rowstore heaps were 1.7x to 25x behind DuckDB. With MAXDOP 2, the Standard-edition ceiling, SQL Server and DuckDB at 2 threads split the six queries three each.

Is DuckDB faster than PostgreSQL for analytics?

On one Apple M4 Max over TPC-H scale factor 10 (a 60,000,000-row lineitem), the DuckDB 2.0.0 pre-release on 16 threads ran six analytical queries 18x to 47x faster than PostgreSQL 18.6 with 8 parallel workers, 8 GB of shared buffers and a warm cache. Held to 2 threads, DuckDB was still 3.7x to 8.6x ahead.

Can PostgreSQL be made columnar?

Through extensions: pg_duckdb 1.0 runs DuckDB's engine inside the PostgreSQL process and reads Parquet, Iceberg and Delta from S3; pg_mooncake and TimescaleDB add columnar storage. Core PostgreSQL stores rows in a heap and executes a tuple at a time, and PostgreSQL 19 turns JIT off by default.

How do you start a lakehouse with DuckDB?

Install the ducklake extension, ATTACH a ducklake: catalog, and create tables; data lands as Parquet and every commit is a snapshot you can query with AT (VERSION => n). For a team, point the catalog at PostgreSQL and DATA_PATH at S3; any number of DuckDB processes share it. DuckLake 1.0 shipped in April 2026 with a backward-compatibility guarantee.

Where does DuckDB run in AWS?

Anywhere there is compute: a laptop against S3, AWS Lambda (10 GB, about 6 vCPUs), an AWS Glue 6.0 job (AWS measured $0.0103 a run against Spark's $0.0205), one EC2 or ECS box, or MotherDuck in six AWS regions. It reads and writes Iceberg through S3 Tables and the Glue Data Catalog with ENDPOINT_TYPE s3_tables or glue.

References & Citations

  • Microsoft (2026). “Editions and supported features of SQL Server 2025.” Microsoft Learn, ms.date 2025-11-19, updated 2026-10-19: Scale limits table (cores, buffer pool 256 GB, columnstore segment cache 32 GB / 352 MB, database size 50 GB); “Scalability and performance” footnote 1 (batch-mode DOP 2 Standard, 1 Express) and footnote 2 (aggregate pushdown, string predicate pushdown, SIMD: Enterprise); “Intelligent query processing” and “Data warehouse” tables.
  • Microsoft (2025). “What's new in columnstore indexes.” Microsoft Learn, ms.date 2025-12-29: edition history (all editions from 2016 SP1), AVX2/SSE4 batch-mode enhancements reserved for Enterprise, 2022 and 2025 ordered columnstore work.
  • Microsoft (2025). “SQL Server 2025 Pricing.” Datasheet, November 2025: Enterprise $15,123 and Standard $3,945 per 2-core pack (open no-level estimated retail); subscription $5,434 and $1,418 per 2-core pack per year; Azure Arc pay-as-you-go $274 and $73 per month, $0.375 and $0.100 per hour.
  • Microsoft (2026). “SQL Server 2025 on Linux release notes.” Container images on x86-64 Linux hosts; emulation or translation environments (Rosetta 2, Prism, QEMU) not tested or supported.
  • PostgreSQL Global Development Group (2026). “Versioning policy.” 18.6 current, 19 RC1 released 15 October 2026. “Resource consumption.” PostgreSQL 18 documentation: defaults for max_parallel_workers_per_gather, work_mem, shared_buffers. “Release 19.” 19rc1 notes: JIT disabled by default.
  • MotherDuck (2025). “Announcing pg_duckdb version 1.0.” Execution model, formats, the up-to-4x figure against indexed tables, replica and resource caveats. “Pricing.” and “Cloud regions.” MotherDuck documentation, read 20 October 2026.
  • TechTarget (2025). “Snowflake acquisition of Crunchy Data adds Postgres database.” June 2025; Databricks' acquisition of Neon, May 2025, as reported there.
  • DuckDB Foundation (2026). Releases (1.5.6, 28 September 2026); Release calendar (2.0.0 planned 21 October 2026; 1.4.x LTS); “Why DuckDB.”; “Concurrency.”; “Iceberg REST catalogs.” (ENDPOINT_TYPE s3_tables and glue, credential_chain secrets, write support).
  • DuckDB Labs (2026). “DuckLake 1.0.” 13 April 2026, and DuckLake documentation: catalog databases, snapshots, inlining, partitioning, sorted tables, deletion vectors.
  • Amazon Web Services (2026). “Cost-effective ETL with DuckDB and Amazon S3 Tables on AWS Glue.” AWS Big Data Blog, 6 October 2026: DuckDB 1.5.1 on Glue 6.0, 2 DPU, 56 s and $0.0103 per run against Spark's 117 s and $0.0205. “Amazon Redshift Serverless 4 RPU minimum in seven additional Regions.” May 2026, $1.50 per hour. Amazon Athena pricing: $5 per TB scanned.
  • The SQL Server measurement: 21 October 2026, an x86 Linux laptop, Intel i9-13900HK (14 cores, 20 logical CPUs), 31 GiB, Linux Mint 22.3 x86_64. SQL Server 2025 RTM-CU9, 17.0.5005.3, Enterprise Developer Edition on the host; max server memory 16,384 MB; MAXDOP 0; cpu_count 12 as reported by sys.dm_os_sys_info. Rowstore heaps via BULK INSERT (165.5 s for all eight tables), columnstore via SELECT INTO plus CREATE CLUSTERED COLUMNSTORE INDEX (87.4 s), UPDATE STATISTICS; Q1 actual plan on the columnstore: 80 operators in batch mode, 2 in row mode (batch mode execution). DuckDB 2.0.0.dev2610192346, 20 and 2 threads, memory_limit 16 GB. Four warm runs each, best of the last three; SQL Server timed server-side with SYSDATETIME around SELECT INTO a temp table, DuckDB around execute and fetchall. Sizes: rowstore schema 10,961 MB, columnstore schema 4,249 MB, DuckDB file 2.83 GB, Parquet 2.35 GB, CSV 11.39 GB.
  • Transaction Processing Performance Council. TPC-H; the dataset here was generated by DuckDB's tpch extension at scale factor 10 and the queries are Q1, Q3, Q5 and Q6 as written plus two of mine. This is not an audited TPC result.
  • The measurement: 20 October 2026, one Apple M4 Max (16 cores, 64 GB, macOS 27.0). PostgreSQL 18.6 (Homebrew), fresh cluster, shared_buffers 8 GB, work_mem 256 MB, max_parallel_workers_per_gather 8, JIT on, heap tables, VACUUM ANALYZE, no indexes. DuckDB 2.0.0 pre-release, PyPI build 2.0.0.dev2610192346 of 19 October 2026, on Python 3.13 (the 2.0.0 release is scheduled for 21 October): native file built from the Parquet export, read-only at 16 and at 2 threads; ZSTD Parquet through views at 16 threads. Four runs each, warm, best of the last three. On-disk: pg_database_size 12 GB (lineitem 7,932 MB); DuckDB 2.0 file 2.81 GB; Parquet 2.36 GB. DuckDB 1.5.6 and 1.4.5 were run the same way for comparison; 2.0.0 was faster than 1.5.6 on five of the six.

Subscribe to new posts from theaivibe.org

No spam — just new posts. One-click unsubscribe.
Share this article

Related Posts