flink sql

数据类型:

-- 字符串类型

# char类型
CHAR
CHAR(n) -- n在 12147483647 之间  未设置n=1
# 字符串类型
VARCHAR
VARCHAR(n)  -- n在 12147483647 之间  未设置n=1
STRING  -- 等于最大的varchar(max)
# 二进制类型
BINARY
BINARY(n) -- 范围同上
# 可变长度二进制类型
VARBINARY
VARBINARY(n)  -- 类似于string
BYTES

-- 数字类型

# 带有精度的十进制数字类型  -- 类似于java中的
DECIMAL
DECIMAL(p)
DECIMAL(p, s)

DEC
DEC(p)
DEC(p, s)

NUMERIC
NUMERIC(p)
NUMERIC(p, s)
# 带符号
TINYINT  -- -128 to 127
SMALLINT -- -32768 to 32767
# 不带符号的
INT  -- 2147483,648 to 2147483647
INTEGER
BIGINT  --  -9223372036854775808 to 9223372036854775807
# 带小数的
FLOAT
DOUBLE

-- 时间类型

#日期
DATE  -- 2020-10-12
#时间
TIME
TIME(p) -- 10:10:12.p  不指定p,p= 0
#时间戳
TIMESTAMP
TIMESTAMP(p) -- 2020-12-12 12:10:11.p

-- 其他类型
#
ARRAY<t>
t ARRAY
#map类型
MAP<kt, vt>

数据类型展开讲解:

好的,我们来详细讲解一下 Apache Flink SQL 所支持的数据类型,并结合示例进行说明。

Flink SQL 的数据类型系统是其能够正确处理和计算数据的基石。它提供了一套丰富的数据类型,从基本类型到复杂的嵌套类型,并且与 Flink 的 `TypeInformation` 体系紧密集成,确保了类型安全和执行效率。

Flink SQL 数据类型主要分为两大类:**原生数据类型** 和 **复合数据类型**---

### 一、原生数据类型 (Primitive Data Types)

也称为基本类型,是构成所有其他类型的基础。

| 数据类型 | 说明 | 示例值 | 在 DDL 中的定义 |
| :--- | :--- | :--- | :--- |
| `CHAR`, `VARCHAR`, `STRING` | 字符串类型。`CHAR(n)` 是固定长度,`VARCHAR(n)` 和 `STRING` 是可变长度。 | `'hello'`, `'Flink'` | `name VARCHAR(20)` |
| `BOOLEAN` | 布尔类型,`TRUE` 或 `FALSE`。 | `TRUE`, `FALSE` | `is_valid BOOLEAN` |
| `BINARY`, `VARBINARY`, `BYTES` | 字节数组类型。 | `x‘A1B2’` | `certificate BYTES` |
| `DECIMAL` | 高精度小数类型。`DECIMAL(p, s)`,p 是总位数,s 是小数位。 | `123.456` | `price DECIMAL(10, 2)` |
| `TINYINT` | 1字节有符号整数。 | `127`, `-128` | `age TINYINT` |
| `SMALLINT` | 2字节有符号整数。 | `32767`, `-32768` | `quantity SMALLINT` |
| `INT` | 4字节有符号整数。 | `2147483647` | `id INT` |
| `BIGINT` | 8字节有符号整数。 | `1234567890L` | `count BIGINT` |
| `FLOAT` | 4字节单精度浮点数。 | `1.23F` | `ratio FLOAT` |
| `DOUBLE` | 8字节双精度浮点数。 | `1.23456` | `salary DOUBLE` |
| `DATE` | 日期类型,格式为 `‘YYYY-MM-DD’`。 | `‘2023-10-26’` | `birthday DATE` |
| `TIME` | 时间类型,精度为毫秒,格式为 `‘HH:MM:SS[.fff]’`。 | `‘12:30:45.123’` | `login_time TIME(3)` |
| `TIMESTAMP` | 时间戳类型,精度可达纳秒。 | `‘2023-10-26 12:30:45.123’` | `event_time TIMESTAMP(3)` |
| `TIMESTAMP_LTZ` | 带本地时区的时间戳。这是处理事件时间的推荐类型。 | `‘2023-10-26 12:30:45.123+08:00’` | `ts TIMESTAMP_LTZ(3)` |
| `INTERVAL` | 时间间隔类型。 | `INTERVAL ‘2’ DAY` | `duration INTERVAL HOUR` |
| `NULL` | 空值类型。 | `NULL` |  |

**示例:使用 DDL 定义包含原生类型的表**

```sql
CREATE TABLE user_actions (
    user_id INT,
    user_name VARCHAR(100),
    is_new_user BOOLEAN,
    action_time TIMESTAMP(3),
    revenue DECIMAL(10, 2),
    WATERMARK FOR action_time AS action_time - INTERVAL '5' SECOND
) WITH (
    'connector' = 'kafka',
    ...
);
```

---

### 二、复合数据类型 (Composite Data Types)

复合类型由其他类型组合而成,允许你构建更复杂的数据结构。

#### 1. ARRAY (数组)

存储同一种数据类型的元素序列。

*   **声明**:`ARRAY<t>`
    *   例如:`ARRAY<INT>` 表示整数数组,`ARRAY<STRING>` 表示字符串数组。
*   **访问**:使用 `[index]` 从 1 开始索引。

**示例:**
```sql
-- 假设有表 orders,其中一列是 tags ARRAY<STRING>
SELECT
    order_id,
    tags[1] AS primary_tag, -- 获取第一个标签
    CARDINALITY(tags) AS tag_count -- 获取数组长度
FROM orders;

-- 也可以这样创建带数组列的表
CREATE TABLE products (
    id INT,
    name STRING,
    prices ARRAY<DECIMAL(8,2)> -- 一个价格数组
);
```

#### 2. MAP (映射)

存储键值对(K-V)集合。键和值可以是任何类型,但键必须是同一类型,值也必须是同一类型。

*   **声明**:`MAP<kt, vt>`
    *   例如:`MAP<STRING, INT>` 表示键为字符串、值为整数的映射。
*   **访问**:使用 `[key]` 来访问对应键的值。

**示例:**
```sql
-- 假设有表 users,其中一列是 properties MAP<STRING, STRING>
SELECT
    user_id,
    properties['country'] AS country, -- 获取 key 为 'country' 的值
    properties['age'] AS age_str
FROM users;

-- 创建带 MAP 列的表
CREATE TABLE configs (
    app_id STRING,
    settings MAP<STRING, STRING>
);
```

#### 3. ROW (行/结构体)

存储多个命名字段,每个字段可以有不同的数据类型。这是最常用的复合类型,可以用来表示嵌套的 JSON 对象。

*   **声明**:`ROW<n0 t0, n1 t1, ...>` 或 `ROW<n0 t0 ‘description’, ...>`
    *   例如:`ROW<city STRING, zipCode INT>`。
*   **访问**:使用点号 `.` 访问字段(例如 `row_column.field_name`)。

**示例:**
```sql
-- 假设有表 shipments,其中一列是 address ROW<city STRING, street STRING, zip INT>
SELECT
    order_id,
    address.city, -- 访问 ROW 类型的字段
    address.zip
FROM shipments;

-- 创建带 ROW 列的表 (类似于定义嵌套的JSON结构)
CREATE TABLE events (
    event_id BIGINT,
    user ROW<
        id INT,
        name STRING,
        device ROW<os STRING, version STRING> -- ROW 中可以嵌套 ROW
    >,
    `timestamp` TIMESTAMP_LTZ(3)
);
```

---

### 三、如何使用:示例演示

让我们通过一个完整的示例来展示如何在 Flink SQL 中定义和使用这些类型。

**场景**:处理来自 Kafka 的用户行为事件,事件是 JSON 格式,包含一些复杂结构。

**1. 原始的 JSON 事件示例:**
```json
{
  "event_id": 123456,
  "user": {
    "id": 888,
    "name": "Alice",
    "tags": ["vip", "new"],
    "properties": {
      "age": "25",
      "country": "US"
    }
  },
  "action": "click",
  "url": "https://example.com",
  "ts": "2023-10-26 10:30:45.120"
}
```

**2. 对应的 Flink DDL 表定义:**

```sql
CREATE TABLE user_behavior (
    -- 原生类型
    event_id BIGINT,
    action STRING,
    url STRING,

    -- ROW 复合类型 (对应JSON中的user对象)
    user ROW<
        id INT,
        name STRING,
        -- ARRAY 复合类型
        tags ARRAY<STRING>,
        -- MAP 复合类型
        properties MAP<STRING, STRING>
    >,

    -- 时间戳(注意:从JSON中读出来是STRING,这里用计算列转换)
    ts_string STRING,
    -- 使用计算列将字符串转为时间戳
    `ts` AS TO_TIMESTAMP(ts_string),
    -- 定义事件时间和水位线
    WATERMARK FOR `ts` AS `ts` - INTERVAL '5' SECOND
) WITH (
    'connector' = 'kafka',
    'topic' = 'user_behavior_topic',
    'properties.bootstrap.servers' = 'localhost:9092',
    'format' = 'json',
    'json.fail-on-missing-field' = 'false',
    'json.ignore-parse-errors' = 'true'
);
```

**3. 查询示例:**

```sql
-- 1. 查询所有字段,包括嵌套字段
SELECT
    event_id,
    action,
    user.id AS uid, -- 访问ROW内的字段
    user.name AS uname,
    user.tags[1] AS primary_tag, -- 访问ARRAY
    user.properties['country'] AS country -- 访问MAP
FROM user_behavior;

-- 2. 过滤出包含特定标签的用户行为
SELECT *
FROM user_behavior
WHERE CARDINALITY(user.tags) > 0 -- 数组长度大于0
  AND user.tags[1] = 'vip'; -- 并且第一个标签是'vip'

-- 3. 进行分组聚合 (例如:按国家和动作分组计数)
SELECT
    user.properties['country'] AS country,
    action,
    COUNT(*) AS action_count
FROM user_behavior
GROUP BY user.properties['country'], action;
```

### 总结

| 类型分类 | 关键类型 | 用途 |
| :--- | :--- | :--- |
| **原生类型** | `VARCHAR`, `INT`, `DOUBLE`, `TIMESTAMP`等 | 处理基本数据元素,如数字、字符串、时间。 |
| **数组 (ARRAY)** | `ARRAY<T>` | 处理列表形式的数据,如标签、历史记录。 |
| **映射 (MAP)** | `MAP<KT, VT>` | 处理键值对形式的动态属性。 |
| **行 (ROW)** | `ROW<f0 T0, f1 T1 ...>` | 处理嵌套结构,是构建复杂数据模型的核心。 |

**最佳实践建议:**

1.  **时间处理**:在处理事件时间时,优先使用 `TIMESTAMP_LTZ(3)` 类型,并正确定义 `WATERMARK`。
2.  **NULL 值**:注意所有数据类型默认都可以是 `NULL`,确保你的业务逻辑能处理 `NULL` 值,或在 DDL 中使用 `NOT NULL` 约束。
3.  **模式演化**:在使用 Kafka 等连接器时,复合类型(尤其是 `ROW`)的模式演化需要谨慎处理。
4.  **性能**:嵌套的复合类型(特别是多级嵌套)可能会带来一定的序列化和反序列化开销,在设计数据结构时要权衡可读性和性能。

熟练掌握这些数据类型是高效编写 Flink SQL 作业的关键,它让你能够灵活地处理各种复杂的实时数据流。

 

DDL

表类型:
好的,我们来深入探讨一下 Apache Flink 中表的类型。这是一个非常核心的概念,理解它对于正确使用 Flink SQL 进行流处理和批处理至关重要。

在 Flink 中,表的类型主要由两个维度决定:
1.  **表的物理存储性质**:是**普通表**还是**临时表**2.  **表所代表的数据特征**:是**流表**还是**批表**?更进一步,流表中又分为**常规流表**和**时态表**。

下面我们结合示例详细讲解。

---

### 一、按物理存储性质划分 (普通表 vs 临时表)

这个维度指的是表在 Flink Catalog(元数据管理中心)中的生命周期和可见性。

#### 1. 普通表 (Permanent Table / Catalog Table)

*   **定义**:在 Flink **Catalog**(如 Hive Metastore、JDBC 数据库或内存中的 `GenericInMemoryCatalog`)中持久化存在的表。其元数据(如表名、列、字段类型、连接器信息等)会被保存下来。
*   **生命周期**:**跨 Flink 会话/作业**。即使重启 Flink 集群或提交新的作业,只要连接到同一个 Catalog,之前创建的普通表依然存在。
*   **用途**:用于定义需要复用的数据源和数据汇,例如连接到 Kafka 的源表、连接到 JDBC 的维表或结果表。
*   **创建方式**:使用 `CREATE TABLE` 语句(如果不存在则 `CREATE TABLE IF NOT EXISTS`)。

**示例:创建一个连接到 Kafka 的普通源表**
```sql
-- 这条语句会将该表的元数据持久化到当前会话所使用的 Catalog 中
CREATE TABLE user_clicks (
    user_id STRING,
    url STRING,
    `time` TIMESTAMP(3),
    WATERMARK FOR `time` AS `time` - INTERVAL '5' SECOND
) WITH (
    'connector' = 'kafka',
    'topic' = 'user-clicks-topic',
    'properties.bootstrap.servers' = 'localhost:9092',
    'format' = 'json'
);
```

#### 2. 临时表 (Temporary Table)

*   **定义**:仅存在于当前 **Flink 会话/作业生命周期** 内的表。它不属于任何 Catalog,其元数据仅保存在内存中。
*   **生命周期**:**限于当前 Flink 会话**。会话结束(如程序退出、SQL 客户端断开)后,表及其元数据都会消失。
*   **用途**:用于存储**中间计算结果**,或者为了方便查询而给某个查询逻辑起的别名。它非常适合在一条复杂的 SQL 中拆分多个步骤。
*   **创建方式***   使用 `CREATE TEMPORARY TABLE` 语句。
    *   使用 `CREATE TEMPORARY VIEW` 语句(从查询逻辑创建,不连接外部系统)。

**示例:创建临时视图和临时表**
```sql
-- 1. 从一个查询创建临时视图(虚拟表,不存储数据)
CREATE TEMPORARY VIEW vip_users AS
SELECT user_id, count(*) as click_count
FROM user_clicks
GROUP BY user_id
HAVING count(*) > 100;

-- 2. 创建一个连接到外部系统的临时表(较少见,但可行)
CREATE TEMPORARY TABLE temp_results (
    user_id STRING,
    score DOUBLE
) WITH (
    'connector' = 'print' -- 只是打印出来,会话结束就没了
);

-- 将 vip_users 的结果插入到临时表
INSERT INTO temp_results
SELECT user_id, click_count * 10 AS score FROM vip_users;
```
**关键区别总结**| 特性 | 普通表 (Permanent) | 临时表 (Temporary) |
| :--- | :--- | :--- |
| **元数据存储** | 持久化在 Catalog 中 | 仅存在于会话内存中 |
| **生命周期** | 跨会话、跨作业 | 仅当前会话有效 |
| **可见性** | 对所有会话可见(如果使用共享Catalog) | 仅对创建它的会话可见 |
| **典型用途** | 源表、维表、最终结果表 | 中间结果、查询逻辑封装 |

---

### 二、按数据特征划分 (流表 vs 批表)

这个维度是由表的底层连接器决定的,它决定了表的行为和可应用的查询操作。

#### 1. 流表 (Streaming Table / Dynamic Table)

*   **定义**:表示一个**无限、持续变化**的数据流。表中的数据是不断追加、更新或删除的。这是 Flink 流处理的核心概念,也称为**动态表***   **数据特征**:无界、实时变化。
*   **查询模式**:查询是**连续查询**。Flink 会持续监控输入流,并在每条新记录到达时不断更新查询结果。结果本身也是一个无限流。
*   **语法支持**:支持所有流处理特定操作,如:
    *   `WATERMARK` 定义:用于处理乱序事件和基于事件时间的窗口。
    *   窗口聚合:`TUMBLE`, `HOP`, `SESSION`。
    *   `MATCH_RECOGNIZE`:复杂事件处理(CEP)。
    *   回撤流:结果更新时,会先发送一条撤回消息(`-U`),再发送一条新消息(`+U`)。

**示例:流表上的连续查询**
```sql
-- 源表 user_clicks 是一个流表
-- 这是一个连续查询,会每5秒输出一次过去10秒内每个用户的点击次数
SELECT
    user_id,
    TUMBLE_START(`time`, INTERVAL '10' SECOND) as window_start,
    COUNT(url) as click_cnt
FROM user_clicks
GROUP BY
    user_id,
    TUMBLE(`time`, INTERVAL '10' SECOND);
```

#### 2. 批表 (Batch Table)

*   **定义**:表示一个**有限、静态**的数据集合。表中的数据在执行查询前就已经完全确定,不会改变。
*   **数据特征**:有界、静态。
*   **查询模式**:查询是**批查询**。Flink 读取全部输入数据,计算后输出最终结果,然后作业就结束了。
*   **语法支持**:主要支持标准的 ANSI SQL 查询语法。不支持流处理特有的操作(如 `WATERMARK`, 窗口聚合等)。

**示例:批表上的批查询**
假设 `user_clicks_batch` 是一个连接到 HDFS parquet 文件的表。
```sql
-- 这是一个批查询,计算所有数据中每个用户的总点击次数,输出最终结果后作业结束
SELECT user_id, COUNT(*) as total_clicks
FROM user_clicks_batch
GROUP BY user_id;
```

**关键区别总结**| 特性 | 流表 (Streaming) | 批表 (Batch) |
| :--- | :--- | :--- |
| **数据范围** | 无界 (Unbounded) | 有界 (Bounded) |
| **数据状态** | 动态、持续变化 | 静态、固定不变 |
| **查询类型** | 连续查询 (Continuous Query) | 批查询 (Batch Query) |
| **结果更新** | 持续更新、可能产生回撤消息 | 一次性最终结果 |
| **典型操作** | 窗口聚合、Top-N、CEP | 标准 GROUP BY, JOIN |

---

### 三、流表中的特殊类型:时态表 (Temporal Table)

时态表是流表的一个特殊子类,用于表示一个**随时间不断变化的维表**。它对于流流 `JOIN`(如双流 `JOIN`)和**维表 `JOIN`** 至关重要。

它又分为两种:

#### 1. 版本表 (Versioned Table) / 时态表 (Temporal Table)

*   **定义**:一个可以追溯历史版本变化的流表。表中的每条记录都有一个**时间属性**(通常是处理时间或事件时间)来标识其有效性。
*   **核心概念**:`PRIMARY KEY` + `事件时间`。Flink 通过主键和时间来跟踪同一主键记录的不同版本。
*   **用途**:用于 `JOIN` 一个主数据流和一个不断变化的维表流(数据库 `CDC` 数据流是典型例子)。

**示例:将 Kafka 中的 CDC 数据流定义为时态表**
```sql
-- 1. 创建一个包含CDC数据的流表(必须有主键和事件时间)
CREATE TABLE currency_rates (
    currency STRING,
    conversion_rate DECIMAL(10, 4),
    update_time TIMESTAMP(3) METADATA FROM 'value.source.timestamp' VIRTUAL, -- 从Kafka记录中获取时间
    WATERMARK FOR update_time AS update_time - INTERVAL '5' SECOND,
    PRIMARY KEY (currency) NOT ENFORCED -- 定义主键至关重要!
) WITH (
    'connector' = 'kafka',
    'topic' = 'currency-rates',
    'value.format' = 'debezium-json' -- 使用Debezium格式解析CDC日志
);

-- 2. 将其转换为时态表函数(旧版写法)或直接用于时态JOIN(新版写法)
-- 新版 Temporal JOIN 语法 (Flink 1.12+)
SELECT
    o.order_id,
    o.amount,
    o.currency,
    o.amount * r.conversion_rate AS amount_usd,
    o.order_time
FROM orders AS o
-- 关键语法:FOR SYSTEM_TIME AS OF 指定基于事件时间的一致性快照
LEFT JOIN currency_rates FOR SYSTEM_TIME AS OF o.order_time AS r
ON o.currency = r.currency;
```

#### 2. 普通表 (普通流表)

就是最常见的流表,不强调版本变化,通常用于主数据流。

---

### 总结与实践建议

| 表类型 | 创建语句 | 核心特征 | 用途 |
| :--- | :--- | :--- | :--- |
| **普通表** | `CREATE TABLE ...` | 元数据持久化,跨会话 | 定义源、汇、维表 |
| **临时表/视图** | `CREATE TEMPORARY ...` / `CREATE TEMPORARY VIEW ...` | 元数据在会话内存中 | 存储中间结果,简化查询 |
| **流表** | `CREATE TABLE ...` + 流连接器(如 Kafka) | 无界、动态、支持连续查询 | 实时流处理 |
| **批表** | `CREATE TABLE ...` + 批连接器(如 Filesystem) | 有界、静态、标准批查询 | 批量数据处理 |
| **时态表** | `CREATE TABLE ...` + `PRIMARY KEY` + 时间属性 | 带版本的流表,可追溯历史 | 流与变化维表 `JOIN` |

**如何判断一张表的类型?**

1.  **看 Connector**:`'connector' = 'kafka'` 表明它是流表;`'connector' = 'filesystem'` 且路径下文件不会变,则可视为批表。
2.  **看是否有 `WATERMARK`**:定义了 `WATERMARK` 的表一定是流表。
3.  **看是否有 `PRIMARY KEY`**:与时间属性结合,可能用于定义时态表。

理解这些表的类型及其区别,能帮助你更好地设计 Flink SQL 作业,选择正确的表类型和查询模式来处理你的数据。

 定义列时可以定义计算列:

 
CREATE TABLE orders (
    order_id STRING,
    user_id STRING,
    amount DOUBLE,
    order_time TIMESTAMP(3),
    -- 这就是一个计算列!
    proc_time AS PROCTIME(), 
    WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (...);

 

使用场景

临时表适用场景

  1. 临时数据分析:当需要临时处理一些中间数据,且不希望影响其他会话时

  2. 测试和调试:快速创建表结构进行SQL验证,无需持久化

  3. 会话级中间结果:在复杂查询中作为中间步骤的存储

  4. 个性化视图:为特定会话创建个性化数据视图

永久表适用场景

  1. 持久化数据存储:需要长期保存的表结构和数据

  2. 跨会话共享:多个Flink作业或会话需要访问相同表定义

  3. 生产环境应用:作为正式数据管道的一部分

  4. 与外部系统集成:需要Hive等其他系统访问的表定义


不同的数据源对应不同的连接器,可以通过官方文档查询连接器以及对应的参数。
阿里云连接器文档:https://help.aliyun.com/zh/flink/developer-reference/supported-connectors?spm=a2c4g.11186623.help-menu-45029.d_5_0_0.69cefa0c8wipPU

生产最佳实践:

非常好的问题,这触及了 Flink SQL 中表生命周期管理和状态管理的核心概念。

### 1. 为什么 Kafka 源流表创建为临时表?

将 Kafka 源表创建为**临时表(TEMPORARY TABLE)** 主要是出于以下几个原因:

**a) 作用域隔离**
临时表只在**当前会话(Session)** 中有效。当 Flink SQL Client 会话结束或作业停止后,这个表定义就会被清除。这样可以避免在 Catalog 中积累大量仅用于特定作业的表定义,保持 Catalog 的整洁。

**b) 避免命名冲突**
如果你有多个作业都要从同一个 Kafka Topic 读取数据但有不同的处理逻辑(比如不同的解析格式、不同的消费组),使用临时表可以让你在每个作业中都使用相同的表名(如 `vehicle_signals`),而不会相互冲突。

**c) 作业自包含性**
使用临时表使得整个作业脚本(CREATE TABLE → 处理逻辑 → INSERT INTO)是自包含的。任何人拿到这个 SQL 脚本都可以直接运行,不需要预先在 Catalog 中创建特定的表。

**d) 开发测试便利性**
在开发和测试阶段,你可能需要频繁修改表结构(比如添加字段、修改连接器参数)。使用临时表可以方便地进行这些修改,而无需担心影响其他作业或需要手动删除已存在的表。

### 2. 临时表作业重启后是否就消失了?

**是的,临时表的定义会消失,但这不会影响作业的状态恢复。**

这是一个非常重要的区别:

- **表定义(元数据)**:临时表的定义存储在**会话的内存**中。作业重启后,需要重新执行 `CREATE TEMPORARY TABLE` 语句来重新创建表定义。
  
- **消费状态(数据位置)**:Kafka 消费的偏移量(offset)是作为 **Flink 作业状态** 的一部分保存的,与表是否是临时的无关。

### 3. 下次消费的点 Flink 将如何处理?

Flink 通过**状态后端(State Backend)** 和 **检查点(Checkpoint)** 机制来管理消费状态:

**a) 状态持久化机制**
```sql
CREATE TEMPORARY TABLE vehicle_signals (
    -- ...
) WITH (
    'connector' = 'kafka',
    'topic' = 'vehicle-signals-topic',
    'properties.bootstrap.servers' = 'localhost:9092',
    'properties.group.id' = 'flink-sql-processor', -- 关键:消费组ID
    'format' = 'json',
    'scan.startup.mode' = 'latest-offset' -- 仅首次启动时生效
);
```

**b) 工作流程:**
1.  **首次启动**:根据 `scan.startup.mode` 决定从哪个位置开始消费(如 `latest-offset` 从最新位置)
2.  **运行中**:Flink 会定期创建**检查点(Checkpoint)**,将 Kafka 消费的偏移量与其他算子的状态一起持久化到状态后端(如 HDFS、S3)
3.  **作业重启**- Flink 会从**最近一次成功的检查点**恢复状态
    - 重新创建临时表定义(通过重新执行 SQL 脚本)
    - 从保存的偏移量位置继续消费,**完全忽略** `scan.startup.mode` 的设置
    - 这样就实现了 **"精确一次(Exactly-Once)"** 的处理语义

**c) 如果没有检查点?**
如果作业是全新启动(没有之前的检查点),那么 `scan.startup.mode` 就会生效,决定从何处开始消费。

### 最佳实践建议

对于生产环境,我推荐这种方式:

```sql
-- 1. 首先设置检查点和状态后端(通常在sql-client-defaults.yaml或代码中配置)
SET 'execution.checkpointing.interval' = '1min';
SET 'state.backend' = 'rocksdb';
SET 'state.checkpoints.dir' = 'hdfs:///flink/checkpoints';

-- 2. 使用临时表定义源表
CREATE TEMPORARY TABLE vehicle_signals (...);

-- 3. 创建目标表(如果是需要长期存在的表,可以不使用TEMPORARY)
CREATE TABLE IF NOT EXISTS filtered_vehicle_signals (...);

-- 4. 执行插入操作
INSERT INTO filtered_vehicle_signals SELECT ...;
```

### 总结

|  aspect | 临时表定义 | Kafka消费状态 |
|---------|------------|---------------|
| **存储位置** | 会话内存 | 状态后端(RocksDB/HDFS) |
| **持久性** | 非持久化(会话结束消失) | 持久化(通过检查点) |
| **恢复机制** | 需要重新执行CREATE语句 | 自动从检查点恢复偏移量 |
| **影响范围** | 仅影响当前会话 | 影响作业的消费位置 |

因此,**即使表定义是临时的,Flink 也能保证在作业重启后从正确的 Kafka 偏移量位置继续消费**,不会丢失数据也不会重复处理数据。

 

DQL

常用函数可以参考flink官方文档:

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/dev/table/functions/systemfunctions/

如果用的是flink官网阿里版,看阿里云的文档即可:

https://help.aliyun.com/zh/flink/developer-reference/overview-7?spm=a2c4g.11174283.help-menu-45029.d_5_2_6_0.2ede2215EXpjic&scm=20140722.H_188424._.OR_help-T_cn~zh-V_1

1.with子句

和ODPS SQL一样使用

2.select和where

和标准SQL一样使用

3.select和distinct

和离线不同,实时的去重需要保留状态,那么要么设置状态时间

state.ttl,或者根据实际情况,通过窗口和group by 去重

4.窗口表值函数TVF
核心是windows,将无限的流分为有限的流:
窗口函数分位滚动、滑动、累计窗口:TUMBLEHOPCUMULATE
窗口本身返回的是一个“表”
示例数据源:
-- 创建Kafka源表
CREATE TABLE user_events (
    user_id BIGINT,
    item_id BIGINT,
    category_id BIGINT,
    behavior STRING,
    event_time TIMESTAMP(3),
    -- 定义事件时间属性
    WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND
) WITH (
    'connector' = 'kafka',
    'topic' = 'user_events',
    'properties.bootstrap.servers' = 'kafka:9092',
    'properties.group.id' = 'user_behavior',
    'scan.startup.mode' = 'latest-offset',
    'format' = 'json'
);
滚动窗口:

TUMBLE(TABLE data, DESCRIPTOR(timecol), size [, offset ])

返回值:

TUMBLE 的返回值是一个新的关系,它包括原来表的所有列以及另外3列“window_start”,“window_end”,“window_time”来表示分配的窗口。

用于计算窗口的表,必须有时间戳字段

-- 计算每5分钟各商品的点击量
CREATE TABLE item_click_counts_5min (
    window_start TIMESTAMP(3),
    window_end TIMESTAMP(3),
    item_id BIGINT,
    click_count BIGINT,
    -- 添加处理时间,便于下游消费
    proc_time AS PROCTIME()
) WITH (
    'connector' = 'kafka',
    'topic' = 'item_click_counts_5min',
    'properties.bootstrap.servers' = 'kafka:9092',
    'format' = 'json',
    'json.ignore-null-fields' = 'false'
);

-- 将窗口计算结果插入到输出表
INSERT INTO item_click_counts_5min
SELECT 
    window_start, 
    window_end,
    item_id,
    COUNT(*) AS click_count
FROM TABLE(
    TUMBLE(TABLE user_events, DESCRIPTOR(event_time), INTERVAL '5' MINUTES)
)
WHERE behavior = 'click'
GROUP BY window_start, window_end, item_id;

滑动窗口类似用法:

-- 创建UV结果表(输出到MySQL)
CREATE TABLE user_activity_uv (
    window_start TIMESTAMP(3),
    window_end TIMESTAMP(3),
    uv BIGINT,
    PRIMARY KEY (window_start, window_end) NOT ENFORCED
) WITH (
    'connector' = 'jdbc',
    'url' = 'jdbc:mysql://mysql:3306/analytics',
    'table-name' = 'user_activity_uv',
    'username' = 'flink',
    'password' = 'flinkpw',
    'sink.buffer-flush.interval' = '1s'
);

-- 计算滑动窗口UV并写入MySQL
INSERT INTO user_activity_uv
SELECT 
    window_start,
    window_end,
    COUNT(DISTINCT user_id) AS uv
FROM TABLE(
    HOP(TABLE user_events, DESCRIPTOR(event_time), 
        INTERVAL '1' MINUTES, INTERVAL '10' MINUTES)
)
GROUP BY window_start, window_end;

 

 

DML

表JOIN:

好的,我们来深入探讨 Apache Flink SQL 中的 `JOIN`。`JOIN` 是 SQL 中最核心、最强大的操作之一,但在流处理引擎中,它的语义和实现与批处理有很大不同,理解这些差异至关重要。

### 1. Flink SQL JOIN 的核心挑战

在批处理(如 Hive, Spark Batch)中,`JOIN` 是针对两个**有界数据集**的完整关联操作,结果是确定且完整的。

而在流处理中,`JOIN` 是针对两个**无限持续的无界流**。这带来了几个关键问题:
*   **状态无限增长**:为了计算 `JOIN`,Flink 需要将两个流中所有到达的数据都保存在状态(State)中,因为任何一条新数据都可能与未来另一条流的数据匹配。如果不加限制,状态会无限增大直至内存耗尽。
*   **结果何时输出**:对于一条刚到达的数据,我们无法知道是否已经看到了它所有可能的匹配项。因此,流 `JOIN` 通常不会输出一个“最终结果”,而是会持续输出更新的结果。

为了解决这些问题,Flink SQL 提供了多种类型的 `JOIN`,每种都有其特定的语义和适用场景。

---

### 2. Flink SQL JOIN 的主要类型

#### 2.1 常规连接 (Regular Joins)

这是最符合传统 SQL 思维的 `JOIN`,也是最需要谨慎使用的一种。

*   **工作原理**:两侧流中任何一条新数据的到来,都会与另一侧流中**所有历史数据**(存储在状态中)进行匹配并输出结果。它会产生更新的结果(即 `Retract` 消息)。
*   **状态**:**会无限增长**,因为需要保存所有历史记录以供未来数据匹配。
*   **输出模式**:**更新流**。结果会不断更新,通常需要下游支持更新(如 Upsert Kafka)或设置键(如 `GROUP BY`)来处理撤回消息。
*   **语法**:就是标准的 `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL OUTER JOIN`。
*   **适用场景**:**默认情况下应避免使用**。仅适用于**维度表连接**(通过 `LOOKUP JOIN` 优化)或已知数据有界(如一个流是有限的控制流)的场景。

**示例**:
```sql
-- 订单流 JOIN 付款流,输出所有匹配的订单支付记录
-- 注意:状态会无限增长!
SELECT *
FROM orders o
INNER JOIN payments p ON o.order_id = p.order_id;
```

#### 2.2 时间区间连接 (Interval Joins)

这是流处理中非常常用且实用的 `JOIN`,通过引入**时间约束**来限制状态保留的时间,从而解决状态无限增长的问题。

*   **工作原理**:它为 `JOIN` 条件添加了一个**时间谓词**。要求两条流中参与连接的两条记录的时间戳在一个指定的时间区间内。例如,一条订单记录只能与它创建时间前后一段时间内的付款记录进行连接。
*   **状态**:**状态会自动清理**。Flink 会根据指定的时间区间保留状态,超出时间范围的状态会被自动清除,防止无限增长。
*   **输出模式**:**仅追加流**。每条数据一旦被处理并超出时间窗口后,就不会再影响后续结果,因此结果是确定的,不会更新。
*   **语法**:在 `ON` 条件中增加时间范围判断。
*   **适用场景**:两个流中的记录存在明确且有限的时间关联性。例如,订单和支付(支付通常在订单后一段时间内发生)、登录和下单等。

**示例**:
```sql
-- 订单必须在支付时间的前后4小时内发生,才能关联上
SELECT *
FROM orders o, payments p
WHERE o.order_id = p.order_id
-- 关键的时间区间条件:使用事件时间
AND o.order_time BETWEEN p.payment_time - INTERVAL '4' HOUR 
                    AND p.payment_time + INTERVAL '4' HOUR;

-- 如果使用处理时间,Flink会自动获取当前时间进行比较
AND o.order_time BETWEEN p.payment_time - INTERVAL '4' HOUR 
                    AND p.payment_time + INTERVAL '4' HOUR;
```

#### 2.3 时态表连接 (Temporal Table Joins)

这是流与**版本化表**进行连接的强大工具,是流处理中“维度表连接”的标准做法。

*   **核心概念**:一张**时态表**(Temporal Table)是一张会随时间变化的表(即维表),它记录了每个主键在不同时间点上的多个版本的值。
*   **工作原理**:主流(事实流)中的每条记录会根据其**时间属性**(事件时间或处理时间),去时态表中查找**当时最新版本**的维度数据进行关联。这就像是在“回放”历史,为每条事实记录匹配上它发生那一刻的维度快照。
*   **状态**:时态表一侧的状态可以被优化(如使用 `LOOKUP`),主流状态不会无限增长。
*   **输出模式**:**仅追加流***   **分类***   **基于事件时间的时态表 JOIN**:最常用。主流使用事件时间,确保结果的可重放性和准确性。
    *   **基于处理时间的时态表 JOIN**:主流使用处理时间,只能得到“当前最新”的维度值,效率最高,但无版本概念。

**语法**:需要使用 `FOR SYSTEM_TIME AS OF` 语法。

**示例**:
假设有一个汇率变化流 `currency_rates`,我们将其物化为一张时态表。

```sql
-- 1. 定义汇率流(维表流)
CREATE TABLE currency_rates (
  currency STRING,
  conversion_rate DECIMAL(32, 2),
  update_time TIMESTAMP(3) METADATA FROM 'timestamp', -- 来自Kafka的时间戳
  WATERMARK FOR update_time AS update_time - INTERVAL '5' SECOND,
  PRIMARY KEY(currency) NOT ENFORCED
) WITH (
  'connector' = 'kafka',
  ...
);

-- 2. 将汇率流声明为时态表
CREATE VIEW versioned_rates AS
SELECT currency, conversion_rate, update_time
FROM currency_rates
/* 声明主键和时间属性,Flink就知道这是一张时态表 */;

-- 3. 主流(订单流)与它进行时态连接
-- “为每个订单匹配它发生时刻(o.order_time)的最新汇率”
SELECT
  o.order_id,
  o.amount,
  o.currency,
  o.order_time,
  r.conversion_rate,
  o.amount * r.conversion_rate AS amount_usd
FROM orders o
LEFT JOIN versioned_rates FOR SYSTEM_TIME AS OF o.order_time r
ON o.currency = r.currency;
```

#### 2.4 查找连接 (Lookup Joins)

这是**处理时间时态表连接**的一种特化和优化,专用于与外部维表系统(如 MySQL、Redis、HBase)进行连接。

*   **工作原理**:对于主流中的每条记录,Flink 会**实时地**(同步或异步)查询外部数据库,获取**当前最新**的维度值。它没有版本概念,只查当下。
*   **状态**:**不缓存或缓存少量状态**。主流状态不增长,外部查询结果可以配置缓存以提高性能。
*   **输出模式**:**仅追加流***   **适用场景**:连接外部维度表,且对数据准确性要求不是极端苛刻(可以接受“当前最新值”),追求低延迟和高吞吐。例如,根据商品ID实时查询商品名称和价格。

**示例**:
```sql
-- 订单流实时查询JDBC中的商品表,获取最新价格
CREATE TABLE customers (
  id INT,
  name STRING,
  country STRING,
  zipcode STRING
) WITH (
  'connector' = 'jdbc',
  'url' = 'jdbc:mysql://mysqlhost:3306/customerdb',
  'table-name' = 'customers'
);

SELECT
  o.order_id,
  o.total,
  c.name,
  c.country
FROM orders o
JOIN customers FOR SYSTEM_TIME AS OF o.proc_time AS c -- 使用处理时间
ON o.customer_id = c.id;
```

---

### 3. JOIN 类型总结与选择指南

| 类型 | 状态管理 | 输出模式 | 特点 | 适用场景 |
| :--- | :--- | :--- | :--- | :--- |
| **Regular Join** | **无限增长** | 更新流 | 功能最强,但最危险 | 避免使用,或用于有界流/维度查询 |
| **Interval Join** | **自动清理** | 仅追加流 | 有时间范围限制,安全 | 双流连接,且有明确时间关系(如订单-支付) |
| **Temporal Join** | 可管理 | 仅追加流 | 流表连接,版本化 | **流与维表连接的标准答案**(基于事件时间) |
| **Lookup Join** | 无/少量 | 仅追加流 | 实时查询外部存储 | 维表连接,要求低延迟,可接受“当前值” |

### 4. 最佳实践与注意事项

1.  **明确时间语义**:始终思考是基于**事件时间**(准确、可重放)还是**处理时间**(低延迟、简单)。
2.  **优先使用 Interval / Temporal Joins**:它们通过时间约束解决了状态问题,是生产环境的首选。
3.  **谨慎使用 Regular Joins**:必须评估状态增长的风险,并设置合理的状态 TTL(`table.exec.state.ttl`),但这可能导致结果不准确。
4.  **设置合理的状态 TTL**:对于 Regular Join,必须设置 `table.exec.state.ttl`(如 `'table.exec.state.ttl' = '7 d'`)来避免状态无限增长。
5.  **使用 `WHERE` 提前过滤**:在 `JOIN` 前尽可能通过 `WHERE` 子句减少参与计算的数据量,降低状态和计算压力。

总之,Flink SQL 的 `JOIN` 非常强大,但“能力越大,责任越大”。正确选择 `JOIN` 类型并理解其底层机制,是构建稳定、高效流处理应用的关键。

 维表JOIN的典型实现:

好的,这是一个非常经典和实际的生产场景。我们来深入探讨一下,当维表是 **T+1 更新的 Hive 表**时,如何进行维表 JOIN,并分别讨论**全量表**和**按天分区表**两种情况。

### 核心概念回顾:维表 JOIN (Dimension Table Join)

维表 JOIN 是指一个**无限的事件流(事实流)** 与一个**相对静态或缓慢变化的维度表**进行关联查询,以补全事实流中的维度信息。

*   **事实流 (Fact Stream)**: 例如,用户点击日志流、订单流。特点是数据量巨大、持续不断。
*   **维度表 (Dimension Table)**: 例如,用户信息表、商品信息表。特点是数据量相对较小,变化缓慢(T+1更新)。

---

### 场景设定

*   **事实流**: `user_clicks` (Kafka 流)
    *   `user_id` (用户ID)
    *   `click_time` (点击时间)
    *   `page_url` (页面URL)
    *   ...(其他事实字段)
*   **维度表**: `dim_user_info` (Hive 表)
    *   `user_id` (用户ID) -- **JOIN KEY**
    *   `user_name` (用户姓名)
    *   `user_age` (用户年龄)
    *   `city` (所在城市)
    *   `dt` (分区字段,如果是分区表)
*   **更新频率**: 每天凌晨 1:00 由离线任务加工生成,覆盖或新增到 Hive 表中。

---

### 方案一:维表是 Hive 全量表 (Non-partitioned Table)

全量表意味着整个表只有一个数据实体,每天凌晨的更新操作是**覆盖重写**(`INSERT OVERWRITE`)整张表。

#### Flink SQL 实现

在这种模式下,我们通常使用 **`LOOKUP JOIN`**,并配置一个 **支持分区发现的 Hive Catalog** 和 **可刷新的 LookupSource**。

```sql
-- 假设已经设置了Hive Catalog,并use了该catalog下的数据库

-- 1. 创建Kafka事实流表
CREATE TABLE user_clicks (
  `user_id` STRING,
  `click_time` TIMESTAMP(3),
  `page_url` STRING,
  WATERMARK FOR `click_time` AS `click_time` - INTERVAL '5' SECOND
) WITH (
  'connector' = 'kafka',
  'topic' = 'user-clicks-topic',
  'properties.bootstrap.servers' = 'localhost:9092',
  'format' = 'json',
  'scan.startup.mode' = 'latest-offset'
);

-- 2. 创建Hive维表
-- 注意:这是一个全量表,没有分区字段
CREATE TABLE dim_user_info (
  `user_id` STRING,
  `user_name` STRING,
  `user_age` INT,
  `city` STRING
) WITH (
  'connector' = 'hive',
  'table-name' = 'default.dim_user_info', -- Hive中的表名
  'lookup.cache' = 'PARTIAL', -- 部分缓存,无法使用ALL,因为表会更新
  'partition.time-extractor.timestamp-pattern' = '$dt 00:00:00', -- 虽然不是分区表,但配置一个时间提取器用于触发刷新
  'streaming-source.enable' = 'true', -- 启用流式源(核心配置!)
  'streaming-source.partition.include' = 'latest', -- 只读取最新的分区(这里指最新的表数据)
  'streaming-source.monitor-interval' = '1 h' -- 监控间隔,检查表/分区是否更新。这里设置1小时
);

-- 3. 执行维表JOIN查询
SELECT
  u.user_id,
  u.click_time,
  u.page_url,
  d.user_name,
  d.user_age,
  d.city
FROM user_clicks u
LEFT JOIN dim_user_info FOR SYSTEM_TIME AS OF u.click_time AS d
ON u.user_id = d.user_id;
```

#### 工作原理与注意事项

1.  **`streaming-source.enable='true'`**: 这是核心配置。它告诉 Flink 这个 Hive 表不是一个静态快照,而是一个会随时间变化的**流式数据源**2.  **`streaming-source.monitor-interval`**: Flink JobManager 会每隔一个间隔(如1小时)去检查一次 Hive 表的**元数据**(`_metadata`文件),判断表是否被更新过。
3.  **数据更新过程**:
    *   凌晨 1:00,离线任务运行,`INSERT OVERWRITE` 覆盖了 `dim_user_info` 表。
    *   Flink JobManager 在下次监控周期(比如 1:30)发现表的 `_metadata` 文件修改时间变了。
    *   JobManager 会**通知所有 TaskManager**:“维表数据已更新,请**重新加载全量数据**到缓存中”。
    *   TaskManager 会异步地重新扫描整个 Hive 表,用新数据**完全替换**旧的缓存数据。
4.  **特点**:
    *   **简单粗暴**: 每次更新都是全量刷新缓存。
    *   **延迟高**: 数据的更新感知依赖于监控间隔,有至少1小时的延迟。无法做到分钟级更新。
    *   **开销大**: 每次更新都需要读取整张表,如果维表很大,对 HDFS 和 Flink 集群都是不小的压力。
    *   **缓存策略**: 只能使用 `PARTIAL` 缓存,因为全量数据会变。

---

### 方案二:维表是 Hive 分区表 (Partitioned Table)

分区表意味着数据按天(`dt=20240315`)或其他维度分区。每天凌晨的更新操作是**向最新分区(如 `dt=20240316`)插入数据**(`INSERT INTO ... PARTITION`),或者覆盖某个分区。

这是**更推荐**的方式。

#### Flink SQL 实现

```sql
-- 1. 事实流表创建同上,略...

-- 2. 创建Hive分区维表
CREATE TABLE dim_user_info_partitioned (
  `user_id` STRING,
  `user_name` STRING,
  `user_age` INT,
  `city` STRING,
  `dt` STRING -- 显式声明分区字段
) PARTITIONED BY (dt) WITH (
  'connector' = 'hive',
  'table-name' = 'default.dim_user_info_partitioned',
  'lookup.cache' = 'PARTIAL',
  'partition.time-extractor.timestamp-pattern' = '$dt 00:00:00', -- 从分区字段值中提取时间
  'streaming-source.enable' = 'true',
  'streaming-source.partition.include' = 'latest', -- 关键!只读取最新的分区
  'streaming-source.monitor-interval' = '15 min' -- 可以设置更短的监控间隔,例如15分钟
  -- 'streaming-source.partition-order' = 'partition-time' -- 默认按分区时间顺序,这是正确的
);

-- 3. 执行维表JOIN查询
-- 注意:JOIN时不需要写分区字段dt
SELECT
  u.user_id,
  u.click_time,
  u.page_url,
  d.user_name,
  d.user_age,
  d.city
FROM user_clicks u
LEFT JOIN dim_user_info_partitioned FOR SYSTEM_TIME AS OF u.click_time AS d
ON u.user_id = d.user_id;
```

#### 工作原理与优势

1.  **`streaming-source.partition.include='latest'`**: 这是关键配置。它告诉 Flink **只缓存并关联最新分区**里的数据。历史分区的数据会被忽略。
2.  **数据更新过程**:
    *   凌晨 1:00,离线任务运行,向分区 `dt='20240316'` 写入了新的维度数据。
    *   Flink JobManager 在下次监控周期(比如 1:15)发现有了一个**更新的分区** `20240316`。
    *   JobManager 通知 TaskManager:“最新分区已更新,请**切换到新分区**的数据”。
    *   TaskManager 会**异步地加载最新分区 `dt='20240316'` 的数据**,并**替换**掉之前缓存的 `dt='20240315'` 分区的数据。
3.  **巨大优势**:
    *   **增量更新**: 每次只需要加载**一个分区**的数据,而不是整张表。**性能极高**,对HDFS压力小。
    *   **延迟降低**: 因为每次加载的数据量小,我们可以放心地将 `monitor-interval` 设置得更短(如15分钟甚至5分钟),**显著降低数据延迟***   **数据版本清晰**: 天然地以分区为单位管理数据版本,逻辑清晰可靠。

#### 重要细节:如何看待历史数据?

对于分区表 `streaming-source.partition.include='latest'` 的模式,有一个非常重要的特性:**它只关心最新分区的数据***   **假设场景**: 用户A在 `dt=20240315` 分区的城市是“北京”,在 `dt=20240316` 分区被修正为“上海”。
*   **JOIN 行为***   在 **3月15日**,Flink 缓存的是 `dt=20240315` 分区,流数据关联到用户A的城市是“北京”。
    *   在 **3月16日 01:15**(监控触发后),Flink 缓存切换到 `dt=20240316` 分区。
    *   **此后所有**到来的事实数据,关联到用户A的城市都会是“上海”。
    *   **对于在3月16日01:15之前到来,但事件时间(`click_time`)是3月15日的流量数据**,它们依然会关联到“上海”。因为 `LOOKUP JOIN` 是基于处理时间感知维表变化的,它**无法根据事件时间回到过去的维表版本**。

如果需要**基于事件时间的版本匹配**(即3月15日的数据永远关联3月15日的维表快照),则应该使用 **`Temporal Table Join`**,但这要求维表是完整的 CDC 流(如数据库 changelog),而不是 T+1 的 Hive 分区表。在当前T+1 Hive维表场景下,这是无法实现的。

---

### 总结与对比

| 特性 | 全量表 (Non-partitioned) | 按天分区表 (Partitioned) |
| :--- | :--- | :--- |
| **Flink 配置** | `streaming-source.enable='true'` | `streaming-source.enable='true'` **`streaming-source.partition.include='latest'`** |
| **数据加载方式** | **全量刷新** | **增量刷新**(仅加载最新分区) |
| **性能开销** | **大**(每次读全表) | **小**(每次读一个分区) |
| **数据延迟** | **高**(监控间隔不敢设太短) | **低**(监控间隔可设短,如5-15分钟) |
| **推荐度** | **不推荐**,适用于极小维表 | **强烈推荐**,生产环境最佳实践 |

**最终建议**:

对于 T+1 更新的 Hive 维表,**务必设计为分区表**,并在 Flink SQL 中配置 `streaming-source.partition.include='latest'` 来实现高效、低延迟的维表 JOIN。全量表方案只应在维表数据量非常小(如不到100MB)且对延迟不敏感的情况下考虑。

 最常用的lookup join,必须使用事件时间,通常由计算列生成:

-- 1. 创建主流数据表(Kafka源)
CREATE TABLE orders (
    order_id STRING,
    currency STRING,
    amount DECIMAL(10, 2),
    order_time TIMESTAMP(3),
    -- 定义处理时间属性,用于JOIN维表时获取最新版本
    proc_time AS PROCTIME(),
    -- 定义事件时间属性及水位线(如果要用事件时间JOIN)
    WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (
    'connector' = 'kafka',
    'topic' = 'orders_topic',
    'properties.bootstrap.servers' = 'localhost:9092',
    'format' = 'json'
);

-- 2. 创建维表(JDBC Lookup表)
CREATE TABLE currency_rates (
    currency STRING,
    rate DECIMAL(10, 4),
    update_time TIMESTAMP(3),
    PRIMARY KEY (currency) NOT ENFORCED -- 声明主键对维表很重要
) WITH (
    'connector' = 'jdbc',
    'url' = 'jdbc:mysql://localhost:3306/finance',
    'table-name' = 'currency_rates',
    'username' = 'user',
    'password' = 'pass',
    
    -- !!!核心:Lookup缓存配置 !!!
    'lookup.cache.max-rows' = '1000',    -- 缓存最多1000条记录
    'lookup.cache.ttl' = '10s'           -- 每条记录缓存10秒
);

-- 3. 执行维表JOIN查询(使用处理时间)
SELECT
    o.order_id,
    o.currency,
    o.amount,
    -- 非常重要:使用主流上的处理时间字段proc_time
    r.rate,
    o.amount * r.rate AS amount_usd
FROM orders AS o
-- 语法:LEFT JOIN 维表名 FOR SYSTEM_TIME AS OF 主流的时间属性字段
LEFT JOIN currency_rates FOR SYSTEM_TIME AS OF o.proc_time AS r
ON o.currency = r.currency;

 简单记忆:Lookup Table 用 PROCTIME();声明式版本表(Temporal Table)用事件时间。

  Lookup Join 总是查询“最新”数据,而维表又在缓慢变化,这就引入了数据一致性的问题。Flink 主要通过 缓存机制 来平衡查询性能和一致性。

 

 

聚合类型:

聚合类型    输入    输出    特点    适用场景
Group Aggregation    流    连续更新流    状态持续增长,需注意TTL    实时大盘总计,用户实时画像
Window Aggregation    流    追加流(每个窗口一个最终结果)    窗口关闭后触发计算,状态可清除    分钟级/小时级PV/UV,周期性报表
OVER Aggregation    流    与输入同行数的流    为每行计算一个聚合值    移动平均、累计排名、最近N条统计

 实时开发常见问题:

企业微信截图_17605180958965

 解法:

企业微信截图_17605181885721

 

posted @ 2025-06-05 14:37  ---江北  阅读(69)  评论(0)    收藏  举报
TOP