The tradeoff is context. DuckDB runs outside Excel, which suits data prep and pipeline work but not formula-driven board pack models where everything needs to live in the workbook.
What read_xlsx Actually Does in DuckDB
DuckDB 1.0 (released June 2024) ships a native Excel reader built on the spatial extension's xlsx support. The syntax is a single function call:
-- Read all rows from a named sheet
SELECT * FROM read_xlsx('revenue_by_sku.xlsx', sheet='P&L Detail');
-- Filter and aggregate on read
SELECT
sku_category,
SUM(revenue) AS total_revenue,
AVG(margin_pct) AS avg_margin
FROM read_xlsx('revenue_by_sku.xlsx', sheet='P&L Detail')
WHERE fiscal_quarter = 'Q3-2026'
GROUP BY sku_category
ORDER BY total_revenue DESC;
DuckDB infers column types from the xlsx data automatically. Dates stored as Excel serials come through as DATE types. Numbers stored as text - the classic problem in any exported report - stay as VARCHAR unless you cast explicitly. Same behavior you'd get from any typed SQL engine, no surprises.
The sheet= parameter targets a specific tab by name. Matters when you're pulling from a multi-tab workbook where Sheet1 is a cover page.
DuckDB vs Excel vs Google Sheets QUERY
Here's how the three options compare for the aggregation-heavy workloads FP&A teams actually run:
| Capability | DuckDB | Excel Power Query | Google Sheets QUERY |
|---|---|---|---|
| Row limit | Effectively unlimited | 1,048,576 rows | ~10M cells per spreadsheet |
| 500k-row GROUP BY | ~0.4s | ~12s | 8-15s |
| Window functions (LAG, RANK) | Full SQL:2003 | M language only | Not supported |
| Multi-file JOINs | Native | Complex M merge | Not supported |
| Lives inside spreadsheet | No | Yes | Yes |
| Formula-linked output | No (export required) | Yes | Yes |
The cell count cap is worth flagging. As of July 2026, Google Sheets caps at 10 million cells per spreadsheet. Excel Online shares a similar constraint in browser sessions. DuckDB has no ceiling - if the file fits on disk, it queries.
When Excel Formulas Hit the Wall
Three scenarios where analysts reach for DuckDB over in-spreadsheet tools:
Multi-file consolidation. Twelve regional P&L files, each with 80k rows of transaction detail. SUMIFS across 12 external workbooks is a maintenance nightmare. DuckDB handles it as a single query across a directory:
-- Consolidate all regional P&Ls in one pass
SELECT
region,
cost_center,
SUM(actuals) AS total_actuals,
SUM(budget) AS total_budget,
SUM(actuals - budget) AS variance
FROM read_xlsx('regional_pnl/*.xlsx', filename=true)
GROUP BY region, cost_center
ORDER BY variance ASC;
Contribution margin by SKU at transaction level. Your 'P&L'!C:C has 400k rows of line-item revenue. =SUMIFS('P&L'!C:C, 'P&L'!B:B, ">="&Assumptions!$B$3, 'P&L'!D:D, SKU_List!A2) on 400k rows recalculates slowly and locks the sheet for 8-15 seconds. DuckDB runs the same aggregation against the exported xlsx in under a second:
SELECT
sku_id,
SUM(revenue) AS gross_revenue,
SUM(revenue - cogs) AS contribution_margin,
ROUND(SUM(revenue - cogs) / SUM(revenue) * 100, 1) AS cm_pct
FROM read_xlsx('sku_transactions.xlsx')
WHERE txn_date >= '2026-04-01'
GROUP BY sku_id
HAVING SUM(revenue) > 50000
ORDER BY cm_pct DESC;
Bank syndicate DCF with external comps. Your comparable company data lives in an xlsx export from Bloomberg or CapIQ, your DCF assumptions live in another workbook. Joining them in Excel means copy-paste or Power Query merges that break when column order changes. In DuckDB:
SELECT
d.company_name,
d.unlevered_fcf / c.ev_ebitda_multiple AS implied_ev,
c.ev_ebitda_multiple,
c.net_debt
FROM read_xlsx('dcf_assumptions.xlsx') d
JOIN read_xlsx('comps_export.xlsx') c
ON d.ticker = c.ticker;
Performance: Why DuckDB Is Faster Than Power Query
The speed gap has a structural explanation. Excel stores data row by row - to aggregate a single column across 500k rows, it touches all 500k rows in memory. DuckDB uses a columnar format: a SUM(revenue) query reads only the revenue column off disk, skipping everything else.
Abadi, Madden, and Hachem (SIGMOD 2008) documented 10-100x performance advantages for aggregation-heavy workloads in columnar engines over row-oriented stores. DuckDB's benchmarks reflect the same pattern at the scale FP&A workloads typically run. For read_xlsx specifically, DuckDB 1.1 (September 2024) benchmarks show 50k rows loading in approximately 0.5 seconds including schema inference, scaling roughly linearly to about 2.5 seconds at 250k rows.
Running DuckDB Against Excel Files: Setup
DuckDB runs as a local binary with no server required. As of July 2026, the setup is straightforward from the command line or Python:
# Install via pip
pip install duckdb
import duckdb
# Query an xlsx file, get a pandas DataFrame back
result = duckdb.sql("""
SELECT
cost_center,
SUM(q1_actuals + q2_actuals + q3_actuals) AS ytd_actuals,
SUM(annual_budget * 0.75) AS ytd_budget,
ROUND(
SUM(q1_actuals + q2_actuals + q3_actuals)
/ SUM(annual_budget * 0.75) * 100, 1
) AS attainment_pct
FROM read_xlsx('board_pack_data.xlsx', sheet='Cost Centers')
WHERE department != 'Corporate Allocations'
GROUP BY cost_center
ORDER BY ytd_actuals DESC
""").df()
# Write the result back to Excel
result.to_excel('ytd_summary.xlsx', index=False)
The round-trip - read xlsx, aggregate, write xlsx - typically runs in under 2 seconds for datasets up to 250k rows. The Python interface returns a pandas DataFrame, which you can push into a database, feed into a reporting pipeline, or write back to Excel via openpyxl.
Keeping SQL Inside the Sheet
The friction point with DuckDB is the context switch. You're running Python or a CLI outside the spreadsheet, which works for data prep pipelines but breaks the flow when you need to iterate on a board pack model where the analyst and the output live in the same workbook.
ModelMonkey's DuckDB integration runs these queries directly inside Excel or Google Sheets without leaving the tab. You write SQL against named ranges or sheet tabs - SELECT * FROM sheet_range('P&L Detail!A1:F50000') - and the results land in a destination range in the same workbook. You can try it free for 14 days.