大資料中必須要掌握的 Flink SQL 詳細剖析

五分鐘學大資料發表於2021-11-05

Flink SQL 是 Flink 實時計算為簡化計算模型,降低使用者使用實時計算門檻而設計的一套符合標準 SQL 語義的開發語言。
自 2015 年開始,阿里巴巴開始調研開源流計算引擎,最終決定基於 Flink 打造新一代計算引擎,針對 Flink 存在的不足進行優化和改進,並且在 2019 年初將最終程式碼開源,也就是我們熟知的 Blink。Blink 在原來的 Flink 基礎上最顯著的一個貢獻就是 Flink SQL 的實現。

Flink SQL 是面向使用者的 API 層,在我們傳統的流式計算領域,比如 Storm、Spark Streaming 都會提供一些 Function 或者 Datastream API,使用者通過 Java 或 Scala 寫業務邏輯,這種方式雖然靈活,但有一些不足,比如具備一定門檻且調優較難,隨著版本的不斷更新,API 也出現了很多不相容的地方。

在這個背景下,毫無疑問,SQL 就成了我們最佳選擇,之所以選擇將 SQL 作為核心 API,是因為其具有幾個非常重要的特點:

  • SQL 屬於設定式語言,使用者只要表達清楚需求即可,不需要了解具體做法;

  • SQL 可優化,內建多種查詢優化器,這些查詢優化器可為 SQL 翻譯出最優執行計劃;

  • SQL 易於理解,不同行業和領域的人都懂,學習成本較低;

  • SQL 非常穩定,在資料庫 30 多年的歷史中,SQL 本身變化較少;

  • 流與批的統一,Flink 底層 Runtime 本身就是一個流與批統一的引擎,而 SQL 可以做到 API 層的流與批統一。

SELECT

SELECT 用於從 DataSet/DataStream 中選擇資料,用於篩選出某些列。

示例:

SELECT * FROM Table; // 取出表中的所有列

SELECT name,age FROM Table; // 取出表中 name 和 age 兩列

與此同時 SELECT 語句中可以使用函式和別名,例如我們上面提到的 WordCount 中:

SELECT word, COUNT(word) FROM table GROUP BY word;

WHERE

WHERE 用於從資料集/流中過濾資料,與 SELECT 一起使用,用於根據某些條件對關係做水平分割,即選擇符合條件的記錄。

示例:

SELECT name,age FROM Table where name LIKE ‘% 小明 %’;

SELECT * FROM Table WHERE age = 20;

WHERE 是從原資料中進行過濾,那麼在 WHERE 條件中,Flink SQL 同樣支援 =、<、>、<>、>=、<=,以及 AND、OR 等表示式的組合,最終滿足過濾條件的資料會被選擇出來。並且 WHERE 可以結合 IN、NOT IN 聯合使用。舉個例子:

SELECT name, age
FROM Table
WHERE name IN (SELECT name FROM Table2)

DISTINCT

DISTINCT 用於從資料集/流中去重根據 SELECT 的結果進行去重。

示例:

SELECT DISTINCT name FROM Table;

對於流式查詢,計算查詢結果所需的 State 可能會無限增長,使用者需要自己控制查詢的狀態範圍,以防止狀態過大。

GROUP BY

GROUP BY 是對資料進行分組操作。例如我們需要計算成績明細表中,每個學生的總分。

示例:

SELECT name, SUM(score) as TotalScore FROM Table GROUP BY name;

UNION  和  UNION ALL

UNION 用於將兩個結果集合並起來,要求兩個結果集欄位完全一致,包括欄位型別、欄位順序。不同於 UNION ALL 的是,UNION 會對結果資料去重。

示例:

SELECT * FROM T1 UNION (ALL) SELECT * FROM T2;

JOIN

JOIN 用於把來自兩個表的資料聯合起來形成結果表,Flink 支援的 JOIN 型別包括:

JOIN - INNER JOIN

LEFT JOIN - LEFT OUTER JOIN

RIGHT JOIN - RIGHT OUTER JOIN

FULL JOIN - FULL OUTER JOIN

這裡的 JOIN 的語義和我們在關係型資料庫中使用的 JOIN 語義一致。

示例:

JOIN(將訂單表資料和商品表進行關聯)

SELECT * FROM Orders INNER JOIN Product ON Orders.productId = Product.id

LEFT JOIN 與 JOIN 的區別是當右表沒有與左邊相 JOIN 的資料時候,右邊對應的欄位補 NULL 輸出,RIGHT JOIN 相當於 LEFT JOIN 左右兩個表互動一下位置。FULL JOIN 相當於 RIGHT JOIN 和 LEFT JOIN 之後進行 UNION ALL 操作。

示例:

SELECT * FROM Orders LEFT JOIN Product ON Orders.productId = Product.id
SELECT * FROM Orders RIGHT JOIN Product ON Orders.productId = Product.id
SELECT * FROM Orders FULL OUTER JOIN Product ON Orders.productId = Product.id

Group Window

根據視窗資料劃分的不同,目前 Apache Flink 有如下 3 種 Bounded Window:

Tumble,滾動視窗,視窗資料有固定的大小,視窗資料無疊加;

Hop,滑動視窗,視窗資料有固定大小,並且有固定的視窗重建頻率,視窗資料有疊加;

Session,會話視窗,視窗資料沒有固定的大小,根據視窗資料活躍程度劃分視窗,視窗資料無疊加。

Tumble Window

Tumble 滾動視窗有固定大小,視窗資料不重疊,具體語義如下:

Tumble 滾動視窗對應的語法如下:

SELECT
    [gk],
    [TUMBLE_START(timeCol, size)],
    [TUMBLE_END(timeCol, size)],
    agg1(col1),
    ...
    aggn(colN)
FROM Tab1
GROUP BY [gk], TUMBLE(timeCol, size)

其中:

[gk] 決定了是否需要按照欄位進行聚合;

TUMBLE_START 代表視窗開始時間;

TUMBLE_END 代表視窗結束時間;

timeCol 是流表中表示時間欄位;

size 表示視窗的大小,如 秒、分鐘、小時、天。

舉個例子,假如我們要計算每個人每天的訂單量,按照 user 進行聚合分組:

SELECT user,
      TUMBLE_START(rowtime, INTERVAL ‘1’ DAY) as wStart,
      SUM(amount)
FROM Orders
GROUP BY TUMBLE(rowtime, INTERVAL ‘1’ DAY), user;

Hop Window

Hop 滑動視窗和滾動視窗類似,視窗有固定的 size,與滾動視窗不同的是滑動視窗可以通過 slide 引數控制滑動視窗的新建頻率。因此當 slide 值小於視窗 size 的值的時候多個滑動視窗會重疊,具體語義如下:

Hop 滑動視窗對應語法如下:

SELECT
    [gk],
    [HOP_START(timeCol, slide, size)] ,
    [HOP_END(timeCol, slide, size)],
    agg1(col1),
    ...
    aggN(colN)
FROM Tab1
GROUP BY [gk], HOP(timeCol, slide, size)

每次欄位的意思和 Tumble 視窗類似:

[gk] 決定了是否需要按照欄位進行聚合;

HOP_START 表示視窗開始時間;

HOP_END 表示視窗結束時間;

timeCol 表示流表中表示時間欄位;

slide 表示每次視窗滑動的大小;

size 表示整個視窗的大小,如 秒、分鐘、小時、天。

舉例說明,我們要每過一小時計算一次過去 24 小時內每個商品的銷量:

SELECT product,
      SUM(amount)
FROM Orders
GROUP BY HOP(rowtime, INTERVAL '1' HOUR, INTERVAL '1' DAY), product

Session Window

會話時間視窗沒有固定的持續時間,但它們的界限由 interval 不活動時間定義,即如果在定義的間隙期間沒有出現事件,則會話視窗關閉。

Seeeion 會話視窗對應語法如下:

SELECT
    [gk],
    SESSION_START(timeCol, gap) AS winStart,
    SESSION_END(timeCol, gap) AS winEnd,
    agg1(col1),
     ...
    aggn(colN)
FROM Tab1
GROUP BY [gk], SESSION(timeCol, gap)

[gk] 決定了是否需要按照欄位進行聚合;

SESSION_START 表示視窗開始時間;

SESSION_END 表示視窗結束時間;

timeCol 表示流表中表示時間欄位;

gap 表示視窗資料非活躍週期的時長。

例如,我們需要計算每個使用者訪問時間 12 小時內的訂單量:

SELECT user,
      SESSION_START(rowtime, INTERVAL ‘12’ HOUR) AS sStart,
      SESSION_ROWTIME(rowtime, INTERVAL ‘12’ HOUR) AS sEnd,
      SUM(amount)
FROM Orders
GROUP BY SESSION(rowtime, INTERVAL ‘12’ HOUR), user

Table API 和 SQL 捆綁在 flink-table Maven 工件中。必須將以下依賴項新增到你的專案才能使用 Table API 和 SQL:

 <dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table_2.11</artifactId>
    <version>${flink.version}</version>
</dependency>

另外,你需要為 Flink 的 Scala 批處理或流式 API 新增依賴項。對於批量查詢,您需要新增:

<dependency>
  <groupId>org.apache.flink</groupId>
<artifactId>flink-scala_2.11</artifactId>
  <version>${flink.version}</version>
</dependency>

1) 批資料 SQL

用法:

  1. 構建 Table 執行環境
  2. 將 DataSet 註冊為一張表
  3. 使用 Table 執行環境的 sqlQuery 方法來執行 SQL 語句

示例:使用 Flink SQL 統計使用者消費訂單的總金額、最大金額、最小金額、訂單總數

訂單 id 使用者名稱 訂單日期 消費金額
1 Zhangsan 2018-10-20 15:30 358.5

測試資料(訂單 ID、使用者名稱、訂單日期、訂單金額):

Order(1, "zhangsan", "2018-10-20 15:30", 358.5),
Order(2, "zhangsan", "2018-10-20 16:30", 131.5),
Order(3, "lisi", "2018-10-20 16:30", 127.5),
Order(4, "lisi", "2018-10-20 16:30", 328.5),
Order(5, "lisi", "2018-10-20 16:30", 432.5),
Order(6, "zhaoliu", "2018-10-20 22:30", 451.0),
Order(7, "zhaoliu", "2018-10-20 22:30", 362.0),
Order(8, "zhaoliu", "2018-10-20 22:30", 364.0),
Order(9, "zhaoliu", "2018-10-20 22:30", 341.0)

步驟:

  1. 獲取一個批處理執行環境
  2. 獲取一個 Table 執行環境
  3. 建立一個樣例類 Order 用來對映資料(訂單名、使用者名稱、訂單日期、訂單金額)
  4. 基於本地 Order 集合建立一個 DataSet source
  5. 使用 Table 執行環境將 DataSet 註冊為一張表
  6. 使用 SQL 語句來運算元據(統計使用者消費訂單的總金額、最大金額、最小金額、訂單總數)
  7. 使用 TableEnv.toDataSet 將 Table 轉換為 DataSet
  8. 列印測試

示例程式碼

import org.apache.flink.api.scala.ExecutionEnvironment
import org.apache.flink.table.api.{Table, TableEnvironment}
import org.apache.flink.table.api.scala.BatchTableEnvironment
import org.apache.flink.api.scala._
import org.apache.flink.types.Row
/**
 * 使用Flink SQL統計使用者消費訂單的總金額、最大金額、最小金額、訂單總數。
 */
object BatchFlinkSqlDemo {
  //3. 建立一個樣例類 Order 用來對映資料(訂單名、使用者名稱、訂單日期、訂單金額)
  case class Order(id:Int, userName:String, createTime:String, money:Double)
  def main(args: Array[String]): Unit = {
    /**
     * 實現思路:
     * 1. 獲取一個批處理執行環境
     * 2. 獲取一個Table執行環境
     * 3. 建立一個樣例類 Order 用來對映資料(訂單名、使用者名稱、訂單日期、訂單金額)
     * 4. 基於本地 Order 集合建立一個DataSet source
     * 5. 使用Table執行環境將DataSet註冊為一張表
     * 6. 使用SQL語句來運算元據(統計使用者消費訂單的總金額、最大金額、最小金額、訂單總數)
     * 7. 使用TableEnv.toDataSet將Table轉換為DataSet
     * 8. 列印測試
     */
    //1. 獲取一個批處理執行環境
    val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
    //2. 獲取一個Table執行環境
    val tabEnv: BatchTableEnvironment = TableEnvironment.getTableEnvironment(env)
    //4. 基於本地 Order 集合建立一個DataSet source
    val orderDataSet: DataSet[Order] = env.fromElements(
      Order(1, "zhangsan", "2018-10-20 15:30", 358.5),
      Order(2, "zhangsan", "2018-10-20 16:30", 131.5),
      Order(3, "lisi", "2018-10-20 16:30", 127.5),
      Order(4, "lisi", "2018-10-20 16:30", 328.5),
      Order(5, "lisi", "2018-10-20 16:30", 432.5),
      Order(6, "zhaoliu", "2018-10-20 22:30", 451.0),
      Order(7, "zhaoliu", "2018-10-20 22:30", 362.0),
      Order(8, "zhaoliu", "2018-10-20 22:30", 364.0),
      Order(9, "zhaoliu", "2018-10-20 22:30", 341.0)
    )

    //5. 使用Table執行環境將DataSet註冊為一張表
    tabEnv.registerDataSet("t_order", orderDataSet)

    //6. 使用SQL語句來運算元據(統計使用者消費訂單的總金額、最大金額、最小金額、訂單總數)
    //使用者消費訂單的總金額、最大金額、最小金額、訂單總數。
    val sql =
      """
        | select
        |   userName,
        |   sum(money) totalMoney,
        |   max(money) maxMoney,
        |   min(money) minMoney,
        |   count(1) totalCount
        |  from t_order
        |  group by userName
        |""".stripMargin  //在scala中stripMargin預設是“|”作為多行連線符

    //7. 使用TableEnv.toDataSet將Table轉換為DataSet
    val table: Table = tabEnv.sqlQuery(sql)
    table.printSchema()
    tabEnv.toDataSet[Row](table).print()
  }
}

2) 流資料 SQL

流處理中也可以支援 SQL。但是需要注意以下幾點:

  1. 要使用流處理的 SQL,必須要新增水印時間
  2. 使用 registerDataStream 登錄檔的時候,使用 ' 來指定欄位
  3. 登錄檔的時候,必須要指定一個 rowtime,否則無法在 SQL 中使用視窗
  4. 必須要匯入 import org.apache.flink.table.api.scala._ 隱式引數
  5. SQL 中使用 trumble(時間列名, interval '時間' sencond) 來進行定義視窗

示例:使用 Flink SQL 來統計 5 秒內 使用者的 訂單總數、訂單的最大金額、訂單的最小金額

步驟

  1. 獲取流處理執行環境
  2. 獲取 Table 執行環境
  3. 設定處理時間為 EventTime
  4. 建立一個訂單樣例類 Order ,包含四個欄位(訂單 ID、使用者 ID、訂單金額、時間戳)
  5. 建立一個自定義資料來源
    • 使用 for 迴圈生成 1000 個訂單
    • 隨機生成訂單 ID(UUID)
    • 隨機生成使用者 ID(0-2)
    • 隨機生成訂單金額(0-100)
    • 時間戳為當前系統時間
    • 每隔 1 秒生成一個訂單
  6. 新增水印,允許延遲 2 秒
  7. 匯入 import org.apache.flink.table.api.scala._ 隱式引數
  8. 使用 registerDataStream 登錄檔,並分別指定欄位,還要指定 rowtime 欄位
  9. 編寫 SQL 語句統計使用者訂單總數、最大金額、最小金額
    分組時要使用 tumble(時間列, interval '視窗時間' second) 來建立視窗
  10. 使用 tableEnv.sqlQuery 執行 sql 語句
  11. 將 SQL 的執行結果轉換成 DataStream 再列印出來
  12. 啟動流處理程式

示例程式碼

import java.util.UUID
import java.util.concurrent.TimeUnit
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.source.{RichSourceFunction, SourceFunction}
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.table.api.{Table, TableEnvironment}
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.types.Row
import scala.util.Random
/**
 * 需求:
 *  使用Flink SQL來統計5秒內 使用者的 訂單總數、訂單的最大金額、訂單的最小金額
 *
 *  timestamp是關鍵字不能作為欄位的名字(關鍵字不能作為欄位名字)
 */
object StreamFlinkSqlDemo {
    /**
     *  1. 獲取流處理執行環境
     * 2. 獲取Table執行環境
     * 3. 設定處理時間為 EventTime
     * 4. 建立一個訂單樣例類 Order ,包含四個欄位(訂單ID、使用者ID、訂單金額、時間戳)
     * 5. 建立一個自定義資料來源
     *    使用for迴圈生成1000個訂單
     *    隨機生成訂單ID(UUID)
     *    隨機生成使用者ID(0-2)
     *    隨機生成訂單金額(0-100)
     *    時間戳為當前系統時間
     *    每隔1秒生成一個訂單
     * 6. 新增水印,允許延遲2秒
     * 7. 匯入 import org.apache.flink.table.api.scala._ 隱式引數
     * 8. 使用 registerDataStream 登錄檔,並分別指定欄位,還要指定rowtime欄位
     * 9. 編寫SQL語句統計使用者訂單總數、最大金額、最小金額
     * 分組時要使用 tumble(時間列, interval '視窗時間' second) 來建立視窗
     * 10. 使用 tableEnv.sqlQuery 執行sql語句
     * 11. 將SQL的執行結果轉換成DataStream再列印出來
     * 12. 啟動流處理程式
     */
    // 3. 建立一個訂單樣例類`Order`,包含四個欄位(訂單ID、使用者ID、訂單金額、時間戳)
    case class Order(orderId:String, userId:Int, money:Long, createTime:Long)
    def main(args: Array[String]): Unit = {
      // 1. 建立流處理執行環境
      val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
      // 2. 設定處理時間為`EventTime`
      env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
      //獲取table的執行環境
      val tableEnv = TableEnvironment.getTableEnvironment(env)
      // 4. 建立一個自定義資料來源
      val orderDataStream = env.addSource(new RichSourceFunction[Order] {
        var isRunning = true
        override def run(ctx: SourceFunction.SourceContext[Order]): Unit = {
          // - 隨機生成訂單ID(UUID)
          // - 隨機生成使用者ID(0-2)
          // - 隨機生成訂單金額(0-100)
          // - 時間戳為當前系統時間
          // - 每隔1秒生成一個訂單
          for (i <- 0 until 1000 if isRunning) {
            val order = Order(UUID.randomUUID().toString, Random.nextInt(3), Random.nextInt(101),
              System.currentTimeMillis())
            TimeUnit.SECONDS.sleep(1)
            ctx.collect(order)
          }
        }
        override def cancel(): Unit = { isRunning = false }
      })
      // 5. 新增水印,允許延遲2秒
      val watermarkDataStream = orderDataStream.assignTimestampsAndWatermarks(
        new BoundedOutOfOrdernessTimestampExtractor[Order](Time.seconds(2)) {
          override def extractTimestamp(element: Order): Long = {
            val eventTime = element.createTime
            eventTime
          }
        }
      )
      // 6. 匯入`import org.apache.flink.table.api.scala._`隱式引數
      // 7. 使用`registerDataStream`登錄檔,並分別指定欄位,還要指定rowtime欄位
      import org.apache.flink.table.api.scala._
      tableEnv.registerDataStream("t_order", watermarkDataStream, 'orderId, 'userId, 'money,'createTime.rowtime)
      // 8. 編寫SQL語句統計使用者訂單總數、最大金額、最小金額
      // - 分組時要使用`tumble(時間列, interval '視窗時間' second)`來建立視窗
      val sql =
      """
        |select
        | userId,
        | count(1) as totalCount,
        | max(money) as maxMoney,
        | min(money) as minMoney
        | from
        | t_order
        | group by
        | tumble(createTime, interval '5' second),
        | userId
      """.stripMargin
      // 9. 使用`tableEnv.sqlQuery`執行sql語句
      val table: Table = tableEnv.sqlQuery(sql)

      // 10. 將SQL的執行結果轉換成DataStream再列印出來
      table.toRetractStream[Row].print()
      env.execute("StreamSQLApp")
    }
}

相關文章