A pluggable storage engine that brings DuckDB's columnar analytical engine inside MariaDB Server.
Create a table with ENGINE=DuckDB and analytical queries against it are executed through DuckDB's columnar vectorized engine — no ETL pipelines, no separate cluster, no additional protocols. One server, one SQL interface, the familiar mariadb client.
- HTAP (Hybrid Transactional/Analytical Processing) — InnoDB handles OLTP, DuckDB handles analytics, both in the same database.
- Ad-hoc analytical queries — complex joins, aggregations, subqueries, and window functions over large datasets without exporting data to a separate system.
- Eliminating ETL complexity — no need for a dedicated analytical cluster or data movement pipelines; the analytical engine runs in-process.
- Analytical GIS — deliver geospatial analytics via DuckDB's Spatial extension.
- Faster HTAP over InnoDB-only data — run queries against InnoDB-only data through DuckDB's vectorized execution.
- Partial query pushdown — implement a derived handler to push parts of complex queries down into DuckDB.
- Data lake access — reach data lake protocols and formats via DuckDB extensions.
TPC-H Scale Factor 10 (~11 GB raw data, 86.6M rows total, ~60M in lineitem).
Hardware / environment: Intel Core i7-13700H (14 cores / 20 threads), 64 GB RAM, NVMe SSD (ext4). MariaDB 11.4.13 with embedded DuckDB v1.5.2, duckdb_memory_limit=8 GiB, threads=20. Warm runs; numbers stable to ~±15%.
| Metric | Result |
|---|---|
Data loading (in-engine COPY) |
33 seconds |
| All 22 TPC-H queries | 4.30 seconds total |
| Query | Time | Query | Time | Query | Time |
|---|---|---|---|---|---|
| q01 | 0.253 | q09 | 0.337 | q17 | 0.116 |
| q02 | 0.076 | q10 | 0.253 | q18 | 0.318 |
| q03 | 0.134 | q11 | 0.049 | q19 | 0.207 |
| q04 | 0.135 | q12 | 0.152 | q20 | 0.166 |
| q05 | 0.142 | q13 | 0.606 | q21 | 0.486 |
| q06 | 0.070 | q14 | 0.128 | q22 | 0.113 |
| q07 | 0.133 | q15 | 0.101 | Total | 4.30 |
| q08 | 0.145 | q16 | 0.178 |
Each query also pays a small fixed cost (~40 ms) for client connect and pushdown setup — noticeable on the cheapest queries, negligible on the heavy analytical ones. See docs/tpch_sf10_query_benchmark.md and docs/tpch_sf10_ingestion_benchmark.md for full methodology.
Step-by-step guides for loading and analyzing open datasets with the DuckDB storage engine:
- NYC Taxi Trips — loading Parquet via
run_in_duckdb(), then cross-engine joins betweenENGINE=DuckDBandENGINE=InnoDBtables. Follows the ClickHouse taxi tutorial. - OWID CO₂ Emissions — loading a CSV dataset and running aggregations and window functions.
DuckDB is an in-process analytical database. Its performance rests on three pillars:
- Columnar storage — minimizes unnecessary data reads.
- Vectorized execution — processes data in batches for maximum CPU cache efficiency.
- Parallelism — leverages all available processor cores.
Tables created with ENGINE=DuckDB store data in DuckDB's native format. Queries are translated and executed by the DuckDB engine. InnoDB and DuckDB tables coexist in the same database.
The engine is built outside the MariaDB server tree against an installed MariaDB plugin SDK (find_package(mariadb-plugin), MDEV-40608). DuckDB v1.5.5 is built from the third_parties/duckdb submodule via ExternalProject_Add.
A MariaDB installation that provides both the server and the Development component (share/cmake/mariadb-plugin/), of the same version, under one prefix. Either:
- the
MariaDB-devel(RPM) orlibmariadb-dev(DEB) package of a MariaDB release that includes MDEV-40608, or - a server built from source and installed together with the Development component, e.g.
cmake --install <server-build> --component Development.
The plugin is ABI-bound to the server it was built against: build.sh refuses to install it into a server whose version differs from the SDK's.
git clone --recurse-submodules git@github.com:drrtuy/duckdb-engine.git
cd duckdb-engine
# Install build dependencies (requires root)
./build.sh -D
# Build only (into ./build)
./build.sh -t RelWithDebInfo -c
# Build, install into the server, keep the existing datadir and start the server
./build.sh -t RelWithDebInfo -n -SWithout -n, build.sh deletes and re-initializes the datadir (/var/lib/mysql by default).
cmake --install puts ha_duckdb.so into the server plugin directory, duckdb.cnf into the server's additional config directory and the MTR suite into <mariadb-test>/plugin/duckdb. If the server does not read that config directory, load the engine with INSTALL SONAME 'ha_duckdb';.
Paths and layout:
| Option | Default | Meaning |
|---|---|---|
--mariadb-prefix <dir> |
/usr |
MariaDB installation with the Development component; used as CMAKE_PREFIX_PATH and as the server prefix |
--build-dir <dir> |
./build |
Plugin build directory |
--datadir <dir> |
/var/lib/mysql |
Server data directory |
--layout DEB|RPM|STANDALONE |
RPM for -p rpm, STANDALONE for -p tgz, otherwise DEB |
Install layout of the server; must match it, otherwise the plugin lands where the server does not look for it |
Run ./build.sh -h for the full list of options.
./build.sh -t RelWithDebInfo -p rpm # Rocky/Fedora/Amazon Linux
./build.sh -t RelWithDebInfo -p deb # Debian/Ubuntu
./build.sh -t RelWithDebInfo -p tgzrun_mtr.sh runs the suite with the installed MTR (<prefix>/share/mariadb/mariadb-test, override with --mtr-dir) against the installed engine, so run build.sh without -c first. Source tests are copied into the installed suite before each run, and -r copies recorded results back to mysql-test/duckdb/r/.
./run_mtr.sh create_table_column # one test
./run_mtr.sh -r create_table_column # run and record
./run_mtr.sh -a # all tests
./run_mtr.sh -a -e # all tests against a running serverThe engine supports cross-engine joins — a single SELECT can combine DuckDB tables with tables from other engines (e.g. InnoDB). When the query planner detects a mix of engines, the select_handler does the following:
- Opens the non-DuckDB tables via MariaDB's handler API and registers them in a thread-local table registry.
- Derives a per-table predicate from the query's
WHERE(viamake_cond_for_table) and registers it alongside each external table. - Pushes the entire query — including all
WHERE,JOIN,GROUP BY, andORDER BYclauses — down to DuckDB as a single SQL statement. - DuckDB's replacement scan callback transparently redirects references to non-DuckDB tables to the
_mdb_scantable function.
The _mdb_scan table function does not loop over the handler directly. On its first call it lazily spawns a cooperative fiber (stack-switching runtime borrowed from libmariadb) running on a dedicated background THD. Inside the fiber it builds a synthetic, projection- and predicate-pushed
SELECT <needed columns> FROM <db>.<table> [WHERE <pushed predicate>]
and executes it through the full MariaDB pipeline (lex_start → parse_sql → mysql_execute_command). A custom select_result_interceptor (select_to_duckdb) converts each result row's Items into a DuckDB DataChunk; when a chunk fills (STANDARD_VECTOR_SIZE) the fiber yields back to DuckDB, which consumes the chunk and resumes the fiber for the next batch.
Because the external scan runs through mysql_execute_command, it uses the MariaDB optimizer and all available access paths (index range/ref access, etc.), and the pushed WHERE predicate is evaluated by MariaDB before rows reach DuckDB. On the DuckDB side, the full query text lets DuckDB's optimizer reorder joins, build hash tables, and run all aggregation/sorting over the streamed rows. A direct ha_rnd_next loop is retained only as a fallback when no fiber is used.
This means queries like the following just work:
SELECT d.id, d.amount, i.name
FROM analytics.orders d -- ENGINE=DuckDB
JOIN inventory.products i -- ENGINE=InnoDB
ON d.product_id = i.id
WHERE d.amount > 1000;DuckDB handles the join, aggregation, and sorting; InnoDB rows are produced on demand by a fiber-driven MariaDB query. No data copying or ETL is required.
- DECIMAL precision > 38 — DuckDB supports up to 38 digits; wider MariaDB DECIMALs will fail on DDL conversion.
- Some MariaDB functions are yet not pushdown-compatible —
GROUP_CONCAT(),DATE_FORMAT(),JSON_CONTAINS(),FOUND_ROWS(),LAST_INSERT_ID(), and a few others have no DuckDB equivalent or differ in syntax. Such queries fall back to MariaDB execution. - Strict GROUP BY — DuckDB rejects
SELECTcolumns not inGROUP BYand not aggregated, even when MariaDB'ssql_modeallows it. - XA transactions —
XA PREPAREis not supported by the engine. - Table partitioning —
CREATE TABLE ... PARTITION BYand converting a DuckDB table withALTER TABLE ... PARTITION BYare not supported; the engine declaresHTON_NO_PARTITION. - Collations — MariaDB UCA-based collation rules are approximated via DuckDB's built-in
NOCASE/NOACCENTcollations for UTF-8 charsets; non-UTF8 charsets fall back to binary comparison. Seedocs/collation-mapping.mdfor the full mapping and known gaps. - Cross-engine scan is yet single-threaded — each external (non-DuckDB) table is produced by a single fiber-driven MariaDB query (
_mdb_scanreportsMaxThreads() == 1); only the DuckDB side of the query is parallelized. - ALTER COLUMN DROP DEFAULT — not propagated to DuckDB catalog.
- Tables without a primary key (allowed only with
duckdb_require_primary_key=OFF) — row-by-rowUPDATE/DELETE(used e.g. with triggers or on a replica) silently skips rows containing NULL values and changes all copies of duplicate rows. Seedocs/architecture.md. - Row-based replication into DuckDB tables requires
binlog_row_image=FULLon the master (the default). A replica rejectsUPDATEevents with a partial after-image (MINIMAL,NOBLOB,FULL_NODUP) instead of filling the missing columns with defaults. The replica does not detect missing or conflicting rows (slave_exec_mode=STRICTis not enforced) and is not crash-safe. Seedocs/architecture.md. - Timezone propagation —
TIMESTAMPcolumns are stored asTIMESTAMPTZ; timezone must be set consistently between MariaDB and DuckDB contexts to avoid shifts.
See docs/mariadb-duckdb-incompatibilities.md for a detailed compatibility matrix.
This project is licensed under the GNU General Public License v2. See COPYING for details.
DuckDB itself is licensed under the MIT License.
Alibaba and the AliSQL Project — A special thank you to Alibaba and their engineering team for open-sourcing AliSQL. AliSQL is a MySQL branch developed at Alibaba Group and extensively used in their production infrastructure. The December 2025 open-source release of AliSQL 8.0 included integration of DuckDB as a native storage engine, providing a valuable reference implementation and validating the viability of embedding DuckDB inside a MySQL-compatible server. The DuckDB Engine for MariaDB draws heavily on this experience.
The DuckDB Project — None of this would be possible without the remarkable work of the DuckDB team. DuckDB has grown from an academic research project into one of the most impressive analytical engines available today — fast, embeddable, dependency-free, and released under the permissive MIT License. Its clean C++ codebase and well-defined embedding API are what make integrations like this one feasible.