Conditions
The conditions are Laravel's. The grammar writes them as ClickHouse SQL, and the connection writes the values as ClickHouse literals. The examples use $query = DB::connection('clickhouse')->table('my_table') and show the SQL of toRawSql().
Basic conditions
$query->where('field_one', 'like', 'a%') // "field_one" like 'a%'
->where(['field_two' => 2, 'user_id' => 7]) // and ("field_two" = 2 and "user_id" = 7)
->orWhere([['field_two', '>', 10], ['user_id', 8]]) // or ("field_two" > 10 or "user_id" = 8)
->whereIn('id', [3], 'or'); // or "id" in (3)- An entry of an array is
[column, value],[column, operator, value]orcolumn => value. An array is one group in parentheses. - The operators are Laravel's, such as
=,<>,!=,<,like,not like,ilikeandnot ilike, in any letter case.in,betweenandis nullare no operators: usewhereIn(),whereBetween()andwhereNull(). - A closure that adds no condition is removed:
where('id', 1)->where(fn ($query) => $query)giveswhere "id" = 1.
Groups
A closure is one group in parentheses. It gets a new builder:
$query->where('a', 1)->where(fn ($query) => $query->where('b', 2)->orWhere('c', 3));
// select * from "my_table" where "a" = 1 and ("b" = 2 or "c" = 3)
$query->where('a', 1)->orWhere(fn ($query) => $query->where('b', 2)->where('c', 3));
// select * from "my_table" where "a" = 1 or ("b" = 2 and "c" = 3)WHERE, PREWHERE and HAVING
ClickHouse reads the columns of a PREWHERE condition first, and then the other columns of the rows that it matches only. Each where method of the builder has a preWhere form: put preWhere in place of where, as in preWhereIn(), orPreWhereColumn() or preWhereDate(). The form takes the arguments of the where method and adds its conditions to the PREWHERE clause:
$query->preWhere('event_date', '2024-01-05')
->orPreWhereIn('user_id', [7, 8])
->preWhereColumn('updated_at', '>', 'created_at')
->where('user_id', 7);
// select * from "my_table" prewhere "event_date" = '2024-01-05' or "user_id" in (7, 8)
// and "updated_at" > "created_at" where "user_id" = 7
$query->preWhere(fn ($query) => $query->where('a', 1)->orWhere('b', 2))->where('c', 3);
// select * from "my_table" prewhere ("a" = 1 or "b" = 2) where "c" = 3- The explicit methods are
preWhere(),orPreWhere(),preWhereRaw(),orPreWhereRaw(),preWhereIn(),orPreWhereIn(),preWhereNotIn(),orPreWhereNotIn(),preWhereNull(),orPreWhereNull(),preWhereNotNull(),orPreWhereNotNull(),preWhereBetween(),orPreWhereBetween(),preWhereNotBetween()andorPreWhereNotBetween(). The other forms, such aspreWhereEmpty(),preWhereAny()ororPreWhereLike(), call thewheremethod of the same name. A form without awheremethod throws aBadMethodCallException. - A closure in
preWhere()is one group, as inwhere().
The HAVING methods are Laravel's: having(), orHaving(), havingRaw(), orHavingRaw(), havingNull(), havingNotNull(), havingBetween(), havingNotBetween() and their or forms, and havingEmpty() and havingNotEmpty() of Empty strings and arrays:
$query->select('user_id', DB::raw('count() as c'))
->groupBy('user_id')
->having('c', '>', 1)
->orHavingRaw('c = 1000');
// select "user_id", count() as c from "my_table" group by "user_id" having "c" > 1 or c = 1000The raw methods add SQL as it is. Do not put user input into them. Give values as ? bindings.
Values
The connection writes PHP values as ClickHouse literals:
| PHP value | SQL |
|---|---|
DateTimeInterface, Carbon included | '2026-10-07 14:30:00'. See Dates and times. |
true, false | 1, 0 |
null | null |
| Float | All digits: 0.30000000000000004 for 0.1 + 0.2. nan, inf and -inf for NAN, INF and -INF. |
| String | A string literal, with ', ", \ and NUL escaped: 'it\'s' |
| Array | An array literal: ['a', 'b'] |
| Backed enum | Its value: 'active', 3 |
| Pure enum | Its name, quoted: 'Active' |
DB::raw() | Its SQL |
smi2 value object or expression, such as UInt64, UUID, MapType or Raw | A literal of its type, or its SQL: 18446744073709551615, map('k', 1), now() |
Other Stringable object, such as Str::of() | Its string, quoted |
$query->where('created_at', '>=', now()->subDay()) // "created_at" >= '2026-10-07 14:30:00'
->where('is_active', true) // and "is_active" = 1
->whereIn('status', [Status::Active, Status::Paused]); // and "status" in ('active', 'paused')In a condition, Laravel flattens an array value: where('tags', ['a', 'b']) compares with 'a', and whereRaw('tags = ?', [['a', 'b']]) throws, since it gives 2 bindings for 1 ?. Write an array with DB::raw(): where('tags', DB::raw("['a', 'b']")).
A value that the grammar cannot write, such as a stdClass object, throws an InvalidArgumentException before the request: Cannot render a value of type stdClass in a ClickHouse query. .... For an Eloquent model, give $model->getKey().
Floats
A condition compares the exact PHP float: where('ratio', 1 / 3) gives "ratio" = 0.3333333333333333. The inserts write all digits too, so the condition finds the row that insert(['ratio' => 1 / 3]) stored.
- A float that PHP calculates can miss a
Decimalvalue.where('price', 4.35 * 100)gives"price" = 434.99999999999994and does not find435.00. Round it:where('price', round(4.35 * 100, 2)), or give a string:where('price', '435.00'). - Named bindings in raw SQL, such as
:ratio, write 14 significant digits. See Named bindings. - No value is equal to NaN.
where('f', NAN)gives"f" = nan, which finds no row. UsewhereRaw('isNaN(f)').
For floats in update(), see Updates.
Raw SQL
Write SQL with DB::raw() or a raw method:
$query->where(DB::raw('field_two + 1'), 6);
// select * from "my_table" where field_two + 1 = 6
$query->whereRaw('isNaN(f) or f > ?', [10]);
// select * from "my_table" where isNaN(f) or f > 10Lists: IN and BETWEEN
whereIn() and whereNotIn() take a PHP array, a collection or another Arrayable, or a sub-query:
$query->whereIn('id', collect([1, 2, 3]));
// select * from "my_table" where "id" in (1, 2, 3)
$query->whereIn('user_id', fn ($query) => $query->select('id')->from('users')->where('banned', 1));
// select * from "my_table" where "user_id" in (select "id" from "users" where "banned" = 1)An empty list matches no row with IN and all rows with NOT IN, as in Laravel:
->whereIn('id', []) // where 0 = 1
->whereNotIn('id', []) // where 1 = 1
->where('field_two', '<', 0)->orWhereIn('id', []) // where "field_two" < 0 or 0 = 1DANGER
->whereNotIn('id', $ids)->delete() with an empty $ids deletes all rows. Check the list before a mutation.
whereBetween() and whereNotBetween() take the first two values of the list, with or without keys:
$query->whereBetween('id', [1, 10]); // "id" between 1 and 10
$query->whereNotBetween('id', ['from' => 1, 'to' => 10]); // "id" not between 1 and 10GLOBAL IN
On a Distributed table, GLOBAL IN runs the sub-query one time and sends its result to all shards:
$query->whereGlobalIn('user_id', fn ($query) => $query->select('id')->from('users'));
// select * from "my_table" where "user_id" global in (select "id" from "users")
$query->where('a', 1)->orWhereGlobalNotIn('user_id', [7]);
// select * from "my_table" where "a" = 1 or "user_id" global not in (7)whereGlobalIn(), orWhereGlobalIn(), whereGlobalNotIn() and orWhereGlobalNotIn() take the values of whereIn().
NULL checks
$query->whereNull('deleted_at')->orWhereNotNull(['email', 'phone']);
// select * from "my_table" where "deleted_at" is null or "email" is not null or "phone" is not null
$query->where('parent_id', null); // "parent_id" is null
$query->where('parent_id', '!=', null); // "parent_id" is not nullOnly a column whose type accepts NULL holds it, such as Nullable(...). On another column, is null matches no row.
Empty strings and arrays
$query->whereEmpty('tags')->orWhereNotEmpty('comment');
// select * from "my_table" where empty("tags") or notEmpty("comment")
$query->select('g')->groupBy('g')->havingNotEmpty('g');
// select "g" from "my_table" group by "g" having notEmpty("g")whereEmpty(),orWhereEmpty(),whereNotEmpty()andorWhereNotEmpty()take a column or a list. A list adds one condition for each column.havingEmpty(),orHavingEmpty(),havingNotEmpty()andorHavingNotEmpty()are theHAVINGforms.preWhereEmpty()is thePREWHEREform.- The functions work on
String,ArrayandMapcolumns.
Compare columns
$query->whereColumn('updated_at', 'created_at')
->whereColumn('updated_at', '>', 'created_at')
->orWhereColumn([['field_one', 'comment'], ['field_two', '<', 'user_id']]);
// select * from "my_table" where "updated_at" = "created_at" and "updated_at" > "created_at"
// or ("field_one" = "comment" or "field_two" < "user_id")
$query->whereColumn('field_two', '<', DB::raw('user_id + 1'));
// select * from "my_table" where "field_two" < user_id + 1
$query->whereBetweenColumns('field_two', ['user_id', DB::raw('user_id + 10')]);
// select * from "my_table" where "field_two" between "user_id" and user_id + 10whereNotBetweenColumns() writes not between. Each method has an or form.
Conditions on many columns
$query->whereAny(['field_one', 'comment'], 'ilike', '%error%')
->whereAll(['field_two', 'user_id'], '>', 0)
->whereNone(['field_one', 'comment'], 'x');
// select * from "my_table" where ("field_one" ilike '%error%' or "comment" ilike '%error%')
// and ("field_two" > 0 and "user_id" > 0) and not ("field_one" = 'x' or "comment" = 'x')An empty list of columns adds no condition, as in Laravel. Check the list before a mutation.
EXISTS sub-queries
$query->select('id')->whereExists(fn ($query) => $query->from('users')->where('banned', 1));
// select "id" from "my_table" where exists (select * from "users" where "banned" = 1)whereExists(),orWhereExists(),whereNotExists()andorWhereNotExists()take a closure or a query builder.- ClickHouse 24.8 leaves columns out of the queries that
INTERSECTandEXCEPTcompare when the outer query does not read all of them. For a sub-query that holdsINTERSECTorEXCEPT, or reads awithExpression()name of an outer query that holds them, the grammar addswhere not ignore(*), so that ClickHouse compares all columns. See INTERSECT and EXCEPT. - ClickHouse 24.8 has no correlated sub-queries. A sub-query cannot read a column of the outer query, as
whereColumn('orders.user_id', 'users.id')does. ClickHouse 26.3 and 26.8 run such aSELECT. For mutations, see Deletions.
Dates and times
$query->whereDate('created_at', '2024-01-05'); // toDate32("created_at") = '2024-01-05'whereDate(), whereTime(), whereDay(), whereMonth() and whereYear() are in Dates and times.
LIKE and ILIKE
$query->whereLike('field_one', 'error%'); // "field_one" ilike 'error%'
$query->whereLike('field_one', 'Error%', true); // "field_one" like 'Error%'
$query->whereNotLike('field_one', '%debug%'); // "field_one" not ilike '%debug%'whereLike() and whereNotLike() ignore case, as in Laravel. With true as the third argument, they compare case. ClickHouse can use the primary key for like 'prefix%', but not for ilike.
Bitwise and null-safe conditions
$query->where('field_two', '&', 4); // bitAnd("field_two", 4) != 0
$query->whereNullSafeEquals('comment', null); // ["comment"] = [null]- The bitwise operators
&,|,^,<<and>>callbitAnd(),bitOr(),bitXor(),bitShiftLeft()andbitShiftRight(), and compare the result with0.&~givesbitAnd("field_two", 4) != "field_two". whereNullSafeEquals()and the<=>operator hold when both sides are equal or both areNULL. ANULLvalue or an expression is compared in arrays,["comment"] = [null], so thatNULLequalsNULL. Another value is compared with=.
JSON paths
A JSON path, such as payload->user->name, reads a value of a JSON document in a String column, as json() creates it. It works in conditions, select(), orderBy() and groupBy():
$query->where('payload->user->name', 'Ann') // JSONExtractString("payload", 'user', 'name') = 'Ann'
->where('payload->size', '>', 2) // JSONExtract("payload", 'size', 'Nullable(Float64)') > 2
->where('payload->active', true) // JSONExtract("payload", 'active', 'Nullable(Bool)') = true
->where('payload->tags[0]', 'en') // JSONExtractString("payload", 'tags', 1) = 'en'
->whereNull('payload->deleted'); // JSONType("payload", 'deleted') = 'Null'
$query->select('id', 'payload->user as user')->orderBy('payload->color');
// select "id", JSONExtractString("payload", 'user') as "user" from "my_table" order by JSONExtractString("payload", 'color') asc- A value is read as a string: a string without its quotes, and a number, a boolean, an object or an array as its JSON text, such as
1.5,trueor{"a":1}.null, a missing key and a document that is no JSON give''. - A comparison with an int or a float reads the value as a
Float64, also inwhereIn()andwhereBetween(). A value that is no number, such as"large", is thenNULL, so no comparison holds for it,!=andwhereNotIn()included. A string that holds a number, such as"2", is read as that number. ClickHouse 26.3 and 26.8 readtrueandfalseas1and0; 24.8 reads them asNULL. trueandfalseread the value as aBool.whereNull()holds fornull, for a missing key and for a document that is no JSON.- An array index counts from
0, as in Laravel:[0]is the first element and[-1]the last. A key can hold any character, such as a space or a dot. - A native
JSONcolumn of ClickHouse 26.x is noString. Read it with its own syntax, such aswhereRaw('payload.user.name = ?', ['Ann']).
$query->whereJsonContains('payload->tags', 'en'); // hasAll(JSONExtract("payload", 'tags', 'Array(String)'), JSONExtract('[\"en\"]', 'Array(String)'))
$query->whereJsonContains('payload->tags', ['en', 'de']); // the array holds every value
$query->whereJsonOverlaps('payload->tags', ['fr', 'de']); // hasAny(...): the array holds one of the values
$query->whereJsonContainsKey('payload->user->name'); // JSONHas("payload", 'user', 'name')
$query->whereJsonLength('payload->tags', '>', 1); // JSONLength("payload", 'tags') > 1whereJsonContains()andwhereJsonOverlaps()read both arrays asArray(String),Array(Float64)orArray(Bool)when the values are all strings, all numbers or all booleans. Other values are compared as their JSON text. An array with keys, which is a JSON object, throws anInvalidArgumentException: ClickHouse cannot check that one object holds another.whereJsonContainsKey()holds for a key whose value isnull.whereJsonLength()counts the elements of an array or the keys of an object.- Each method has its
orform, and each method butwhereJsonLength()itsdoesntform, as in Laravel.
To set a JSON path, see Updates.
Full-text search
whereFullText() checks the tokens of a column. A text index of the column speeds it up. See Text indexes. The tokenizer of the index then splits the value and the column: with ngrams(3), whereFullText('title', 'qui') matches quick. The rules below hold for a column without a text index and for the default tokenizer, splitByNonAlpha.
$query->whereFullText('body', 'quick fox'); // hasAllTokens("body", 'quick fox')
$query->whereFullText(['title', 'body'], 'fox', ['mode' => 'any']); // (hasAnyTokens("title", 'fox') or hasAnyTokens("body", 'fox'))- The value is split into tokens at each ASCII character that is no letter or digit, such as a space,
-or_. Other characters, such asü, are part of a token:Grüßeis one token. Without a mode, every token must be in the column. With['mode' => 'any'], one of them. Tokens compare with their letter case:Lazydoes not matchlazy. Another mode, such as MySQL'sboolean, throws anInvalidArgumentException. - ClickHouse 26.3 and 26.8 have
hasAllTokens()andhasAnyTokens(). 24.8 does not, and the query fails withUNKNOWN_FUNCTION.
Vector search
Laravel's vector methods use the cosine distance, 1 - the cosine similarity, as PostgreSQL's pgvector:
$query->whereVectorSimilarTo('embedding', $vector, 0.8);
// where cosineDistance("embedding", CAST('[0.1,0.2,...]' AS Array(Float32))) <= 0.19999999999999996
// order by cosineDistance("embedding", CAST('[0.1,0.2,...]' AS Array(Float32))) asc
$query->select('id')->selectVectorDistance('embedding', $vector, 'distance')->orderByVectorDistance('embedding', $vector)->limit(10);- The maximum distance is
1 - 0.8, a PHP float, which the grammar writes with all its digits. - The column is an
Array(Float32), asvector()creates it. ClickHouse 24.8, 26.3 and 26.8 havecosineDistance(). - A vector similarity index of the column speeds up an
orderByVectorDistance()with alimit(). See Vector indexes. - A string as the vector calls Laravel's
toEmbeddings(), as in Laravel.
Random order
inRandomOrder() gives order by rand(). ClickHouse has no random function with a seed, so a seed throws an InvalidArgumentException. For an order that repeats, order by a hash: orderByRaw('cityHash64(?, id)', [$seed]).