前言
在 PHA-X 项目早期开发阶段,由于对业务模型和数据库设计经验不足,HAZOP 分析模块采用了不合理的数据库结构设计方案。
1.旧版数据库结构
使用 hazop_projects 表记录所有项目信息;
以 hazop_{project_id} 表存储单个项目下的节点列表;
再通过 hazop_{project_id}_{node_id} 表记录每个节点对应的分析数据。
该设计在初期具有一定直观性,在可视化界面中层级关系清晰、易于理解。然而,随着项目数量的持续增长,该方案的结构性问题逐渐暴露:
- 每新增一个节点都会生成新的数据表;
- 在数十个项目后,数据库中已产生数百张业务表;
- 数据库结构高度碎片化,表命名复杂,维护成本显著上升;
- 数据迁移、版本升级、权限控制及通用查询变得困难。

图1 早期数据库结构
尽管 SQLite 在中小规模数据量下对“多表”场景的性能影响并不十分显著(测试文章),但从可维护性、可扩展性角度来看,该结构无法作为持续迭代的基础架构。
因此,需要对 HAZOP 模块进行重构,对数据库模型进行重新设计,以减少表数量、统一数据结构、提升整体可读性和工程可持续性,还技术债务。
2.新版数据库结构
重构目标:
- 减少表数量,避免随项目和节点数量导致表数量膨胀
- 统一模型,消除动态表名带来的维护与查询复杂度
- 明确数据表边界,清晰划分“项目—节点—分析记录”的层级关系
新的设计采用固定表结构 + 逻辑关联的方式,以关系建模替代“一项目一表 / 一节点一表”的物理拆分策略,最终数据库结构如下:

图2 改进后的数据库表结构
3. 新结构查询性能
在统一表结构(hazop_projects + hazop_nodes + hazop_tables)下:
- 单节点分析数据查询耗时稳定控制在 3 ms 以内
- 查询性能与项目数量、节点数量解耦
- 未观察到随数据规模增长而出现的明显性能退化
该结果表明,基于 固定表结构 + 索引过滤(project_id / node_id) 的查询方式在 SQLite 环境下具有良好的稳定性和可预测性。
4. 旧结构查询性能对比
在旧版“动态表结构”方案下,查询性能随表数量明显下降:
| 数据规模 | 单节点查询耗时 |
|---|---|
| 约 300 张分析表 | ~150 ms |
| 1000+ 张分析表 | ~300 ms |
5.结构及逻辑重构
本次重构中最复杂、工作量最大的部分在于所有 API 接口逻辑的系统性重构。
在旧架构下,依赖以下特性:
- 动态表名拼接(
hazop_{projectId}、hazop_{projectId}_{nodeName}) - 项目、节点、分析数据之间的处理逻辑
几乎所有接口都需要重新设计数据访问路径、事务处理及权限判断逻辑。
DELETE /api/table/project/{projectId}
删除 HAZOP 项目(同时执行数据备份逻辑)
GET /api/table/project/{projectId}
获取项目基本信息
PUT /api/table/project/{projectId}
更新项目信息
POST /api/table/project/{projectId}/lock
锁定项目
POST /api/table/project/{projectId}/unlock
解锁项目
GET /api/table/project/{projectId}/nodes
查询项目下的所有节点
POST /api/table/project/{projectId}/nodes
新建 HAZOP 节点
DELETE /api/table/project/{projectId}/node/{nodeName}
删除节点
PUT /api/table/project/{projectId}/nodes/reorder
节点重新排序
GET /api/table/project/{projectId}/node/{nodeName}/info
获取节点详细信息
PUT /api/table/project/{projectId}/node/{nodeName}/info
更新节点信息
GET /api/table/project/{projectId}/data
查询项目全部 HAZOP 分析数据
GET /api/table/project/{projectId}/node/{nodeName}
查询指定节点的分析数据
GET /api/table/project/{projectId}/impact
获取项目影响信息
GET /api/table/project/{projectId}/suggestions
获取项目建议措施数据
DELETE /api/table/project/:projectId/suggestions/:suggestionId
删除单个建议措施
GET /api/table/project/:projectId/summary
获取项目风险汇总数据
GET /api/table/project-nodes/:projectId
获取节点列表
GET /project/:projectId/node/:nodeName/text-styles
获取单元格文字样式
GET /project/:projectId/high-risk-scenarios
获取高风险场景
GET /project/:projectId/check-analysis-tables
检查分析表
POST /project/:projectId/node/:nodeName/save
保存节点数据
POST /project/:projectId/node/:nodeName/text-styles
保存文字样式
POST /project/:projectId/replace
项目替换
PUT /project/:projectId/suggestions
更新建议措施
6. 数据迁移
节点表迁移代码
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const dbPath = path.join(__dirname, 'hazop.db');
const db = new sqlite3.Database(dbPath);
db.serialize(() => {
const createNodesTableSQL = `
CREATE TABLE IF NOT EXISTS hazop_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
name TEXT NOT NULL,
zysb TEXT,
description TEXT,
node_sequence TEXT,
design_intent TEXT,
medium_hazard_params TEXT,
pid TEXT,
data_table TEXT,
merge_cells TEXT,
text_styles TEXT,
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES hazop_projects(id) ON DELETE CASCADE
)
`;
db.run(createNodesTableSQL, (err) => {
});
db.run('CREATE INDEX IF NOT EXISTS idx_hazop_nodes_project_id ON hazop_nodes(project_id)', (err) => {
if (err) {
console.error('创建索引失败:', err);
});
db.run('CREATE INDEX IF NOT EXISTS idx_hazop_nodes_name ON hazop_nodes(project_id, name)', (err) => {
if (err) {
console.error('创建索引失败:', err);
} else {
}
});
db.all('SELECT id, node_table FROM hazop_projects WHERE node_table IS NOT NULL', [], (err, projects) => {
console.log(` ${projects.length} 个项目需要迁移`);
let migratedCount = 0;
let totalNodes = 0;
projects.forEach((project, index) => {
const projectId = project.id;
const nodeTable = project.node_table;
db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, [nodeTable], (err, tableExists) => {
db.all(`SELECT * FROM ${nodeTable}`, [], (err, nodes) => {
if (err) {
console.error(` 项目 ${projectId}: 读取节点失败:`, err);
migratedCount++;
if (migratedCount === projects.length) {
finishMigration();
}
return;
}
const insertSQL = `
INSERT INTO hazop_nodes (
project_id, name, zysb, description, node_sequence,
design_intent, medium_hazard_params, pid, data_table,
merge_cells, text_styles, sort_order, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
let insertedNodes = 0;
nodes.forEach(node => {
db.run(insertSQL, [
projectId,
node.name,
node.zysb,
node.description,
node.node_sequence,
node.design_intent,
node.medium_hazard_params,
node.pid,
node.data_table,
node.merge_cells,
node.text_styles,
node.sort_order || 0,
node.created_at || null,
node.updated_at || null
], (err) => {
insertedNodes++;
if (insertedNodes === nodes.length) {
totalNodes += nodes.length;
migratedCount++;
if (migratedCount === projects.length) {
finishMigration();
}
}
});
});
});
});
});
function finishMigration() {
db.close();
}
});
});
分析表迁移代码
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const dbPath = path.join(__dirname, 'hazop.db');
const db = new sqlite3.Database(dbPath);
db.serialize(() => {
const createTablesSQL = `
CREATE TABLE IF NOT EXISTS hazop_tables (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
node_id INTEGER NOT NULL,
row_order INTEGER,
number TEXT,
parameter TEXT,
deviation TEXT,
cause TEXT,
category TEXT,
consequence_description TEXT,
risk_L INTEGER,
risk_S INTEGER,
risk_R TEXT,
existing_measures TEXT,
residual_risk1_AL INTEGER,
residual_risk1_AS INTEGER,
residual_risk1_AR TEXT,
recommendation TEXT,
recommendation_reason TEXT,
recommendation_category TEXT,
residual_risk2_AL INTEGER,
residual_risk2_AS INTEGER,
residual_risk2_AR TEXT,
responsible_party TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES hazop_projects(id) ON DELETE CASCADE,
FOREIGN KEY (node_id) REFERENCES hazop_nodes(id) ON DELETE CASCADE
)
`;
db.run(createTablesSQL, (err) => {
});
db.run('CREATE INDEX IF NOT EXISTS idx_hazop_tables_project_node ON hazop_tables(project_id, node_id)', (err) => {
if (err) {
console.error('创建索引失败:', err);
} else {
}
});
db.run('CREATE INDEX IF NOT EXISTS idx_hazop_tables_row_order ON hazop_tables(project_id, node_id, row_order)', (err) => {
if (err) {
console.error('创建索引失败:', err);
}
});
db.all('SELECT id, project_id, data_table FROM hazop_nodes WHERE data_table IS NOT NULL AND data_table != ""', [], (err, nodes) => {
console.log(` ${nodes.length} 个节点需要迁移分析数据`);
let migratedCount = 0;
let totalRows = 0;
nodes.forEach((node) => {
const nodeId = node.id;
const projectId = node.project_id;
const dataTable = node.data_table;
db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, [dataTable], (err, tableExists) => {
db.all(`SELECT * FROM ${dataTable} ORDER BY row_order ASC`, [], (err, rows) => {
if (err) {
console.error(` 节点 ${nodeId}: 读取分析数据失败:`, err);
migratedCount++;
if (migratedCount === nodes.length) {
finishMigration();
}
return;
}
const insertSQL = `
INSERT INTO hazop_tables (
project_id, node_id, row_order,
number, parameter, deviation, cause, category, consequence_description,
risk_L, risk_S, risk_R,
existing_measures,
residual_risk1_AL, residual_risk1_AS, residual_risk1_AR,
recommendation, recommendation_reason, recommendation_category,
residual_risk2_AL, residual_risk2_AS, residual_risk2_AR,
responsible_party,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
let insertedRows = 0;
rows.forEach(row => {
db.run(insertSQL, [
projectId,
nodeId,
row.row_order,
row.number,
row.parameter,
row.deviation,
row.cause,
row.category,
row.consequence_description,
row.risk_L,
row.risk_S,
row.risk_R,
row.existing_measures,
row.residual_risk1_AL,
row.residual_risk1_AS,
row.residual_risk1_AR,
row.recommendation,
row.recommendation_reason,
row.recommendation_category,
row.residual_risk2_AL,
row.residual_risk2_AS,
row.residual_risk2_AR,
row.responsible_party,
row.created_at || null,
row.updated_at || null
], (err) => {
if (err) {
console.error(` 行 ${row.id} 迁移失败:`, err);
}
insertedRows++;
});
});
});
});
});
function finishMigration() {
db.close();
}
});
});
