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.
This commit is contained in:
2026-08-25 18:35:05 +08:00
parent fdbcc5c033
commit fa188af0b1
181 changed files with 8446 additions and 33546 deletions
+454
View File
@@ -0,0 +1,454 @@
# 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 设备配置的后端接口。以后增加这类写接口时,也要在提交后调用统一的物化视图刷新函数。
- 用户角色和项目角色还未定型,数据库只要求字段非空,不限制具体取值。