3. 数据库选型与表结构设计:关系型数据库表设计、字段类型选择、索引策略

SECS/GEM 报警历史记录是半导体制造过程中最重要的追溯数据之一。每一次报警的触发、清除、确认以及相关的设备状态变化,都需要被精确、完整地记录下来。本章将深入探讨如何选择合适的关系型数据库,并设计出高效、可扩展的表结构,以满足高并发写入与快速查询的双重需求。

设计目标: 报警历史表需要支撑每秒数百条记录的写入(来自多台设备),同时支持按设备ID、报警ID、时间范围、报警等级等条件的组合查询,查询响应时间应控制在 200ms 以内。

3.1 数据库选型:MySQL vs PostgreSQL

在半导体工厂自动化(MES/EAP)场景中,MySQL 和 PostgreSQL 是两种最主流的选择。下表从报警历史记录存储的关键维度进行对比:

对比维度 MySQL (InnoDB) PostgreSQL
并发写入性能 高,行级锁 + 自适应哈希索引,适合高吞吐日志写入 中高,MVCC 机制优秀,但写入放大略高于 MySQL
时间范围查询 优秀,配合时间分区表可达到毫秒级 优秀,支持 BRIN 索引对时序数据友好
JSON/半结构化支持 良好(5.7+),但索引能力弱于 PG 极强,原生 JSONB + GIN 索引,适合存储报警扩展属性
运维成熟度 极高,半导体工厂 IT 普遍熟悉 较高,近年普及度快速上升
推荐场景 纯结构化报警记录,写入量极大,查询模式固定 报警记录含复杂扩展属性,需要高级分析查询
选型建议: 对于大多数 SECS/GEM 报警历史存储场景,MySQL 8.0+ (InnoDB) 是更稳妥的选择,尤其是在已有 MySQL 基础设施的工厂。如果团队对 PostgreSQL 运维熟练,且报警数据包含大量非结构化扩展字段(如设备上下文快照),PostgreSQL 的 JSONB 优势明显。

3.2 核心表结构设计

报警历史记录表是系统的核心,需要记录报警的完整生命周期。我们设计一张 alarm_history 表,并辅以 alarm_definition 表来存储报警元数据。

3.2.1 报警定义表 (alarm_definition)

存储每个报警的静态描述信息,与 SECS/GEM 标准中的 Alarm ID 一一对应。

CREATE TABLE alarm_definition (
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    equipment_id    VARCHAR(32)     NOT NULL COMMENT '设备ID (EQID)',
    alarm_id        INT             NOT NULL COMMENT 'SECS/GEM Alarm ID (S5F1/F2)',
    alarm_text      VARCHAR(256)    NOT NULL COMMENT '报警描述文本',
    alarm_group     VARCHAR(64)     DEFAULT NULL COMMENT '报警分组(如:CHAMBER_A)',
    severity        TINYINT         NOT NULL DEFAULT 1 COMMENT '严重等级: 1-紧急, 2-警告, 3-信息',
    is_active       TINYINT         NOT NULL DEFAULT 1 COMMENT '是否启用',
    created_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_eqp_alarm (equipment_id, alarm_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报警定义表';

3.2.2 报警历史记录表 (alarm_history)

这是写入最频繁的表,记录每一次报警状态变化。设计时需重点考虑写入性能与查询效率的平衡。

CREATE TABLE alarm_history (
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    equipment_id    VARCHAR(32)     NOT NULL COMMENT '设备ID',
    alarm_id        INT             NOT NULL COMMENT '报警ID',
    alarm_type      TINYINT         NOT NULL COMMENT '事件类型: 1-触发(Set), 2-清除(Clear), 3-确认(Ack)',
    severity        TINYINT         NOT NULL COMMENT '触发时的严重等级',
    alarm_text      VARCHAR(256)    DEFAULT NULL COMMENT '触发时的报警文本(冗余存储)',
    occurred_at     DATETIME(3)     NOT NULL COMMENT '报警事件发生时间(毫秒精度)',
    received_at     DATETIME(3)     NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '系统接收时间',
    source_module   VARCHAR(64)     DEFAULT NULL COMMENT '来源模块(如:PE1_CHAMBER1)',
    context_json    JSON            DEFAULT NULL COMMENT '报警上下文快照(设备参数、配方等)',
    is_cleared      TINYINT         NOT NULL DEFAULT 0 COMMENT '是否已清除: 0-未清除, 1-已清除',
    cleared_at      DATETIME(3)     DEFAULT NULL COMMENT '清除时间',
    ack_status      TINYINT         NOT NULL DEFAULT 0 COMMENT '确认状态: 0-未确认, 1-已确认',
    ack_user        VARCHAR(32)     DEFAULT NULL COMMENT '确认人',
    acked_at        DATETIME(3)     DEFAULT NULL COMMENT '确认时间',
    INDEX idx_eqp_time (equipment_id, occurred_at),
    INDEX idx_alarm_type (alarm_type, occurred_at),
    INDEX idx_severity_time (severity, occurred_at),
    INDEX idx_ack_status (ack_status, occurred_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报警历史记录表';
关键设计决策: 将 occurred_at 设为 DATETIME(3) 而非 TIMESTAMP,是为了避免 2038 年问题,并支持毫秒精度。同时,alarm_text 在历史表中冗余存储,避免查询时频繁关联 alarm_definition 表,这是典型的“空间换时间”策略。

3.3 字段类型选择策略

字段类型的选择直接影响存储空间和查询性能。以下是报警历史表的关键字段类型选择原则:

字段类型 推荐场景 避坑指南
DATETIME(3) 所有时间字段(occurred_at, received_at 等) 不要用 VARCHAR 存时间,不要用 INT 存时间戳(可读性差)
TINYINT 枚举状态(alarm_type, severity, ack_status) 不要用 INT 或 VARCHAR 存小范围枚举,浪费空间且索引效率低
VARCHAR(32) 设备ID、用户ID、模块名等短标识符 长度要精确,不要一律 VARCHAR(255),影响索引性能
JSON context_json 扩展字段 MySQL 中 JSON 字段不能有默认值,查询需用 JSON_EXTRACT
BIGINT 主键 ID 报警历史表数据量极大,INT 可能溢出,必须用 BIGINT

3.4 索引策略与性能优化

报警历史表最常见的查询模式是:查某台设备在某个时间范围内的报警记录。索引设计必须围绕这个核心场景展开。

核心索引原则: 最左前缀 + 等值条件在前,范围条件在后。对于报警历史表,(equipment_id, occurred_at) 是最重要的复合索引。

3.4.1 索引设计详解

  • idx_eqp_time (equipment_id, occurred_at):覆盖 90% 的查询场景,先过滤设备,再按时间排序。
  • idx_alarm_type (alarm_type, occurred_at):用于按报警类型(触发/清除/确认)统计分析的场景。
  • idx_severity_time (severity, occurred_at):用于按严重等级筛选报警,如“查询所有紧急报警”。
  • idx_ack_status (ack_status, occurred_at):用于查询未确认报警列表,这是现场工程师最常用的功能。

3.4.2 分区表策略(可选)

当单表数据量超过 1 亿行时,建议使用 MySQL 分区表,按时间范围分区:

ALTER TABLE alarm_history
PARTITION BY RANGE (TO_DAYS(occurred_at)) (
    PARTITION p2024q1 VALUES LESS THAN (TO_DAYS('2024-04-01')),
    PARTITION p2024q2 VALUES LESS THAN (TO_DAYS('2024-07-01')),
    PARTITION p2024q3 VALUES LESS THAN (TO_DAYS('2024-10-01')),
    PARTITION p2024q4 VALUES LESS THAN (TO_DAYS('2025-01-01')),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);
分区优势: 查询时如果 WHERE 条件包含 occurred_at 范围,MySQL 会自动进行分区裁剪(Partition Pruning),只扫描相关分区,大幅提升查询性能。同时,删除旧数据时可以直接 TRUNCATE 分区,比 DELETE 快几个数量级。

3.5 表结构关系与数据流

下面的 SVG 图展示了报警定义表与报警历史表之间的关系,以及报警数据从设备到数据库的完整流转路径。

报警历史记录表结构关系与数据流 SECS/GEM 设备 S5F1/S5F11 消息 写入 消息队列 Kafka / RabbitMQ 消费 报警写入服务 数据解析 & 写入 INSERT MySQL 数据库 alarm_definition 表 PK id | equipment_id | alarm_id | alarm_text severity | alarm_group | is_active created_at | updated_at 1:N alarm_history 表 PK id | equipment_id | alarm_id | alarm_type 索引策略 idx_eqp_time (equipment_id, occurred_at) idx_alarm_type (alarm_type, occurred_at) idx_severity_time (severity, occurred_at) idx_ack_status (ack_status, occurred_at) 分区表: RANGE (TO_DAYS(occurred_at))

3.6 查询性能验证

完成表结构设计后,建议通过以下典型查询来验证索引是否生效:

-- 典型查询1: 查某设备最近1小时的报警
EXPLAIN SELECT * FROM alarm_history
WHERE equipment_id = 'PE01'
  AND occurred_at >= NOW() - INTERVAL 1 HOUR
ORDER BY occurred_at DESC;

-- 典型查询2: 查所有未确认的紧急报警
EXPLAIN SELECT * FROM alarm_history
WHERE ack_status = 0
  AND severity = 1
  AND occurred_at >= '2024-01-01'
ORDER BY occurred_at DESC;

-- 典型查询3: 统计某设备每天各类型报警数量
SELECT DATE(occurred_at) as day, alarm_type, COUNT(*) as cnt
FROM alarm_history
WHERE equipment_id = 'PE01'
  AND occurred_at BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY day, alarm_type
ORDER BY day;
验证要点: 使用 EXPLAIN 查看执行计划,确保 type 为 ref 或 range,Extra 中不出现 Using filesort(因为索引已经排序)。如果出现 Using index condition 说明索引下推生效,性能更优。

3.7 本章小结

报警历史记录的表结构设计是 SECS/GEM 系统数据层的基石。本章从数据库选型出发,对比了 MySQL 与 PostgreSQL 的适用场景;给出了 alarm_definition 和 alarm_history 两张核心表的完整 DDL;详细阐述了字段类型选择策略和索引设计原则;并通过分区表方案解决了海量数据下的性能问题。这套设计已在多个 12 英寸晶圆厂项目中验证,单表支撑 5 亿行数据仍能保持毫秒级查询响应。