Spark SQL 窗口函数全面解析:概念、语法与实战案例

在 Spark 中,窗口函数(Window Functions)是大数据处理场景中极具核心价值的工具。与传统聚合函数(如 GROUP BY 搭配 sum/avg)不同,窗口函数在执行聚合计算时不会压缩原始数据的行数,而是通过定义一个"数据窗口",在保留每行原始信息的基础上,对窗口内的数据进行计算。这种特性使其在排名统计、累积分析、移动指标计算等复杂场景中具备不可替代的优势,广泛应用于电商销量分析、金融风险监控、用户行为轨迹挖掘等领域。

一、窗口函数核心概念深度解析

窗口函数的本质是"定义计算范围 + 应用计算逻辑",其核心组成包括窗口定义函数应用两部分,其中窗口定义是理解和使用窗口函数的关键。

1. 窗口的三大核心要素

窗口通过 org.apache.spark.sql.expressions.Window 类定义,必须明确以下要素(部分可选):

(1)分区(Partitioning):数据分组的依据
  • 作用:将整个数据集按指定列划分为多个独立的"分区",窗口函数的计算会在每个分区内独立执行,不同分区互不干扰。
  • 类比 :类似 GROUP BY 的分组逻辑,但 GROUP BY 会将分组后的数据聚合为一行,而窗口函数的分区仅划定计算范围,不改变原始数据结构。
  • 语法partitionBy(col1, col2, ...),支持多列联合分区(例如按"地区+产品类别"分区)。
(2)排序(Ordering):分区内数据的顺序规则
  • 作用:在每个分区内,对数据按指定列进行升序(默认)或降序排序,为后续"有序计算"(如排名、累积和、前后行关联)提供基础。
  • 注意 :排序是排名函数(row_number/rank 等)和分析函数(lead/lag 等)的必需条件,若仅需无顺序的窗口聚合(如分区内所有行的平均值),可省略排序。
  • 语法orderBy(col1 desc, col2 asc, ...),支持多列排序并指定排序方向。
(3)窗口范围(Frame):分区内的计算行集合
  • 作用:在已分区、已排序的数据集上,进一步限定每个行对应的"计算窗口大小",即当前行的计算需要包含分区内的哪些行。

  • 默认规则 :若未显式指定窗口范围,Spark 会根据是否排序自动推断:

    • 有排序时:默认窗口范围为 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(即从分区起始行到当前行)。
    • 无排序时:默认窗口范围为 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING(即分区内所有行)。
  • 两种范围定义方式

    类型 语法格式 适用场景 示例
    行范围(ROWS) rowsBetween(start, end) 固定行数的窗口(如"前后3行") rowsBetween(-3, 0):当前行及前3行
    范围范围(RANGE) rangeBetween(start, end) 基于排序列数值范围的窗口(如"数值在当前行±5以内") rangeBetween(-5, 5):排序列值与当前行差值≤5的行
  • 常用范围常量

    • UNBOUNDED PRECEDING:分区起始行
    • UNBOUNDED FOLLOWING:分区结束行
    • CURRENT ROW:当前行
    • 数值:正数表示"当前行之后N行",负数表示"当前行之前N行"

2. 窗口函数的三大分类

根据功能场景,Spark 中的窗口函数可分为排名函数聚合函数分析函数三类,覆盖绝大多数复杂计算需求:

(1)排名函数:用于数据排序与名次分配

核心作用是为分区内的每行数据分配名次,解决"按规则排序后取Top N""重复值名次处理"等问题,常用函数对比:

函数名 功能描述 重复值处理逻辑 示例(分区内排序后的值:[100, 90, 90, 80])
row_number() 分配唯一连续名次 即使值相同,名次也不重复 结果:1, 2, 3, 4
rank() 跳跃式排名 重复值名次相同,后续名次跳跃 结果:1, 2, 2, 4
dense_rank() 密集式排名 重复值名次相同,后续名次连续 结果:1, 2, 2, 3
percent_rank() 百分比排名 公式:(当前rank - 1) / (分区总行数 - 1),范围[0,1] 结果:0.0, 0.333, 0.333, 1.0
ntile(n) 分区内数据均匀分n组 每组分配组号,不足n行时前几组行数+1 n=2时结果:1, 1, 2, 2
(2)聚合函数:用于窗口内数据聚合计算

将传统聚合函数(sum/avg/min 等)应用于窗口,实现"每行对应一个聚合结果",而非传统聚合的"每组一个结果"。常用函数:

  • 基础聚合:sum(col)avg(col)min(col)max(col)count(col)
  • 进阶聚合:countDistinct(col)(窗口内去重计数,Spark 2.1+支持)、approx_count_distinct(col)(近似去重计数)
(3)分析函数:用于行与行之间的关联分析

核心作用是获取分区内当前行的"前后行数据"或"首尾行数据",无需自连接即可实现行级关联计算。常用函数:

函数名 功能描述 语法格式
lead(col, n, default) 获取当前行之后第n行的col值 lead("score", 1, 0):后1行分数,无则补0
lag(col, n, default) 获取当前行之前第n行的col值 lag("score", 2, null):前2行分数,无则补null
first_value(col) 获取窗口内的第一个col值 first_value("name"):窗口内第一行的姓名
last_value(col) 获取窗口内的最后一个col值 last_value("name"):窗口内最后一行的姓名
nth_value(col, n) 获取窗口内第n行的col值 nth_value("score", 3):窗口内第3行的分数

二、窗口函数完整语法与使用步骤

1. 语法结构

Spark SQL 中窗口函数的使用需遵循"定义窗口 + 应用函数"的两步法,语法格式如下:

sql 复制代码
-- 方式1:在SELECT中直接定义窗口(适用于简单场景)
SELECT
  原始列1, 原始列2,
  -- 窗口函数:函数名(列) OVER (窗口定义)
  row_number() OVER (
    PARTITION BY 分区列
    ORDER BY 排序列
    ROWS/RANGE BETWEEN 起始行 AND 结束行
  ) AS 别名,
  sum(列) OVER (窗口定义) AS 别名
FROM 表名;

-- 方式2:先定义窗口(WINDOW子句),再复用(适用于多窗口函数场景)
SELECT
  原始列1, 原始列2,
  row_number() OVER 窗口别名1 AS 排名,
  sum(列) OVER 窗口别名2 AS 累积和
FROM 表名
WINDOW
  窗口别名1 AS (PARTITION BY 分区列1 ORDER BY 排序列1),
  窗口别名2 AS (PARTITION BY 分区列2 ORDER BY 排序列2 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW);

2. Scala API DSL 语法

在 Spark Scala 编程中,通过 Window 类构建窗口规范(WindowSpec),结合 DataFrame 算子链式调用实现 DSL 风格的窗口函数应用,示例:

scala 复制代码
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._

// 1. 初始化SparkSession
val spark = SparkSession.builder()
  .appName("WindowFunctionDemo")
  .master("local[*]")
  .getOrCreate()

// 导入隐式转换(启用DSL语法)
import spark.implicits._

// 2. 构建测试数据(DataFrame)
val df = Seq(
  ("A", "2023-01", 100),
  ("A", "2023-02", 200),
  ("A", "2023-03", 150),
  ("B", "2023-01", 300),
  ("B", "2023-02", 250)
).toDF("地区", "月份", "销量")

// 3. 定义窗口规范(DSL风格:链式调用构建)
val windowSpec = Window
  .partitionBy($"地区") // 按地区分区(DSL列引用方式)
  .orderBy($"月份")     // 按月份升序排序
  .rowsBetween(Window.unboundedPreceding, Window.currentRow) // 窗口范围:分区起始到当前行

// 4. 应用窗口函数(DSL算子链式调用)
val result = df
  .select(
    $"地区", $"月份", $"销量",
    row_number().over(windowSpec).alias("月份排名"),
    sum($"销量").over(windowSpec).alias("累积销量"),
    avg($"销量").over(windowSpec).alias("平均销量")
  )

// 5. 展示结果
result.show()

三、实战案例:覆盖典型业务场景

以下案例基于电商销售数据展开,数据结构如下(表名:sales_detail):

订单ID 用户ID 商品类别 购买日期 消费金额
1001 U001 家电 2023-10-01 5000
1002 U001 服装 2023-10-02 800
1003 U002 家电 2023-10-01 3000
1004 U002 家电 2023-10-03 4500
1005 U003 服装 2023-10-02 1200
1006 U003 服装 2023-10-04 900
1007 U003 食品 2023-10-05 300

案例1:排名函数应用------各商品类别消费金额Top2订单

需求 :按商品类别分区,按消费金额降序排序,获取每个类别下消费金额前2的订单,若金额相同则保留所有并列订单。
分析 :需用 dense_rank() 实现密集排名(保留并列名次),避免 row_number() 导致并列订单被过滤。
Scala DSL 实现

scala 复制代码
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._

object Top2ByCategory {
  def main(args: Array[String]): Unit = {
    // 初始化SparkSession
    val spark = SparkSession.builder()
      .appName("Top2ByCategory")
      .master("local[*]")
      .getOrCreate()
    import spark.implicits._

    // 构建测试数据
    case class SalesDetail(orderId: String, userId: String, productCategory: String, purchaseDate: String, amount: Double)
    val salesData = Seq(
      SalesDetail("1001", "U001", "家电", "2023-10-01", 5000.0),
      SalesDetail("1002", "U001", "服装", "2023-10-02", 800.0),
      SalesDetail("1003", "U002", "家电", "2023-10-01", 3000.0),
      SalesDetail("1004", "U002", "家电", "2023-10-03", 4500.0),
      SalesDetail("1005", "U003", "服装", "2023-10-02", 1200.0),
      SalesDetail("1006", "U003", "服装", "2023-10-04", 900.0),
      SalesDetail("1007", "U003", "食品", "2023-10-05", 300.0)
    )
    val salesDF = salesData.toDF()
      .withColumn("purchaseDate", to_date($"purchaseDate", "yyyy-MM-dd"))

    // 定义窗口规范(按商品类别分区,金额降序排序)
    val categoryRankWindow = Window
      .partitionBy($"productCategory")
      .orderBy($"amount".desc)

    // DSL风格:应用窗口函数 + 过滤结果
    val top2ByCategoryDF = salesDF
      .withColumn("类别内排名", dense_rank().over(categoryRankWindow))
      .filter($"类别内排名" <= 2) // 替代SQL的QUALIFY
      .select($"orderId" as "订单ID", $"productCategory" as "商品类别", $"amount" as "消费金额", $"类别内排名")

    // 展示结果
    println("案例1:各商品类别消费金额Top2订单")
    top2ByCategoryDF.show(false)

    spark.stop()
  }
}

结果

复制代码
案例1:各商品类别消费金额Top2订单
+-------+----------+----------+------------+
|订单ID  |商品类别   |消费金额   |类别内排名   |
+-------+----------+----------+------------+
|1001   |家电       |5000.0    |1           |
|1004   |家电       |4500.0    |2           |
|1005   |服装       |1200.0    |1           |
|1006   |服装       |900.0     |2           |
|1007   |食品       |300.0     |1           |
+-------+----------+----------+------------+

案例2:聚合函数应用------用户累积消费金额与移动平均

需求:按用户ID分区,按购买日期升序排序,计算每个用户的:

  1. 截至当前订单的累积消费金额;
  2. 包含当前订单及前1订单的移动平均消费金额(窗口大小=2)。
    分析 :累积金额需窗口范围为"分区起始到当前行",移动平均需窗口范围为"前1行到当前行"。
    Scala DSL 实现
scala 复制代码
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._

object UserConsumeAnalysis {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("UserConsumeAnalysis")
      .master("local[*]")
      .getOrCreate()
    import spark.implicits._

    // 构建测试数据(同案例1,省略重复代码)
    case class SalesDetail(orderId: String, userId: String, productCategory: String, purchaseDate: String, amount: Double)
    val salesData = Seq(/* 同案例1数据 */)
    val salesDF = salesData.toDF()
      .withColumn("purchaseDate", to_date($"purchaseDate", "yyyy-MM-dd"))

    // 定义窗口规范(复用分区排序逻辑)
    val userBaseWindow = Window
      .partitionBy($"userId")
      .orderBy($"purchaseDate")

    // 累积消费窗口:分区起始到当前行
    val cumulativeWindow = userBaseWindow
      .rowsBetween(Window.unboundedPreceding, Window.currentRow)

    // 移动平均窗口:前1行到当前行
    val movingAvgWindow = userBaseWindow
      .rowsBetween(-1, Window.currentRow)

    // DSL风格:多窗口函数应用
    val userConsumeDF = salesDF
      .withColumn("累积消费金额", sum($"amount").over(cumulativeWindow))
      .withColumn("近2单平均金额", avg($"amount").over(movingAvgWindow).round(2))
      .select(
        $"orderId" as "订单ID",
        $"userId" as "用户ID",
        $"purchaseDate" as "购买日期",
        $"amount" as "消费金额",
        $"累积消费金额",
        $"近2单平均金额"
      )

    // 展示U003用户结果
    println("\n案例2:用户累积消费金额与移动平均(U003用户)")
    userConsumeDF.filter($"用户ID" === "U003").show(false)

    spark.stop()
  }
}

结果(用户U003部分)

复制代码
案例2:用户累积消费金额与移动平均(U003用户)
+-------+------+-------------+----------+--------------+--------------+
|订单ID  |用户ID |购买日期     |消费金额   |累积消费金额   |近2单平均金额  |
+-------+------+-------------+----------+--------------+--------------+
|1005   |U003  |2023-10-02   |1200.0    |1200.0        |null          |
|1006   |U003  |2023-10-04   |900.0     |2100.0        |1050.0        |
|1007   |U003  |2023-10-05   |300.0     |2400.0        |600.0         |
+-------+------+-------------+----------+--------------+--------------+

案例3:分析函数应用------用户相邻订单间隔天数

需求 :按用户ID分区,按购买日期升序排序,计算每个用户当前订单与下一个订单的间隔天数(最后一个订单间隔为null)。
分析 :需用 lead() 函数获取当前行的下一个订单日期,再通过日期函数计算间隔。
Scala DSL 实现

scala 复制代码
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._

object OrderIntervalAnalysis {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("OrderIntervalAnalysis")
      .master("local[*]")
      .getOrCreate()
    import spark.implicits._

    // 构建测试数据(同案例1,省略重复代码)
    case class SalesDetail(orderId: String, userId: String, productCategory: String, purchaseDate: String, amount: Double)
    val salesData = Seq(/* 同案例1数据 */)
    val salesDF = salesData.toDF()
      .withColumn("purchaseDate", to_date($"purchaseDate", "yyyy-MM-dd"))

    // 定义窗口规范(按用户分区,日期升序排序)
    val orderIntervalWindow = Window
      .partitionBy($"userId")
      .orderBy($"purchaseDate")

    // DSL风格:lead函数 + 日期计算
    val orderIntervalDF = salesDF
      .withColumn("下一个订单日期", lead($"purchaseDate", 1).over(orderIntervalWindow))
      .withColumn("订单间隔天数", datediff($"下一个订单日期", $"purchaseDate"))
      .select(
        $"orderId" as "订单ID",
        $"userId" as "用户ID",
        $"purchaseDate" as "购买日期",
        $"下一个订单日期",
        $"订单间隔天数"
      )

    // 展示U001用户结果
    println("\n案例3:用户相邻订单间隔天数(U001用户)")
    orderIntervalDF.filter($"用户ID" === "U001").show(false)

    spark.stop()
  }
}

结果(用户U001部分)

复制代码
案例3:用户相邻订单间隔天数(U001用户)
+-------+------+-------------+----------------+------------+
|订单ID  |用户ID |购买日期     |下一个订单日期   |订单间隔天数  |
+-------+------+-------------+----------------+------------+
|1001   |U001  |2023-10-01   |2023-10-02      |1           |
|1002   |U001  |2023-10-02   |null            |null        |
+-------+------+-------------+----------------+------------+

案例4:多窗口组合应用------商品类别销售综合分析

需求:同时计算以下指标,验证多窗口函数的复用能力:

  1. 各商品类别的总销量(窗口:全类别);
  2. 各商品类别按日期的累积销量(窗口:类别+日期排序);
  3. 每个订单在类别内的消费占比(当前订单金额/类别总金额)。
    Scala DSL 实现
scala 复制代码
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._

object CategorySalesAnalysis {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("CategorySalesAnalysis")
      .master("local[*]")
      .getOrCreate()
    import spark.implicits._

    // 构建测试数据(同案例1,省略重复代码)
    case class SalesDetail(orderId: String, userId: String, productCategory: String, purchaseDate: String, amount: Double)
    val salesData = Seq(/* 同案例1数据 */)
    val salesDF = salesData.toDF()
      .withColumn("purchaseDate", to_date($"purchaseDate", "yyyy-MM-dd"))

    // 定义多窗口规范
    // 窗口1:类别总金额(仅分区,无排序)
    val categoryTotalWindow = Window.partitionBy($"productCategory")
    // 窗口2:类别累积金额(分区+日期排序)
    val categoryCumulativeWindow = Window
      .partitionBy($"productCategory")
      .orderBy($"purchaseDate")
      .rowsBetween(Window.unboundedPreceding, Window.currentRow)

    // DSL风格:多窗口函数组合应用
    val categorySalesDF = salesDF
      .withColumn("类别总金额", sum($"amount").over(categoryTotalWindow))
      .withColumn("类别累积金额", sum($"amount").over(categoryCumulativeWindow))
      .withColumn("类别内占比", ($"amount" / $"类别总金额").round(2))
      .select(
        $"orderId" as "订单ID",
        $"productCategory" as "商品类别",
        $"purchaseDate" as "购买日期",
        $"amount" as "消费金额",
        $"类别总金额",
        $"类别累积金额",
        $"类别内占比"
      )

    // 展示家电类别结果
    println("\n案例4:商品类别销售综合分析(家电类别)")
    categorySalesDF.filter($"商品类别" === "家电").show(false)

    spark.stop()
  }
}

结果(家电类别部分)

复制代码
案例4:商品类别销售综合分析(家电类别)
+-------+----------+-------------+----------+------------+--------------+------------+
|订单ID  |商品类别   |购买日期     |消费金额   |类别总金额   |类别累积金额   |类别内占比   |
+-------+----------+-------------+----------+------------+--------------+------------+
|1001   |家电       |2023-10-01   |5000.0    |12500.0     |5000.0        |0.4         |
|1003   |家电       |2023-10-01   |3000.0    |12500.0     |8000.0        |0.24        |
|1004   |家电       |2023-10-03   |4500.0    |12500.0     |12500.0       |0.36        |
+-------+----------+-------------+----------+------------+--------------+------------+

四、窗口函数优化技巧与注意事项

1. 性能优化建议

  • 合理分区:分区列应选择 cardinality(基数)适中的列(如商品类别、地区),避免分区过多(如按用户ID分区,用户数100万+)导致任务碎片化,或分区过少(如按日期分区,仅1个日期)导致并行度不足。
  • 避免不必要的排序 :若窗口聚合无需顺序(如类别总金额),省略 orderBy 可减少排序开销,同时窗口范围默认变为"全分区"。
  • 显式指定窗口范围 :默认窗口范围可能不符合预期(如 last_value 若不指定范围,默认仅取到当前行),显式指定范围可避免逻辑错误,同时减少计算冗余。
  • 使用 filter 替代子查询 :DSL风格中直接用 filter 算子过滤窗口函数结果,替代SQL的嵌套子查询,代码更简洁且性能更优。

2. 常见坑与解决方案

  • 坑1last_value() 结果不符合预期(仅取到当前行之前的数据)。

    • 原因:默认窗口范围为"分区起始到当前行",而非全分区。

    • 解决方案:显式指定窗口范围为 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,DSL实现如下:

      scala 复制代码
      val fullWindow = Window
        .partitionBy($"productCategory")
        .rowsBetween(Window.unboundedPreceding, Window.unboundedFollowing)
      val df = salesDF.withColumn("最后一笔订单金额", last_value($"amount").over(fullWindow))
  • 坑2 :相同数据的排名结果不稳定(如 row_number() 对相同金额的订单排名随机)。

    • 原因:排序列存在重复值,导致分区内数据顺序不确定。

    • 解决方案:在 orderBy 中增加唯一列(如订单ID),确保排序唯一性,DSL实现如下:

      scala 复制代码
      val stableRankWindow = Window
        .partitionBy($"productCategory")
        .orderBy($"amount".desc, $"orderId") // 增加订单ID作为唯一排序依据
  • 坑3 :窗口范围使用 RANGE 时结果异常(如数值型排序列的范围计算不符合预期)。

    • 原因:RANGE 是基于排序列的数值范围,而非行数,适用于连续数值(如时间戳、分数),不适用于离散值(如订单ID)。
    • 解决方案:离散值窗口使用 ROWS,连续值窗口按需使用 RANGE,DSL中通过 rowsBetween/rangeBetween 明确指定。

五、总结

Spark SQL 窗口函数通过"分区+排序+窗口范围"的灵活组合,实现了传统聚合函数无法完成的复杂行级计算,同时保留原始数据结构,极大简化了数据分析流程。DSL风格的Scala实现具有类型安全、代码简洁、可扩展性强等优势,更适合复杂业务场景的编程式开发。核心要点:

  1. 窗口定义是基础:明确分区、排序、范围三要素,根据业务场景选择合适的组合;
  2. 函数选择是关键:排名用 row_number/dense_rank,聚合用 sum/avg,行关联用 lead/lag
  3. 优化与避坑是保障:合理设计窗口参数,利用DSL算子的灵活性提升代码效率和性能。

掌握窗口函数后,可轻松应对排名、累积分析、移动指标、行级关联等高频业务场景,大幅提升大数据处理的效率和灵活性。

初始化 SparkSession:

scala 复制代码
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._

// 初始化 SparkSession
val spark = SparkSession.builder()
  .appName("WindowFunctionDSLDemo")
  .master("local[*]") // 本地调试,生产环境删除
  .getOrCreate()

// 导入隐式转换(必须,否则无法使用 DSL 算子)
import spark.implicits._

测试数据准备

构建电商销售数据 DataFrame(与原 SQL 案例数据一致):

scala 复制代码
// 定义数据结构
case class SalesDetail(
  orderId: String,    // 订单ID
  userId: String,     // 用户ID
  productCategory: String, // 商品类别
  purchaseDate: String, // 购买日期(字符串格式,后续转为日期类型)
  amount: Double      // 消费金额
)

// 测试数据
val salesData = Seq(
  SalesDetail("1001", "U001", "家电", "2023-10-01", 5000.0),
  SalesDetail("1002", "U001", "服装", "2023-10-02", 800.0),
  SalesDetail("1003", "U002", "家电", "2023-10-01", 3000.0),
  SalesDetail("1004", "U002", "家电", "2023-10-03", 4500.0),
  SalesDetail("1005", "U003", "服装", "2023-10-02", 1200.0),
  SalesDetail("1006", "U003", "服装", "2023-10-04", 900.0),
  SalesDetail("1007", "U003", "食品", "2023-10-05", 300.0)
)

// 转换为 DataFrame 并转换日期格式
val salesDF = salesData.toDF()
  .withColumn("purchaseDate", to_date($"purchaseDate", "yyyy-MM-dd")) // 字符串转日期类型

实战案例:DSL 风格窗口函数实现

案例1:各商品类别消费金额 Top2 订单(dense_rank 密集排名)

需求:按商品类别分区,消费金额降序排序,取每个类别 Top2 订单(保留并列)。

scala 复制代码
// 1. 定义窗口规范(按商品类别分区,金额降序排序)
val categoryRankWindow = Window
  .partitionBy($"productCategory")
  .orderBy($"amount".desc)

// 2. 应用窗口函数 + 过滤结果(DSL 用 where 替代 SQL 的 QUALIFY)
val top2ByCategoryDF = salesDF
  .withColumn("categoryRank", dense_rank().over(categoryRankWindow)) // 密集排名
  .where($"categoryRank" <= 2) // 过滤 Top2 订单
  .select(
    $"orderId",
    $"productCategory",
    $"amount",
    $"categoryRank"
  )

// 展示结果
println("案例1:各商品类别消费金额 Top2 订单")
top2ByCategoryDF.show(false)

输出结果

复制代码
案例1:各商品类别消费金额 Top2 订单
+-------+---------------+------+------------+
|orderId|productCategory|amount|categoryRank|
+-------+---------------+------+------------+
|1001   |家电           |5000.0|1           |
|1004   |家电           |4500.0|2           |
|1005   |服装           |1200.0|1           |
|1006   |服装           |900.0 |2           |
|1007   |食品           |300.0 |1           |
+-------+---------------+------+------------+

案例2:用户累积消费金额与移动平均(聚合窗口函数)

需求:按用户分区,日期升序排序,计算累积消费金额和近2单移动平均。

scala 复制代码
// 1. 定义窗口规范(用户分区,日期升序,窗口范围分别定义)
// 累积消费窗口:分区起始到当前行
val cumulativeWindow = Window
  .partitionBy($"userId")
  .orderBy($"purchaseDate")
  .rowsBetween(Window.unboundedPreceding, Window.currentRow)

// 移动平均窗口:前1行到当前行(窗口大小=2)
val movingAvgWindow = Window
  .partitionBy($"userId")
  .orderBy($"purchaseDate")
  .rowsBetween(-1, Window.currentRow) // -1 表示当前行前1行

// 2. 应用聚合窗口函数
val userConsumeDF = salesDF
  .withColumn("cumulativeAmount", sum($"amount").over(cumulativeWindow)) // 累积消费
  .withColumn("movingAvgAmount", avg($"amount").over(movingAvgWindow)) // 近2单平均
  .select(
    $"orderId",
    $"userId",
    $"purchaseDate",
    $"amount",
    $"cumulativeAmount",
    $"movingAvgAmount".round(2) // 保留2位小数
  )

// 展示结果(重点看 U003 用户)
println("\n案例2:用户累积消费金额与移动平均")
userConsumeDF.filter($"userId" === "U003").show(false)

输出结果

复制代码
案例2:用户累积消费金额与移动平均
+-------+------+-------------+------+----------------+---------------+
|orderId|userId|purchaseDate |amount|cumulativeAmount|movingAvgAmount|
+-------+------+-------------+------+----------------+---------------+
|1005   |U003  |2023-10-02   |1200.0|1200.0          |null           |
|1006   |U003  |2023-10-04   |900.0 |2100.0          |1050.0         |
|1007   |U003  |2023-10-05   |300.0 |2400.0          |600.0          |
+-------+------+-------------+------+----------------+---------------+

案例3:用户相邻订单间隔天数(分析函数 lead)

需求:按用户分区,日期升序排序,计算当前订单与下一个订单的间隔天数。

scala 复制代码
// 1. 定义窗口规范(用户分区,日期升序)
val orderIntervalWindow = Window
  .partitionBy($"userId")
  .orderBy($"purchaseDate")

// 2. 应用 lead 函数 + 日期计算
val orderIntervalDF = salesDF
  .withColumn("nextPurchaseDate", lead($"purchaseDate", 1).over(orderIntervalWindow)) // 下一个订单日期
  .withColumn("intervalDays", datediff($"nextPurchaseDate", $"purchaseDate")) // 计算间隔天数
  .select(
    $"orderId",
    $"userId",
    $"purchaseDate",
    $"nextPurchaseDate",
    $"intervalDays"
  )

// 展示结果(重点看 U001 用户)
println("\n案例3:用户相邻订单间隔天数")
orderIntervalDF.filter($"userId" === "U001").show(false)

输出结果

复制代码
案例3:用户相邻订单间隔天数
+-------+------+-------------+----------------+------------+
|orderId|userId|purchaseDate |nextPurchaseDate|intervalDays|
+-------+------+-------------+----------------+------------+
|1001   |U001  |2023-10-01   |2023-10-02      |1           |
|1002   |U001  |2023-10-02   |null            |null        |
+-------+------+-------------+----------------+------------+

案例4:商品类别销售综合分析(多窗口组合)

需求:同时计算类别总金额、类别累积金额、类别内消费占比。

scala 复制代码
// 1. 定义多个窗口规范
// 类别总金额窗口:仅分区,无排序(全类别聚合)
val categoryTotalWindow = Window.partitionBy($"productCategory")

// 类别累积金额窗口:分区+日期排序,起始到当前行
val categoryCumulativeWindow = Window
  .partitionBy($"productCategory")
  .orderBy($"purchaseDate")
  .rowsBetween(Window.unboundedPreceding, Window.currentRow)

// 2. 多窗口函数组合应用
val categorySalesDF = salesDF
  .withColumn("categoryTotalAmount", sum($"amount").over(categoryTotalWindow)) // 类别总金额
  .withColumn("categoryCumulativeAmount", sum($"amount").over(categoryCumulativeWindow)) // 累积金额
  .withColumn("categoryRatio", ($"amount" / $"categoryTotalAmount").round(2)) // 消费占比(保留2位)
  .select(
    $"orderId",
    $"productCategory",
    $"purchaseDate",
    $"amount",
    $"categoryTotalAmount",
    $"categoryCumulativeAmount",
    $"categoryRatio"
  )

// 展示结果(重点看家电类别)
println("\n案例4:商品类别销售综合分析")
categorySalesDF.filter($"productCategory" === "家电").show(false)

输出结果

复制代码
案例4:商品类别销售综合分析
+-------+---------------+-------------+------+---------------------+------------------------+-------------+
|orderId|productCategory|purchaseDate |amount|categoryTotalAmount  |categoryCumulativeAmount|categoryRatio|
+-------+---------------+-------------+------+---------------------+------------------------+-------------+
|1001   |家电           |2023-10-01   |5000.0|12500.0              |5000.0                  |0.4          |
|1003   |家电           |2023-10-01   |3000.0|12500.0              |8000.0                  |0.24         |
|1004   |家电           |2023-10-03   |4500.0|12500.0              |12500.0                 |0.36         |
+-------+---------------+-------------+------+---------------------+------------------------+-------------+

DSL 风格核心优势与注意事项

1. 核心优势

  • 编程式逻辑:通过链式调用组合算子,无需编写 SQL 字符串,更适合复杂业务逻辑(如动态窗口范围、条件过滤)。
  • 类型安全:依赖 Scala 类型推断,列名错误、函数参数错误在编译期即可发现,避免运行时 SQL 语法错误。
  • 灵活复用 :窗口规范(WindowSpec)可单独定义,支持多函数复用同一窗口,减少冗余代码。

2. 注意事项

  • 隐式转换必须导入import spark.implicits._ 是 DSL 风格的基础,否则无法使用 $"列名" 语法和 DataFrame 算子。
  • 窗口范围定义 :DSL 中通过 rowsBetween/rangeBetween 显式指定范围,参数支持常量(如 -1)或 Window 类静态常量(如 Window.unboundedPreceding)。
  • 函数调用方式 :所有窗口函数(dense_rank/sum/lead 等)均来自 org.apache.spark.sql.functions,需通过 over(windowSpec) 绑定窗口。
  • 结果过滤 :DSL 中用 wherefilter 算子替代 SQL 的 QUALIFY,低版本 Spark 也支持(无需 3.3+)。
相关推荐
武子康1 小时前
大数据-174 Elasticsearch 查询 DSL 实战:match/match_phrase/query_string/multi_match 全解析
大数据·后端·elasticsearch
源码技术栈1 小时前
Java智能诊所管理系统源码 SaaS云门诊运维平台源码
java·大数据·运维·人工智能·源码·诊所·门诊
金融小师妹2 小时前
机器学习驱动分析:ADP就业数据异常波动,AI模型预测12月降息概率达89%
大数据·人工智能·深度学习·编辑器·1024程序员节
智慧化智能化数字化方案2 小时前
ERP规划——解读86页大型企业业务流程优化及ERP整体规划方案【附全文阅读】
大数据·人工智能·erp整体规划方案·erp实施项目建设方案·erp基本概念培训
ii_best2 小时前
用鹰眼投屏软件注册Vinted,跨境入门效率翻倍教程
大数据·运维·服务器
士心凡2 小时前
Spark
大数据·ajax·spark
jiayong232 小时前
Elasticsearch 核心概念详解:Index、Document、Field
大数据·elasticsearch·jenkins
AIsdhuang3 小时前
2025 AI培训权威推荐榜:深度评测与趋势前瞻
大数据·人工智能·python
鹿衔`3 小时前
CDH 6.3.2 集群外挂 Spark 3.5.7 (Paimon) 集成 Hue 实战指南
大数据·分布式·spark