在这篇博文中,我们介绍了Apache Spark 1.4中添加的新窗口功能。 窗口函数允许Spark SQL的用户计算结果,例如给定行的排名或输入行范围内的移动平均值。 它们显着提高了Spark的SQL和DataFrame API的表现力。 本博客将首先介绍窗口函数的概念,然后讨论如何将它们与Spark SQL和Spark的DataFrame API一起使用。
什么是窗口功能?
在1.4之前,Spark SQL支持两种可用于计算单个返回值的函数。 内置函数或UDF(例如substr或round)将单行中的值作为输入,并为每个输入行生成单个返回值。 聚合函数(如SUM或MAX)对一组行进行操作,并为每个组计算单个返回值。
虽然这些在实践中都非常有用,但仍然存在许多单独使用这些类型的功能无法表达的操作。 具体来说,无法同时对一组行进行操作,同时仍为每个输入行返回单个值。 这种限制使得难以进行各种数据处理任务,例如计算移动平均值,计算累积和,或访问出现在当前行之前的行的值。 幸运的是,对于Spark SQL的用户来说,窗口函数填补了这个空白。
窗口函数的核心是根据一组行(称为Frame)计算表的每个输入行的返回值。 每个输入行都可以有一个与之关联的唯一帧。 窗口函数的这种特性使它们比其他函数更强大,并且允许用户表达各种数据处理任务,这些任务很难(如果不是不可能的话)在没有窗口函数的情况下以简洁的方式表达。 现在,我们来看看两个例子。
假设我们有一个productRevenue表,如下所示。
我们想回答两个问题:
每个类别中最畅销和第二畅销的产品是什么?
每种产品的收入与该产品同类产品中最畅销产品的收入之间有何差异?
要回答第一个问题“每个类别中哪些是畅销产品和第二畅销产品?”,我们需要根据产品收入对产品进行排名,并选择最畅销和第二畅销产品。 产品根据排名。 下面是用于通过使用窗口函数dense_rank来回答这个问题的SQL查询(我们将在下一节中解释使用窗口函数的语法)。
SELECT
product,
category,
revenue
FROM (
SELECT
product,
category,
revenue,
dense_rank() OVER (PARTITION BY category ORDER BY revenue DESC) as rank
FROM productRevenue) tmp
WHERE
rank <= 2
此查询的结果如下所示。 在不使用窗口函数的情况下,很难用SQL表达查询,即使可以表达SQL查询,底层引擎也很难有效地评估查询。
Note:这里category分组取top2的结果中,category为Cell Phone中有三条数据。
如果使用row_number() ;category为Cell Phone中只有二条数据。
先说明一下,row_number()开窗函数,它的作用是什么?
其实,就是给每个分组的数据,按照其排序顺序,打上一个分组内的行号!
如:有一个分组date=20160706,里面看有3数据,11211,11212,11213
那么对这个分组的每一行使用row_number()开窗函数以后,这个三行会打上一个组内的行号!!!
行号是从1开始递增!!! 比如最后结果就是 11211 1, 11212 2, 11213 3SELECT
product,
category,
revenue
FROM (
SELECT
product,
category,
revenue,
row_number() OVER (PARTITION BY category ORDER BY revenue DESC) as rank
FROM productRevenue) tmp
WHERE
rank <= 2
对于第二个问题“每个产品的收入与同类产品中最畅销产品的收入之间有什么区别?”,要计算产品的收入差异,我们需要找到每种产品的同类产品最高的收入值。 下面Python DataFrame程序可以解决此问题。
import sys
from pyspark.sql.window import Window
import pyspark.sql.functions as func
windowSpec = \
Window
.partitionBy(df['category']) \
.orderBy(df['revenue'].desc()) \
.rangeBetween(-sys.maxsize, sys.maxsize)
dataFrame = sqlContext.table("productRevenue")
revenue_difference = \
(func.max(dataFrame['revenue']).over(windowSpec) - dataFrame['revenue'])
dataFrame.select(
dataFrame['product'],
dataFrame['category'],
dataFrame['revenue'],
revenue_difference.alias("revenue_difference"))
该程序的结果如下所示。 如果不使用窗口函数,用户必须找到所有类别的所有最高收入值,然后将此派生数据集与原始productRevenue表连接以计算收入差异。
不使用窗口函数实现方式
select t3.*,t3.max_revenue-t3.revenue as difference from (
select t1.*,t2.max_revenue from df t1 left join
(SELECT category,max(revenue) max_revenue FROM df group by category) t2
on t1.category=t2.category
) t3
使用窗口函数
Spark SQL支持三种窗口函数:排名函数,分析函数和聚合函数。 可用的排名函数和分析函数总结在下表中。 对于聚合函数,用户可以使用任何现有的聚合函数作为窗口函数。
要使用窗口函数,用户需要标记一个函数被用作窗口函数
在SQL中受支持的函数之后添加OVER子句,例如 avg(revenue)over(...); 要么
在DataFrame API中的受支持函数上调用over方法 rank().over(...)
.。
一旦将函数标记为窗口函数后,下一个关键步骤是定义与此函数关联的窗口规范。窗口规范定义哪些行包含在与给定输入行关联的frame中。窗口规范包括三个部分:
分区规范:控制在给定的行数据中,哪些行位于同一分区中。也就是说,用户希望在排序和frame之前确保将具有相同类别值的所有行收集到同一台机器上。如果没有给出分区规范,则必须将所有数据收集到一台机器上。(在分区的基础上排序)
排序规范:控制分区中行的排序方式,确定给定行在其分区中的位置。
frame规范:根据它们与当前行的相对位置,说明当前输入行的frame中将包含哪些行。例如,“当前行之前的三行到当前行”描述了包括当前输入行和当前行之前出现的三行的frame
在SQL中,PARTITION BY和ORDER BY关键字分别用于指定分区规范的分区表达式和排序规范的排序表达式。 SQL语法如下所示。
OVER (PARTITION BY ... ORDER BY ...)
在DataFrame API中,我们提供实用程序函数来定义窗口规范。 以Python为例,用户可以指定分区表达式和排序表达式,如下所示。
from pyspark.sql.window import Window
windowSpec = \
Window \
.partitionBy(...) \
.orderBy(...)
除了排序和分区之外,用户还需要定义frame的起始边界,frame的结束边界和frame的类型,它们是frame规范的三个组成部分。
有五种类型的边界,它们是
UNBOUNDED PRECEDING,UNBOUNDED FOLLOWING,CURRENT ROW,<value> PRECEDING和<value> FOLLOWING。 UNBOUNDED PRECEDING和UNBOUNDED FOLLOWING分别表示分区的第一行和分区的最后一行。
对于其他三种类型的边界,它们指定与当前输入行的位置的偏移量,并且它们的具体含义是基于frame的类型定义的。 有两种类型的frame,ROW frame和RANGE frame。
Row Frame
ROW Frame基于当前输入行位置的物理偏移,这意味着CURRENT ROW,<value> PRECEDING或<value> FOLLOWING指定物理偏移。
如果CURRENT ROW用作边界,则表示当前输入行。 <value> PRECEDING和<value> FOLLOWING分别描述当前输入行之前和之后出现的行数。 下图说明了一个ROW Frame,其中1 PRECEDING作为起始边界,1 FOLLOWING作为结束边界(SQL语法中的1前1行和下1行)。
range Frame
RANGE Frame基于来自当前输入行的位置的逻辑偏移,并且具有与ROW Frame类似的语法。逻辑偏移是当前输入行的排序表达式的值与Frame的边界行的相同表达式的值之间的差。由于此定义,当使用RANGE Frame时,仅允许单个排序表达式。此外,对于RANGE Frame,就边界计算而言,具有与当前输入行的排序表达式的相同值的所有行被认为是相同的行。
现在,我们来看一个例子。在此示例中,排序表达式是收入;起始边界是2000 PRECEDING;结束边界为1000 FOLLOWING(此Frame在SQL语法中定义为2000 PRECEDING和1000 FOLLOWING范围)。以下五个图说明了如何使用当前输入行的更新来更新 Frame。基本上,对于每个当前输入行,根据收入的价值,我们计算收入范围[当前收入值 - 2000,当前收入值+ 1000]。收入值落在此范围内的所有行都位于当前输入行的Frame中。
总之,要定义窗口规范,用户可以在SQL中使用以下语法。
OVER(PARTITION BY ... ORDER BY ... frame_type BETWEEN start AND end)
这里,frame_type可以是ROWS(对于ROW Frame)或RANGE(对于RANGE Frame); start可以是UNBOUNDED PRECEDING,CURRENT ROW,<value> PRECEDING和<value> FOLLOWING中的任何一个; 和end可以是UNBOUNDED FOLLOWING,CURRENT ROW,<value> PRECEDING和<value> FOLLOWING中的任何一个。
在Python DataFrame API中,用户可以按如下方式定义窗口规范。
下一步是什么?
自Spark 1.4发布以来,我们一直积极与社区成员合作进行优化,以提高性能并减少操作员评估窗口函数的内存消耗。其中一些将在Spark 1.5中添加,其他将在我们的未来版本中添加。除了性能改进工作之外,我们将在不久的将来添加两个功能,以使Spark SQL中的窗口功能支持更加强大。首先,我们一直致力于为Date和Timestamp数据类型添加Interval数据类型支持(SPARK-8943)。使用Interval数据类型,用户可以将间隔用作<value> PRECEDING和<value> FOLLOWING for RANGE框架中指定的值,这样可以更轻松地使用窗口函数进行各种时间序列分析。其次,我们一直致力于在Spark SQL(SPARK-3947)中添加对用户定义聚合函数的支持。通过我们的窗口功能支持,用户可以立即使用其用户定义的聚合函数作为窗口函数来执行各种高级数据分析任务。