SPL compatibility matrix
Phase zero. "Pushed" means the command compiles into the ClickHouse query; "in-process" means it runs in the engine on the query result. Everything not listed parses as a generic command and fails with "not supported yet".
Search expressions#
| Feature | Status | Notes |
|---|---|---|
implicit AND, OR, NOT, parentheses |
pushed | boolean keywords must be upper case, as in Splunk |
| free-text terms | pushed | case-insensitive; single tokens use hasTokenCaseInsensitive, phrases and punctuation use substring search |
| wildcards in terms and values | pushed | * only; compiled to ILIKE |
field=value, field!=value |
pushed | value compare is case-insensitive like Splunk; index= is exact |
field<n, <=, >, >= |
pushed | numeric when the value parses as a number, string otherwise |
field IN (a, b) |
pushed | |
earliest=, latest= |
pushed | relative modifiers with snap, epoch, MM/DD/YYYY:HH:MM:SS, ISO 8601; read in the search's time zone (tz in POST /api/search, --tz on the CLI, UTC by default): snaps land on local midnight across daylight-saving changes and a timestamp without an offset is local, as in Splunk |
subsearch [ ... ] |
parsed only | planner rejects it |
search later in the pipeline |
pushed | free-text terms need _raw to still be present |
Commands#
| Command | Status | Notes |
|---|---|---|
stats |
pushed | by-fields with null values are dropped, output sorted by by-fields, as Splunk does |
eval |
pushed | see functions below; each assignment sees earlier ones |
where |
pushed | |
fields +/- |
pushed | fields + keeps internal _ fields like Splunk; fields + wildcards match materialized columns only, fields - wildcards also hide search-time fields |
rename |
pushed | wildcards with the same number of * on both sides (rename client* AS src_*, rename avg(*) AS *); pairs apply in order; columns are renamed in SQL, search-time fields through the rename (in event output too) |
table |
pushed | |
sort |
pushed | default limit 10000 like Splunk; sort 0 for unlimited; mixed numeric/string ordering, which survives table and fields turning a search-time field into a string |
head |
pushed | head N, head limit=N, head (expr) with keeplast and null; an expression without a count has no default limit, and with a count the stricter one wins, as in Splunk; head (expr) cannot follow a timechart pivot yet |
dedup |
pushed | dedup N fields and sortby (+|-)field...; sortby decides which results are kept and orders the output, ties keep the incoming order; no keepevents, keepempty, consecutive yet |
top, rare |
pushed | limit (limit=0 for every value), showcount, showperc, countfield, percentfield, useother, otherstr, by; the OTHER row sums the count and percent of the values past the limit and comes last in its group |
timechart |
pushed, pivot in-process | span= or automatic; empty buckets filled; by yields one column per series; limit=N, limit=topN, limit=bottomN (default 10, 0 for all) keep the series with the largest (smallest) sum of the first aggregation, and useother (default true) with otherstr sums the rest into an OTHER series, as Splunk does; usenull (default true) and nullstr (default NULL) count events without the split field as a series of their own; cont=false leaves out the buckets no event fell in |
bin, bucket |
pushed | time spans and numeric spans |
rex |
pushed | named groups; max_match=N (0 for every match) makes multivalue fields; mode=sed with s/regex/replacement/flags (flags g, i; \1 to \9 in the replacement) and y/from/to/, any delimiter; RE2 syntax rather than PCRE; no s///N for the Nth match other than the first |
chart |
pushed, pivot in-process | chart agg... over x by y, by x y, by x, or no split; options before or after the split fields. Without a column field it is stats by the row field. With one, one column per value of it, rows sorted by the row field, and cells no result filled stay null (chart ... | fillnull value=0 fills them); limit=N, topN, bottomN (default 10, 0 for all), useother, otherstr as in timechart; usenull (default true) and nullstr (default NULL) count results without the column field under a column of their own. Results without the row field are dropped. Several aggregations give agg: value columns. span= buckets the row field first, as bin does (a time span on _time, a number on a number field). A where clause (where max in top3, where sum notin bottom2, where count > 100; count, sum, avg, min or max of the first aggregation's cells) keeps the columns that pass, in place of limit and without an OTHER column, as in Splunk. No bins, cont, format, sep or agg= yet |
eventstats |
pushed | every stats function, with by: window aggregates over the whole result or per group, added to every result in its order; a result without a BY field gets no values, as in Splunk; a field of the same name is replaced. No allnum yet |
streamstats |
pushed | every stats function, with by, current (default true), window=N (0 for every result so far) and global (default true: with by, the group's results among the last N of any group; false: the group's own last N), as window aggregates in result order (newest first for events, as Splunk streams them). A result without a BY field, or whose window is empty (the first with current=false), gets no values. reset_on_change=true (with by) starts the counts over where the BY values change from one result to the next; reset_before="(expr)" starts over at a result where the expression holds, reset_after="(expr)" after it; time_window=<span> (seconds to weeks, not months; with current=true and no window=) counts the results within that much time before each one, in the stream's time order. No allnum yet |
mvexpand |
pushed | mvexpand field [limit=N]: one result per value, in the multivalue field's order, the rest of the result copied (ARRAY JOIN); limit keeps the first N values. A multivalue field with no values keeps its result, with the field null; a single value stays as it is. A search-time field holding a JSON array expands into its elements |
spath |
pushed; in-process without a path once the events' fields are gone | input= (default _raw), output=, path= or a bare path: keys joined with ., {} for every element of an array (a multivalue field). A JSON string comes out unquoted, any other value as its JSON text, null or a missing path as no value. Without a path the input's JSON object joins the search-time fields, so later commands (SQL included) see its keys as a.b.c; after table, fields or a transforming command that runs in-process instead, with Splunk's names (a{}, a{}.b). {N} is the element N of an array, counting from 0 as mvindex does (a{0}.b, c{1}{0}), and no value past its end. JSON only, no XML |
fillnull |
pushed; in-process without a field list on events | value= (default 0) and a field list, whose fields are created where they do not exist, as in Splunk; without a field list every field is filled. A number column filled with a number stays a number, anything else becomes a string; a multivalue field with no values counts as null. Without a field list on events the fields to fill are only known per event, so it fills the flattened events in-process, and what follows runs in-process too (see below); after a pivot it runs in-process as well. No wildcards in the field list yet |
transaction, join, append, lookup |
not yet | parse as generic, rejected by the planner |
In-process execution#
Once a command has to run in the engine (the timechart and chart pivot,
fillnull without a field list on events, spath without a path once the
events' fields are gone), everything after it runs in the engine too, on the
rows the query returned. These commands have an in-process form, with the
semantics of their SQL form: search (terms against _raw, field
comparisons), where, eval (every function below), fields, table,
rename, head (with an expression too), sort, dedup, bin, rex
(extraction and mode=sed), stats, eventstats, streamstats (with
its resets and time_window), top, rare, timechart, chart,
fillnull, spath and mvexpand. A transforming command there turns the
output into a results table. The rest (transaction, lookup, ...) are
not built at all yet.
The in-process steps see only the rows the query returned: after
fillnull or spath on events that is at most the event cap (see
SEARCH-LIMITS.md), so an in-process stats over more
events than that counts only the newest ones, and the result says
limited. After a pivot the rows are the chart's, which is the usual case.
Known deviations in commands#
timechart,binand the timeline bucket time in UTC even when the search has another time zone, sospan=1dbuckets start at UTC midnight; Splunk uses the user's zone.timechart's andchart's OTHER series is the cell-by-cell sum of the series it replaces, whatever the aggregation, which is how Splunk filters series after aggregating; foravg,maxordcthat is a sum of averages, maxima or distinct counts.chart's NULL column (usenull=true) is ranked forlimitlike any other column, and so istimechart's NULL series.chart span=on a number field labels each bucket with its lower bound (0,10000), asbindoes; Splunk labels it as a range (0-10000).chart'swhereclause takesin topN,notin topN,in bottomN,notin bottomN, or a comparison to a number; Splunk'slimitinside it is not accepted.streamstats time_windowfollows the time order the results stream in (newest first for events, oldest first aftersort _time) and uses the_timefield, so it cannot follow a command that drops_time;reset_on_changecompares the BY values as the previous result had them, so a result without the BY field (which gets no values) still ends a run.spath's array index is{N}counting from 0, and{-1}is rejected; Splunk documents{N}without saying where it counts from.- A
rexwithmax_matchother than 1 that matches nothing gives an empty multivalue field rather than no field, because ClickHouse arrays cannot be null;mvcountof it is 0, not null. fillnullwithout a field list on events fills the fields of the events returned (at most the event cap), so a field that only appears in events past the cap is not created in the ones shown.spathwithout a path, while the events' search-time fields are still there, keeps a JSON array under its own key (tags, multivalue) rather than Splunk'stags{}, and does not flatten objects inside arrays (Splunk'sitems{}.id); the in-process form, aftertableorfields, uses Splunk's names.spathwithout a path does not overwrite a field the search already has as a column (hostand the other default fields, or one made byevalorrex): the column wins. Splunk overwrites it. Afields -or wildcardrenameof search-time fields beforespathalso applies to the fieldsspathextracts.spathwith a path gives numbers as ClickHouse prints them (1.50as1.5,1e3as1000); in-processspathkeeps the JSON text.- In-process
minandmaxcompare numerically when every value has a number and as strings otherwise, where SQL goes by the column's type;valuessorts as strings in both. In-processstdevandvarof a single value are null, where SQL givesnan. - In-process
stats,chartandtopkeep a search-time field's stored type in the BY values (astatusingested as a number comes out as a number), where SQL groups by the field's text. - In-process
searchmatches a free-text term against_rawthe way the query would (a bare word as a token, a phrase as a substring,*as a wildcard, without regard to case), andfield=valuewith the same case and wildcard rules;earliest=andlatest=are rejected there as they are in a latersearchin SQL. - A wildcard
renamethat moves a search-time field onto the name of a column the query already has (rename data.* AS *with a JSON keydata.host) keeps the column's value; Splunk overwrites it.
Stats functions#
count, count(field), count(eval(cond)), dc/distinct_count, estdc, sum, sumsq, avg/mean, min, max, median, perc<N>/p<N>/exactperc<N>, stdev, stdevp, var, varp, range, mode, first, last, earliest, latest, values, list, rate.
Known deviations:
min/maxon search-time fields compare numerically; Splunk falls back to lexicographic order when values are not numbers.first/lastuse_timeorder, which is what Splunk does for a time-ordered search but not after asort; the same goes foreventstatsandstreamstats.listkeeps the first 100 values, Splunk's defaultlist_maxsize;valueshas no cap.rate(x)is (latest x − earliest x) / (latest − earliest time) over the events that have x, null when those times are equal.
Eval functions#
if, case, coalesce, nullif, null, isnull, isnotnull, isnum, isint, isstr, true, false, exact, len, lower, upper, trim, ltrim, rtrim, substr, replace, split, mvcount, mvindex, mvjoin, mvfilter, round, abs, ceil/ceiling, floor, sqrt, exp, ln, log, pow, max, min, random, tonumber (with a base from 2 to 36), tostring (with "hex", "commas", "duration"), now, relative_time, strftime, strptime, match, like, cidrmatch, md5, sha1, sha256, sha512, urldecode, json_extract, spath.
Known deviations:
- String concatenation with
.treats null as an empty string. _timeinside eval is epoch seconds, as in Splunk;strftimeandstrptimetake Splunk's directives:%M,%hand%r, which ClickHouse reads differently, are translated, and the rest of the common set (%Y %y %m %d %e %H %I %S %p %j %a %b %T) means the same in both; Splunk's subsecond%Nis not supported.round(x, n)needs a literaln.relative_time(x, modifier), the base oftonumberand the format oftostringneed literals;relative_time,strftimeandstrptimework in the search's time zone.tostring(x, "commas")rounds to two decimals and drops trailing zeros (1,234.5);tostring(x, "duration")ignores fractions of a second and writesD+HH:MM:SSpast a day.mvfilterreads one field; with nothing left it gives an empty multivalue rather than null, asrexdoes.- Regular expressions in
match,replaceandrexare RE2; PCRE-only syntax such as lookbehind will fail.