业务对象及其到数据的映射:不要把「航班号」拆成两半

业务对象到数据的映射原则:不要把「航班号」这样的复合字段拆成两半。

一、从一个反直觉的结论说起

HU1237 是一个完整的业务对象,不能拆分为"HU"和"1237"两个字段存入数据库。

如果你第一次听到这句话,可能会觉得奇怪——HU 明明是航空公司代码,1237 是航班序号,拆开不是很"规范化"吗?这不正是数据库范式设计所鼓励的吗?

恰恰相反。这种"理所当然"的拆分,是业务对象与数据之间最常见的断裂点。本文就以航班号为例,阐述什么是业务对象、为什么它的完整性不可侵犯,以及从业务对象到数据映射的三条核心原则。

二、什么是业务对象?

业务对象(Business Object),是业务人员在日常工作中直接使用、直接理解、直接决策的语义单元。它不是技术概念,不是数据库表,不是字段——它是"业务语言中的名词"。

| 领域 | 业务对象示例 |

| 航空 | 航班号(HU1237)、PNR 订座记录、航段(PEK→SHA) |

| 零售 | SKU(库存保有单位)、订单、会员卡号 |

| 金融 | 合约编号、交易流水号、产品代码 |

| 制造 | 工单号、物料编码、BOM 版本号 |

| 医疗 | 病历号、处方编号、ICD 诊断编码 |

业务对象的三个基本特征

一个业务对象有三个基本特征:

  1. 语义完整性:拆开就失去业务含义。把"HU1237"拆成"HU"和"1237",航司代码仍然有意义,但航班号这个业务对象就消失了。
  2. 业务可操作性:业务人员用它来完成工作。"帮我查一下 HU1237 今天的准点率"——这个句子里的主语就是航班号。
  3. 跨系统一致性:无论在订座系统、离港系统、运控系统还是财务系统,HU1237 指的都是同一件事。

三、航班号的解剖:为什么不能拆?

3.1 看起来有两个"部分"

以中国民航的航班号为例:

HU 1 2 3 7
│   │
│   └── 数字部分(1~4位):航班序号
└────── 字母部分(2位):航空公司 IATA 代码
  • HU = 海南航空(IATA 两字码)
  • 1237 = 该航司内部航班序号

从编码规则上看,它们确实是两个独立的信息源:航司代码由 IATA 分配,航班序号由航司自行编排。但从业务语义上看,它们必须同时存在才构成一个航班号。

航班号内部结构

3.2 拆分的灾难

假设你设计了这样一张表:

-- 错误示范:拆分航班号
CREATE TABLE flights (
    airline_code VARCHAR(2),   -- HU, CA, MU...
    flight_number VARCHAR(4),  -- 1237, 7071, 5101...
    ...
);

这看似"规范化",实则埋下了三颗雷:

雷一:1100 ≠ 1100

| 航班号 | 含义 |

| HU1100 | 海航 1100 号航班 |

| CA1100 | 国航 1100 号航班 |

| MU1100 | 东航 1100 号航班 |

flight_number = 1100 拿出来做聚合、对比、排名——没有任何业务意义。不同航司的"1100"是完全不同的东西,就像不同城市的"中山路"一样,不可比较。

1100不等于1100

雷二:业务查询被迫拼接

-- 每次查询都要重新"造"航班号
SELECT airline_code || flight_number AS flight_no
FROM flights
WHERE ...;

原本是原子的业务对象,现在变成了每次使用都要缝合的破碎零件。一旦某个系统忘记拼接,就会出现"CA" + NULL = NULL 的数据丢失。

雷三:跨系统对齐成本激增

订座系统返回 CA7071,你的数据库存的是 airline_code='CA', flight_number='7071'。导入时要拆,导出时要合,API 对接时要转。每多一个环节,就多一个出错点。而所有这些环节,都在做一件毫无业务增值的事——把航班号拆开再拼回去。

3.3 正确的设计

-- 正确示范:航班号是一个字段
CREATE TABLE flights (
    flight_no VARCHAR(6) NOT NULL,  -- HU1237, CA7071, MU5101
    airline_code VARCHAR(2) GENERATED ALWAYS AS (LEFT(flight_no, 2)) STORED,
    flight_number VARCHAR(4) GENERATED ALWAYS AS (RIGHT(flight_no, 4)) STORED,
    ...
);

航班号作为主存储字段保持完整;航司代码和航班序号作为派生字段(计算列)当且仅当确实需要单独使用时才生成。这样既保证了业务对象的完整性,又不丧失对子信息的访问能力。

两种数据库设计对比

核心原则:存储完整,派生子集。不要反过来。

四、业务对象到数据映射的三条原则

原则一:不要拆散原子业务对象

如果一个东西在业务对话中作为一个整体被使用,它就应该作为一个字段被存储。

反例:

  • SKU "APL-iP15P-256-BLK" 拆成 brand / product / storage / color 四个字段
  • 身份证号 "110101199001011234" 拆成省份 / 出生日期 / 性别 / 顺序码
  • URL "https://example.com/products/123" 拆成 protocol / domain / path

正例:

  • 存储完整的 SKU、身份证号、URL
  • 通过计算列或视图派生子信息(出生日期、性别等)

原则二:聚合的意义来自业务,不来自数据

只有业务上属于同一类的东西,聚合才有意义。
  • 所有"航班号"可以按航司分组——因为航司是航班号的业务属性
  • 但所有"1100"不能跨航司聚合——因为"数字 1100"不是业务属性,只是编码片段
  • 同理:所有"中山路"的销售额不能加总,但"上海市中山路 100 号"作为一个门店是完整的业务对象

数据的聚合能力,取决于业务对象的完整性。

原则三:存储层不优化查询层

为了"查询方便"而拆分业务对象,是典型的以技压业。

你可能会说:"可是我经常要按航司筛选呀,拆开不是更快?"

回答问题:

  1. LEFT(flight_no, 2) 有索引的情况下,性能差异可以忽略
  2. 用计算列 / 虚拟列,一样可以建索引
  3. 即使拆成两个字段,你的 SQL 也省不了几个字符
  4. 但你损失的,是业务语义的完整性和系统的长期可维护性

优先保护业务语义,其次才是性能优化。 而且绝大多数情况下,二者并不矛盾。

五、更多例子:看看你身边有没有这样的"拆分"

例子一:订单号

ORD-2026-0429-0038
│    │    │    │
│    │    │    └── 当日序号
│    │    └────── 日期
│    └─────────── 年份
└──────────────── 订单前缀

错误做法:拆成 order_prefixorder_yearorder_dateorder_seq 四个字段。

正确做法order_no VARCHAR 作为完整存储,派生日期字段用于筛选。

理由:订单号作为一个整体在客服、物流、财务之间流转。没有人会说"帮我把那个 2026 年 0429 第三十八号单退一下"——他们会说"ORD-2026-0429-0038 这个单子"。

例子二:银行卡号

6222 0200 0001 2345 678
│    │
│    └── 发卡行识别码(IIN)
└────── 卡组织标识

错误做法:拆成卡组织、发卡行、卡序号。

正确做法card_no VARCHAR 完整存储,通过 BIN 表关联发卡行信息。

理由:银行卡号在任何业务场景中都是一个整体。支付接口按完整卡号处理,对账单按完整卡号展示。把卡号拆开,除了增加 PCI-DSS 合规风险外,没有任何好处。

例子三:车牌号

京 A·12345
│   │
│   └── 序号部分
└────── 省份简称 + 发牌机关代号

错误做法:拆成 plate_provinceplate_codeplate_number

正确做法plate_no VARCHAR 完整存储。需要按省份统计时,用 LEFT(plate_no, 1) 派生。

理由:车牌号是车辆在交通管理体系中的唯一标识,"京A12345"和"沪A12345"在 ETC、违章、保险等场景中是完全不同的对象。拆开之后,"12345"这个片段没有任何业务含义。

六、当拆分是合理的:一个判断框架

当然,不是所有带"编码格式"的东西都不能拆。什么时候拆是合理的?

| 条件 | 可以拆 | 不能拆 |

| 拆开后各部分有独立业务含义? | 是 | 否 |

| 拆开后各部分可以跨实例比较? | 是 | 否 |

| 业务中是否单独使用某一部分? | 经常 | 从不 |

| 拆分是不是为了消解多值依赖? | 是(范式化) | 否 |

拆分决策流程图

典型合理拆分的例子:

  • 地址:省/市/区/街道 拆分是合理的,因为"广东省"本身有独立的统计意义,"深圳市"本身可以作为筛选条件
  • 日期:年/月/日 拆分为独立维度字段,用于多维分析
  • 姓名:姓和名在某些文化中有独立使用场景(如按姓氏分组统计)

关键区别在于:地址的"省"单独拿出来,依然是"省"这个业务对象。但航班号的"1237"单独拿出来,它不是"航班"——它什么都不是。

七、总结

业务对象到数据的映射,本质上是业务语言到数据结构的翻译。翻译的第一原则是"信"——忠于原文。

| 原则 | 一句话 |

| 原子性 | 业务中作为一个整体使用的对象,作为一个字段存储 |

| 聚合语义 | 只有业务上同类的东西,聚合才有意义 |

| 存储优先 | 存储完整业务对象,查询需求用派生字段满足 |

| 拆分的标准 | 拆开后各部分本身能作为独立的业务对象存在时,才拆分 |

回到开头那个反直觉的结论:HU1237 是一个完整的业务对象。 不是因为它的字母和数字连在一起,而是因为——在整个航空业务的语境中,"HU1237"就是那个不可再分的、有独立语义的、被所有人共同理解的最小编码单元。

把它拆开,你就不是在建模数据;你是在拆解业务。

——唯有知识让我们免于平庸

📖 相关文章
 【致知篇44】逻辑世界:数据、佛法与体系
从问题到可视化:业务分析通识
[航空] 旅客航司权限控制:两种方法对比与最佳实践
8.1  计算的演进及分类:从Excel、SQL到Tableau
Global Education Center Shift Infographic
——————————————————————————————

No comments yet