WiFi-DensePose 数据库设计解析:4 张表如何接住高频姿态数据流
【免费下载链接】RuViewπ RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video.项目地址: https://gitcode.com/GitHub_Trending/wi/RuView
WiFi-DensePose 把普通 WiFi 信号变成姿态检测,落库侧的真实压力是:纳秒级 CSI 时序高频写入、多设备并发上报、按时间窗实时回放。这套数据库设计就是在回答"怎么接住"。
数据从哪来、到哪去:先看整体流向
Wi-Fi 收发端经 CSI 相位净化与模态转换网络,输出多人体姿态;图中右侧的关键点序列即 pose_detections 表的内容
数据链路很短:收发端持续产出 CSI 帧,先过相位净化再进模态转换网络,输出每个目标人的关键点与置信度。落到关系模型上是一条单向流水线——设备表登记采集端,会话表圈定"一次采集"的时间边界,CSI 数据表以只追加方式落原始信号,姿态检测表以只追加方式落模型输出。日常查询几乎只碰后两张高频表,设备与会话属于低频维度表。这个分层直接决定了后面所有表的设计取向:高频表做窄、做平,低频表允许厚一些。
逐表拆解:每张表在解决什么问题
设备表:采集端如何"认人"
设备表是采集端登记表,一张设备一行,写入频率最低。
class Device(Base, UUIDMixin, TimestampMixin): __tablename__ = "devices" name = Column(String(255), nullable=False) mac_address = Column(String(17), unique=True, nullable=False) # 业务身份键 status = Column(String(20), default="inactive", nullable=False) # 四态枚举 coordinates_x = Column(Float, nullable=True) # 部署坐标,三列分开 config = Column(JSON, nullable=True) # 厂商自定义配置 # ...三个取舍值得注意。MAC 地址做唯一约束是刻意的:重命名或换 IP 时,以 MAC 为锚的数据关联不断链,配合@validates在应用层先把格式校验掉。坐标拆成三列而非打包进 JSON,因为布点规划需要按坐标做范围计算。config 用 JSON 则因为不同厂商设备的固件参数差异极大,硬建模只会频繁改表结构。
会话表:一次采集的时间盒
会话表把"一次采集"圈成时间盒,CSI 与检测明细都挂在它下面。
class Session(Base, UUIDMixin, TimestampMixin): __tablename__ = "sessions" started_at = Column(DateTime(timezone=True), nullable=True) ended_at = Column(DateTime(timezone=True), nullable=True) status = Column(String(20), default="active", nullable=False) device_id = Column(UUID, ForeignKey("devices.id"), nullable=False) # 归属设备 total_frames = Column(Integer, default=0, nullable=False) # 帧数计数 processed_frames = Column(Integer, default=0, nullable=False) # 已处理计数 # ...核心设计是冗余计数:total_frames 与 processed_frames 让"这次采集处理到哪了"变成对 sessions 单行的一次索引查询,不用对明细表做全量 count。代价是计数器与明细可能短暂不一致,设计上只在会话收尾时对账。config 与 meta_data 同样交给 JSON——采集参数随实验变化,属于典型的可变结构。
CSI 数据表:原始信号的落地格式
这张表是写入压力最大的地方,一行对应一帧 CSI。
class CSIData(Base, UUIDMixin, TimestampMixin): __tablename__ = "csi_data" sequence_number = Column(Integer, nullable=False) # 设备内序号 timestamp_ns = Column(Integer, nullable=False) # 纳秒时间戳 device_id = Column(UUID, ForeignKey("devices.id"), nullable=False) session_id = Column(UUID, ForeignKey("sessions.id"), nullable=True) # 允许后补 amplitude = Column(FloatArray, nullable=False) # 幅度谱,原生数组 phase = Column(FloatArray, nullable=False) # 相位谱,原生数组 num_subcarriers = Column(Integer, nullable=False) # 子载波数量 processing_status = Column(String(20), default="pending", nullable=False) # ...两个关键决定。第一,幅度与相位用 PostgreSQL 原生数组类型而非拆行存储:一帧信号必须原子地读完整,拆行既让行数爆炸,又破坏"一帧一行"的读取语义。第二,timestamp_ns 用整型纳秒而非 DateTime,绕开浮点秒的精度损失,且整型比较与范围索引都更快。session_id 设为可空,对应真实部署中"信号先落库、会话边界后补挂"的写入顺序。
姿态检测表:输出结果与版本追溯
姿态检测表存的是模型输出,不存中间态。
class PoseDetection(Base, UUIDMixin, TimestampMixin): __tablename__ = "pose_detections" frame_number = Column(Integer, nullable=False) # 帧号 timestamp_ns = Column(Integer, nullable=False) session_id = Column(UUID, ForeignKey("sessions.id"), nullable=False) # 必须有归属 person_count = Column(Integer, default=0, nullable=False) # 目标人数 keypoints = Column(JSON, nullable=True) # 关键点序列 bounding_boxes = Column(JSON, nullable=True) # 边界框 detection_confidence = Column(Float, nullable=True) # 检测置信度 pose_confidence = Column(Float, nullable=True) # 姿态置信度 model_version = Column(String(50), nullable=True) # 模型版本 # ...keypoints 与 bounding_boxes 用 JSON 是因为人数可变、结构嵌套,拆成独立明细表查询时反而要做按人聚合的 N+1 操作。三个 confidence 拆列存储,让下游可以按不同阈值分别过滤"漏检"和"姿态模糊",而不必依赖一个笼统的 overall 值。model_version 逐行冗余是刻意冗余——模型升级后,历史结果仍可按版本回放与对比。
表与表之间怎么"说话":关系与约束
关系结构是一棵以 Device 为根的单向树:Device → Session → CSIData / PoseDetection。三处级联都声明为cascade="all, delete-orphan",含义是删除设备会递归带走它的会话、CSI 与检测明细,数据库层不留下指向已删设备的孤儿行。若没有这层约束,删设备后查询会抛出外键违反,明细表的"孤儿行"则永远无法被时间窗查询正确过滤——对时序数据来说这是最隐蔽的数据污染。
一个刻意的不对称:CSI 表的 session_id 可空,检测表的 session_id 不可空。原始信号允许先落库再挂会话,而检测结果必须带着归属出生——没有会话上下文的姿态输出无法被解释。约束侧,置信度列被 CheckConstraint 锁在 [0,1] 区间,person_count 与帧计数器锁为非负;状态列一律用IN (...)白名单,非法状态在入库前被数据库直接拒绝,而不是等到查询时才暴露脏数据。
生产环境踩坑指南:索引、约束与审计
- 给高频查询路径建覆盖索引。时间窗回放是主查询形态,单列索引
idx_csi_timestamp之外,按device_id + timestamp_ns的组合才是真实路径。验证方式:EXPLAIN ANALYZE确认走了 Index Scan 且没有回表热点。 - 状态机列必须加 IN 约束。状态流转错误是这类系统最常见的脏数据源。验证方式:尝试写入白名单外的值,确认被数据库拒绝而非应用层吞掉。
- 幂等写入靠复合唯一约束。CSI 表用
UniqueConstraint("device_id", "sequence_number", "timestamp_ns")拦重放,进程重启后重发同一帧不会产生重复行。验证方式:并发重放同一帧序列,确认第二笔被唯一约束拒绝。 - 审计表记状态快照而非流水文字。AuditLog 用 before_state / after_state 两个 JSON 列存变更前后快照,配合
resource_type + resource_id联合索引定位变更对象。验证方式:抽取一段事件序列回放,确认能重建任意时刻的资源状态。
约束与索引在模型层的典型写法:
__table_args__ = ( Index("idx_csi_timestamp", "timestamp_ns"), UniqueConstraint("device_id", "sequence_number", "timestamp_ns", name="uq_csi_device_seq_time"), CheckConstraint("frequency > 0", name="check_frequency_positive"), )规模上去之后怎么办:性能与扩展性
数据类型选择。定长浮点信号一律用原生数组类型,实测比 JSON 序列化更省空间且支持数组内下标操作;JSON 只留给真正可变的结构(配置、关键点、审计快照)。判断标准:一个字段的结构一年内不会变,就不该用 JSON。
分区策略。csi_data 与 pose_detections 是两张无限增长的表,单表超过 5000 万行时按 timestamp_ns 做范围分区,归档退化为按分区 DROP,比 DELETE 快几个数量级。
批量写入。高频路径禁止逐帧 insert,按秒级时间片攒批用 executemany 提交,把数据库交互次数压到帧率以下。验证标准:写入吞吐不低于采集帧率,且无连接池耗尽告警。
查询优化。点查全部走 session_id + timestamp_ns 的索引路径;对 keypoints 这类 JSON 列避免全表过滤,先用 person_count 等标量列缩小候选集,再在应用层解析 JSON。
这套设计的适用边界很清晰:低频维度表 + 中频会话 + 高频只追加明细,复合唯一约束保幂等,JSON 与原生数组各司其职——把它平移到其他时序感知系统基本可以直接复用。全部模型定义见 archive/v1/src/database/models.py,自定义数组类型在 archive/v1/src/database/model_types.py 中定义,排查查询性能问题可结合 docs/TROUBLESHOOTING.md 使用。
【免费下载链接】RuViewπ RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video.项目地址: https://gitcode.com/GitHub_Trending/wi/RuView
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考