Window functions

Updated at:

Window functions perform calculations across a set of rows related to the current row. Unlike aggregate functions, window functions return one output row per input row — individual rows are not collapsed into groups.

Supported features

ApsaraDB for ClickHouse supports the standard SQL syntax for window functions. The following table shows which features are available.

FeatureSupported?
Ad hoc window specification (count(*) over (partition by id order by time desc))Yes
Expressions involving window functions, e.g., (count(*) over ()) / 2Yes
WINDOW clause (select ... from table window w as (partition by id))Yes
ROWS frameYes
RANGE frameYes (default)
INTERVAL syntax for DateTime RANGE OFFSET frameNo (specify the number of seconds instead; RANGE works with any numeric type)
GROUPS frameNo
Aggregate functions over a frame (sum(value) over (order by time))Yes (all aggregate functions are supported)
rank(), dense_rank(), row_number()Yes
percent_rank()Yes
cume_dist()Yes
lag / leadYes
ntile(buckets)Yes

Syntax

aggregate_function(column_name)
  OVER ([[PARTITION BY grouping_column] [ORDER BY sorting_column]
        [ROWS or RANGE expression_to_bound_rows_within_the_group]] | [window_name])
FROM table_name
WINDOW window_name AS (
  [PARTITION BY grouping_column]
  [ORDER BY sorting_column]
  [ROWS or RANGE expression_to_bound_rows_within_the_group]
)
ClauseDescription
PARTITION BYDivides the result set into groups. The window function is applied independently within each group.
ORDER BYDefines the row order within each partition for the window calculation.
ROWS or RANGEDefines the frame boundary — the subset of rows within a partition that the function operates on.
WINDOWNames a window definition so multiple expressions can reuse it.

Frame boundaries

The frame determines which rows are included in each calculation relative to the current row.

      PARTITION
┌─────────────────┐  <-- UNBOUNDED PRECEDING (beginning of partition)
│                 │
│=================│  <-- N PRECEDING  <─┐
│      N ROWS     │                     │  F
│  Before CURRENT │                     │  R
│~~~~~~~~~~~~~~~~~│  <-- CURRENT ROW    │  A
│     M ROWS      │                     │  M
│   After CURRENT │                     │  E
│=================│  <-- M FOLLOWING  <─┘
│                 │
└─────────────────┘  <-- UNBOUNDED FOLLOWING (end of partition)

Default frame behavior:

  • With no ORDER BY: the frame spans the entire partition (UNBOUNDED PRECEDING to UNBOUNDED FOLLOWING).

  • With ORDER BY: the frame spans from UNBOUNDED PRECEDING to the current row.

Declare frame boundaries explicitly when the default behavior could produce unexpected results.

Window-only functions

The following functions can only be used as window functions.

FunctionDescription
row_number()Returns the sequential number of the current row within its partition, starting from 1.
rank()Ranks the current row within its partition, with gaps for tied values.
dense_rank()Ranks the current row within its partition, without gaps for tied values. Alias: denseRank().
percent_rank()Returns the relative rank of the current row as a value between 0 and 1. Alias: percentRank().
cume_dist()Returns the cumulative distribution of the current row's value within its partition — the fraction of rows with values less than or equal to the current row.
ntile(buckets)Distributes rows into the specified number of buckets and returns the bucket number for each row. Use with ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
first_value(x)Returns the first value in the ordered frame.
last_value(x)Returns the last value in the ordered frame.
nth_value(x, offset)Returns the first non-NULL value at the nth row within the ordered frame.
lagInFrame(x)Returns the value at a specified number of rows before the current row within the frame.
leadInFrame(x)Returns the value at a specified number of rows after the current row within the frame.
For lag and lead behavior that respects the window frame, use lagInFrame and leadInFrame. To get behavior identical to standard lag/lead, use ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.

ClickHouse-specific function

nonNegativeDerivative(metric_column, timestamp_column[, INTERVAL X UNITS]) returns the non-negative derivative of metric_column over timestamp_column. The INTERVAL defaults to 1 second. Returns 0 for the first row. For subsequent rows, it computes (metric_i - metric_i-1) / (timestamp_i - timestamp_i-1) * interval.

Examples

Numbering rows

Use row_number(), rank(), and dense_rank() to assign sequence numbers to rows. The difference appears when rows have tied values.

CREATE TABLE salaries
(
    `team`     String,
    `player`   String,
    `salary`   UInt32,
    `position` String
)
ENGINE = Memory;

INSERT INTO salaries FORMAT Values
    ('Port Elizabeth Barbarians', 'Gary Chen',       195000, 'F'),
    ('New Coreystad Archdukes',   'Charles Juarez',  190000, 'F'),
    ('Port Elizabeth Barbarians', 'Michael Stanley', 150000, 'D'),
    ('New Coreystad Archdukes',   'Scott Harrison',  150000, 'D'),
    ('Port Elizabeth Barbarians', 'Robert George',   195000, 'M');

SELECT
    player,
    salary,
    row_number() OVER (ORDER BY salary ASC) AS row,
    rank()       OVER (ORDER BY salary ASC) AS rank,
    dense_rank() OVER (ORDER BY salary ASC) AS dense_rank
FROM salaries;
┌─player──────────┬─salary─┬─row─┬─rank─┬─dense_rank─┐
│ Michael Stanley │ 150000 │   1 │    1 │          1 │
│ Scott Harrison  │ 150000 │   2 │    1 │          1 │
│ Charles Juarez  │ 190000 │   3 │    3 │          2 │
│ Gary Chen       │ 195000 │   4 │    4 │          3 │
│ Robert George   │ 195000 │   5 │    4 │          3 │
└─────────────────┴────────┴─────┴──────┴────────────┘

row_number() increments for every row. rank() leaves gaps (jumps from 1 to 3 after the tie). dense_rank() has no gaps.

Maximum and total salary per department

Calculate each employee's salary as a percentage of department totals without losing per-employee rows.

CREATE TABLE employees
(
    `department`    String,
    `employee_name` String,
    `salary`        Float
)
ENGINE = Memory;

INSERT INTO employees FORMAT Values
    ('Finance', 'Jonh', 200),
    ('Finance', 'Joan', 210),
    ('Finance', 'Jean', 505),
    ('IT',      'Tim',  200),
    ('IT',      'Anna', 300),
    ('IT',      'Elen', 500);

SELECT
    department,
    employee_name AS emp,
    salary,
    max_salary_per_dep,
    total_salary_per_dep,
    round((salary / total_salary_per_dep) * 100, 2) AS `share_per_dep(%)`
FROM
(
    SELECT
        department,
        employee_name,
        salary,
        max(salary) OVER wndw AS max_salary_per_dep,
        sum(salary) OVER wndw AS total_salary_per_dep
    FROM employees
    WINDOW wndw AS (
        PARTITION BY department
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    )
    ORDER BY department ASC, employee_name ASC
);
┌─department─┬─emp──┬─salary─┬─max_salary_per_dep─┬─total_salary_per_dep─┬─share_per_dep(%)─┐
│ Finance    │ Jean │    505 │                505 │                  915 │            55.19 │
│ Finance    │ Joan │    210 │                505 │                  915 │            22.95 │
│ Finance    │ Jonh │    200 │                505 │                  915 │            21.86 │
│ IT         │ Anna │    300 │                500 │                 1000 │               30 │
│ IT         │ Elen │    500 │                500 │                 1000 │               50 │
│ IT         │ Tim  │    200 │                500 │                 1000 │               20 │
└────────────┴──────┴────────┴────────────────────┴──────────────────────┴──────────────────┘

The named WINDOW clause (wndw) lets both max() and sum() share the same partition definition without repeating it.

Cumulative sum

Track the running stock balance per item over time.

CREATE TABLE warehouse
(
    `item`  String,
    `ts`    DateTime,
    `value` Float
)
ENGINE = Memory;

INSERT INTO warehouse VALUES
    ('sku38', '2020-01-01', 9),
    ('sku38', '2020-02-01', 1),
    ('sku38', '2020-03-01', -4),
    ('sku1',  '2020-01-01', 1),
    ('sku1',  '2020-02-01', 1),
    ('sku1',  '2020-03-01', 1);

SELECT
    item,
    ts,
    value,
    sum(value) OVER (PARTITION BY item ORDER BY ts ASC) AS stock_balance
FROM warehouse
ORDER BY item ASC, ts ASC;
┌─item──┬──────────────────ts─┬─value─┬─stock_balance─┐
│ sku1  │ 2020-01-01 00:00:00 │     1 │             1 │
│ sku1  │ 2020-02-01 00:00:00 │     1 │             2 │
│ sku1  │ 2020-03-01 00:00:00 │     1 │             3 │
│ sku38 │ 2020-01-01 00:00:00 │     9 │             9 │
│ sku38 │ 2020-02-01 00:00:00 │     1 │            10 │
│ sku38 │ 2020-03-01 00:00:00 │    -4 │             6 │
└───────┴─────────────────────┴───────┴───────────────┘

With ORDER BY and no explicit frame, the default frame is UNBOUNDED PRECEDING to CURRENT ROW — exactly what a running total needs.

Moving average (per 3 rows)

Smooth sensor readings using a 3-row sliding window.

CREATE TABLE sensors
(
    `metric` String,
    `ts`     DateTime,
    `value`  Float
)
ENGINE = Memory;

INSERT INTO sensors VALUES
    ('cpu_temp', '2020-01-01 00:00:00', 87),
    ('cpu_temp', '2020-01-01 00:00:01', 77),
    ('cpu_temp', '2020-01-01 00:00:02', 93),
    ('cpu_temp', '2020-01-01 00:00:03', 87),
    ('cpu_temp', '2020-01-01 00:00:04', 87),
    ('cpu_temp', '2020-01-01 00:00:05', 87),
    ('cpu_temp', '2020-01-01 00:00:06', 87),
    ('cpu_temp', '2020-01-01 00:00:07', 87);

SELECT
    metric,
    ts,
    value,
    avg(value) OVER (
        PARTITION BY metric
        ORDER BY ts ASC
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS moving_avg_temp
FROM sensors
ORDER BY metric ASC, ts ASC;
┌─metric───┬──────────────────ts─┬─value─┬───moving_avg_temp─┐
│ cpu_temp │ 2020-01-01 00:00:00 │    87 │                87 │
│ cpu_temp │ 2020-01-01 00:00:01 │    77 │                82 │
│ cpu_temp │ 2020-01-01 00:00:02 │    93 │ 85.66666666666667 │
│ cpu_temp │ 2020-01-01 00:00:03 │    87 │ 85.66666666666667 │
│ cpu_temp │ 2020-01-01 00:00:04 │    87 │                89 │
│ cpu_temp │ 2020-01-01 00:00:05 │    87 │                87 │
│ cpu_temp │ 2020-01-01 00:00:06 │    87 │                87 │
│ cpu_temp │ 2020-01-01 00:00:07 │    87 │                87 │
└──────────┴─────────────────────┴───────┴───────────────────┘

ROWS BETWEEN 2 PRECEDING AND CURRENT ROW includes the current row and the two rows immediately before it — always exactly 3 rows except at the start of the partition.

Moving average (per 10 seconds)

Use RANGE instead of ROWS to define the window by value interval rather than row count.

SELECT
    metric,
    ts,
    value,
    avg(value) OVER (
        PARTITION BY metric
        ORDER BY ts
        RANGE BETWEEN 10 PRECEDING AND CURRENT ROW
    ) AS moving_avg_10_seconds_temp
FROM sensors
ORDER BY metric ASC, ts ASC;

RANGE BETWEEN 10 PRECEDING AND CURRENT ROW includes all rows whose ts value is within 10 seconds of the current row's ts. Because RANGE works with any numeric type, specify time offsets as seconds rather than using INTERVAL syntax.

Moving average (per 10 days)

To compute a day-level moving average from second-precision timestamps, apply toDate() in the ORDER BY clause. This converts the frame unit from seconds to days.

SELECT
    metric,
    ts,
    value,
    round(avg(value) OVER (
        PARTITION BY metric
        ORDER BY toDate(ts)
        RANGE BETWEEN 10 PRECEDING AND CURRENT ROW
    ), 2) AS moving_avg_10_days_temp
FROM sensors
ORDER BY metric ASC, ts ASC;

What's next

  • Window function reference — complete upstream documentation, including individual function pages for row_number, rank, dense_rank, first_value, last_value, nth_value, lag, lead, lagInFrame, leadInFrame, cume_dist, and percent_rank.