不可能的规范化:为什么你的数据库讨厌生物学 🧬
Source: Dev.to
用户对象
我们需要讨论 用户对象。
如果你在网页开发领域已经超过五分钟,你可能已经构建过电子商务后端或待办事项应用。数据建模通常是这样的:
- 存在一个
User(用户)。 - 存在一个
Product(产品)(它有 SKU、价格和描述)。 - 用户购买该产品。
这很清晰且确定。如果我购买一件 红色 T‑Shirt(Size L),该对象不会改变。它在仓库中存放,随后发货,最终以 红色 T‑Shirt 的形式到达。
现在,想象一下这件 红色 T‑Shirt 可以根据天气自行改变尺码,或者自称为“有点红粉色,但触摸时会疼”。
欢迎来到 HealthTech。
“简单”症状陷阱
假设你正在为患者构建一个追踪症状的应用。我们内心的初级开发者会立刻想到:“很简单。SQL 表。”
CREATE TABLE symptoms (
id SERIAL PRIMARY KEY,
user_id INT,
name VARCHAR(255), -- "Headache"
severity INT, -- 1 to 10
created_at TIMESTAMP
);
直接上船,对吧?
错误。
两天后,用户写道:“我的头痛是 7/10,但具体在左眼后方,站起来时会跳动。”
你的 name 列已经崩溃。你需要位置(左眼后方)、性质(跳动)以及诱因(站立)。
于是你想:“好吧,我改用 NoSQL 文档存储。我直接扔一个 JSON 块!”
{
"symptom": "Headache",
"meta": {
"location": "Left retro-orbital",
"quality": "Pulsating",
"trigger": "Orthostatic"
}
}
太好了。现在尝试在 10,000 名用户中查询所有“头部问题”。做不到,因为用户 A 写了 “Headache”,用户 B 写了 “Migraine”,而用户 C 写了 “My head hurts”。
进入本体论:SNOMED‑CT 和 LOINC
为了解决这个问题,业界发明了标准术语。这里的大老板是 SNOMED‑CT,它为几乎所有医学概念分配唯一代码。头痛不再是 “Headache”,而是 SCTID: 25064002。
这听起来像是开发者的梦想(规范化!),但实际上会变成实现噩梦。生物学是一个图,而不是列表。某种特定类型的头痛是 “Pain” 的 子概念,而 “Pain” 又是 “Clinical Finding” 的 子概念。
要正确存储这些信息,我们通常会使用 FHIR(快速医疗互操作资源)。下面是一个体温观察的简化 JSON 示例:
{
"resourceType": "Observation",
"status": "final",
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs",
"display": "Vital Signs"
}
]
}
],
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "8310-5",
"display": "Body temperature"
}
]
},
"valueQuantity": {
"value": 39.1,
"unit": "degrees C",
"system": "http://unitsofmeasure.org",
"code": "Cel"
}
// ... timestamps, performers, device used ...
}
我们从 temperature: 39.1 变成了一个约 30 行的 JSON 对象,因为在生物学中,语境就是一切。口服测得的 39.1 °C 与腋下测得的 39.1 °C 是不同的。如果不存储这些元数据,数据就会变得在医学上极其危险。
地图不是领土
我们作为工程师面临的最大哲学障碍是 人类健康是连续的,而数据库是离散的。
当用户从下拉菜单中选择“中度疼痛”时,我们把一种复杂、流动的生物感受压缩成一个明确的枚举,失去了分辨率。
我在个人博客上写了更多关于这种数据分辨率损失的内容——如果你对更深入的架构理论感兴趣,查看我的其他文章。
挑战在于创建一种看起来有人情味的 UI(例如,“疼痛部位在哪里?”),同时将其映射到像 FHIR 和 SNOMED 这样刚性的、复杂的本体论,而用户根本不需要了解这些标准。
那么,解决方案是什么?
没有灵丹妙药,但以下模式在实际使用中往往有效:
- 存储原始意图: 保留用户实际输入或点击的内容。绝不要丢失真相来源。
- 后期映射: 使用数据摄取管道将 “My head hurts” 转换为
SCTID: 25064002。不要强迫用户直接使用 SNOMED。 - 混合模式: 对高层数据(患者 ID、日期)使用关系型列,对临床负载(FHIR 资源)使用
JSONB列。
这很乱、令人沮丧,确实比卖 T‑恤要困难得多。但至少永远不会无聊。
还有人现在正为 HL7/FHIR 集成而苦恼吗?在评论里一起抱怨吧。 😭