A native Microsoft Excel XLL add-in for querying Excel ranges with DuckDB SQL and parameter binding.
=DUCKDB.EXEC(
"SELECT cif, SUM(amount)
FROM xlrange(1)
GROUP BY cif",
A1:D10000
)
Query Excel ranges directly with DuckDB SQL and return the result as a dynamic array.
- Native DuckDB integration
- Query Excel ranges
- Parameter binding from Excel values
- Asynchronous execution
- No .NET runtime required
Stable
Core functionality is considered stable and suitable for production use. Future releases will prioritize backward compatibility with existing workbooks.
This project was inspired by xlDuckDB, an XLL add-in that integrates DuckDB with Microsoft Excel.
| Feature | DuckDBExcelAddin | xlDuckDB |
|---|---|---|
| Native Excel formula experience | ✅ | ✅ |
| Dynamic array (spill) results | ✅ | ✅ |
| Query external files | ✅ | ✅ |
| Query Excel ranges | ✅ | ✅ |
| Parameter binding from Excel values | ✅ | ❌ |
xlrange type inference options |
✅ | ❌ |
| Helpers for Excel date and time values | ✅ | ❌ |
| Async execution | ✅ | ❓ |
| XLL implementation | ✅ | ✅ |
| .NET free | ✅ | ❌ |
| Runtime DuckDB DLL upgrade | ✅ | ❓ |
Comparison based on publicly documented features available at the time of writing.
- Microsoft Excel 64-bit with Dynamic Array (Spill Range) support:
- Microsoft 365 Excel
- Excel 2024
- Excel 2021
- DuckDB 1.5.x (
duckdb.dll)
Copy these files to the same folder:
DuckDBExcelAddIn.xll
duckdb.dll
Open the XLL directly, or add it through the Excel Add-ins dialog.
DuckDB is loaded dynamically at runtime.
Upgrading DuckDB generally requires only replacing:
duckdb.dll
with a newer compatible version.
- Excel ranges are exposed as
xlrange()table function:
=DUCKDB.EXEC(
"SELECT *
FROM xlrange(1)
WHERE cif = 10001",
A1:D100
)
- With inference options:
=DUCKDB.EXEC(
"SELECT *
FROM xlrange(1, sample=50)
WHERE cif = 10001",
A1:D100
)
=DUCKDB.EXEC(
"SELECT *
FROM xlrange(1, all_varchar=true)
WHERE cif = 10001",
A1:D100
)
- Use database file as default database:
=DUCKDB.EXECA(
"Path\db.duckdb",
"SELECT * FROM table_name;"
)
- Use DuckDB
read_xxx()functions:
=DUCKDB.EXEC(
"SELECT *
FROM read_duckdb('Path\db.duckdb', table_name='mytable')
LIMIT 100"
)
- Auto-incremented parameters:
=DUCKDB.EXEC(
"SELECT *
FROM xlrange(?)
WHERE cif = ?",
A1:D100,
1,
10001
)
- Positional parameters:
=DUCKDB.EXEC(
"SELECT *
FROM xlrange($1)
WHERE cif = $2",
A1:D100,
1,
10001
)
- Dynamic range selector:
=DUCKDB.EXEC(
"SELECT *
FROM xlrange(?)
WHERE cif = ?",
A1:D100,
F1:I200,
2,
10001
)
- Dynamic file reader:
=DUCKDB.EXEC(
"SELECT *
FROM read_duckdb(?, table_name=?)
LIMIT 100",
"Path\db.duckdb",
"mytable"
)
- Dynamic column selector:
=DUCKDB.EXEC(
"SELECT columns(?)
FROM read_duckdb(?, table_name=?)
LIMIT 100",
"column1",
"Path\db.duckdb",
"mytable"
)
- Dynamic table selector:
=DUCKDB.EXEC(
"CREATE TABLE mytable(col1) AS
SELECT * FROM range(10);
SELECT *
FROM query_table(?)",
"mytable"
)
Adds scalar functions to convert Excel date and time values stored as DOUBLE to DuckDB DATE, TIME and TIMESTAMP:
=DUCKDB.EXEC(
"SELECT xldate(date_col)
FROM xlrange(1)",
A1:D100
)
=DUCKDB.EXEC(
"SELECT xltime(time_col)
FROM xlrange(1)",
A1:D100
)
=DUCKDB.EXEC(
"SELECT xldatetime(datetime_col)
FROM xlrange(1)",
A1:D100
)
DuckDBExcelAddin executes SQL inside the Excel process using the embedded DuckDB engine.
Excel ranges are exposed to DuckDB through the xlrange() table function, allowing worksheet data to participate in SQL queries.
SQL statements are extracted, then each statement is prepared, bound with parameters, and executed.
The result of the final statement is materialized and returned to Excel as a dynamic array (spill range).
| Type | Support | Note |
|---|---|---|
Auto incremented ? |
✅ | |
Positional $1 |
✅ | Must reset parameter index for each statement. |
Named $param |
❌ | Excel doesn't support named parameters. |
| Function | Since | Syntax | Purpose | Equivalent xlDuckDB Formula |
|---|---|---|---|---|
| DUCKDB.EXEC / DUCKDB.EXEC.ASYNC | =DUCKDB.EXEC(sql, [range1], [range2], ..., [param1], [param2], ...) |
Execute SQL using in-memory database. | =DuckDbQuery(sql,, range) |
|
| DUCKDB.EXECX / DUCKDB.EXECX.ASYNC | =DUCKDB.EXECX([init_sql], sql, [range1], [range2], ..., [param1], [param2], ...) |
Execute initialization SQL, then main SQL using in-memory database. | ||
| DUCKDB.EXECA / DUCKDB.EXECA.ASYNC | 1.1.0 | =DUCKDB.EXECA([db_file_path], sql, [range1], [range2], ..., [param1], [param2], ...) |
Execute SQL using a DuckDB file as the default database. | =DuckDbQuery(sql, dbfilepath, range) |
| DUCKDB.EXECAX / DUCKDB.EXECAX.ASYNC | 1.1.0 | =DUCKDB.EXECAX([db_file_path], [init_sql], sql, [range1], [range2], ..., [param1], [param2], ...) |
EXECA plus initialization SQL. | |
| DUCKDB.INFO | =DUCKDB.INFO() |
Return add-in and DuckDB runtime information. |
- Only sql is required, other parameters can be ignored. For example:
=DUCKDB.EXECA( , A1). - Ranges are exposed to
xlrange()and must appear before bound parameters. - When multiple SQL statements are supplied, all statements are executed sequentially, but only the result of the final statement is returned to Excel.
- Asynchronous formulas do not block Excel recalculation, but they introduce overhead due to thread creation and deep copying of worksheet ranges.
- Initialization SQL is executed before the main query and can be used to define reusable macros, views, or other helper objects.
- Parameters are not bound in initialization SQL.
Exposes Excel ranges as a table function.
Supports column projection pushdown since version 1.7.0.
Syntax:
xlrange(
index,
sample=30,
all_varchar=false,
header=true,
strict=true,
ignore_errors=false
)| Parameter | Since | Status | Default | Description |
|---|---|---|---|---|
index |
🔴 Required | 1-based position of an Excel range passed to formula. | ||
all_varchar |
🟢 Optional | false |
- When true, all values are returned as VARCHAR and type inference is disabled.- When false, column type is inferred. |
|
sample |
🟢 Optional | 30 |
Number of data rows used for type inference. - A value of 0 samples all data rows.- Ignored when all_varchar=true. |
|
header |
1.2.0 | 🟢 Optional | true |
- When true, the first row is interpreted as column names. - When false, column names are generated as column_0, column_1, ... |
strict |
1.3.0 | 🟢 Optional | true |
- When true, column names must be non-empty and unique; otherwise, an error is raised.- When false, empty column names are generated as unnamed_0, unnamed_1, and so on. Duplicated names are renamed as name, name_1, name_2, and so on.- Ignored when header=false. |
ignore_errors |
1.5.0 | 🟢 Optional | false |
- When false an error is raised if values incompatible with inferred type are encountered.- When true incompatible values are silently converted to NULL. |
- Scan for the first non-empty value and use its type as the candidate column type.
| Value | Inferred As |
|---|---|
| Whole numbers within the INT32 range and their text representations(*) |
INTEGER |
| Other finite numbers and their text representations(*) |
DOUBLE |
| TRUE/FALSE and recognized text representations(*), including T/F, YES/NO, and Y/N, case-insensitive |
BOOLEAN |
| Other text | VARCHAR |
| Empty, Excel error | Ignored during inference |
(*)Text values with leading or trailing whitespace are trimmed during inference.
-
Sample the remaining rows up to the configured sample limit.
-
If incompatible types are encountered, the column type is promoted to DOUBLE or VARCHAR.
Examples can be found in examples\test_cases.xlsx.
If sample is less than number of data rows, out-of-sample values are converted to inferred type. An error is returned if conversion fails.
| Inferred type | Compatible value |
|---|---|
| INTEGER | Same as inference rule TRUE/FALSE-like values are incompatible |
| DOUBLE | Same as inference rule |
| BOOLEAN | Same as inference rule, plus 0/1-like values are compatible |
| VARCHAR | Text, number, TRUE/FALSE |
Examples can be found in examples\test_cases.xlsx.
| Function | Purpose | Syntax |
|---|---|---|
xldate |
Convert an Excel serial date value to a DuckDB DATE. |
xldate(value) |
xltime |
Convert the fractional portion of an Excel serial value to a DuckDB TIME. |
xltime(value) |
xldatetime |
Convert an Excel serial datetime value to a DuckDB TIMESTAMP. |
xldatetime(value) |
Formulas cannot access cell number formats.
When using xlrange, numeric header cells formatted as dates or times are
registered using their underlying numeric values rather than their displayed
formats. For example, an Excel date displayed as December 31, 2026 may be
registered as the column name 46387.
Workaround: Convert Excel date to text with =TEXT(value, date_format) formula first.
Results are returned as Excel Dynamic Arrays.
Excel limits apply:
| Limit | Value |
|---|---|
| Rows | 1,048,576 |
| Columns | 16,384 |
| String length | 32,767 |
Queries exceeding these limits are not supported.
Large DuckDB numeric types (BIGINT, HUGEINT, DECIMAL) may lose precision when converted to Excel numbers (DOUBLE).
Data is exchanged through Excel ranges.
- Excel worksheet limits apply.
- Large datasets may consume significant memory.
- Input and output data must fit within Excel worksheets.
For large datasets, query DuckDB-supported sources directly (Parquet, CSV, DuckDB databases, etc.) and return only the required results to Excel.
SELECT *
FROM read_parquet('large_dataset.parquet')
LIMIT 1000The following DuckDB types are not supported as Excel results:
- LIST
- STRUCT
- MAP
- UNION
- VARIANT
Workaround: Convert or serialize the value to VARCHAR before returning it to Excel.
Currently, xlrange does not support row filter pushdown because the DuckDB C API does not expose pushed filters to table functions.
Currently, a dynamic PIVOT, meaning a PIVOT without an IN clause, cannot use parameters in its source query. This is a limitation of DuckDB’s dynamic PIVOT implementation.
For example:
=DUCKDB.EXEC(
"PIVOT xlrange(?) ON col1 USING sum(col2) GROUP BY col0",
A1:C5,
1
)
Returns:
Parser Error: PIVOT statements with pivot elements extracted from the data cannot have parameters in their source...
Using a CTE does not bypass the restriction:
=DUCKDB.EXEC(
"WITH cte AS (from xlrange(?))
PIVOT cte ON col1 USING sum(col2) GROUP BY col0",
A1:C5,
1
)
This may return an internal error such as:
INTERNAL Error: Attempted to dereference unique_ptr that is NULL! ...
Workaround:
- Specify the pivot values explicitly with an
INclause. - Store the parameter value in a DuckDB variable and reference the variable from the query, where applicable.
- Materialize the parameterized source in a temporary table or temporary view, and then pivot that object without parameters.
| Issue | Verify |
|---|---|
| Add-in fails to load | - Excel is 64-bit - duckdb.dll is located next to DuckDBExcelAddIn.xll- duckdb.dll version is 1.5.x- The add-in is not blocked |
#VALUE! returned |
- The add-in is loaded - Dynamic Arrays are supported - Input ranges and Result size do not exceed Excel limits |
#SPILL! error |
- The destination spill range is empty - There are enough rows and columns available to display the result |
- Excel XLL SDK
- DuckDB C API (
duckdb.h) - uthash by troydhanson and maintained Arthur O'Dwyer
Development and testing are performed primarily using w64devkit (MinGW-w64).
Other toolchains such as Visual Studio (MSVC) may work but are currently unverified.
Contributions and testing reports are welcome.
XLCALL.H XLOPER struct contain a member named:
boolwhich conflicts with C bool keyword.
You need to edit the header and rename the member to, for example:
xbooland update the corresponding references in FRAMEWRK.C.
This modification only affects local compilation and does not affect runtime behavior because XLOPER is never used.
- Development
make EXCEL_SDK_PATH=<excel-sdk> DUCKDB_INC_PATH=<duckdb-include> xll- Release
make ADDIN_VERSION=vx.x.x EXCEL_SDK_PATH=<excel-sdk> DUCKDB_INC_PATH=<duckdb-include> xllThe release package includes a workbook containing examples and regression tests for the major features of DuckDBExcelAddin:
examples\test_cases.xlsx
Special thanks to the DuckDB team and contributors for creating an exceptional embedded analytical database.
This project was inspired by xlDuckDB, particularly its formula-based integration approach and the xlrange concept.
Additional thanks to:
- uthash by troydhanson and maintained Arthur O'Dwyer
- Microsoft Excel XLL SDK
MIT License
