I run a few projects that move a lot of data through ClickHouse. Telemetry pipelines, ad-hoc analytics, log ingest. The two pure-PHP options today are smi2/phpClickHouse (HTTP) and lizhichao/one-ck (binary TCP). HTTP gets you a 30-40% slowdown on hot loops; one-ck shipped its last release in 2020 and isn't keeping up with the column types ClickHouse keeps adding. The reference native binary client from ClickHouse themselves is C++ only.

A PHP extension already wraps that C++ client: SeasX/SeasClick. The last accepted external PR there is from 2020, and a handful of follow-up PRs have been sitting open since, including my own PR #4 from 2019 that added the fetch-mode constants (FETCH_ONE, FETCH_KEY_PAIR, FETCH_COLUMN, DATE_AS_STRINGS) and a SeasClickException class. When I finally got around to updating that PR to merge cleanly, I realised the extension was kinda dead and had a few issues. So I forked it, renamed it to php_clickhouse, swapped the vendored client (an old artpaul fork) for the official ClickHouse/clickhouse-cpp v2.6.1, and added the column types and connection knobs that had landed upstream since 2020: Date32, Time, Time64, DateTime64, Int128, UInt128, Decimal128, LowCardinality, Map, plus TLS, multi-endpoint failover, ZSTD compression, and query_id propagation.

Today I'm releasing 0.6.0. It's a security and correctness pass on top of the rename. I closed a SQL-injection class through the {placeholder} substitution path, added identifier validation to insert() and writeStart(), fixed the lifecycle so a failed __construct or an orphaned writeStart no longer crashes the next call, and patched two upstream bugs in clickhouse-cpp v2.6.1 locally (one with a PR queued upstream). Full notes are in the 0.6.0 GitHub release.

If you were running the original SeasClick, the API hasn't changed. The SeasClick and SeasClickException class names stay around as deprecated aliases. Update your use statements to ClickHouse and ClickHouseException and you're done.

How it works

php_clickhouse vendors clickhouse-cpp v2.6.1 directly into the extension's .so. LZ4, ZSTD, abseil's int128, and CityHash are vendored alongside it. No external library to link against, no cmake required, no runtime dependencies. The whole thing compiles into a single shared object. TLS is optional; building with --enable-clickhouse-openssl links the system OpenSSL.

The PHP API is small. One class, one exception:

$ch = new ClickHouse([
    "host"        => "127.0.0.1",
    "port"        => 9000,
    "database"    => "test",
    "user"        => "default",
    "passwd"      => "",
    "compression" => "lz4",   // or "zstd", "none", true, false
]);

$ch->execute("CREATE TABLE IF NOT EXISTS events (
    id UInt32, ts DateTime64(3), tag LowCardinality(String)
) ENGINE = Memory");

$ch->insert("events", ["id", "ts", "tag"], [
    [1, time(), "alpha"],
    [2, time(), "beta"],
]);

foreach ($ch->select("SELECT id, ts, tag FROM events ORDER BY id",
                     [], ClickHouse::DATE_AS_STRINGS) as $row) {
    print_r($row);
}

For ingest workloads that don't fit in memory, there's a streaming insert path:

$ch->writeStart("events", ["id", "ts", "tag"]);
foreach ($source as $batch) {
    $ch->write($batch);  // each call sends a Block on the wire
}
$ch->writeEnd();

select() accepts a fetch-mode bitmask of ClickHouse::FETCH_ONE, FETCH_KEY_PAIR, FETCH_COLUMN, and DATE_AS_STRINGS. The first three reshape the result; the fourth swaps int epoch values for Y-m-d H:i:s strings. execute() runs DDL. ping() round-trips a heartbeat without firing a query.

Performance

I benchmarked against smi2/phpClickHouse. PHP 8.4, ClickHouse 26.3, localhost loopback, Memory engine, single core. Each cell is wall-clock seconds for selectCount queries plus a single bulk insert of dataCount rows.

dataCount × selectCount × limit phpClickHouse (HTTP) php_clickhouse (uncompressed) php_clickhouse (LZ4) php_clickhouse (ZSTD)
10000 × 1 × 5000 0.112 0.085 0.074 0.023
10000 × 1 × 5000 0.104 0.030 0.024 0.081
10000 × 100 × 5000 0.298 0.263 0.209 0.218
10000 × 100 × 10000 0.303 0.210 0.265 0.215
1000 × 200 × 500 0.558 0.416 0.415 0.413
1000 × 200 × 1000 0.611 0.408 0.410 0.395
1000 × 500 × 500 1.428 1.063 0.976 0.982
1000 × 500 × 1000 1.383 0.959 1.025 1.030
1000 × 800 × 500 2.477 1.533 1.569 1.543
1000 × 800 × 1000 2.498 1.588 1.563 1.519

At high select counts (200, 500, 800), the native binary protocol runs 30-40% faster than HTTP. On the small-burst workload at the top of the table (dataCount=10000, selectCount=1), php_clickhouse with ZSTD or LZ4 is the fastest of the four. Reproducible scripts are in bench/.

The headline number isn't 15x like mdparser's was. HTTP isn't slow at any one thing; it's just spending an extra few hundred microseconds per query on framing, request headers, and JSON parsing. The native binary protocol skips most of that. The win is consistent and grows with concurrency, because HTTP keepalive can't avoid the per-query overhead.

Supported types

Category Types
Integers Int8..Int64, UInt8..UInt64, Int128, UInt128 (decimal-string round-trip)
Floats Float32, Float64
Decimal Decimal, Decimal32, Decimal64, Decimal128(P, S)
Strings String, FixedString(N), LowCardinality(String), LowCardinality(FixedString(N))
Dates and times Date, Date32, DateTime, DateTime64(N[, tz]), Time, Time64(N) (CH 25+)
Enums Enum8, Enum16
Network IPv4, IPv6, UUID
Container Array(T), Tuple, Nullable(T)
Map Map(String, String), Map(String, Int64), Map(String, UInt64), Map(String, Float64), Map(Int64, String)

What it doesn't do

php_clickhouse is scoped to what clickhouse-cpp v2.6.1 supports plus a thin PHP-facing layer. It doesn't cover the geo types (Point, Ring, Polygon, MultiPolygon). Map(K, V) arbitrary key/value pairs aren't there; the five common shapes above are. There's no Windows build for now, but it should work fine on Linux and macOS. ZTS PHP refuses to load with a clear error message, because the per-Client state lives in process-global maps that would race across worker threads.

If you need geo types or arbitrary Map keys, smi2/phpClickHouse remains the fallback until those are added in future releases.

Compatibility

PHP 7.4, 8.1, 8.2, 8.3, 8.4, 8.5 on Linux x86_64. CI runs the full suite on every version, plus an AddressSanitizer job on PHP 8.4. WSL2 works for development. The build needs a C++17 compiler (GCC 8+, Clang 7+). TLS adds a libssl-dev (or distro equivalent) requirement.

Installation

Via PIE:

pie install iliaal/php_clickhouse
# or with TLS:
pie install iliaal/php_clickhouse --enable-clickhouse-openssl

From source:

git clone https://github.com/iliaal/php_clickhouse.git
cd php_clickhouse
phpize
./configure --enable-clickhouse
make -j && sudo make install

Add extension=clickhouse.so to your php.ini and you're done.

Links

Add a comment