clickhouse--开窗函数(window function)的用法

目录

  • 数据准备
  • 开窗排序
    • 排序方法——rank()
    • 排序方法——dense_rank()
    • 排序方法——row_number()
  • 开窗聚合
    • 常规聚合
    • 开窗累计
      • 窗口参数设置实现累计
      • array join实现累计
        • 方法一 先排序后累计
        • 方法二 arraySort实现
  • 同比环比
    • lag 与 lead
    • 数据准备
    • 上个月环比
    • 上一年同比
      • 月份连续的情况
      • 月份不连续的情况
  • range between
    • 不设置order by
    • 设置order by
      • 时间列排序
      • 数值列排序
    • 累计
      • 按月实现累计
      • 按sales大小实现累计


盼星星盼月亮终于盼来了clickhouse开窗函数的功能,不过当前为试验阶段,不建议在生产环境中使用,可以先学习一下准备着,等官网去掉了实验标记就可以愉快的使用啦~
clickhouse官网window-functions
在部分clickhouse版本中尚未默认开启窗函数功能,可以通过参数设置开启:
SETTINGS allow_experimental_window_functions = 1


数据准备

首先我们把相关的数据准备一下,如下所示,建表并插入数据:

CREATE TABLE employee_salary_1
(
month Date,
name String ,
department String,
salary UInt32
) ENGINE = MergeTree()
partition by month
ORDER BY month

INSERT INTO employee_salary_1 VALUES
('2020-01-01', 'Ali', 'Sales', 6000),
('2020-01-01', 'Bob', 'Sales', 6000),
('2020-01-01', 'Cindy', 'Sales', 5000),
('2020-01-01', 'Davd', 'Finance', 8000),
('2020-01-01', 'Elena', 'Sales', 9000),
('2020-01-01', 'Fancy', 'Finance', 10000),
('2020-01-01', 'George', 'Finance', 10000),
('2020-01-01', 'Haffman', 'Marketing', 6000),
('2020-01-01', 'Ilaja', 'Marketing', 7000),
('2020-01-01', 'Joey', 'Sales', 8000)

可以看到建好的表数据如下:

select * from employee_salary_1;
结果如下:
┌──────month─┬─name────┬─department─┬─salary─┐
│ 2020-01-01 │ Ali     │ Sales      │   6000 │
│ 2020-01-01 │ Bob     │ Sales      │   6000 │
│ 2020-01-01 │ Cindy   │ Sales      │   5000 │
│ 2020-01-01 │ Davd    │ Finance    │   8000 │
│ 2020-01-01 │ Elena   │ Sales      │   9000 │
│ 2020-01-01 │ Fancy   │ Finance    │  10000 │
│ 2020-01-01 │ George  │ Finance    │  10000 │
│ 2020-01-01 │ Haffman │ Marketing  │   6000 │
│ 2020-01-01 │ Ilaja   │ Marketing  │   7000 │
│ 2020-01-01 │ Joey    │ Sales      │   8000 │
└────────────┴─────────┴────────────┴────────┘

接下来我们就可以用这张表来看看开窗函数的各种用法了。

开窗排序

对上面那张表按部门对不同职员进行薪资排序,得到每个职员的排序序号。

排序方法——rank()

rank方法允许并列排名,后续排名序号往后顺延。比如有两个第一,则接着后面就是第三了。我们先看一下下面的sql语句:

select 
	name, department, month, salary,
	rank() OVER w AS rank
from employee_salary_1
WINDOW w AS (partition by department ORDER BY salary desc)
SETTINGS allow_experimental_window_functions = 1

在该sql语句中WINDOW w AS (partition by department ORDER BY salary desc)这一行就是定义一个分组窗,partition by department是指按照部门进行分组,ORDER BY salary desc则是在分组内对salary按照从大到小进行排序,这个排序主要是为了让rank()函数依据该结果得到排序后的序号,排序后的结果如下所示:

┌─name────┬─department─┬──────month─┬─salary─┬─rank─┐
│ Fancy   │ Finance    │ 2020-01-01 │  10000 │    1 │
│ George  │ Finance    │ 2020-01-01 │  10000 │    1 │
│ Davd    │ Finance    │ 2020-01-01 │   8000 │    3 │
│ Ilaja   │ Marketing  │ 2020-01-01 │   7000 │    1 │
│ Haffman │ Marketing  │ 2020-01-01 │   6000 │    2 │
│ Elena   │ Sales      │ 2020-01-01 │   9000 │    1 │
│ Joey    │ Sales      │ 2020-01-01 │   8000 │    2 │
│ Ali     │ Sales      │ 2020-01-01 │   6000 │    3 │
│ Bob     │ Sales      │ 2020-01-01 │   6000 │    3 │
│ Cindy   │ Sales      │ 2020-01-01 │   5000 │    5 │
└─────────┴────────────┴────────────┴────────┴──────┘

从结果可以看到,rank函数得到的就是每个成员在各自部门分组内的排序序号,而序号也是不连续的。

排序方法——dense_rank()

dense_rank()方法允许出现并列排名,但是后续排名序号不顺延,也就是会出现连续的序号。如下所示为dense_rank的排序sql语句和结果:

select 
	name, department, month, salary,
	dense_rank() OVER w AS dense_rank
from employee_salary_1
WINDOW w AS (partition by department ORDER BY salary desc)
SETTINGS allow_experimental_window_functions = 1
┌─name────┬─department─┬──────month─┬─salary─┬─dense_rank─┐
│ Fancy   │ Finance    │ 2020-01-01 │  10000 │          1 │
│ George  │ Finance    │ 2020-01-01 │  10000 │          1 │
│ Davd    │ Finance    │ 2020-01-01 │   8000 │          2 │
│ Ilaja   │ Marketing  │ 2020-01-01 │   7000 │          1 │
│ Haffman │ Marketing  │ 2020-01-01 │   6000 │          2 │
│ Elena   │ Sales      │ 2020-01-01 │   9000 │          1 │
│ Joey    │ Sales      │ 2020-01-01 │   8000 │          2 │
│ Ali     │ Sales      │ 2020-01-01 │   6000 │          3 │
│ Bob     │ Sales      │ 2020-01-01 │   6000 │          3 │
│ Cindy   │ Sales      │ 2020-01-01 │   5000 │          4 │
└─────────┴────────────┴────────────┴────────┴────────────┘

排序方法——row_number()

row_number()不允许并列排名,所有序号需连续排列。其实准确的说row_number()方法并不是一个严格意义的排序方法,它的本质是获得每一行的行号,但在某些排序场景中还是可以用到该方法的。比如当常规排序后只想要保留第一条数据(并列的也只取一个),那么就可以用row_number()来解决这样的问题了。需要注意的是由于row_number()不允许出现并列的序号,那么对于并列的两行数据,重复执行的话结果行号可能会不一样。以下是执行结果:

select 
	name, department, month, salary,
	row_number() OVER w AS row_number
from employee_salary_1
WINDOW w AS (partition by department ORDER BY salary desc)
SETTINGS allow_experimental_window_functions = 1
┌─name────┬─department─┬──────month─┬─salary─┬─row_number─┐
│ Fancy   │ Finance    │ 2020-01-01 │  10000 │          1 │
│ George  │ Finance    │ 2020-01-01 │  10000 │          2 │
│ Davd    │ Finance    │ 2020-01-01 │   8000 │          3 │
│ Ilaja   │ Marketing  │ 2020-01-01 │   7000 │          1 │
│ Haffman │ Marketing  │ 2020-01-01 │   6000 │          2 │
│ Elena   │ Sales      │ 2020-01-01 │   9000 │          1 │
│ Joey    │ Sales      │ 2020-01-01 │   8000 │          2 │
│ Ali     │ Sales      │ 2020-01-01 │   6000 │          3 │
│ Bob     │ Sales      │ 2020-01-01 │   6000 │          4 │
│ Cindy   │ Sales      │ 2020-01-01 │   5000 │          5 │
└─────────┴────────────┴────────────┴────────┴────────────┘

开窗聚合

开窗功能除了用来进行组内排序,还经常用来进行组内的数据统计,比如求和、均值、最大值等。下面我们按部门对薪资进行统计分析。

常规聚合

常规聚合一般包含计算数据条数、最小值、最大值、总数、平均值等,实现如下:

select 
	name, department, month, salary,
	count(*) OVER w AS count,
	sum(salary) OVER w AS sum_wage,
	avg(salary) OVER w AS avg_wage,
	max(salary) OVER w AS max_wage,
	min(salary) OVER w AS min_wage
from employee_salary_1
WINDOW w AS (partition by department)
SETTINGS allow_experimental_window_functions = 1

观察一下sql语句我们会发现,在分组窗的定义语句中,只有partition by,却没有了刚刚说的order by。这是因为在统计聚合中,我们无需用到排序方法,因此在分组窗中也就无需对指定列排序了。统计结果如下(为了结果看起来规整删掉了部分小数,读者可自己运行看下结果):

┌─name────┬─department─┬──────month─┬─salary─┬─count─┬─sum_wage─┬─avg_wage─┬─max_wage─┬─min_wage─┐
│ Davd    │ Finance    │ 2020-01-01 │   8000 │     3 │    28000 │     9333 │    10000 │     8000 │
│ Fancy   │ Finance    │ 2020-01-01 │  10000 │     3 │    28000 │     9333 │    10000 │     8000 │
│ George  │ Finance    │ 2020-01-01 │  10000 │     3 │    28000 │     9333 │    10000 │     8000 │
│ Haffman │ Marketing  │ 2020-01-01 │   6000 │     2 │    13000 │     6500 │     7000 │     6000 │
│ Ilaja   │ Marketing  │ 2020-01-01 │   7000 │     2 │    13000 │     6500 │     7000 │     6000 │
│ Ali     │ Sales      │ 2020-01-01 │   6000 │     5 │    34000 │     6800 │     9000 │     5000 │
│ Bob     │ Sales      │ 2020-01-01 │   6000 │     5 │    34000 │     6800 │     9000 │     5000 │
│ Cindy   │ Sales      │ 2020-01-01 │   5000 │     5 │    34000 │     6800 │     9000 │     5000 │
│ Elena   │ Sales      │ 2020-01-01 │   9000 │     5 │    34000 │     6800 │     9000 │     5000 │
│ Joey    │ Sales      │ 2020-01-01 │   8000 │     5 │    34000 │     6800 │     9000 │     5000 │
└─────────┴────────────┴────────────┴────────┴───────┴──────────┴──────────┴──────────┴──────────┘

开窗累计

除了常规的统计值,在实际工作中,可能会碰到需要计算累计和的场景,例如计算累计分布。这时,我们就需要按行去累计某个指定指标值了。

窗口参数设置实现累计

clickhouse窗函数参数设置

首先介绍一下各种窗口参数含义:

  • UNBOUNDED :不受控的,无限的;
  • PRECEDING : 在…之前;
  • FOLLOWING: 在…之后;

rows between的相关参数

使用格式:rows between …… and ……

unbounded preceding 前面所有行
unbounded following 后面所有行
current row 当前行
n following 后面n行
n preceding 前面n行

学习了上面的窗口参数之后,我们来看下面的sql语句,其就是开窗排序并求累计和。

SELECT
    name,
    department,
    month,
    salary,
    row_number() OVER w AS row,
    sum(salary) OVER w AS sum_wage,
	sum(salary) over (partition by department ORDER BY salary DESC rows between unbounded preceding and current row) as sum_wage_2
FROM employee_salary_1
WINDOW  w AS (PARTITION BY department ORDER BY salary DESC )
SETTINGS allow_experimental_window_functions = 1

我们看到,在上面的sql语句中有一行和其他行很不一样,之前也没有见到这样的用法,就是这行:sum(salary) over (partition by department ORDER BY salary DESC rows between unbounded preceding and current row) as sum_wage_2。在该代码中,多了一个rows between unbounded preceding and current row,意思是从该分组内的第一行开始到当前行都纳入到计算中,那么在指定求和的话,该代码得到的就是一个累计值。而为什么不直接使用整段代码最后的分组窗,而要自己额外定义一个分组窗呢?这是因为在本段代码中还有其他统计值,他们是不需要进行累计的因此也不需要定义窗口范围(默认就是组内所有行)。有感兴趣的朋友可以自己试一下将其他统计值去掉,然后把实现累计功能的窗口定义放到WINDOW那一行去看下结果如何。
为了进行对比,笔者也将普通的求和结果放上去了,看下结果:

┌─name────┬─department─┬──────month─┬─salary─┬─row─┬─sum_wage─┬─sum_wage_2─┐
│ Fancy   │ Finance    │ 2020-01-01 │  10000 │   1 │    20000 │      10000 │
│ George  │ Finance    │ 2020-01-01 │  10000 │   2 │    20000 │      20000 │
│ Davd    │ Finance    │ 2020-01-01 │   8000 │   3 │    28000 │      28000 │
│ Ilaja   │ Marketing  │ 2020-01-01 │   7000 │   1 │     7000 │       7000 │
│ Haffman │ Marketing  │ 2020-01-01 │   6000 │   2 │    13000 │      13000 │
│ Elena   │ Sales      │ 2020-01-01 │   9000 │   1 │     9000 │       9000 │
│ Joey    │ Sales      │ 2020-01-01 │   8000 │   2 │    17000 │      17000 │
│ Ali     │ Sales      │ 2020-01-01 │   6000 │   3 │    29000 │      23000 │
│ Bob     │ Sales      │ 2020-01-01 │   6000 │   4 │    29000 │      29000 │
│ Cindy   │ Sales      │ 2020-01-01 │   5000 │   5 │    34000 │      34000 │
└─────────┴────────────┴────────────┴────────┴─────┴──────────┴────────────┘

从结果我们可以看到,sum_wage_2确实是在分组内进行累计求和,而与之对应的,sum_wage很奇怪,看起来似乎是累计,但是又出现有并列的结果。推测应该是因为ORDER BY salary DESC之后,默认是并列排序,因此同样序号的结果就被计算到一个累计值中了,这个也提醒大家注意在实际求和时,可千万别随便使用order by。

array join实现累计

在开窗函数出现之前对于累计功能,我们使用的是array join来实现,也将这种方法放在这里。

方法一 先排序后累计

这种方式比较繁琐,有好几层嵌套查询,先要进行排序,然后使用groupArray将列数据放到一个数组中,之后再用array join进行展开,展开的同时用arrayCumSum来获取数组中每个位置的累计值,具体大家看下代码研究下:

select name, department, month, wage, rank, sum_wage
from 
(select 
	department,
	groupArray(name) name,
	groupArray(month) month,
	groupArray(salary) salary
from 
(select *
from employee_salary_1
order by department, salary desc 
)
group by department
) 
array join 
	name, month, salary as wage, arrayCumSum(salary) as sum_wage, arrayEnumerate(salary) AS rank
┌─name────┬─department─┬──────month─┬──wage─┬─rank─┬─sum_wage─┐
│ Elena   │ Sales      │ 2020-01-01 │  9000 │    1 │     9000 │
│ Joey    │ Sales      │ 2020-01-01 │  8000 │    2 │    17000 │
│ Ali     │ Sales      │ 2020-01-01 │  6000 │    3 │    23000 │
│ Bob     │ Sales      │ 2020-01-01 │  6000 │    4 │    29000 │
│ Cindy   │ Sales      │ 2020-01-01 │  5000 │    5 │    34000 │
│ Fancy   │ Finance    │ 2020-01-01 │ 10000 │    1 │    10000 │
│ George  │ Finance    │ 2020-01-01 │ 10000 │    2 │    20000 │
│ Davd    │ Finance    │ 2020-01-01 │  8000 │    3 │    28000 │
│ Ilaja   │ Marketing  │ 2020-01-01 │  7000 │    1 │     7000 │
│ Haffman │ Marketing  │ 2020-01-01 │  6000 │    2 │    13000 │
└─────────┴────────────┴────────────┴───────┴──────┴──────────┘

方法二 arraySort实现

这种方式其实类似,但是可以减少一层查询,具体做法是在对数组进行展开的时候按照指定方式进行排序,这样就避免了一开始的内层嵌套排序查询。因为涉及到好几个array函数,可以先去看下官方文档中关于这些函数的用法。

select name_, department, month_, wage_sort, rank, sum_wage
from 
(select 
	department,
	groupArray(name) name,
	groupArray(month) month,
	groupArray(salary) salary
from employee_salary_1
group by department
) 
array join 
	arraySort((x, y) -> -y, name, salary) as name_, 
	arraySort((x, y) -> -y, month, salary) as month_, 
	arraySort((x) -> -x, salary) as wage_sort,
	arrayCumSum(arraySort((x) -> -x, salary)) as sum_wage,
	arrayEnumerate(arraySort((x) -> -x, salary)) AS rank
┌─name_───┬─department─┬─────month_─┬─wage_sort─┬─rank─┬─sum_wage─┐
│ Elena   │ Sales      │ 2020-01-01 │      9000 │    1 │     9000 │
│ Joey    │ Sales      │ 2020-01-01 │      8000 │    2 │    17000 │
│ Ali     │ Sales      │ 2020-01-01 │      6000 │    3 │    23000 │
│ Bob     │ Sales      │ 2020-01-01 │      6000 │    4 │    29000 │
│ Cindy   │ Sales      │ 2020-01-01 │      5000 │    5 │    34000 │
│ Fancy   │ Finance    │ 2020-01-01 │     10000 │    1 │    10000 │
│ George  │ Finance    │ 2020-01-01 │     10000 │    2 │    20000 │
│ Davd    │ Finance    │ 2020-01-01 │      8000 │    3 │    28000 │
│ Ilaja   │ Marketing  │ 2020-01-01 │      7000 │    1 │     7000 │
│ Haffman │ Marketing  │ 2020-01-01 │      6000 │    2 │    13000 │
└─────────┴────────────┴────────────┴───────────┴──────┴──────────┘

同比环比

lag 与 lead

lag(上一个), lead(下一个)在clickhouse开窗中尚未支持,可通过间接方式实现,通过指定计算的行范围来实现,如下所示:

SELECT
    department,
    month,
    name,
    salary,
    any(salary) OVER (PARTITION BY department ORDER BY salary DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS lag,
    any(salary) OVER (PARTITION BY department ORDER BY salary DESC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS lead
FROM employee_salary_1
SETTINGS allow_experimental_window_functions = 1
┌─department─┬──────month─┬─name────┬─salary─┬───lag─┬──lead─┐
│ Finance    │ 2020-01-01 │ Fancy   │  10000 │     0 │ 10000 │
│ Finance    │ 2020-01-01 │ George  │  10000 │ 10000 │  8000 │
│ Finance    │ 2020-01-01 │ Davd    │   8000 │ 10000 │     0 │
│ Marketing  │ 2020-01-01 │ Ilaja   │   7000 │     0 │  6000 │
│ Marketing  │ 2020-01-01 │ Haffman │   6000 │  7000 │     0 │
│ Sales      │ 2020-01-01 │ Elena   │   9000 │     0 │  8000 │
│ Sales      │ 2020-01-01 │ Joey    │   8000 │  9000 │  6000 │
│ Sales      │ 2020-01-01 │ Ali     │   6000 │  8000 │  6000 │
│ Sales      │ 2020-01-01 │ Bob     │   6000 │  6000 │  5000 │
│ Sales      │ 2020-01-01 │ Cindy   │   5000 │  6000 │     0 │
└────────────┴────────────┴─────────┴────────┴───────┴───────┘

需要注意的是neighbor()这个方法也可以用在环比同比中,感兴趣的可以研究下。

数据准备

为了测试同比环比的方法,我们先准备如下数据:

CREATE TABLE commodity_sales
(
	month Date,
	goods String,
	sales UInt32
) ENGINE = MergeTree()
partition by month
ORDER BY month

INSERT INTO commodity_sales VALUES
('2020-01-01', 'apple', 8000),('2020-01-01', 'orange', 7000),
('2020-01-01', 'banana', 6500),('2020-02-01', 'apple', 5000),
('2020-02-01', 'orange', 5000),('2020-02-01', 'banana', 5000),
('2020-03-01', 'apple', 6000),('2020-03-01', 'orange', 5000),
('2020-03-01', 'banana', 5500),('2020-04-01', 'apple', 7000),
('2020-04-01', 'orange', 6000),('2020-04-01', 'banana', 6500),
('2020-05-01', 'apple', 7000),('2020-05-01', 'orange', 6000),
('2020-05-01', 'banana', 7000),('2020-06-01', 'apple', 6700),
('2020-06-01', 'orange', 6700),('2020-06-01', 'banana', 7700),
('2020-07-01', 'apple', 9000),('2020-07-01', 'orange', 6000),
('2020-07-01', 'banana', 7200),('2020-08-01', 'apple', 9000),
('2020-08-01', 'banana', 6500),('2020-09-01', 'apple', 7000),
('2020-09-01', 'banana', 7000),('2020-10-01', 'apple', 9000),
('2020-10-01', 'banana', 7800),('2020-11-01', 'apple', 7000),
('2020-11-01', 'banana', 7400),('2020-12-01', 'apple', 8000),
('2020-12-01', 'banana', 7500),
('2021-01-01', 'apple', 9000),('2021-01-01', 'orange', 8000),
('2021-01-01', 'banana', 8500),('2021-02-01', 'apple', 9500),
('2021-02-01', 'orange', 9000),('2021-02-01', 'banana', 9500),
('2021-03-01', 'apple', 9500),('2021-03-01', 'orange', 8000),
('2021-03-01', 'banana', 9500),('2021-04-01', 'apple', 9000),
('2021-04-01', 'orange', 8000),('2021-04-01', 'banana', 7500),
('2021-05-01', 'apple', 10000),('2021-05-01', 'orange', 9000),
('2021-05-01', 'banana', 8500),('2021-06-01', 'apple', 10000),
('2021-06-01', 'orange', 9000),('2021-06-01', 'banana', 9000),
('2021-07-01', 'apple', 11000),('2021-07-01', 'orange', 10000),
('2021-07-01', 'banana', 9500)

上个月环比

开窗在组内获取每条数据指定列的上个月结果,从而计算当月环比上月增长率等指标。

SELECT
    goods, month, sales,
    any(sales) OVER (PARTITION BY goods ORDER BY month ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS lag
FROM commodity_sales
WHERE goods = 'apple'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock

由于本例中月份是连续的,因此环比仅需要获取相邻的上一个月份结果再进一步处理即可。结果如下:

┌─goods─┬──────month─┬─sales─┬───lag─┐
│ apple │ 2020-01-01 │  8000 │     0 │
│ apple │ 2020-02-01 │  5000 │  8000 │
│ apple │ 2020-03-01 │  6000 │  5000 │
│ apple │ 2020-04-01 │  7000 │  6000 │
│ apple │ 2020-05-01 │  7000 │  7000 │
│ apple │ 2020-06-01 │  6700 │  7000 │
│ apple │ 2020-07-01 │  9000 │  6700 │
│ apple │ 2020-08-01 │  9000 │  9000 │
│ apple │ 2020-09-01 │  7000 │  9000 │
│ apple │ 2020-10-01 │  9000 │  7000 │
│ apple │ 2020-11-01 │  7000 │  9000 │
│ apple │ 2020-12-01 │  8000 │  7000 │
│ apple │ 2021-01-01 │  9000 │  8000 │
│ apple │ 2021-02-01 │  9500 │  9000 │
│ apple │ 2021-03-01 │  9500 │  9500 │
│ apple │ 2021-04-01 │  9000 │  9500 │
│ apple │ 2021-05-01 │ 10000 │  9000 │
│ apple │ 2021-06-01 │ 10000 │ 10000 │
│ apple │ 2021-07-01 │ 11000 │ 10000 │
└───────┴────────────┴───────┴───────┘

上一年同比

获取去年相同月份的值。

月份连续的情况

在数据月份连续的情况下,仅需要获取12个月以前的数据,具体到代码中就是往前数12行,代码如下:

SELECT
    goods, month, sales,
    any(sales) OVER (PARTITION BY goods ORDER BY month ROWS BETWEEN 12 PRECEDING AND 12 PRECEDING) AS lag
FROM commodity_sales
WHERE goods = 'apple'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock
┌─goods─┬──────month─┬─sales─┬──lag─┐
│ apple │ 2020-01-01 │  8000 │    0 │
│ apple │ 2020-02-01 │  5000 │    0 │
│ apple │ 2020-03-01 │  6000 │    0 │
│ apple │ 2020-04-01 │  7000 │    0 │
│ apple │ 2020-05-01 │  7000 │    0 │
│ apple │ 2020-06-01 │  6700 │    0 │
│ apple │ 2020-07-01 │  9000 │    0 │
│ apple │ 2020-08-01 │  9000 │    0 │
│ apple │ 2020-09-01 │  7000 │    0 │
│ apple │ 2020-10-01 │  9000 │    0 │
│ apple │ 2020-11-01 │  7000 │    0 │
│ apple │ 2020-12-01 │  8000 │    0 │
│ apple │ 2021-01-01 │  9000 │ 8000 │
│ apple │ 2021-02-01 │  9500 │ 5000 │
│ apple │ 2021-03-01 │  9500 │ 6000 │
│ apple │ 2021-04-01 │  9000 │ 7000 │
│ apple │ 2021-05-01 │ 10000 │ 7000 │
│ apple │ 2021-06-01 │ 10000 │ 6700 │
│ apple │ 2021-07-01 │ 11000 │ 9000 │
└───────┴────────────┴───────┴──────┘

从结果中发现,对于没有同比结果的行,clickhouse默认给了一个数值0。

月份不连续的情况

理想的情况是数据按月连续,但实际中也可能出现月份不连续的情况,先看下数据:

select * from commodity_sales where goods='orange' order by month FORMAT PrettyCompactMonoBlock;
┌──────month─┬─goods──┬─sales─┐
│ 2020-01-01 │ orange │  7000 │
│ 2020-02-01 │ orange │  5000 │
│ 2020-03-01 │ orange │  5000 │
│ 2020-04-01 │ orange │  6000 │
│ 2020-05-01 │ orange │  6000 │
│ 2020-06-01 │ orange │  6700 │
│ 2020-07-01 │ orange │  6000 │
│ 2021-01-01 │ orange │  8000 │
│ 2021-02-01 │ orange │  9000 │
│ 2021-03-01 │ orange │  8000 │
│ 2021-04-01 │ orange │  8000 │
│ 2021-05-01 │ orange │  9000 │
│ 2021-06-01 │ orange │  9000 │
│ 2021-07-01 │ orange │ 10000 │
└────────────┴────────┴───────┘

对于这种数据,我们通过表连接的方式来得到同比数据,具体实现方式如下:

SELECT
    t1.month,
    t1.goods,
    t1.sales,
    last_sales
FROM
(
    SELECT *
    FROM commodity_sales
    WHERE goods = 'orange'
) AS t1
LEFT JOIN
(
    SELECT
        addMonths(month, 12) AS month,
        goods,
        sales AS last_sales
    FROM commodity_sales
    WHERE goods = 'orange'
    ORDER BY month ASC
) AS t2 ON (t1.month = t2.month) AND (t1.goods = t2.goods)
ORDER BY month ASC
FORMAT PrettyCompactMonoBlock
┌──────month─┬─goods──┬─sales─┬─last_sales─┐
│ 2020-01-01 │ orange │  7000 │          0 │
│ 2020-02-01 │ orange │  5000 │          0 │
│ 2020-03-01 │ orange │  5000 │          0 │
│ 2020-04-01 │ orange │  6000 │          0 │
│ 2020-05-01 │ orange │  6000 │          0 │
│ 2020-06-01 │ orange │  6700 │          0 │
│ 2020-07-01 │ orange │  6000 │          0 │
│ 2021-01-01 │ orange │  8000 │       7000 │
│ 2021-02-01 │ orange │  9000 │       5000 │
│ 2021-03-01 │ orange │  8000 │       5000 │
│ 2021-04-01 │ orange │  8000 │       6000 │
│ 2021-05-01 │ orange │  9000 │       6000 │
│ 2021-06-01 │ orange │  9000 │       6700 │
│ 2021-07-01 │ orange │ 10000 │       6000 │
└────────────┴────────┴───────┴────────────┘

进一步的,我们将月份格式化为年月格式,并计算同比增长率,实现如下:

SELECT
    t1.now_month,
    now_sales,
    last_sales,
    multiIf(isNull(last_sales) OR (last_sales = 0), 0, round((now_sales - last_sales) / last_sales, 2)) AS ratio
FROM
(
    SELECT
        formatDateTime(month, '%Y-%m') AS now_month,
        sum(sales) AS now_sales
    FROM commodity_sales
    WHERE goods = 'orange'
    GROUP BY formatDateTime(month, '%Y-%m')
    ORDER BY now_month ASC
) AS t1
LEFT JOIN
(
    SELECT
        formatDateTime(addMonths(month, 12), '%Y-%m') AS now_month,
        sum(sales) AS last_sales
    FROM commodity_sales
    WHERE goods = 'orange'
    GROUP BY formatDateTime(addMonths(month, 12), '%Y-%m')
    ORDER BY now_month ASC
) AS t2 ON t1.now_month = t2.now_month
ORDER BY now_month ASC
┌─now_month─┬─now_sales─┬─last_sales─┬─ratio─┐
│ 2020-01   │      7000 │          0 │     0 │
│ 2020-02   │      5000 │          0 │     0 │
│ 2020-03   │      5000 │          0 │     0 │
│ 2020-04   │      6000 │          0 │     0 │
│ 2020-05   │      6000 │          0 │     0 │
│ 2020-06   │      6700 │          0 │     0 │
│ 2020-07   │      6000 │          0 │     0 │
│ 2021-01   │      8000 │       7000 │  0.14 │
│ 2021-02   │      9000 │       5000 │   0.8 │
│ 2021-03   │      8000 │       5000 │   0.6 │
│ 2021-04   │      8000 │       6000 │  0.33 │
│ 2021-05   │      9000 │       6000 │   0.5 │
│ 2021-06   │      9000 │       6700 │  0.34 │
│ 2021-07   │     10000 │       6000 │  0.67 │
└───────────┴───────────┴────────────┴───────┘

range between

clickhouse window function
前面我们用到的各种开窗功能基本上都是使用rows between实现,其实clickhouse还有个range between,我们也来了解一下。

不设置order by

首先看一个简单的例子,开窗求和,但窗范围限制在当前行,结果如下:

SELECT
    goods,
    month,
    sales,
    sum(sales) OVER (PARTITION BY goods RANGE BETWEEN CURRENT ROW AND CURRENT ROW) AS sum_sales
FROM commodity_sales
WHERE goods = 'orange'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock
┌─goods──┬──────month─┬─sales─┬─sum_sales─┐
│ orange │ 2021-06-01 │  9000 │    102700 │
│ orange │ 2021-04-01 │  8000 │    102700 │
│ orange │ 2021-03-01 │  8000 │    102700 │
│ orange │ 2021-05-01 │  9000 │    102700 │
│ orange │ 2021-02-01 │  9000 │    102700 │
│ orange │ 2021-01-01 │  8000 │    102700 │
│ orange │ 2021-07-01 │ 10000 │    102700 │
│ orange │ 2020-06-01 │  6700 │    102700 │
│ orange │ 2020-05-01 │  6000 │    102700 │
│ orange │ 2020-07-01 │  6000 │    102700 │
│ orange │ 2020-04-01 │  6000 │    102700 │
│ orange │ 2020-02-01 │  5000 │    102700 │
│ orange │ 2020-01-01 │  7000 │    102700 │
│ orange │ 2020-03-01 │  5000 │    102700 │
└────────┴────────────┴───────┴───────────┘

结果可以看到,实际上求和是对所有行,并没有受参数影响。这其实是因为range between在未指定order by列时,默认在开窗分组中所有行进行统计。
下面是指定了order by的结果,看起来确实和预想的一致。

SELECT
    goods,
    month,
    sales,
    sum(sales) OVER (PARTITION BY goods ORDER BY month ASC RANGE BETWEEN CURRENT ROW AND CURRENT ROW) AS sum_sales
FROM commodity_sales
WHERE goods = 'orange'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock
┌─goods──┬──────month─┬─sales─┬─sum_sales─┐
│ orange │ 2020-01-01 │  7000 │      7000 │
│ orange │ 2020-02-01 │  5000 │      5000 │
│ orange │ 2020-03-01 │  5000 │      5000 │
│ orange │ 2020-04-01 │  6000 │      6000 │
│ orange │ 2020-05-01 │  6000 │      6000 │
│ orange │ 2020-06-01 │  6700 │      6700 │
│ orange │ 2020-07-01 │  6000 │      6000 │
│ orange │ 2021-01-01 │  8000 │      8000 │
│ orange │ 2021-02-01 │  9000 │      9000 │
│ orange │ 2021-03-01 │  8000 │      8000 │
│ orange │ 2021-04-01 │  8000 │      8000 │
│ orange │ 2021-05-01 │  9000 │      9000 │
│ orange │ 2021-06-01 │  9000 │      9000 │
│ orange │ 2021-07-01 │ 10000 │     10000 │
└────────┴────────────┴───────┴───────────┘

设置order by

时间列排序

有的时候,我们可能需要对时间列进行排序,同时也要对指定行范围进行求和,这时候在range between中,如果没有使用固定的关键字(如unbounded preceding,current row),而是指定了数值行,那么结果并不会像我们想象的那样。具体可以先看下面例子:

SELECT
    goods,
    month,
    sales,
    sum(sales) OVER (PARTITION BY goods ORDER BY month ASC RANGE BETWEEN 31 PRECEDING AND 31 FOLLOWING) AS sum_sales
FROM commodity_sales
WHERE goods = 'orange'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock

该例子中,指定了BETWEEN 31 PRECEDING AND 31 FOLLOWING,看起来好像是前面31行后面31行,实际上并不是。因为在order by中使用了时间列,因此这个数值是和时间有关的。在本例中31指的是天,也就是前面31天到后面31天的范围来求和。结果也符合预期,是在前后三行进行求和。

┌─goods──┬──────month─┬─sales─┬─sum_sales─┐
│ orange │ 2020-01-01 │  7000 │     12000 │
│ orange │ 2020-02-01 │  5000 │     17000 │
│ orange │ 2020-03-01 │  5000 │     16000 │
│ orange │ 2020-04-01 │  6000 │     17000 │
│ orange │ 2020-05-01 │  6000 │     18700 │
│ orange │ 2020-06-01 │  6700 │     18700 │
│ orange │ 2020-07-01 │  6000 │     12700 │
│ orange │ 2021-01-01 │  8000 │     17000 │
│ orange │ 2021-02-01 │  9000 │     25000 │
│ orange │ 2021-03-01 │  8000 │     25000 │
│ orange │ 2021-04-01 │  8000 │     25000 │
│ orange │ 2021-05-01 │  9000 │     26000 │
│ orange │ 2021-06-01 │  9000 │     28000 │
│ orange │ 2021-07-01 │ 10000 │     19000 │
└────────┴────────────┴───────┴───────────┘

数值列排序

回到正常对数值列排序的场景中,我们需要在排序后进行求和。而在指定行范围时,需要注意的是,如果不是使用固定的关键字(如unbounded preceding,current row),而是使用数值时,实际上数值并不是指定行范围,而是指定数值范围,看如下例子:

SELECT
    goods,
    month,
    sales,
    sum(sales) OVER (PARTITION BY goods ORDER BY sales DESC RANGE BETWEEN 500 PRECEDING AND 500 FOLLOWING) AS sum_sales
FROM commodity_sales
WHERE goods = 'orange'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock

该例子中BETWEEN 500 PRECEDING AND 500 FOLLOWING,就是指当前行数值减500得到左区间,加500得到右区间,例如对于10000来说,得到的区间范围就是[9500, 10500],结果如下:

┌─goods──┬──────month─┬─sales─┬─sum_sales─┐
│ orange │ 2021-07-01 │ 10000 │     10000 │
│ orange │ 2021-06-01 │  9000 │     27000 │
│ orange │ 2021-05-01 │  9000 │     27000 │
│ orange │ 2021-02-01 │  9000 │     27000 │
│ orange │ 2021-04-01 │  8000 │     24000 │
│ orange │ 2021-03-01 │  8000 │     24000 │
│ orange │ 2021-01-01 │  8000 │     24000 │
│ orange │ 2020-01-01 │  7000 │     13700 │
│ orange │ 2020-06-01 │  6700 │     13700 │
│ orange │ 2020-05-01 │  6000 │     18000 │
│ orange │ 2020-07-01 │  6000 │     18000 │
│ orange │ 2020-04-01 │  6000 │     18000 │
│ orange │ 2020-02-01 │  5000 │     10000 │
│ orange │ 2020-03-01 │  5000 │     10000 │
└────────┴────────────┴───────┴───────────┘

拿其中9000来看,区间范围应该是[8500, 9500],而在这个范围内的仅有三个9000是符合的,因此结果就是他们三个数值相加,得到的就是27000。

累计

按月实现累计

SELECT
    goods, month, sales,
    sum(sales) OVER (PARTITION BY goods ORDER BY month desc range BETWEEN unbounded preceding and current row) as sum_sales
FROM commodity_sales
WHERE goods = 'orange'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock
┌─goods──┬──────month─┬─sales─┬─sum_sales─┐
│ orange │ 2021-07-01 │ 10000 │     10000 │
│ orange │ 2021-06-01 │  9000 │     19000 │
│ orange │ 2021-05-01 │  9000 │     28000 │
│ orange │ 2021-04-01 │  8000 │     36000 │
│ orange │ 2021-03-01 │  8000 │     44000 │
│ orange │ 2021-02-01 │  9000 │     53000 │
│ orange │ 2021-01-01 │  8000 │     61000 │
│ orange │ 2020-07-01 │  6000 │     67000 │
│ orange │ 2020-06-01 │  6700 │     73700 │
│ orange │ 2020-05-01 │  6000 │     79700 │
│ orange │ 2020-04-01 │  6000 │     85700 │
│ orange │ 2020-03-01 │  5000 │     90700 │
│ orange │ 2020-02-01 │  5000 │     95700 │
│ orange │ 2020-01-01 │  7000 │    102700 │
└────────┴────────────┴───────┴───────────┘

按sales大小实现累计

SELECT
    goods, month, sales,
    rank() over (PARTITION BY goods ORDER BY sales desc) as rank,
	row_number() over (PARTITION BY goods ORDER BY sales desc) as row,
    sum(sales) OVER (PARTITION BY goods ORDER BY sales desc range BETWEEN unbounded preceding and current row) as sum_sales
FROM commodity_sales
WHERE goods = 'orange'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock
┌─goods──┬──────month─┬─sales─┬─rank─┬─row─┬─sum_sales─┐
│ orange │ 2021-07-01 │ 10000 │    1 │   1 │     10000 │
│ orange │ 2021-06-01 │  9000 │    2 │   2 │     37000 │
│ orange │ 2021-05-01 │  9000 │    2 │   3 │     37000 │
│ orange │ 2021-02-01 │  9000 │    2 │   4 │     37000 │
│ orange │ 2021-04-01 │  8000 │    5 │   5 │     61000 │
│ orange │ 2021-03-01 │  8000 │    5 │   6 │     61000 │
│ orange │ 2021-01-01 │  8000 │    5 │   7 │     61000 │
│ orange │ 2020-01-01 │  7000 │    8 │   8 │     68000 │
│ orange │ 2020-06-01 │  6700 │    9 │   9 │     74700 │
│ orange │ 2020-05-01 │  6000 │   10 │  10 │     92700 │
│ orange │ 2020-07-01 │  6000 │   10 │  11 │     92700 │
│ orange │ 2020-04-01 │  6000 │   10 │  12 │     92700 │
│ orange │ 2020-02-01 │  5000 │   13 │  13 │    102700 │
│ orange │ 2020-03-01 │  5000 │   13 │  14 │    102700 │
└────────┴────────────┴───────┴──────┴─────┴───────────┘

会发现累计和是按照sales的大小序号进行求和的,存在并列的情况,这种情况下,还是需要用rows between来实现,如下所示:

SELECT
    goods, month, sales,
    rank() over (PARTITION BY goods ORDER BY sales desc) as rank,
	row_number() over (PARTITION BY goods ORDER BY sales desc) as row,
    sum(sales) OVER (PARTITION BY goods ORDER BY sales desc rows BETWEEN unbounded preceding and current row) as sum_sales
FROM commodity_sales
WHERE goods = 'orange'
SETTINGS allow_experimental_window_functions = 1
FORMAT PrettyCompactMonoBlock
┌─goods──┬──────month─┬─sales─┬─rank─┬─row─┬─sum_sales─┐
│ orange │ 2021-07-01 │ 10000 │    1 │   1 │     10000 │
│ orange │ 2021-02-01 │  9000 │    2 │   2 │     19000 │
│ orange │ 2021-06-01 │  9000 │    2 │   3 │     28000 │
│ orange │ 2021-05-01 │  9000 │    2 │   4 │     37000 │
│ orange │ 2021-01-01 │  8000 │    5 │   5 │     45000 │
│ orange │ 2021-04-01 │  8000 │    5 │   6 │     53000 │
│ orange │ 2021-03-01 │  8000 │    5 │   7 │     61000 │
│ orange │ 2020-01-01 │  7000 │    8 │   8 │     68000 │
│ orange │ 2020-06-01 │  6700 │    9 │   9 │     74700 │
│ orange │ 2020-05-01 │  6000 │   10 │  10 │     80700 │
│ orange │ 2020-07-01 │  6000 │   10 │  11 │     86700 │
│ orange │ 2020-04-01 │  6000 │   10 │  12 │     92700 │
│ orange │ 2020-02-01 │  5000 │   13 │  13 │     97700 │
│ orange │ 2020-03-01 │  5000 │   13 │  14 │    102700 │
└────────┴────────────┴───────┴──────┴─────┴───────────┘

你可能感兴趣的:(大数据,clickhouse,sql,数据库,大数据)