genfay/laravel-clickhouse
A Laravel package for integrating ClickHouse, the column-oriented analytics database. It provides connection configuration and query-building support so you can run ClickHouse queries and use it alongside your Laravel app for fast reporting and analytics workloads.
Install the builder and its Laravel integration, then register a dedicated connection:
composer require the-tinderbox/clickhouse-builder
Add a connection in config/database.php alongside your primary database:
'clickhouse' => [
'driver' => 'clickhouse',
'host' => env('CLICKHOUSE_HOST', '127.0.0.1'),
'port' => env('CLICKHOUSE_PORT', 8123),
'database' => env('CLICKHOUSE_DATABASE', 'default'),
'username' => env('CLICKHOUSE_USERNAME', 'default'),
'password' => env('CLICKHOUSE_PASSWORD', ''),
'options' => [],
],
Look here first: the connection class is now Tinderbox\ClickhouseBuilder\Integrations\Laravel\Connection. As of 1.0.4 this is the canonical FQCN — the package no longer ships its own wrapper, so any use statement, service-provider binding, or 'class' key in a connection array pointing at an older, package-local class must be repointed. Grep for Clickhouse.*Connection before upgrading.
First use case: ad-hoc analytics against an events table without touching your OLTP database.
use Tinderbox\ClickhouseBuilder\Integrations\Laravel\Connection;
$ch = app(Connection::class); // or DB::connection('clickhouse')
$ch->select('SELECT count() FROM events WHERE date = today()');
For anything beyond a trivial query, drive it through the builder rather than raw SQL — that is where the package earns its keep.
Keep ClickHouse as a secondary connection. Never make it the default. Your day-to-day is: Eloquent/MySQL for transactional work, the ClickHouse connection for reporting, aggregation, and append-only event data. Explicit DB::connection('clickhouse') everywhere makes the boundary obvious in review.
Prefer the query builder over string SQL. The builder gives you ClickHouse-specific clauses that are awkward to hand-roll:
$ch->table('events')
->select(['user_id', 'count() as hits'])
->where('date', '>=', $from)
->prewhere('event_type', 'purchase') // cheap filter before reading columns
->groupBy('user_id')
->limit(100)
->get();
prewhere, final, sample, and per-query settings() are the clauses you will reach for most; they are the reason to use this package rather than a generic PDO wrapper.
Write in batches, not rows. ClickHouse punishes single-row inserts with too many parts. Aggregate in a queued job and insert a few thousand rows at a time. This is the single implementation decision that most affects production stability.
Do not model analytics tables with Eloquent. The builder is intentionally model-less. If you want a familiar surface, write a thin repository class per table that wraps the connection and returns plain arrays or DTOs. Treat it as read-mostly; Eloquent's save/delete semantics do not map onto ClickHouse.
Own your schema outside Laravel migrations. Run CREATE TABLE DDL through the connection in a dedicated artisan command or a deployment script. Laravel's schema builder does not understand MergeTree, partitioning, or ordering keys, and half-supported migration tooling is worse than none.
Testing: bind a fake or a dedicated test connection in phpunit.xml, and assert on generated SQL (->toSql()) for builder logic. Reserve real ClickHouse round-trips for an integration suite — most CI environments will not have it.
The 1.0.4 namespace change is the upgrade risk. It is a patch number, but swapping a package-local connection class for the upstream Tinderbox\ClickhouseBuilder\Integrations\Laravel\Connection will fatal on any stale reference. After upgrading: grep for old FQCNs, run php artisan config:clear (cached config will hold the old class array), and check any custom service provider that resolved the connection.
Port confusion is the most common setup failure. 8123 is the HTTP interface, 9000 is native TCP. The Laravel integration talks HTTP by default — if you paste a native port into config you get a hang, not a clear error.
No transactions. DB::transaction() on this connection will not do what you expect; ClickHouse has no transactional semantics across statements. Idempotent writes and deduplication keys are the substitute.
Mutations are asynchronous. UPDATE/DELETE either error or return before the data changes. Design tables as append-only and reconcile in queries. If your builder call silently "works" but the row count is unchanged, this is why.
Inserts return no meaningful lastInsertId. Do not build flows that depend on it.
Debugging: log queries on the clickhouse connection in local dev, and check system.query_log server-side for what actually executed — the builder's output and the server's interpretation can diverge on settings and clause ordering.
Extension points: the connection class is a normal Laravel connection, so you can subclass it, override getDefaultQueryGrammar() or the builder factory, and rebind via the container. This is the clean place to inject default settings() (e.g. max_execution_time, readonly) rather than repeating them on every call.
How can I help you explore Laravel packages today?