SET dialect = 'clickhouse' switches back. SET is the one SQL statement recognized while
the KQL dialect is active, so a session can always get out of it.
What is supported
This is a deliberately small subset. A KQL construct is either translated with the semantics Kusto documents, or rejected by name with a parse error — nothing is approximated silently. If a query parses, its result is meant to match Kusto’s. Sources: a table name,print, datatable, range, a parenthesized pipeline, and union.
A range goes from a number by a number, from a datetime by a timespan, or from a timespan by
a timespan.
Operators: where / filter, extend, project, project-away, project-keep,
project-rename, summarize, sort by / order by, take / limit, top, distinct,
count, mv-expand, join, union, as, render.
Scalar operators: ==, !=, <, <=, >, >=, =~, !~, in, in~, between,
contains, startswith, endswith, has, hasprefix, hassuffix, their _cs
(case-sensitive) and ! (negated) forms, has_any, has_all, and matches regex.
in and !in also take a tabular expression whose first column supplies the values
(x in (T | project key)); in~ takes only a list. A lone name inside in (...) that no
let binds reads as a column, since the parser has no schema to tell a column from a table;
bind the table with let, qualify it (db.table), or add a pipe to get the tabular form.
Statements: let, binding a scalar, a whole tabular expression, or a function:
T: (*) or T: (col: type, ...) which must come first. A tabular parameter that
names its columns shows the body only those columns of the argument, so a body reading an
undeclared column is rejected even when the concrete argument happens to have it; T: (*)
passes the argument through as it is. The declared types are enforced at the call boundary:
an argument (or a declared column of a tabular argument) whose type does not belong to the
declared KQL type is rejected, a lossless conversion such as long to real is applied, and
a value that does not fit the declared type — an int overflow, say — is an error rather
than a silent truncation. Arguments may be passed by
name in any order (f(c = 7, a = 12)). A body is any number of let statements followed by
one expression, and it can see the bindings that enclose it. A function whose body is a
pipeline is a table rather than a value, so it is rejected where an expression is expected -
in extend, where or print. A parameterless function may be
called with or without parentheses. view () is accepted and, since nothing here resolves
union * wildcards, means the same as (). Recursion is rejected, as it is in Kusto.
A let binds only for the statement that follows it, because one KQL statement is one
ClickHouse query — that is also what keeps a binding from leaking into a concurrent query. A
name two statements both need has to be bound twice.
Literals: strings (including verbatim @'...'), numbers, datetime(...), guid(...),
timespans such as 1d / 2.5h / 500ms, and dynamic([...]) arrays.
Around 130 scalar and aggregate functions are translated. As in Kusto, the aggregate
functions may only be called in the aggregation list of summarize; print count() is
rejected rather than passed through to the ClickHouse aggregate of the same name.
ClickHouse functions are reachable too. A name the KQL registry does not know is passed
through to ClickHouse under the spelling you wrote, so a query can use anything the server
offers:
range is the clearest case — range(1, 3, 1) is [1, 2, 3] in Kusto and [1, 2] in
ClickHouse, so writing it in KQL is an error rather than a wrong answer.
Coverage against the Kusto reference
Measured against Microsoft’s own indexes (tabular operators, scalar functions, aggregation functions):
The supported operators are the ones Microsoft’s own Learn common operators tutorial teaches,
plus
datatable, range, print, union and join — enough for the shape of query that
tutorial and the KQL Quick Reference build up to.
What is not supported
Rejected with a parse error, rather than mistranslated:- Operators:
search,parse,mv-apply,lookup,evaluate,invoke,facet,top-nested,make-series,sample,serialize,partition,rangeas an operator. - Functions: the
series_*family,bag_*/pack_*,parse_url,parse_csv,parse_json,todynamic,toscalar,format_timespan,format_datetime,extract_all,range, thepercentiles*family and therow_*window functions. (format_datetimeandextract_allare rejected rather than approximated: Kusto’syyyy-MM-ddformat specifiers are not ClickHouse’s, and Kusto’sextract_allreturns one array per capture group.) - Kusto names that collide with a ClickHouse function of a different meaning:
range(shown above),repeat,replace,translateandmaterialize. Passing them through would quietly compute something else — Kusto’srepeat(1, 3)is the array[1, 1, 1], while ClickHouse’srepeatrepeats a string — so each is rejected by name. - The geospatial functions that take or return GeoJSON — every
geo_*_to_central_point, and everything operating on polygons and lines. The point, geohash and H3 functions that work in plain longitude/latitude are supported.geo_point_to_s2cellis not: ClickHouse has no S2 token form. dynamicobjects (dynamic({"a": 1})), member access (x.y) and lookup by key (x['k']). Onlydynamicarrays are mapped, onto ClickHouseArray.dynamicas a declared type — in adatatableschema, atypeof(...)or a function parameter — is also rejected: the annotation carries no element type, so there is nothing faithful to map it to.- Cross-cluster and cross-database references such as
cluster(...)anddatabase(...). - Query and
joinhints (hint.strategy,hint.shufflekey, …). - Operator options:
mv-expand ... to typeof(T)/limit N/bagexpansion,summarizehints,union kind=/withsource=/isfuzzy=,join hint.*. - Wildcard column patterns in
project-awayandproject-keep(project-away Tmp*): expanding one needs the schema, which is not visible while parsing. Spell the columns out. - The
evaluateplugin mechanism entirely, and with itbag_unpack,pivot,narrow,python,Rand the rest. - Application statements:
alias database,declare pattern,declare query_parameters,restrict access to. - Obfuscated string literals (
h"...") and multi-line literals (triple backtick).
Behaviour worth knowing
- Timespans are
Intervalvalues.1dbecomestoIntervalNanosecond(86400000000000). Setinterval_output_format = 'kusto'to render them the Kusto way (1.00:00:00) rather than as a number. - Division follows Kusto:
7 / 2is3, because both operands are integers, and a timespan divided by a timespan is their real-valued ratio (15ms / 10msis1.5). This is implemented bykqlDivide, which decides from the argument types. - Subtracting two datetimes gives a number of seconds, where Kusto gives a timespan. Adding or subtracting a timespan works as expected.
sortdefaults to descending, unlike SQL, and puts nulls at the small end.project-renamemoves the renamed column to the end of the row. Kusto keeps its original position; reproducing that would require knowing the schema while parsing.unionrequires the operands to have compatible schemas. Kusto widens to the union of all columns and pads with nulls; ClickHouse’sUNION ALLdoes not.- String operators are matching functions, not
LIKEpatterns.contains '50%'looks for a literal per cent sign. geo_*takes longitude before latitude, as Kusto does.geo_distance_2pointsuses ClickHouse’sgreatCircleDistance, a fast approximation that differs from Kusto in the fourth significant figure — about 600 m over 1500 km — anduse_spheroid = trueselectsgeoDistance, the ellipsoid formula, as it does in Kusto. Exact agreement is not a goal: these functions are usually filtering rather than reporting. Note that a coordinate outside [-180, 180] or [-90, 90] yields a meaningless number rather than the null Kusto returns: neither ClickHouse function range-checks its arguments, and checking would cost eight comparisons per row.dayofweek()returns a timespan, not a number: a Monday is1.00:00:00.tohex()renders a negative value at 64-bit width. Kusto renders it at the width of the argument’s own type, which is not visible while parsing.- Datetimes are real
DateTime64values, so they print the ClickHouse way (2017-01-01 00:00:00) rather than Kusto’s2017-01-01T00:00:00.0000000. The previous implementation produced a formatted string, which looked like Kusto but did not compare or sort as a datetime. - ClickHouse’s parametric aggregates (
quantileExact(0.5)(x)) have no KQL spelling. Use a named alternative such asmedianExact(x).