New Support for defining window functions in extensions was introduced in DuckDB 2.0. The API is part of the C++ extension API and may still change.
Every aggregate function can be used as a window function, but not every windowed operation is an aggregate. For example, fill interpolates missing values, a reduce(init, lambda) function could apply running updates without a frame, and a rank(value) function could compute the “hypothetical rank” of a value. Since DuckDB 2.0, window functions are catalog entries, so extensions can define new ones using the same API that the built-in window functions use.
Registering Window Functions
To define a window function, derive from the WindowFunction class and register it in the extension's load function using the ExtensionLoader:
DuckWeedFunction duckweed;
loader.RegisterFunction(duckweed);
Registered window functions are listed by duckdb_functions() with the function type window and can be looked up with the CLI's .manual dot command.
Binding
The WindowFunction constructor takes the function's name, argument types, return type, and callbacks. The optional bind callback receives the arguments along with the types of the window's ORDER BY and the argument ORDER BY clauses, so it can resolve the return type and add bind data. The following methods declare which modifiers the function supports:
| Method | Modifier | Default |
|---|---|---|
SetCanDistinct |
DISTINCT arguments |
false |
SetCanFilter |
FILTER clause |
false |
SetCanOrderBy |
ORDER BY arguments |
true |
SetCanExclude |
EXCLUDE clause |
false |
SetCanIgnoreNulls |
IGNORE NULLS |
true |
DuckDB 2.1 adds the
SetIsRankingmethod (default:false), which marks the function as a ranking function such asrankorrow_number. The planner uses it to exclude ranking functions from optimizations for degenerate frames, i.e., frames that only contain the current row. This property is currently only available on themainbranch of theduckdb/duckdbrepository and is not part of the DuckDB 2.0.0 release.
Blocking Interface
Window functions are typically evaluated in a blocking fashion: the window operator first partitions and sorts the input into hash groups, then evaluates the function on each hash group. A blocking window function implements the following callbacks:
| Callback | Description |
|---|---|
bounds |
Requests the bounds that the function needs: partition (PARTITION BY), peers (RANGE and GROUPS), valid values (NULL handling), and frame (BETWEEN). |
sharing |
Registers the argument expressions so that their values are shared with other window functions, either as a fully materialized collection or as chunks during the sink and evaluate phases. |
global, local |
Construct the global and thread-local states for a hash group. |
sink, finalize |
Make a first pass over the data to construct acceleration structures such as merge sort trees. This phase is fully parallel. There is no combine step, so updates to the global state must be synchronized, e.g., using locks. |
evaluate |
Computes the result Vector for a chunk of rows using the arguments, the index of the first row within the hash group, the requested bounds, and the global and local states. Evaluation is fully parallel, so the chunks are usually but not necessarily consecutive. |
Streaming Interface
Some window functions can also be streamed, i.e., evaluated one chunk at a time without materializing the input. DuckDB only considers streaming if the window has no PARTITION BY, no ORDER BY, and no EXCLUDE clause. For aggregates, this covers “running totals” (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). A window function opts in by implementing the following callbacks:
| Callback | Description |
|---|---|
can_stream |
Decides whether the window expression can be streamed, given the maximum number of rows that the operator can buffer. |
streaming_state |
Constructs the state, which derives from WindowExecutorStreamingState. Its offset field sets the buffering offset, which is how lead and lag are streamed. Streaming is single-threaded, so there is only one state. |
stream |
Computes the result Vector for a chunk of rows using the input, the buffered (delayed) input, and the state. |
Example
The loadable extension demo defines duckweed, a “fill down” window function that replaces NULL values with the last non-NULL value and implements both the blocking and the streaming interface. With the demo extension loaded, the following query uses the streaming interface:
SELECT i, m, duckweed(m) OVER () AS d
FROM (
SELECT i, if(i % 5, i, NULL) AS m
FROM range(8) t(i)
);
| i | m | d |
|---|---|---|
| 0 | NULL | NULL |
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 3 | 3 | 3 |
| 4 | 4 | 4 |
| 5 | NULL | 4 |
| 6 | 6 | 6 |
| 7 | 7 | 7 |
Adding PARTITION BY or ORDER BY to the OVER clause switches to the blocking interface.
For more details, see Richard Wesley's talk “Window Extensions: Defining New Window Functions”.