4. ELN数据库选型与Schema实现
电子实验记录系统(ELN)的数据模型有一个显著特点:核心结构高度稳定,但实验内容高度异构。无论记录的是有机合成、材料表征还是生物实验,都必然存在“谁在什么时间做了哪个实验、属于哪个项目、引用了哪些样品”这类强一致、强关联的元数据;而实验步骤、参数、观测值、图谱附件描述等部分,则随实验类型差异极大。数据库选型的关键,不是判断哪种数据库“更好”,而是判断哪些数据必须由关系型数据库保证完整性,哪些数据适合以文档形式灵活承载。
一、两类数据库在ELN中的职责边界
关系型数据库(以 PostgreSQL 为代表)擅长表达实体之间的强关系:实验记录与项目、人员、样品、仪器之间存在明确的外键依赖,删除一个项目前必须确认其下没有未归档的实验记录,这类约束由数据库层强制执行,比应用层校验更可靠。文档数据库(以 MongoDB 为代表)擅长表达单条记录内部结构可变的数据:同一张“实验记录”集合中,不同实验类型的 payload 字段可以拥有完全不同的嵌套结构,无需为每种实验类型建一张表或频繁执行 ALTER TABLE。
因此,ELN 的合理架构通常是混合持久化:用关系型数据库保存实验记录的“骨架”(身份、归属、状态、时间、审计),用文档字段或独立文档集合保存实验的“血肉”(步骤、参数、观测、附件元信息)。下面这张图展示了这一分工关系。
二、Schema 设计:以 experiments 表为核心
关系型部分的设计原则是“把约束交给数据库”。以下 DDL 以 PostgreSQL 为例,展示了 ELN 核心骨架表的建表语句。注意 payload 字段使用 JSONB 类型,这是混合架构的关键:它让关系表也能承载异构内容,同时保留 SQL 查询能力。
-- 用户表
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
role VARCHAR(16) NOT NULL DEFAULT 'researcher'
CHECK (role IN ('researcher','reviewer','admin')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 项目表
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
owner_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
status VARCHAR(16) NOT NULL DEFAULT 'active'
CHECK (status IN ('active','archived')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 实验记录主表
CREATE TABLE experiments (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
operator_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
title VARCHAR(200) NOT NULL,
exp_type VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft','submitted','reviewed','archived')),
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 样品表
CREATE TABLE samples (
id BIGSERIAL PRIMARY KEY,
experiment_id BIGINT NOT NULL REFERENCES experiments(id) ON DELETE CASCADE,
sample_code VARCHAR(64) NOT NULL,
amount_mg NUMERIC(10,3),
UNIQUE (experiment_id, sample_code)
);
-- 审计日志表
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
experiment_id BIGINT NOT NULL REFERENCES experiments(id) ON DELETE CASCADE,
action VARCHAR(32) NOT NULL,
actor_id BIGINT NOT NULL REFERENCES users(id),
detail JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
这里有三处约束值得强调。ON DELETE RESTRICT 用于 projects 和 users:只要项目下还有实验记录,或用户仍是某条记录的负责人,就不允许删除,防止出现“孤儿记录”。ON DELETE CASCADE 用于 samples 和 audit_log:它们从属于实验记录,实验记录被删除时随之清理是合理的。CHECK 约束把状态枚举限制在数据库层,避免应用层写入非法状态值。
三、索引设计:围绕真实查询路径
索引不是越多越好,应针对 ELN 中最频繁的查询模式建立。典型查询包括:按项目列出实验记录、按操作人筛选、按状态过滤待审核记录、按时间倒序分页、在 payload 中检索特定实验参数。
-- 项目维度查询:列出某项目下所有实验
CREATE INDEX idx_exp_project ON experiments(project_id, created_at DESC);
-- 操作人维度:某人的实验记录
CREATE INDEX idx_exp_operator ON experiments(operator_id, created_at DESC);
-- 状态过滤:待审核队列
CREATE INDEX idx_exp_status ON experiments(status) WHERE status = 'submitted';
-- JSONB 内容检索:查找特定试剂
CREATE INDEX idx_exp_payload_gin ON experiments USING GIN (payload jsonb_path_ops);
-- 审计日志按实验追溯
CREATE INDEX idx_audit_exp ON audit_log(experiment_id, created_at DESC);
其中 idx_exp_status 是部分索引,只索引 submitted 状态的记录。由于待审核记录通常只占总量很小比例,部分索引体积远小于全表索引,查询待审核队列时效率更高。idx_exp_payload_gin 使用 GIN 索引支持 JSONB 包含查询,例如 payload @> '{"reagent":"Pd/C"}',让异构内容也能被高效检索。
四、初始化数据与验证
Schema 建好后,需要插入最小可用数据集来验证约束和索引是否按预期工作。以下初始化脚本创建两名用户、一个项目、一条实验记录及其样品。
INSERT INTO users (username, role) VALUES
('zhang_wei', 'researcher'),
('li_na', 'reviewer');
INSERT INTO projects (name, owner_id) VALUES
('催化剂筛选-2024Q3', 1);
INSERT INTO experiments
(project_id, operator_id, title, exp_type, status, payload)
VALUES
(1, 1, 'Pd/C 催化加氢条件筛选', 'synthesis', 'submitted',
'{"reagent":"Pd/C","temp_c":60,"time_h":4,
"measurements":{"yield":0.87,"purity":0.95}}'::jsonb);
INSERT INTO samples (experiment_id, sample_code, amount_mg)
VALUES (1, 'S-2024-001', 125.500);
INSERT INTO audit_log (experiment_id, action, actor_id, detail)
VALUES (1, 'submit', 1, '{"from":"draft","to":"submitted"}'::jsonb);
插入完成后,用几条查询验证设计是否生效。第一条验证外键约束:尝试删除 projects 中 id=1 的记录,应被拒绝,因为存在引用它的实验记录。第二条验证 JSONB 检索:
SELECT id, title
FROM experiments
WHERE payload @> '{"reagent":"Pd/C"}'
AND status = 'submitted';
若返回刚才插入的记录,说明 GIN 索引路径可用。第三条验证部分索引:执行 EXPLAIN ANALYZE 查看待审核查询是否命中 idx_exp_status,若执行计划中出现 Index Scan using idx_exp_status,则索引生效。若出现 Seq Scan,需检查数据量是否过小导致优化器放弃索引,或统计信息是否过期。
EXPLAIN (ANALYZE, BUFFERS) 观察实际扫描行数与缓存命中情况。数据量小于几百行时,顺序扫描往往比索引更快,这是正常现象,不代表索引设计有误。五、选型判断的实用准则
回到选型本身,可以用三条准则快速判断某类数据应放在哪一侧。第一,是否需要跨实体引用完整性:需要外键、需要事务保证多表一致,放关系型。第二,结构是否随实验类型剧烈变化:同一集合内文档字段差异大、且会持续新增字段,放文档型或 JSONB。第三,是否需要复杂聚合与关联查询:需要多表 JOIN、窗口函数、严格排序分页,放关系型。
对于多数中小型 ELN,PostgreSQL 单库配合 JSONB 已能覆盖绝大部分场景,无需引入第二套数据库,从而避免跨库一致性问题。只有当实验内容体量极大、写入吞吐很高、或需要独立演进文档结构时,才考虑引入 MongoDB 等文档数据库,并通过 experiment_id 建立逻辑关联,由应用层保证两侧数据同步。无论选择哪种方案,外键约束、状态枚举、审计日志这三项都应在数据库层落实,它们是 ELN 数据可信度的底线。