Skip to content

ScoutQL functions

These are the names ScoutQL can call. There is no implicit aggregation: a column named on its own in SELECT is a raw value, and a number per group is always an aggregate you wrote.

That is the trade the language makes. It costs six extra characters to say COUNT(*), and in exchange every number in a report says what it is, derived statistics need no new vocabulary, and a query means the same thing here as it would in any SQL console.

An aggregate collapses the rows of a group into one number.

SignatureReturnsDescription
COUNT(*)BIGINTRow count. COUNT(*) counts rows, COUNT(x) non-NULL values, COUNT(DISTINCT x) distinct values.
COUNT(x)
COUNT(DISTINCT x)
SUM(x)BIGINT for integer input, DOUBLE otherwiseSum of a numeric expression. Booleans need an explicit cast: SUM(win::INT).
AVG(x)DOUBLEArithmetic mean. Rates are AVG(flag::INT) — e.g. AVG(win::INT) is the win rate.
MIN(x)same as the argumentSmallest value.
MAX(x)same as the argumentLargest value.
MEDIAN(x)DOUBLEMedian (50th percentile).
QUANTILE_CONT(x, q)DOUBLEContinuous (interpolated) quantile — QUANTILE_CONT(damage, 0.9) is the 90th percentile.
STDDEV(x)DOUBLESample standard deviation.

Three things apply to all of them.

FILTER (WHERE …) narrows one aggregate without narrowing the query. Wins, losses, and a solo-queue win rate can share a single pass over the same rows, which is both faster and directly comparable — three separate queries could each be measuring a different set of games.

SELECT COUNT(*) AS games,
COUNT(*) FILTER (WHERE win) AS wins,
AVG(win::INT) FILTER (WHERE queue = 'solo') AS solo_win_rate
FROM match_participants
WHERE game_creation_at >= CURRENT_TIMESTAMP - INTERVAL 30 DAY
GROUP BY player

DISTINCT counts unique values. COUNT(DISTINCT champion_id) is a champion pool size. It is not additive, so it cannot be accumulated across buckets with cumulative.

A rate is the average of a cast boolean. DuckDB will not average a BOOLEAN, so AVG(win::INT) is required — and the cast is what turns a count of wins into a fraction of games.

A scalar function transforms a single value, inside an aggregate’s argument, a grouping expression, or a predicate.

SignatureReturnsDescription
ROUND(x[, digits])DOUBLERound to a number of decimal places.
FLOOR(x)same numeric class as the argumentRound down. FLOOR(x / w) * w is the bucketing idiom for histograms.
CEIL(x)same numeric class as the argumentRound up.
ABS(x)same as the argumentAbsolute value.
COALESCE(a, b, …)common type of the argumentsFirst non-NULL argument.
NULLIF(a, b)type of the first argumentNULL when a = b, else a. x / NULLIF(y, 0) avoids division by zero.
GREATEST(a, b, …)common type of the argumentsRow-wise maximum of its arguments.
LEAST(a, b, …)common type of the argumentsRow-wise minimum of its arguments.
DATE_TRUNC('day'|'week'|'month', ts)TIMESTAMPTruncate a timestamp to a calendar bucket. Time-series queries GROUP BY DATE_TRUNC('week', game_creation_at).

FLOOR(x / width) * width is the bucketing idiom: dividing assigns the bucket, multiplying restores its real starting value, so the axis reads in the column’s own units rather than in bucket indexes.

Macros stand for an expression that is easy to get subtly wrong. They expand at compile time into ordinary SQL, so they behave exactly like the expression they name — including under FILTER (WHERE …).

SignatureReturnsDescription
kda()DOUBLEAggregate KDA: (SUM(kills) + SUM(assists)) / GREATEST(SUM(deaths), 1) — a perfect KDA shows takedowns.
per_minute(x)DOUBLERate per minute played: SUM(x) / NULLIF(SUM(time_played) / 60, 0).

kda() divides by at least one death, which is why a deathless streak still ranks by takedowns instead of dividing by zero. per_minute(x) guards the same way against a group with no recorded playtime.

These two resolve a human name to the thing the data actually stores.

SignatureReturnsDescription
player('name')BOOLEAN (WHERE only)Filter to one tracked player's rows: WHERE player('Bob') (or WHERE player = player('Bob')). The name resolves at run time.
champion('Name')INTEGER constant (a champion_id)Resolve a champion display name to its numeric id at compile time: WHERE champion_id = champion('Jinx').

champion('…') folds to a numeric id when the query compiles, so a misspelling is caught with a suggestion before the report ever runs. player('…') resolves when the report runs, so it follows renames and newly linked accounts on its own.

:: casts a value, and CAST(x AS INT) is the same thing written long. The casts that come up in practice are ::INT on a boolean (to average it), ::DOUBLE on an integer (to divide it without truncating), and ::DATE on a timestamp (to compare whole days).

Scout is an independent League of Legends companion.

Scout isn’t endorsed by Riot Games and doesn’t reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games and all associated properties are trademarks or registered trademarks of Riot Games, Inc.