Files
TJWaterServerBinary/resources/db_v2/DATABASE_ARCHITECTURE.md
T
jiang fa188af0b1 refactor(db)!: adopt project-routed pooled databases
Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
2026-08-25 18:35:05 +08:00

455 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TJWater 数据库改造说明与当前结构
> 本文记录 2026-08-25 的数据库实际状态。结构、约束、行数、TimescaleDB chunk 和策略均直接读取数据库,不以仓库中的 SQL 脚本为依据。文中不包含主机、端口、账号、密码或 DSN。
## 改造范围与当前状态
本次改造保留原 `tjwater` 业务库和时序库,新建 `tjwater_next` 作为隔离验证环境。元数据库仍为 `system_hub`,新项目通过 `biz_data``iot_data` 两条路由分别关联新业务库与新时序库。项目完成迁移和联调后已切换为 `active`,原数据库没有被覆盖,仍可用于对照和回退。
已经完成的数据库修改包括:
- 新建并迁移 `tjwater_next` 业务库,将旧 `public` 中混合存放的管网、GIS、SCADA 配置和分析数据按领域拆分。
- 新建并迁移 `tjwater_next` 时序库,将 SCADA、实时计算和分析计算结果分开存放。
- `realtime` 采用冷热数据策略,72 小时后的 chunk 自动转为有序列存。
- `analysis``stored_at` 分区,入库满 24 小时的 chunk 自动转为有序列存。
- GIS 查询层改用物化视图,当前 7 张物化视图均已填充。
- GeoServer 已建立 `tjwater_next` 工作空间和同名数据存储,从业务库 `gis` schema 发布 7 个图层。GeoWebCache 的服务端与客户端缓存有效期均为 300 秒。
- 旧库中的 `operation``current_operation``batch_operation``operation_table``restore_operation``snapshot_operation` 没有进入新业务库。
- `system_hub.public` 补充了项目数据库外键、数据库路由约束、连接池约束、必要的非空约束,以及 5 张表和 44 个字段的中文数据库注释。
- 用户角色和项目角色仍是可扩展字符串,没有增加枚举检查约束。
- `audit_logs.user_id``audit_logs.project_id` 仍为逻辑关联,没有增加外键。
- 业务库 48 个表或物化视图、192 个字段,以及时序库 7 张表、47 个字段均已写入中文数据库注释。
- 后端已对接新 schema。WNDB、PostgreSQL 管理连接和同步 TimescaleDB 访问均使用有界连接池,闲置项目按最近使用顺序回收,实时覆盖写入使用单一事务。
- 后端批量元素查询读取 GIS 物化视图,模型增删改和 INP 导入提交后执行并发刷新;批量事务只刷新一次。
- `pattern_values``pattern_flow_samples``curve_points``demands``link_vertices` 的顺序号按所属父对象编号,主键已改为父对象 ID 与 `sequence_no` 的复合键。
`tjwater_next` 当前为 `active`。排水项目 `lingang` 已迁入 `system_hub.public`,供水和排水后端共用同一套项目、成员、数据库路由和审计表。
## 数据库总体关系
```mermaid
flowchart LR
subgraph META["system_hub 元数据库"]
MP["public<br/>统一元数据<br/>5 个项目"]
end
subgraph OLD["原数据库,保持不变"]
OB["tjwater 业务库<br/>public"]
OT["tjwater 时序库<br/>scada / realtime / scheme"]
end
subgraph NEXT["隔离验证数据库"]
NB["tjwater_next 业务库<br/>network / gis / asset / analysis"]
NT["tjwater_next 时序库<br/>scada / realtime / analysis"]
end
GS["GeoServer<br/>tjwater_next 工作空间"]
WEB["供水前端<br/>tjwater_next 图层配置"]
MP -->|"tjwater 的 biz_data"| OB
MP -->|"tjwater 的 iot_data"| OT
MP -->|"tjwater_next 的 biz_data"| NB
MP -->|"tjwater_next 的 iot_data"| NT
MP -->|"lingang 的两条数据库路由"| DRAIN["排水项目数据库"]
NB -->|"gis 物化视图"| GS -->|"WFS / WMTS"| WEB
```
一个项目对应一个业务数据库和一个时序数据库。项目本地表不保存 `project_id`,项目边界由元数据库路由和数据库连接共同确定。
## 元数据库 system_hub
### public:当前主元数据
`public` 当前有 5 个项目、1 个用户、5 条成员关系和 10 条数据库路由。5 个项目均配置了一条 `biz_data` 和一条 `iot_data` 路由。审计日志数量会随接口请求持续增加,不在文档中固化行数。
| 表 | 用途 | 主要字段 |
| --- | --- | --- |
| `users` | Keycloak 用户身份快照和系统授权状态 | `id``keycloak_id``username``email``role``is_active``is_superuser``attributes`、时间字段 |
| `projects` | 项目基本信息和地图配置 | `id``name``code``gs_workspace``map_extent``map_config``status`、时间字段 |
| `user_project_membership` | 用户和项目的成员关系 | `id``user_id``project_id``project_role` |
| `project_databases` | 项目业务库和时序库路由 | `id``project_id``db_role``db_type``dsn_encrypted``pool_min_size``pool_max_size` |
| `audit_logs` | 独立保留的操作审计 | `id``user_id``project_id``action`、资源字段、请求字段、`response_status``timestamp` |
```mermaid
erDiagram
USERS {
uuid id PK
uuid keycloak_id UK
varchar username UK
varchar email UK
varchar role
boolean is_active
boolean is_superuser
}
PROJECTS {
uuid id PK
varchar code UK
varchar name
varchar gs_workspace UK
jsonb map_extent
jsonb map_config
varchar status
}
USER_PROJECT_MEMBERSHIP {
uuid id PK
uuid user_id FK
uuid project_id FK
varchar project_role
}
PROJECT_DATABASES {
uuid id PK
uuid project_id FK
varchar db_role
varchar db_type
text dsn_encrypted
int pool_min_size
int pool_max_size
}
AUDIT_LOGS {
uuid id PK
uuid user_id
uuid project_id
varchar action
timestamptz timestamp
}
USERS ||--o{ USER_PROJECT_MEMBERSHIP : "参与项目"
PROJECTS ||--o{ USER_PROJECT_MEMBERSHIP : "包含成员"
PROJECTS ||--o{ PROJECT_DATABASES : "配置路由"
```
数据库强制执行以下关系和约束:
- 成员表的 `user_id``project_id` 分别引用用户和项目,删除用户或项目时级联删除成员关系。
- `project_databases.project_id` 引用项目,删除项目时级联删除数据库路由。
- 同一项目的 `db_role` 唯一。
- `biz_data` 必须使用 `postgresql``iot_data` 必须使用 `timescaledb`
- `pool_min_size` 不小于 1`pool_max_size` 不小于 `pool_min_size`
- 项目状态限定为 `active``inactive``archived`
- 审计表中的用户和项目 ID 不设置外键,删除业务对象不会连带删除历史日志。
### 排水元数据合并
排水后端原先使用独立的 `hub` schema,其中有 1 个 `lingang` 项目、2 条数据库路由和 203 条审计记录,没有用户或成员关系。项目 UUID 保持不变,两条路由转换为 `public.project_databases` 使用的 Fernet 加密格式,连接池参数由 `pool_size + max_overflow` 映射为 `pool_min_size + pool_max_size`。原审计记录已迁入 `public.audit_logs`
排水后端已改用 `public.projects``public.project_databases``public.user_project_membership``public.users``public.audit_logs`。供水和排水服务读取同一份项目状态、Keycloak 身份、项目权限及数据库路由。原 `hub` schema 已在迁移校验和连接测试通过后删除。
## 原业务库 tjwater
原业务库将大部分业务对象放在 `public`,当前有 68 张表、9 张视图和 2 张物化视图。管网模型、GIS、SCADA 配置、分析结果、方案数据、临时表和操作记录混在同一命名空间中。
其中还安装了 `postgis_tiger_geocoder``postgis_topology`,因此存在 `tiger``topology` 扩展 schema。新业务库只保留当前实际使用的 PostGIS 能力,没有继续安装这两个扩展。
## 相对原数据库的结构变化
以下对比以当前仍保留的原业务库 `tjwater`、原时序库 `tjwater` 与新库 `tjwater_next` 的实际对象为准。对象名称的对应关系表示业务实体或数据职责的迁移方向,不表示所有字段均一对一复制。
### 归并和调整
| 原库对象或职责 | 新库对象或职责 | 调整内容 |
| --- | --- | --- |
| `_node``junctions``reservoirs``tanks` | `network.nodes` 及节点类型子表 | 节点统一由主表管理,类型专有字段保留在共享主键子表。 |
| `_link``pipes``pumps``valves` | `network.links` 及连接类型子表 | 连接统一记录端点和类型,管道、泵、阀门参数移入对应子表。 |
| `coordinates``vertices` | `gis.node_geometries``gis.link_vertices` | 管网几何从模型参数中拆出,分别保存节点位置和连接折点。 |
| 旧的 GIS 视图和物化视图 | `gis` 下 7 张物化视图 | 前端查询层统一为节点、连接和设备的物化视图,底层仍读取 `network``gis``asset` 的规范化表。 |
| SCADA 设备配置表 | `asset.scada_devices` | 设备配置集中到资产域,并以外键关联一个节点或一条连接。当前已迁入 118 台设备。 |
| `scheme_list` 及方案结果相关表 | `analysis.runs``analysis.results` | 运行批次与非时序摘要结果留在业务库,逐时逐元素结果迁入时序库。当前已迁入 124 次运行和 17 条非时序结果。 |
| `scada.scada_data` | `scada.measurements` | SCADA 测量值迁入新时序库并按设备和时间保存。 |
| `realtime.node_simulation``realtime.link_simulation` | `realtime.node_results``realtime.link_results` | 实时仿真结果保留为节点和连接两类时序数据。 |
| `scheme.node_simulation``scheme.link_simulation` | `analysis.node_results``analysis.link_results` | 方案或分析的元素时序结果以 `run_id` 区分批次,和业务库中的 `analysis.runs` 形成逻辑关联。 |
### 已删减且未迁入的对象
- 云端操作记录相关的 `operation``current_operation``batch_operation``operation_table``restore_operation``snapshot_operation` 未进入新业务库。该设计不再作为业务数据模型的一部分。
- 临时处理表 `temp_link_1``temp_link_2``temp_node``temp_region``temp_vd_topology` 未迁入。它们属于历史处理过程的中间对象,不应成为长期库结构。
- 原库中的 `_node``_link``_pattern``_curve``_region` 等内部或过渡表不再单独存在。新库以明确的领域表和外键关系表达同一类数据。
- 原库的 `tiger``topology` 扩展 schema 未在新库安装,`tjwater_next` 仅保留 PostGIS 及其 `public` 系统对象。
### 尚未完整承接的范围
原库有 `region``region_dma``region_sa``region_vd``region_wda` 五张区域细分表。检查时这些表均无数据,因此当前新库只建立了通用的 `gis.regions``gis.region_nodes`,没有为 DMA、分区计量、分区调度或用水分区固化专用结构。
这不是删除已有业务数据,而是暂缓固化尚未使用的模型。后续确认 DMA 和 `VA` 的业务含义后,再决定是在 `gis` 中增加区域类型专有表,还是放入独立的业务 schema。当前库中没有名为 `va` 的表;若该名称指阀门,则对应 `network.valves`,若指 `region_vd`,则属于上述尚未迁入的区域细分模型。
### 新增的结构能力
- 业务库由单一 `public` 命名空间拆为 `network``gis``asset``analysis` 四个领域 schema,降低模型、空间数据、设备配置和分析记录之间的耦合。
- `gis` 增加了面向前端的 7 张物化视图,以及 `gis.refresh_all_materialized_views` 刷新过程。后端在模型批量修改和 INP 导入提交后统一刷新,不使用数据库触发器或定时任务。
- 新时序库增加 `migration` schema,用于保留迁移过程的设置和日志,不与业务时序数据混放。
- `realtime` 两张 hypertable 已启用 72 小时后的列存压缩策略,`analysis` 两张 hypertable 按入库时间执行 24 小时冷热转换。`scada` 保持独立行存。
- 元数据库的 `public.project_databases` 增加项目外键、数据库角色和类型约束,以及连接池上下限约束,用于保证每个项目的业务库和时序库路由有效。
## 新业务库 tjwater_next
新业务库使用 PostgreSQL 和 PostGIS。业务对象分布在 4 个 schema 中,`public` 只保留 PostGIS 提供的系统对象。
| Schema | 普通表 | 物化视图 | 用途 |
| --- | ---: | ---: | --- |
| `network` | 32 | 0 | 供水管网模型、仿真参数、规则和曲线 |
| `gis` | 6 | 7 | 原始空间数据、区域关系和前端查询层 |
| `asset` | 1 | 0 | SCADA 设备配置及其管网关联 |
| `analysis` | 2 | 0 | 分析运行记录和非时序结果 |
当前迁移数据包含 87,907 个节点、91,054 条连接、118 个 SCADA 设备、124 次分析运行和 17 条非时序分析结果。其中有 87,894 个普通节点、13 个水源、91,052 条管道和 2 个阀门。
### network:管网模型
`network.nodes``network.links` 是节点、连接的统一主表。具体类型通过共享主键的一对一子表扩展:
- 节点:`junctions``reservoirs``tanks`
- 连接:`pipes``pumps``valves`
- 模式和曲线:`patterns``pattern_values``pattern_flow_samples``curves``curve_points`
- 节点附属数据:`demands``emitters``sources``initial_quality``tank_mixing``node_tags`
- 连接附属数据:`link_initial_settings``link_tags``pump_energy_settings``pipe_reaction_coefficients``tank_reaction_coefficients`
- 模型配置:`controls``rules``simulation_settings``time_settings``report_settings``energy_settings``reaction_settings``model_titles`
```mermaid
erDiagram
NODES {
text id PK
text node_type
}
JUNCTIONS {
text node_id PK,FK
float elevation
}
RESERVOIRS {
text node_id PK,FK
float head
text pattern_id FK
}
TANKS {
text node_id PK,FK
float elevation
float initial_level
text volume_curve_id FK
}
LINKS {
text id PK
text link_type
text start_node_id FK
text end_node_id FK
}
PIPES {
text link_id PK,FK
float length
float diameter
text status
}
PUMPS {
text link_id PK,FK
float power
text head_curve_id FK
text pattern_id FK
}
VALVES {
text link_id PK,FK
float diameter
text valve_type
text setting
}
PATTERNS {
text id PK
}
CURVES {
text id PK
text curve_type
}
NODES ||--o| JUNCTIONS : "节点类型"
NODES ||--o| RESERVOIRS : "节点类型"
NODES ||--o| TANKS : "节点类型"
NODES ||--o{ LINKS : "起点"
NODES ||--o{ LINKS : "终点"
LINKS ||--o| PIPES : "连接类型"
LINKS ||--o| PUMPS : "连接类型"
LINKS ||--o| VALVES : "连接类型"
PATTERNS ||--o{ RESERVOIRS : "水位模式"
PATTERNS ||--o{ PUMPS : "运行模式"
CURVES ||--o{ PUMPS : "扬程曲线"
CURVES ||--o{ TANKS : "容积曲线"
```
删除节点或连接时,对应类型子表、GIS 几何、标签和关联参数按外键规则同步删除。连接的起点和终点必须引用已有节点,且不能是同一个节点。
`pattern_values``pattern_flow_samples``curve_points``sequence_no` 分别表示同一模式或曲线内部的顺序,`demands.sequence_no` 表示同一节点下多条需水记录的顺序。因此,这四张明细表使用父对象 ID 与 `sequence_no` 组成主键,不要求顺序号在全表唯一。该约束与 WNDB 的局部编号、INP 导入和按父对象排序查询一致。
### gis:空间数据和查询层
基础空间表包括:
| 表 | 用途 | 主要关系 |
| --- | --- | --- |
| `node_geometries` | 节点点位 | `node_id` 引用 `network.nodes` |
| `link_vertices` | 连接中间折点 | `link_id` 引用 `network.links` |
| `labels` | 地图标注 | 可选关联节点 |
| `backdrops` | 模型背景配置 | 单条配置记录 |
| `regions` | 区域边界 | 保存区域类型和面几何 |
| `region_nodes` | 区域和节点的多对多关系 | 同时引用区域和节点 |
`link_vertices` 使用 `(link_id, sequence_no)` 复合主键。折点顺序只在同一条连接内部有效,不同连接可以从相同的顺序号开始编号。
前端查询使用 7 张物化视图:`junctions``reservoirs``tanks``pipes``pumps``valves``scada_devices`。这些视图把模型属性和几何合并,避免前端与 GeoServer 每次重复执行跨表计算。当前 7 张视图均已填充,并为元素 ID 建立唯一索引,为几何建立 GiST 索引。
原始模型几何继续使用项目坐标系 `EPSG:900914`。点要素物化视图中的 `x``y` 仍保存该坐标系下的模型坐标,供 WNDB 和 INP 读写使用;提供给 GeoServer 的 `geom` 统一转换为 `EPSG:3857`。管道发布为线,水泵和阀门使用连接线中点发布为点,SCADA 设备按设备自有位置、关联节点位置或关联连接中点依次取值。当前数据库中的 7 个 `geom` 字段均已核对为 `EPSG:3857`
数据库中的 `gis.refresh_all_materialized_views(boolean)` 存储过程统一刷新全部物化视图。后端批量读取普通节点、水源、水箱、管道、水泵、阀门和 SCADA 设备时直接查询这些视图;单条模型增删改提交后立即刷新,批量修改和 INP 导入成功提交后只刷新一次。实际库中一次全量并发刷新约 6 秒。视图已有数据和唯一索引,因此刷新期间查询仍可继续。
```mermaid
flowchart LR
N["network 节点和连接"] --> G["gis 原始几何"]
A["asset.scada_devices"] --> G
N --> MV["gis 物化视图"]
G --> MV
A --> MV
MV --> GS["GeoServer tjwater_next"]
GS --> WFS["WFS 要素查询"]
GS --> GWC["GeoWebCache WMTS"]
WFS --> F["供水前端"]
GWC --> F
R["refresh_all_materialized_views"] --> MV
```
### GeoServer 与前端图层
`system_hub.public.projects` 中的 `tjwater_next` 项目已配置 `gs_workspace=tjwater_next`,当前状态为 `active`。GeoServer 的 `tjwater_next` 数据存储连接同名业务库并限定到 `gis` schema,图层名称直接采用物化视图名称。7 个图层使用相同的项目管网发布边界,空图层和视口内没有要素的瓦片会返回空 MVT,不会产生越界错误。
| 前端数据源 | GeoServer 图层 | 几何 | 当前要素数 |
| --- | --- | --- | ---: |
| `junctions` | `tjwater_next:junctions` | Point`EPSG:3857` | 87,894 |
| `reservoirs` | `tjwater_next:reservoirs` | Point`EPSG:3857` | 13 |
| `tanks` | `tjwater_next:tanks` | Point`EPSG:3857` | 0 |
| `pipes` | `tjwater_next:pipes` | LineString`EPSG:3857` | 91,052 |
| `pumps` | `tjwater_next:pumps` | Point`EPSG:3857` | 0 |
| `valves` | `tjwater_next:valves` | Point`EPSG:3857` | 2 |
| `scada` | `tjwater_next:scada_devices` | Point`EPSG:3857` | 118 |
供水前端的 `refactor/tjwater-next-integration` 分支已切换到这些图层和新字段名。地图切片走 `WebMercatorQuad` 的 MVT,定位和详情查询走 WFS。旧 `geo_pipes_mat``geo_junctions_mat` 和其他 `geo_*` 图层名不再作为新前端的兼容别名。
方案查询统一读取 `/api/v1/analysis/runs`,详情和时序结果按 UUID `run_id` 关联。时间轴读取 `/api/v1/timeseries/analysis/runs/{run_id}/values`,爆管定位的模拟数据源也传递 `simulation_run_id`。监测点优化使用 `/api/v1/sensor-placement-runs`。前端不再调用旧的 `/schemes``/timeseries/schemes``/sensor-placement-schemes` 接口。
模型修改提交后,后端先调用 `gis.refresh_all_materialized_views(boolean)` 刷新数据库查询层。GeoWebCache 不会感知 PostgreSQL 物化视图刷新,因此 7 个新图层配置了 300 秒的服务端和客户端缓存有效期,前端最迟在 5 分钟后读到新瓦片。部署或批量迁移完成后仍可执行一次图层缓存清空,避免等待已有瓦片自然过期。
### assetSCADA 设备配置
`asset.scada_devices` 保存设备类型、采集接口标识、传输模式、频率、可靠性和可选几何。每台设备必须关联一个节点或一条连接,不能同时关联两者。设备的历史测量值不放在业务库,保存在时序库 `scada.measurements`
### analysis:分析运行和非时序结果
`analysis.runs` 表示一次实际执行,保存运行名称、类型、创建人、开始时间、状态和参数。当前没有单独的 `scenarios` 模板表。
每次执行都会创建新的 `run_id`,名称和业务时间相同也不会覆盖旧结果。扩展仿真开始写结果前,业务库先记录 `running`;时序库写入完成后更新为 `completed`,写入失败则保留为 `failed`。业务库和时序库据此共用同一个执行标识。
`analysis.results` 保存不适合放入时序表的结果摘要或结构化业务结果。每条结果必须属于一个运行,可以关联一个节点、一条连接,或者不关联任何管网元素。元素级时序结果保存在时序库,某次分析只有摘要结果、只有元素时序结果或两者都有都是合法状态。
```mermaid
erDiagram
RUNS {
uuid run_id PK
text name
text run_type
text created_by
timestamptz started_at
text status
jsonb parameters
}
RESULTS {
uuid result_id PK
uuid run_id FK
text result_type
text node_id FK
text link_id FK
jsonb payload
}
RUNS ||--o{ RESULTS : "产生可选结果"
```
### 业务表如何扩展
当前没有预建空的 `business` schema。管网模型放在 `network`,空间区域和 DMA 放在 `gis`,设备配置放在 `asset`,运行和结果放在 `analysis`。后续出现工单、巡检、告警或资产养护等明确业务实体时,再按稳定的业务边界增加独立 schema;不把新业务表重新堆回 `public`,也不为尚未确定的角色或流程提前建表。
## 原时序库 tjwater
原时序库运行 TimescaleDB 2.21.3,共有 5 张 hypertable
- `scada.scada_data`
- `realtime.node_simulation`
- `realtime.link_simulation`
- `scheme.node_simulation`
- `scheme.link_simulation`
这些表均为行存,没有启用压缩策略。方案结果使用 `scheme_name` 关联业务库中的方案记录。
## 新时序库 tjwater_next
新时序库仍运行 TimescaleDB 2.21.3。业务时序数据分为 `scada``realtime``analysis`,迁移过程状态单独放在 `migration`
| 表 | 主键 | Chunk 间隔 | 当前行数 | 当前占用 |
| --- | --- | --- | ---: | ---: |
| `scada.measurements` | `(time, device_id)` | 7 天 | 1,249,602 | 约 0.18 GiB |
| `realtime.node_results` | `(time, node_id)` | 1 天 | 159,111,670 | 约 1.89 GiB |
| `realtime.link_results` | `(time, link_id)` | 1 天 | 164,807,740 | 约 2.18 GiB |
| `analysis.node_results` | `(stored_at, time, run_id, node_id)` | 1 天 | 21,800,936 | 约 2.93 GiB |
| `analysis.link_results` | `(stored_at, time, run_id, link_id)` | 1 天 | 22,581,392 | 约 4.82 GiB |
### scada
`scada.measurements` 保存设备测量值和清洗值。`device_id` 与业务库的 `asset.scada_devices.device_id` 是跨数据库逻辑关联,数据库不能建立物理外键。
时序表当前有 118 个设备标识,与业务库的 118 台设备一一对应。迁移后曾发现设备标识 `11``12``13``14` 没有对应的 SCADA 点位配置;确认属于无效孤立数据后,已从新时序库删除其 5,376 条记录。原 `tjwater` 时序库保持不变,必要时仍可追溯迁移前数据。
### realtime
`realtime.node_results``realtime.link_results` 保存当前实时计算窗口。主键保证同一时刻、同一元素只能有一条记录。相同时间窗口由后端先删除、再通过 `COPY` 批量插入;节点和连接两次替换位于同一个最外层事务,其中任一步失败都会整体回滚。
两张表当前各有 19 个 chunk,均已转为列存,因为现有数据都早于 72 小时热窗口。数据库每小时执行一次策略检查,将 72 小时以前的 chunk 转为有序列存。节点结果按 `node_id, time DESC` 排序,连接结果按 `link_id, time DESC` 排序。新写入的数据使用 1 天 chunk,并在 72 小时内保持行存。
### analysis
`analysis.node_results``analysis.link_results` 保存每次分析运行的元素级时间序列,通过 `run_id` 与业务库 `analysis.runs` 逻辑关联。`time` 是仿真业务时刻,继续用于曲线和时间范围查询;`stored_at` 是该批结果的入库完成时间,作为 hypertable 的分区时间。
两张表按 1 天创建 chunk,最近 24 小时保持热数据。超过 24 小时的 chunk 会由压缩策略转为列存冷数据,策略按 `run_id` 与元素 ID 分段、按仿真 `time DESC` 排序。重新计算相同历史时间段时,新结果写入当前的 `stored_at` 热分区,不需要改写历史冷分区。当前各有 20 个 chunk,均因入库时间超过 24 小时而完成压缩。
迁移期间曾保留按仿真时间分区的 `*_time_partitioned_legacy` 表用于回退校验。清理前已核对 46 个 `run_id`:节点 21,800,936 行、连接 22,581,392 行的总数、逐运行数量、时间范围和全字段校验值均一致。两张旧表随后删除,释放约 12.16 GiB;原 `tjwater` 时序库仍保留,不受此次清理影响。
### migration
`migration.copy_log` 记录每个来源表、迁移时间窗口和源行数,当前有 150 条记录。`migration.settings` 保存迁移截止时间等运行参数,当前有 1 条记录。它们是迁移审计和断点信息,不是业务数据,也不是仍待处理的中间结果。正式数据迁移已经完成。
## 后端连接与事务
元数据库通过 SQLAlchemy 异步连接池访问;项目请求按 `system_hub.public.project_databases` 路由到业务库和时序库。异步业务查询和异步时序查询由项目级动态池管理,原生 WNDB 同步访问使用按数据库缓存的 `psycopg_pool.ConnectionPool`,数据库创建、复制和删除使用独立的 PostgreSQL 管理池,同步 TimescaleDB 访问也使用按数据库缓存的连接池。应用目录中已没有直接调用 `psycopg.connect` 的业务代码。
WNDB 批量修改和 INP 数据导入在同一条池连接和同一事务中执行,提交后再刷新 GIS 物化视图。实时节点和连接结果也在一个事务中执行先删后写,同一结果时间使用事务级锁避免并发覆盖竞态;分析结果按 `run_id` 加事务级锁,防止同一运行被并发写入两次。
自动化真实数据库测试分别执行 64 次业务库和 64 次时序库并发借用,查询结果一致,连接均能归还池中。嵌套 WNDB 写入和分析运行生命周期测试会在外层强制回滚,数据库没有残留记录。`DatabaseCommand` 的 pattern 新增、修改、删除也在同一池化事务中完成,并验证了五张明细表的复合主键、级联解除需求模式关联、结果变更和整体回滚。实时覆盖测试确认第二批数据替换第一批数据,外层回滚后测试记录为 0。
## 跨数据库逻辑关系
PostgreSQL 不能对另一个数据库中的表建立外键。业务库与时序库之间通过稳定 ID 保持逻辑一致性,完整性由迁移校验和后端事务负责。
```mermaid
flowchart LR
P["system_hub.public.projects"] -->|"biz_data 路由"| B["项目业务库"]
P -->|"iot_data 路由"| T["项目时序库"]
BN["network.nodes.id"] -. "node_id" .-> RN["realtime.node_results"]
BN -. "node_id" .-> AN["analysis.node_results"]
BL["network.links.id"] -. "link_id" .-> RL["realtime.link_results"]
BL -. "link_id" .-> AL["analysis.link_results"]
SD["asset.scada_devices.device_id"] -. "device_id" .-> SM["scada.measurements"]
AR["analysis.runs.run_id"] -. "run_id" .-> AN
AR -. "run_id" .-> AL
```
实线表示元数据库保存的项目路由,虚线表示跨数据库逻辑关联。每个项目使用独立业务库和时序库,不需要在项目本地表中重复保存 `project_id`
## 仍需处理的事项
- 当前没有修改 SCADA 设备配置的后端接口。以后增加这类写接口时,也要在提交后调用统一的物化视图刷新函数。
- 用户角色和项目角色还未定型,数据库只要求字段非空,不限制具体取值。