2021-05-14
开窗函数在clickhouse的用法
很多 SQL 一旦碰到“分组后还想保留明细行”这个需求,写法就会开始打结。
比如你想做这些事:
这时候,窗口函数就不是“高级语法加分项”了,而是最顺手的工具。
ClickHouse 官方文档对它的定义很清楚:窗口函数允许你针对一组与当前行相关的记录做计算,但不会像普通聚合那样把多行折叠成一行,明细仍然保留。Window functions | ClickHouse Docs
这句话特别值钱,因为它正好点出了窗口函数的气质:
既要算聚合,又不想丢行。
如果你以前总把窗口函数看成“有点复杂的聚合”,那很容易越写越乱。
更稳的理解方式是:
GROUP BY 是把行收拢比如一张销售表:
department | employee | amount
你想知道“每个员工金额减去部门平均值”,如果用普通聚合,最后只能拿到部门级结果;如果用窗口函数,就能做到:
这就是窗口函数最实用的地方。
官方文档在窗口函数页一开始给了一张支持情况表,这张表很有含金量。Window functions | ClickHouse Docs
里面几个特别值得先记住的点:
WINDOW 子句复用窗口定义ROWS 支持RANGE 支持,而且它是默认 frameGROUPS frame 不支持DateTime 的 RANGE OFFSET,不能直接写 INTERVAL 语法;官方建议用秒数来指定row_number、rank、dense_rank、percent_rank、cume_dist、lag/lead、ntile 都支持如果你只先记一句话,我会建议记这句:
ClickHouse 的窗口函数已经足够覆盖大部分分析 SQL,但 frame 细节一定要看清。
官方文档给的核心语法骨架大概就是下面这样:Window functions | ClickHouse Docs
aggregate_function(column_name)
OVER (
[PARTITION BY ...]
[ORDER BY ...]
[ROWS or RANGE ...]
)
或者先起一个窗口名:
SELECT ...
WINDOW w AS (
PARTITION BY ...
ORDER BY ...
ROWS BETWEEN ...
)
里面最核心的 4 个零件是:
PARTITION BYORDER BYROWS / RANGEWINDOW理解这 4 个以后,绝大多数窗口 SQL 都能顺下来。
PARTITION BY 的作用,就是决定窗口函数的“分组范围”。
官方示例里用 part_key 做分组,然后对每组内的 value 计算 groupArray(value),最终同一个分组里的每一行都能看到这组值的完整数组。Window functions | ClickHouse Docs
这个例子特别适合建立直觉:
PARTITION BY 决定“跟谁一起玩”一个很实用的部门例子:
SELECT
department,
employee,
amount,
avg(amount) OVER (PARTITION BY department) AS dept_avg,
amount - dept_avg AS diff_from_avg
FROM sales;
这个查询的味道就是:
这也是窗口函数最经典的一类用法。
只分组不排序时,你能做的事情有限。
一旦你想做下面这些操作:
ORDER BY 就必须站出来。
因为窗口里的“前”“后”“当前”这些概念,全都依赖排序。
举个最经典的例子:
SELECT
employee,
amount,
row_number() OVER (ORDER BY amount ASC) AS row_no,
rank() OVER (ORDER BY amount ASC) AS rk,
dense_rank() OVER (ORDER BY amount ASC) AS dense_rk
FROM sales;
官方文档也给了类似示例,展示 row_number、rank 和 dense_rank 在并列值出现时的区别。Window functions | ClickHouse Docs
简单说就是:
row_number():机械编号,不管并列rank():并列同名次,但会跳号dense_rank():并列同名次,不跳号这个差别在榜单、Top N、组内排序场景里非常常见。
这部分是很多人最容易半懂不懂的地方。
官方文档明确写了:
ROWS frame 支持RANGE frame 支持,而且是默认值如果你写:
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
意思更偏向:
“看当前行,再往前数两行。”
这很适合:
比如:
SELECT
user_id,
seq,
amount,
sum(amount) OVER (
PARTITION BY user_id
ORDER BY seq
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS rolling_sum_3_rows
FROM payments;
RANGE 就不是简单数几行了,而是看排序列的值范围。
官方文档还专门给了移动平均示例,比如“按最近 10 个单位范围”的平均值。Window functions | ClickHouse Docs
这在按数值区间、按秒级范围做分析时会很顺手。
但这里有一个 ClickHouse 细节特别值得记住:
官方文档明确说,针对 DateTime 的 RANGE OFFSET,不能直接写 INTERVAL 语法;建议直接写秒数,因为 RANGE 本身是对数值类型工作的。Window functions | ClickHouse Docs
也就是说,别写成你在别的数据库里熟悉的那种:
-- 这个思路在 ClickHouse 这里不要想当然照搬
RANGE BETWEEN INTERVAL 10 SECOND PRECEDING AND CURRENT ROW
更稳的是显式把排序依据变成数值语义,或者直接按文档建议的方式用秒数范围理解它。
官方文档里那张 frame 示意图其实非常好,核心就是这几个边界词:Window functions | ClickHouse Docs
UNBOUNDED PRECEDINGN PRECEDINGCURRENT ROWM FOLLOWINGUNBOUNDED FOLLOWING一个很常见的累计和写法:
SELECT
user_id,
seq,
amount,
sum(amount) OVER (
PARTITION BY user_id
ORDER BY seq
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_amount
FROM payments;
这个语义很清楚:
如果你看到 UNBOUNDED PRECEDING,脑子里就把它翻译成“从开头开始”,会舒服很多。
官方文档明确说明,所有聚合函数都支持在窗口 frame 上计算。Window functions | ClickHouse Docs
这意味着:
sumavgmaxmincountgroupArray都能直接用在 OVER (...) 上。
这是 ClickHouse 窗口函数最香的一点之一,因为分析查询里最常见的很多动作,本质上就是:
比如“每条订单旁边显示客户累计下单金额”:
SELECT
customer_id,
order_seq,
amount,
sum(amount) OVER (
PARTITION BY customer_id
ORDER BY order_seq
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS customer_running_total
FROM orders;
这类 SQL 在报表、用户行为分析、交易链路里都很常见。
官方文档把 percent_rank() 单独拎出来强调了一下,说它能高效计算分组内某个值的相对位置,而且能替代一段更啰嗦的手写 SQL。Window functions | ClickHouse Docs
这很适合:
而 cume_dist() 表示的是“当前值以下(含当前值)累计占比”。官方文档也明确说明了这一点。Window functions | ClickHouse Docs
如果你经常做“这条记录已经超过了多少比例的数据”这种分析,这两个函数会比手搓公式优雅很多。
这是 ClickHouse 窗口函数里一个非常值得单独讲的点。
官方文档写得很明确:
lag/lead(value, offset) 支持lagInFrame/leadInFrame 也支持lagInFrame/leadInFrame 会遵守当前 framelag/lead 一样的行为,需要把 frame 显式写成 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING这个区别非常关键。
简单记忆:
lag / lead:更像你熟悉的标准偏移lagInFrame / leadInFrame:偏移要受 frame 约束一个很适合看环比的写法:
SELECT
user_id,
seq,
amount,
lag(amount, 1) OVER (
PARTITION BY user_id
ORDER BY seq
) AS prev_amount,
amount - prev_amount AS diff_from_prev
FROM payments;
如果你换成 lagInFrame,那就要特别留意你当前 frame 到底怎么写,不然结果可能和你预期不一样。
官方文档确认 WINDOW 子句是支持的。Window functions | ClickHouse Docs
这在一条 SQL 里要重复用同一套窗口定义时,会非常省事。
比如:
SELECT
department,
employee,
amount,
avg(amount) OVER w AS dept_avg,
max(amount) OVER w AS dept_max,
rank() OVER w AS dept_rank
FROM sales
WINDOW w AS (
PARTITION BY department
ORDER BY amount DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
);
这比每个 OVER (...) 都重复写一遍窗口定义要干净很多,也更不容易在后期维护时改漏。
官方文档列出的 ClickHouse 专属窗口函数是 nonNegativeDerivative(metric_column, timestamp_column[, INTERVAL X UNITS])。Window functions | ClickHouse Docs
它的作用很适合计数器类指标,比如:
官方描述里提到:
0如果你做监控或时序分析,这个函数会比你自己一层层拼前后行差值舒服很多。
ClickHouse 官方 Python 集成页给的核心驱动是 clickhouse-connect,并且安装方式很直接:pip install clickhouse-connect。Python integration with ClickHouse Connect
官方文档也提到:
Clientimport clickhouse_connect
client = clickhouse_connect.get_client(
host='localhost',
port=8123,
username='default',
password=''
)
client.command("""
CREATE TABLE IF NOT EXISTS sales_demo
(
department String,
employee String,
seq UInt32,
amount UInt32
)
ENGINE = MergeTree
ORDER BY (department, seq)
""")
rows = [
['ops', 'amy', 1, 120],
['ops', 'amy', 2, 180],
['ops', 'ben', 3, 160],
['sales', 'carl', 1, 200],
['sales', 'dora', 2, 260],
['sales', 'emma', 3, 260],
]
client.insert('sales_demo', rows)
result = client.query("""
SELECT
department,
employee,
seq,
amount,
avg(amount) OVER (PARTITION BY department) AS dept_avg,
rank() OVER (PARTITION BY department ORDER BY amount DESC) AS dept_rank,
sum(amount) OVER (
PARTITION BY department
ORDER BY seq
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM sales_demo
ORDER BY department, seq
""")
for row in result.result_rows:
print(row)
这段 Python 的好处在于,你可以很直观地看到:
result = client.query("""
SELECT
department,
employee,
seq,
amount,
lag(amount, 1) OVER (
PARTITION BY department
ORDER BY seq
) AS prev_amount
FROM sales_demo
ORDER BY department, seq
""")
print(result.result_rows)