# PHP PDO DuckDB logo DuckDB is an embedded SQL database designed for high-performance analytics (OLAP). `pdo_duckdb` is a native DuckDB database driver for the PHP Data Objects (PDO) interface.\ As a native PHP extension, it is implemented in C/C++ and does not require PHP FFI or preloading.\ It is also thread safe and fully tested with FrankenPHP (PHP-ZTS).\ The release packages contain pre-compiled binaries for all supported platforms and DuckDB is directly included.\ DuckDB extensions work the same way as they do in DuckDB CLI. This extension supports all DuckDB types: Text, Numeric, Date, Time, Interval, JSON, Array, Struct, Map, List, Enum, Variant, Geometry, Union, Bitstring, Blob and Boolean. Supported PHP versions (nts & zts): 8.2 8.3 8.4 8.5 8.6 Supported operating systems: Ubuntu 24.04/26.04, Debian 12/13, Fedora 42/43, AmazonLinux, openSUSE 16, Wolfi OS, Windows Server 2022/2025 (x64), macOS 14-26 (arm64) Supported SAPIs: php-cli, php-fpm, FrankenPHP, TrueAsync, mod_php ## Install and setup with ๐Ÿฅง [PIE](https://github.com/php/pie) ```bash pie install thomas-0816/pdo-duckdb-php ``` ## Install and setup with ๐ŸงŸ [FrankenPHP](https://frankenphp.dev/) (Debian/Ubuntu) ```bash sudo curl -s https://pkg.henderkes.com/api/packages/85/debian/repository.key -o /etc/apt/keyrings/static-php85.asc echo "deb [signed-by=/etc/apt/keyrings/static-php85.asc] https://pkg.henderkes.com/api/packages/85/debian php-zts main" | \ sudo tee -a /etc/apt/sources.list.d/static-php85.list sudo apt-get update sudo apt-get install php-zts-cli php-zts-pdo frankenphp pie-zts sudo pie-zts install thomas-0816/pdo-duckdb-php # test frankenphp php-cli -r 'print_r((new PDO("duckdb::memory:"))->query("SELECT 42 as n")->fetch(PDO::FETCH_ASSOC));' ``` ## Install and setup with Docker ``` FROM php:8.5-cli RUN <query("SELECT 42 as n")->fetch(PDO::FETCH_ASSOC));' EOF ``` ## Usage examples ```php $duckDb = new PDO('duckdb::memory:'); $duckDb->exec("CREATE TABLE table1 (id INTEGER, amount DECIMAL(10, 2), description VARCHAR)"); $statement = $duckDb->prepare("INSERT INTO table1 VALUES (?, ?, ?)"); $statement->execute([1, 42.21, 'Hello DuckDB! ๐Ÿ˜ ๐Ÿ’“ ๐Ÿฆ†']); $statement = $duckDb->query("SELECT * FROM table1"); print_r($statement->fetchAll(PDO::FETCH_ASSOC)); # Array # [0] => Array # [id] => 1 # [amount] => 42.21 # [description] => Hello DuckDB! ๐Ÿ˜ ๐Ÿ’“ ๐Ÿฆ† ``` ## Open databases from disk or in-memory ```php $db = new PDO('duckdb::memory:'); // open in-memory database $db = new PDO('duckdb:/tmp/test.db'); // open database file from disk // open database file as read-only $db = new PDO('duckdb:/tmp/test.db', null, null, [ PDO::DUCKDB_ATTR_CONFIG => ['access_mode' => 'read_only'] ]); ``` ## Read and write Parquet files ```php $db = new PDO('duckdb::memory:'); $db->exec("CREATE TABLE table1 (id INTEGER, text VARCHAR USING COMPRESSION zstd, data JSON)"); $statement = $db->prepare("INSERT INTO table1 VALUES (?, ?, ?)"); $statement->execute([1, 'Hello DuckDB ๐Ÿฆ†', ['foo' => 'bar', 'baz' => 42]]); $db->exec("COPY (SELECT * FROM table1) TO '/tmp/table1.parquet' (COMPRESSION zstd)"); foreach ($db->query("SELECT * FROM '/tmp/table1.parquet'", PDO::FETCH_ASSOC) as $row) { print_r($row); } # Array # [id] => 1 # [text] => Hello DuckDB ๐Ÿฆ† # [data] => Array # [foo] => bar # [baz] => 42 ``` __Apache Parquet__: very fast and efficient column based storage file format containing one table of data.\ Each column is split into several column groups. Depending on the query, the file can be read partially by certain columns groups.\ Different compression or dictionary algorithms can be applied to each column. Also supports encryption. __Note__: You can read and save Parquet files on local file systems or directly on [S3 object storage](https://duckdb.org/docs/lts/core_extensions/httpfs/s3api). ## Read CSV files with SQL ```php $list = [ ['aaa', 'bbb', 'ccc'], ['123', '456', '789'], ['aaa', 'bbb', 'ccc'] ]; $fp = fopen('/tmp/test.csv', 'w'); foreach ($list as $fields) { fputcsv($fp, $fields, ',', '"', ""); } fclose($fp); $db = new PDO('duckdb::memory:'); $statement = $db->query("SELECT * FROM '/tmp/test.csv'"); print_r($statement->fetchAll(PDO::FETCH_ASSOC)); # Array # [0] => Array # [aaa] => 123 # [bbb] => 456 # [ccc] => 789 # [1] => Array # [aaa] => aaa # [bbb] => bbb # [ccc] => ccc ``` ## Read JSON files with SQL ```php file_put_contents('/tmp/logs.json', json_encode(['log' => 'log text']) . PHP_EOL, FILE_APPEND); file_put_contents('/tmp/logs.json', json_encode(['log' => 'log text 2']) . PHP_EOL, FILE_APPEND); $db = new PDO('duckdb::memory:'); $statement = $db->query("SELECT * FROM '/tmp/logs.json'"); print_r($statement->fetchAll(PDO::FETCH_ASSOC)); # Array # [0] => Array # [log] => log text # [1] => Array # [log] => log text 2 $db->exec("COPY (SELECT * FROM '/tmp/logs.json') TO '/tmp/logs_json.parquet' (COMPRESSION zstd)"); ``` ## Use structured columns with a fixed schema ```php // s is array{v: string, i: int, a: string[], d: float} $db = new PDO('duckdb::memory:'); $db->exec("CREATE TABLE table1 (s STRUCT(v VARCHAR, i INTEGER, a VARCHAR[], d DECIMAL))"); $statement = $db->prepare("INSERT INTO table1 VALUES (?)"); $statement->execute([['v' => 'foo', 'i' => 21, 'a' => ['b', 'c'], 'd' => 42.21]]); $statement = $db->query("SELECT * FROM table1"); print_r($statement->fetch(PDO::FETCH_ASSOC)); # Array # [s] => Array # [v] => foo # [i] => 21 # [a] => Array # [0] => b # [1] => c # [d] => 42.21 ``` ## Cast array columns to JSON-string ```php $db = new PDO('duckdb::memory:'); $db->exec("CREATE TABLE table1 (v VARCHAR[])"); $db->exec("INSERT INTO table1 VALUES (['a', 'b'])"); $statement = $db->query("SELECT v FROM table1"); print_r($statement->fetch(PDO::FETCH_ASSOC)); # Array # [v] => Array # [0] => a # [1] => b $statement = $db->query("SELECT v::json::varchar as v FROM table1"); print_r($statement->fetch(PDO::FETCH_ASSOC)); # Array # [v] => ["a","b"] ``` ## Auto increment columns ```php $db = new PDO('duckdb::memory:'); $db->exec('CREATE SEQUENCE table1_id'); $db->exec("CREATE TABLE table1 (id INTEGER PRIMARY KEY DEFAULT nextval('table1_id'))"); $statement = $db->query("INSERT INTO table1 VALUES (default) RETURNING *"); print_r($statement->fetch(PDO::FETCH_ASSOC)); # Array # [id] => 1 ``` ## Differences to MySQL / MariaDB ```php $db = new PDO('duckdb::memory:'); $statement = $db->query("SELECT 0/0, 1/0, -1/0, nullif(0/0, 'NAN'), nullif(1/0, 'INF'), nullif(-1/0, '-INF')"); var_export($statement->fetch(PDO::FETCH_NUM)); # array ( # 0 => NAN, // MySQL,MariaDB: NULL # 1 => INF, // MySQL,MariaDB: NULL # 2 => -INF, // MySQL,MariaDB: NULL # 3 => NULL, # 4 => NULL, # 5 => NULL, # ) ``` ## Copy data from MySQL or MariaDB to Parquet Start MariaDB container, create and fill "orders" table: ```bash docker run --rm -it -p 3306:3306 -e MARIADB_ROOT_PASSWORD=secret -e MARIADB_DATABASE=testdb mariadb:12 mysql -h 127.0.0.1 -u root -psecret testdb -e " CREATE TABLE orders (id integer primary key, customer integer, amount decimal(12, 2), origin varchar(255)); INSERT INTO orders VALUES (1, 42, 123.42, 'shop'); INSERT INTO orders VALUES (2, 21, 12.21, 'offline'); " ``` Use DuckDB [MySQL extension](https://duckdb.org/docs/lts/core_extensions/mysql) to copy "orders" table from MariaDB to a parquet file: ```php $db = new PDO('duckdb::memory:'); $db->exec('INSTALL mysql'); $db->exec("ATTACH 'host=127.0.0.1 port=3306 user=root password=secret database=testdb' AS testdb (TYPE mysql)"); $db->exec("COPY (select * from testdb.orders) TO '/tmp/orders.parquet' (FORMAT parquet)"); $rows = $db->query("SELECT * from '/tmp/orders.parquet'")->fetchAll(PDO::FETCH_ASSOC); print_r($rows); # Array # [0] => Array # [id] => 1 # [customerId] => 42 # [amount] => 123.42 # [origin] => shop # [1] => Array # [id] => 2 # [customerId] => 21 # [amount] => 12.21 # [origin] => offline ``` ## Copy data from PostgreSQL to Parquet Start PostgreSQL container, create and fill "orders" table: ```bash docker run --rm -it -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:18 PGPASSWORD=secret psql -h 127.0.0.1 -U postgres -c " CREATE TABLE orders (id integer primary key, customer integer, amount decimal(12, 2), origin varchar(255)); INSERT INTO orders VALUES (1, 42, 123.42, 'shop'); INSERT INTO orders VALUES (2, 21, 12.21, 'offline'); " ``` Use DuckDB [PostgreSQL extension](https://duckdb.org/docs/lts/core_extensions/postgres) to copy "orders" table from PostgreSQL to a parquet file: ```php $db = new PDO('duckdb::memory:'); $db->exec('INSTALL postgres'); $db->exec("ATTACH 'host=127.0.0.1 port=5432 user=postgres password=secret' AS testdb (TYPE postgres)"); $db->exec("COPY (select * from testdb.orders) TO '/tmp/orders.parquet' (FORMAT parquet)"); $rows = $db->query("SELECT * from '/tmp/orders.parquet'")->fetchAll(PDO::FETCH_ASSOC); print_r($rows); # Array # [0] => Array # [id] => 1 # [customerId] => 42 # [amount] => 123.42 # [origin] => shop # [1] => Array # [id] => 2 # [customerId] => 21 # [amount] => 12.21 # [origin] => offline ``` ## Read public data using HTTPs, JSON and CSV ```php $db = new PDO('duckdb::memory:'); $url = 'https://bulk.meteostat.net/v2/stations/lite.json.gz'; $rows = $db->query("select id, name.en from read_json('{$url}') WHERE name.en like '%Berlin%' limit 2"); echo json_encode($rows->fetchAll(PDO::FETCH_ASSOC)), PHP_EOL; $url = 'https://data.meteostat.net/hourly/2026/10381.csv.gz'; $rows = $db->query("select hour, temp from read_csv('{$url}') where year = 2026 and month = 7 and day = 25 and hour > 9 limit 4"); echo json_encode($rows->fetchAll(PDO::FETCH_ASSOC)), PHP_EOL; # [{"id":"10381","en":"Berlin \/ Dahlem"},{"id":"10382","en":"Berlin \/ Tegel"}] # [{"hour":10,"temp":24.1},{"hour":11,"temp":25.5},{"hour":12,"temp":26.4},{"hour":13,"temp":27.4}] ``` ## Community extensions Textplot brings text-based data visualization directly to your SQL queries: ```php $db = new PDO('duckdb::memory:'); $db->exec('INSTALL textplot FROM community'); $db->exec('LOAD textplot'); print_r($db->query(" SELECT tp_bar(0.8, thresholds := [ (0.7, 'green'), (0.5, 'yellow'), (0, 'red') ]) ")->fetchAll(PDO::FETCH_COLUMN)); # Array # [0] => ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉโฌœโฌœ print_r($db->query(" SELECT tp_bar(n, shape := 'circle', off_color := 'black', thresholds := [(0.8, 'green'), (0.65, 'orange'), (0.5, 'yellow'), (0.0, 'red')]) FROM (select random() as n from generate_series(4)); ")->fetchAll(PDO::FETCH_COLUMN)); # Array # [0] => ๐Ÿ”ด๐Ÿ”ด๐Ÿ”ด๐Ÿ”ดโšซโšซโšซโšซโšซโšซ # [1] => ๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸขโšซ # [2] => ๐ŸŸ ๐ŸŸ ๐ŸŸ ๐ŸŸ ๐ŸŸ ๐ŸŸ ๐ŸŸ โšซโšซโšซ # [3] => ๐ŸŸก๐ŸŸก๐ŸŸก๐ŸŸก๐ŸŸก๐ŸŸกโšซโšซโšซโšซ # [4] => ๐Ÿ”ดโšซโšซโšซโšซโšซโšซโšซโšซโšซ ``` open_prompt integrates LLMs into your SQL queries: ```php # ./llama-server -hf JetBrains/Mellum2-12B-A2.5B-Thinking-GGUF-Q4_K_M --parallel 1 --ctx-size 16384 --temp 0.6 --top-k 20 --reasoning off $db = new PDO('duckdb::memory:'); $db->exec('INSTALL open_prompt FROM community'); $db->exec('LOAD open_prompt'); $db->exec("SET VARIABLE openprompt_api_url = 'http://127.0.0.1:8080/v1/chat/completions'"); $db->exec('create table customers (id integer primary key, first_name varchar, last_name varchar, birth_date date)'); $result = $db->query(" SELECT open_prompt('write duckdb sql, no markdown, find customers older than 30, schema: ' || group_concat(sql)) FROM duckdb_tables()")->fetch(PDO::FETCH_COLUMN); echo $result, PHP_EOL; # SELECT * FROM customers WHERE age(birth_date) > 30; ``` More extensions: [List of Core Extensions](https://duckdb.org/docs/lts/core_extensions/overview), [List of Community Extensions](https://duckdb.org/community_extensions/list_of_extensions) ## Performance DuckDB is extremely fast when it comes to analytic queries.\ Here is an example with 10M rows, performing in __170ms on 4 threads with 128M ram__: ```sql .timer on /* generate 10M rows with random data */ COPY ( SELECT i, (random()*1_000)::decimal(11,2) as d1, (random()*1_000)::int as i1, to_hex((random()*100000)::int) as h1, to_timestamp((i+1_0000_000) * random() * 100)::timestamp as created FROM generate_series(10_000_000) s(i) ) TO '/tmp/test.parquet' (format parquet, compression zstd); /* Run Time (s): real 4.158 user 4.002094 sys 0.154674 */ SET threads = 4; SET memory_limit = '128M'; SELECT count(*), sum(i), avg(d1), stddev(i1), avg(length(h1)), avg(date_diff('day', current_date, created)) FROM '/tmp/test.parquet'; /* Run Time (s): real 0.170 user 0.616465 sys 0.051658 */ ``` ## Security ```sql # Disable extension loading SET autoload_known_extensions = false; SET autoinstall_known_extensions = false; SET allow_community_extensions = false; # Disable external file access, directory white listing SET allowed_directories = ['/tmp']; SET enable_external_access = false; # Resource limits SET threads = 4; SET memory_limit = '4GB'; SET max_temp_directory_size = '4GB'; https://duckdb.org/docs/lts/operations_manual/securing_duckdb/overview ``` ## Compile NTS ```bash git clone --depth=1 --branch=main https://github.com/thomas-0816/pdo-duckdb.git cd pdo_duckdb wget https://github.com/duckdb/duckdb/releases/download/v1.5.5/libduckdb-src.zip unzip -o libduckdb-src.zip duckdb.h duckdb.hpp -d ./ wget https://github.com/duckdb/duckdb/releases/download/v1.5.5/static-libs-linux-amd64.zip unzip -o static-libs-linux-amd64.zip libduckdb_static.a -d ./ phpize ./configure --with-pdo-duckdb make NO_INTERACTION=1 TEST_PHP_ARGS="--show-diff --show-clean -q" make test sudo make install sudo sh -c 'echo "extension=pdo_duckdb.so" > /etc/php/8.5/mods-available/pdo_duckdb.ini' sudo phpenmod pdo_duckdb php -m | grep duckdb php test.php ``` ## Compile ZTS ```bash git clone --depth=1 --branch=main https://github.com/thomas-0816/pdo-duckdb.git cd pdo_duckdb wget https://github.com/duckdb/duckdb/releases/download/v1.5.5/libduckdb-src.zip unzip -o libduckdb-src.zip duckdb.h duckdb.hpp -d ./ wget https://github.com/duckdb/duckdb/releases/download/v1.5.5/static-libs-linux-amd64.zip unzip -o static-libs-linux-amd64.zip libduckdb_static.a -d ./ phpize-zts ./configure --with-pdo-duckdb --with-php-config=php-config-zts make NO_INTERACTION=1 TEST_PHP_ARGS="--show-diff --show-clean -q" make test sudo make install sudo sh -c 'echo "extension=pdo_duckdb.so" > /etc/php-zts/conf.d/pdo_duckdb.ini' php-zts -m | grep duckdb php-zts test.php ``` ## Compile with PHP TrueAsync ```bash docker build --no-cache -f Dockerfile.trueasync2 -t pdo_duckdb_trueasync2 . docker run --rm -it pdo_duckdb_trueasync2 php -m docker run --rm -it -v $(pwd):/app pdo_duckdb_trueasync2 php test_trueasync.php ``` ## Why DuckDB? In-Process Architecture: Like SQLite, DuckDB embeds directly into host applications, eliminating the need for a separate server setup. Extreme Analytical Speed: It uses columnar storage and vectorized (batch) processing, running analytics 10โ€“100x faster than traditional row-oriented databases. "Larger-than-Memory" Processing: DuckDB gracefully spills data to disk, allowing you to process massive datasets (e.g., 50GB+) on a machine with minimal RAM (e.g., 1GB). File-Format Agnostic: It can query flat files (JSON, CSV, and Parquet) directly via SQL without needing to import or load the data into a database first. No Infrastructure Cost: It brings data warehouse-level performance to your local laptop or local server. DuckDB achieves blazing-fast analytical performance through its embedded, serverless multi-core architecture combined with columnar storage and vectorized execution. By executing queries directly within the host application, it eliminates serialization and network overhead, processing data in batches (vectors) rather than row-by-row for unparalleled speed. https://duckdb.org/why_duckdb Key Performance Advantages: Vectorized Query Execution: Unlike row-oriented engines, DuckDB processes data in cache-friendly batches (vectors). This allows modern hardware to operate on entire arrays of data simultaneously, drastically reducing CPU cycles per query. Columnar Storage: Data is stored by column rather than by row. For analytical queries that only require a few metrics, DuckDB only reads the relevant columns from disk/memory, saving massive amounts of I/O. Zero-Copy In-Process Engine: As an in-process database, DuckDB runs directly in the memory space of your application. Advanced Query Optimizer: DuckDB features an advanced query optimizer that handles filter pushdowns, unnesting of subqueries, and dynamic runtime filters. This ensures queries only scan necessary data and avoids full-table sorting when possible. Direct File Querying: You can query large datasets in open formats like Parquet and CSV directly on disk or in cloud storage (like AWS S3) without needing to import or convert the data first. ## Development ```bash # sanity check to detect crashes php -d extension=$(pwd)/modules/pdo_duckdb.so test.php php run-tests.php -d extension=$(pwd)/modules/pdo_duckdb.so --show-diff --show-clean -q php-zts run-tests.php -d extension=$(pwd)/modules/pdo_duckdb.so --show-diff --show-clean -q # test PHP 8.2-8.5 docker build --no-cache -f Dockerfile -t pdo_duckdb . docker run --rm -it pdo_duckdb make EXTRA_CFLAGS="-Wall -Wextra -Wno-unused-parameter" EXTRA_CXXFLAGS="-Wall -Wextra -Wno-unused-parameter" ``` ## AI Disclosure The C code is written by AI, the tests are written without AI. ## License MIT License