3、特征注册表设计:MySQL表结构设计、核心字段定义与索引策略
特征注册表是整个特征平台的「元数据中心」。说白了,它不存特征值,只存特征的「身份证信息」——叫什么名字、什么类型、从哪来、现在什么状态。我见过不少团队一上来就急着写特征计算逻辑,结果特征一多,命名乱套、类型冲突、找不到归属……嗯,这时候再回头补注册表,成本就高了。
这一章,我们就来聊聊怎么把这张表设计得既干净又扛打。
3.1 核心字段定义:一张表管住所有特征元数据
先上我最常用的一张表结构。它经历过日均百万级特征调用的考验,字段不多,但每个都有存在的理由。
CREATE TABLE `feature_registry` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`feature_id` VARCHAR(64) NOT NULL COMMENT '特征唯一标识,业务含义编码',
`feature_name` VARCHAR(128) NOT NULL COMMENT '特征中文名/可读名',
`feature_type` VARCHAR(32) NOT NULL COMMENT '数据类型:INT, FLOAT, STRING, BOOLEAN, VECTOR...',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-草稿, 1-启用, 2-废弃, 3-灰度',
`source_type` VARCHAR(32) NOT NULL COMMENT '数据来源:HIVE, KAFKA, HTTP, DB, FILE',
`source_config` JSON COMMENT '来源配置,如Hive表名/Kafka topic',
`owner` VARCHAR(64) NOT NULL COMMENT '负责人工号或团队名',
`description` VARCHAR(512) COMMENT '特征含义说明',
`tags` JSON COMMENT '标签数组,用于分类/检索',
`version` INT 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,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_feature_id` (`feature_id`),
KEY `idx_status_type` (`status`, `feature_type`),
KEY `idx_owner` (`owner`),
KEY `idx_updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特征注册表';
我来拆开讲讲几个关键字段的设计思路。
feature_id:特征的「身份证号」
我个人习惯用带业务含义的编码,比如 user_age_online_v2 或 order_amt_30d_sum。为什么不用自增ID?因为特征会被上下游大量引用——模型训练脚本、特征计算任务、在线服务配置……如果只用数字ID,人根本记不住,排查问题得来回查表。feature_id 一旦发布,禁止修改。我见过有人手滑改了个ID,结果线上模型加载失败,回滚花了两个小时。
feature_type:类型即契约
特征类型不只是给人类看的,它直接决定了在线服务怎么反序列化、怎么做校验。我建议至少支持这几种:
| 类型值 | 说明 | 典型场景 |
|---|---|---|
| INT | 整型 | 年龄、次数、等级 |
| FLOAT | 浮点型 | 金额、概率、评分 |
| STRING | 字符串 | 城市、设备ID、类别标签 |
| BOOLEAN | 布尔型 | 是否新用户、是否VIP |
| VECTOR | 浮点向量 | Embedding、图像特征 |
| MAP | KV结构 | 用户偏好、属性集合 |
你可能会问:为什么不用枚举?因为特征类型未来可能扩展,用字符串更灵活。我在项目中就踩过这个坑——一开始用 TINYINT 映射类型,后来新增 VECTOR 类型,所有客户端都得同步更新枚举映射表,太痛苦了。
status:特征的生命周期
状态机设计是注册表的核心。我把它分成四个阶段:
- 草稿(0):刚录入,还在配置中,不可被服务发现。
- 启用(1):正常服务中,可被在线和离线任务使用。
- 灰度(3):只对部分流量生效,用于 A/B 测试。
- 废弃(2):不再使用,但保留元数据供历史回溯。
这里有个细节:废弃不是删除。我曾经手欠删了一条废弃特征,结果三个月后要复现一个老模型,特征ID找不到了,只能从代码里反推……从那以后,我规定所有特征只能标记废弃,不能物理删除。
source_config:用 JSON 承载灵活性
不同特征的来源差异很大。Hive 特征需要表名和分区字段,Kafka 特征需要 topic 和解析逻辑,HTTP 特征需要 URL 和超时配置。如果每种来源都单独建字段,表会膨胀得没法看。JSON 字段正好解决这个问题——结构灵活,MySQL 8.0 以上还支持 JSON 索引和函数检索。
version 子字段,记录数据源的版本号。比如 Hive 表结构变更时,你可以通过版本号快速判断哪些特征需要重新校准。
3.2 索引设计策略:查询快、写入稳、不冗余
表结构定好了,接下来是索引。注册表的写入量不大(一天几百条顶天了),但查询场景多——按ID查、按状态查、按负责人查、按更新时间查……索引设计要覆盖这些高频路径,又不能太多拖慢写入。
主键与唯一索引
PRIMARY KEY (id):自增主键,InnoDB 聚簇索引,没啥好说的。UNIQUE KEY uk_feature_id (feature_id):业务唯一键。这是最重要的索引,几乎所有在线查询都走它。注意是唯一索引,不是普通索引——防止重复注册。
联合索引:状态 + 类型
KEY idx_status_type (status, feature_type) 是我最常用的联合索引。为什么?因为后台管理页面最常见的操作就是「查所有启用中的 INT 类型特征」或「查所有废弃特征」。把 status 放左边,是因为它区分度高(只有4个值),能快速过滤掉大部分数据。
负责人索引与时间索引
KEY idx_owner (owner):用于「我负责哪些特征」这类查询。权限管理和通知推送都会用到。KEY idx_updated_at (updated_at):用于增量同步。离线任务每天拉取「今天有变更的特征列表」,走这个索引非常快。
📌 索引设计原则总结:
- 在线查询(按 feature_id)走唯一索引,延迟控制在 1ms 以内。
- 后台查询(按状态/类型/负责人)走联合索引或单列索引。
- 增量同步(按更新时间)走时间索引,避免全表扫描。
- 不要给 JSON 字段建太多虚拟列索引,除非你真的需要按 JSON 内部字段过滤。
3.3 注册表核心流程:一张图看懂
下面这张图展示了特征从注册到下线的完整生命周期,以及注册表在其中扮演的角色。
流程其实不复杂:注册 -> 入库 -> 状态流转 -> 被消费 -> 治理回收。但每一步都依赖注册表提供准确的元数据。你想想看,如果注册表里 feature_type 写错了,在线服务反序列化就会抛异常;如果 status 没及时更新,离线任务可能还在用已经废弃的特征……所以这张表虽然小,却是整个特征平台的「宪法」。
3.4 一些实战中的小建议
- 字段命名统一用下划线风格:Java 端用 MapStruct 或 MyBatis 自动映射时,下划线转驼峰是标配,别搞特殊。
- JSON 字段不要滥用:只有确实结构多变的配置才放 JSON。像 owner、status 这种固定字段,老老实实用独立列,查询效率高得多。
- 预留扩展字段:我习惯加一个
ext_info JSON字段,用于未来可能出现的临时需求。比如某天业务方说「我想给特征加个优先级」,直接往 ext_info 里塞,不用改表结构。 - 定期巡检废弃特征:写一个定时任务,每周扫描 status=2 且 updated_at 超过 90 天的特征,推送给 owner 确认是否可归档。避免注册表无限膨胀。