Schema 基础概念:数据世界的骨架

聊流批一体之前,咱们得先把地基打牢。这个地基就是 Schema。

我记得刚入行那会儿,带我的老大哥跟我说过一句话,我一直记到现在:「没有 Schema 的数据,就是一盘散沙。」当时我不太理解,觉得数据不就是存起来嘛,要什么自行车。直到后来我亲手把一个没有 Schema 约束的 Kafka topic 搞崩过——嗯,那又是另一个故事了。

什么是 Schema?

说白了,Schema 就是数据的「说明书」。它告诉你:

  • 数据长什么样
  • 每个字段叫什么名字
  • 字段是什么类型(整数、字符串、时间戳……)
  • 字段之间有没有约束关系

举个最简单的例子。你写一个 JSON:

{
  "user_id": 12345,
  "name": "张三",
  "age": 28,
  "signup_time": "2024-03-15T10:30:00Z"
}

这个 JSON 本身没有 Schema 声明。但如果你用 Avro 定义一下:

{
  "type": "record",
  "name": "User",
  "fields": [
    {"name": "user_id", "type": "int"},
    {"name": "name", "type": "string"},
    {"name": "age", "type": "int"},
    {"name": "signup_time", "type": "string", "logicalType": "timestamp-millis"}
  ]
}

这就是 Schema。它明确告诉所有消费方:user_id 必须是整数,signup_time 必须是时间戳格式。谁要是传个字符串的 user_id 进来,对不起,解析直接报错。

核心一句话:Schema 是数据生产者和消费者之间的「契约」。没有契约,两边各说各话,数据质量就崩了。

Schema 在数据系统中的角色

你想想看,一个典型的数据系统里,数据要经过多少环节?

采集 → 传输 → 存储 → 计算 → 输出

每个环节都可能对数据做「手脚」。没有 Schema 会怎样?

  • 采集端:字段名随意改,今天叫 user_name,明天叫 username,下游全挂
  • 传输端:序列化/反序列化全靠猜,性能差还容易出错
  • 存储端:列式存储不知道列类型,压缩率上不去
  • 计算端:Spark 读 Parquet 文件,没有 Schema 就得全表扫描推断类型
  • 输出端:BI 报表字段对不上,业务方天天找你喝茶

我在项目中遇到过最典型的场景:一个实时数仓项目,Kafka 里的 JSON 日志字段类型不固定。某天业务方在 age 字段里塞了个 "unknown" 字符串,下游 Flink 任务直接抛异常,整个 pipeline 停了 40 分钟。原因?没有 Schema 校验。

所以 Schema 在数据系统里扮演的角色,我总结为三个:

角色 说明 类比
契约 上下游约定数据格式,谁改谁负责 API 接口文档
蓝图 告诉系统数据怎么存、怎么读、怎么优化 建筑图纸
护栏 在数据进入系统前拦截异常格式 高速公路护栏
我的习惯:任何数据进入系统之前,先过 Schema 校验。哪怕只是简单的字段类型检查,也能拦住 80% 的线上故障。

结构化数据 vs 半结构化数据:Schema 的差异

这个问题,很多同学容易搞混。咱们掰开揉碎讲清楚。

结构化数据

典型代表:关系型数据库的表(MySQL、PostgreSQL)。

特点:

  • Schema 是强制的,必须先定义表结构,才能写入数据
  • 字段类型严格:INT 就是 INT,VARCHAR(100) 就是 100 个字符
  • 支持约束:主键、外键、唯一索引、非空
  • 修改 Schema 成本高:ALTER TABLE 可能锁表

举个例子:

CREATE TABLE users (
  user_id    INT PRIMARY KEY,
  name       VARCHAR(50) NOT NULL,
  age        INT CHECK (age >= 0 AND age <= 150),
  signup_ts  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

这种 Schema 的好处是「确定性」极强。你读出来的数据,字段类型、长度、范围都是确定的。坏处是「灵活性」差——业务变了,加个字段可能要 DBA 审批三天。

半结构化数据

典型代表:JSON、XML、Avro、Parquet(带 Schema 的也算半结构化)。

特点:

  • Schema 可以是隐式的(JSON 本身不声明类型)
  • 字段可以缺失、可以嵌套、可以类型不一致
  • 修改成本低:加个字段,直接写进去就行
  • 但解析成本高:每个读取方都要自己处理类型推断

还是那个用户数据,用 JSON 写:

{
  "user_id": "12345",      // 注意:这里是字符串
  "name": "张三",
  "age": "28",             // 这里也是字符串
  "signup_time": "2024-03-15T10:30:00Z",
  "extra_field": "随便加"  // 随时可以加新字段
}

看到了吗?user_id 和 age 都变成了字符串。下游如果按整数解析,直接报错。这就是半结构化的「双刃剑」——灵活,但容易出幺蛾子。

核心差异对比

维度 结构化数据 半结构化数据
Schema 定义时机 写入前(Schema-on-Write) 读取时(Schema-on-Read)
类型约束 严格,写入即校验 松散,读取时推断
字段可变性 低,需 DDL 操作 高,随时增减字段
存储效率 高(定长字段、列式压缩) 低(字段名重复存储)
查询性能 高(预编译、索引友好) 低(需解析、类型转换)
适用场景 OLTP、核心交易系统 日志、事件流、数据湖
我曾经踩过的坑:在一个实时推荐项目中,上游把用户画像数据以 JSON 格式写入 Kafka,下游 Flink 任务用 Avro Schema 解析。上游某天在 JSON 里加了一个嵌套字段,下游 Avro Schema 没同步更新,结果反序列化失败,整个推荐 pipeline 挂了 2 小时。从那以后,我要求所有跨团队的数据接口必须使用 Schema Registry 管理版本。

流批一体下的 Schema 挑战

好,现在我们把话题拉回流批一体。为什么 Schema 在流批一体场景下特别重要?

因为流和批对 Schema 的「容忍度」完全不同:

  • 批处理:读一次全量数据,Schema 不匹配?大不了重跑。时间成本高,但能接受。
  • 流处理:数据源源不断进来,Schema 不匹配?直接报错,数据就丢了。没有「重跑」这个选项。

你想想看,同一个数据 pipeline,既要跑批(比如 T+1 的全量聚合),又要跑流(比如秒级的实时指标)。如果 Schema 不一致,批跑出来 100 万行,流跑出来 95 万行——业务方该信谁?

所以流批一体对 Schema 的要求,我总结为三点:

  1. 统一:流和批用同一套 Schema 定义,不能各搞各的
  2. 可演化:业务在变,Schema 也要变,但不能破坏兼容性
  3. 可回溯:任何时候都能查到某个时间点的 Schema 版本,方便历史数据重跑

这三点,就是后面几章要深入讲的内容。今天先把概念理清楚。

本章知识体系

下面这张图,把 Schema 的核心概念串起来了。我建议你保存下来,后面讲到 Schema Registry、兼容性策略时,可以回来对照着看。

Schema 核心概念知识体系 Schema 核心概念 什么是 Schema 数据的「说明书」 字段名 + 字段类型 + 约束 生产者和消费者的契约 Schema 的角色 契约:上下游约定格式 蓝图:指导存储与计算 护栏:拦截异常数据 结构化 vs 半结构化 Schema-on-Write vs Read 严格约束 vs 灵活松散 OLTP vs 日志/事件流 流批一体对 Schema 的三大要求 统一:流批同一套 Schema 可演化:兼容性变更 可回溯:版本可追溯

这张图把今天讲的内容串起来了。从 Schema 是什么,到它在系统里的三个角色,再到结构化与半结构化的差异,最后落到流批一体的三大要求。后面的章节,我们会围绕「统一、可演化、可回溯」这三个点,一步步展开。