上一篇文章讲到了多维主键,这期就来详细讲一讲。 比如我要查找一个视频里面前30s的内容,里面有视频,图片,文字,图表,音频。
如何设计主键,让一次查询就可以知道所有的东西?
多维主键是什么?
"多维主键"并不是一个严格的数据库术语,而是工程口语化的叫法 ,用来描述把多个业务维度 同时编码进一个可排序、可前缀匹配、可范围扫描 的键(或主键),从而用"一次索引查找"就能完成多条件过滤、排序和路由。
简单一句话:把原本需要多个 WHERE 子句的过滤条件,压缩进一条键的前缀里。
1. 传统视角 vs 多维主键视角
场景 | 传统做法 | 多维主键做法 |
---|---|---|
查询最近 1 小时的文本 | WHERE ts >= now-1h AND modality='txt' |
键前缀 1722843000000:txt: 直接范围扫描 |
路由到热库 | 由业务代码或中间件判断 | 键前缀 202531: 自动落周库 |
保证唯一性 | 额外建联合唯一索引 | 键里自带 scene_hash ,天然唯一 |
2. 一条"多维主键"长什么样?
css
{时间戳}:{模态}:{语义ID}:{随机salt}
1722843000000:txt:c8f9e:7b3
- 时间戳 → 排序、冷热分层
- 模态 → 过滤、路由到表/分片
- 语义ID → 业务主键,保证唯一
- 随机salt → 防止热点写(可选)
3. 为什么叫"多维"?
因为它把时间维、模态维、业务维 等多个查询条件物理地打平到一条键的字节顺序里,数据库只需一次 B+ 树 / LSM-Tree 的前缀匹配就能完成原本需要多条索引才能做的事。
它们底层仍然是一条普通主键,但业务层把"多维"压缩进去,所以口语上就叫"多维主键"。
路线一:Elasticsearch / OpenSearch(最轻量、最通用)
- 键格式
_id = {ts_ms}:{modality}:{scene_hash}
例:1722843000000:txt:c8f9e
- 优势
- 天然支持前缀、范围、多条件复合查询。
- 分片(shard)即天然"路由",一条 DSL 即可同时命中"最近 1 小时 + txt|img"。
- 冷热分层用 Index Lifecycle Management (ILM) 自动迁移到冷节点。
- 开源 & 工业级
- ES/OpenSearch 8.x 单集群 PB 级、腾讯/阿里/滴滴/美团均在用。
- 注意
- 单分片建议 ≤ 50 GB;索引按周滚动即可保证 100 GB 上限。
路线二:Cassandra / ScyllaDB(高吞吐、去中心化)
-
键设计(PRIMARY KEY)
javaPRIMARY KEY ((year_month), ts_ms, modality, scene_hash)
- 分区键
year_month
保证时间切库。 - 聚簇列
ts_ms, modality
保证时间+模态顺序扫描。 scene_hash
保证唯一性。
- 分区键
-
优势
- 线性扩展、无单点、写吞吐百万级 QPS。
- 二级索引 / SASI 索引可额外按
modality
过滤。
-
开源 & 工业级
- Apple iCloud、Netflix、Instagram 均跑在 Cassandra;ScyllaDB 兼容协议,C++ 重写后性能 ×10。
-
注意
- 必须避免分区热点,可把
year_month
再细粒度化成year_month_day
。
- 必须避免分区热点,可把
路线三:TiDB / CockroachDB(兼容 MySQL 协议,HTAP)
-
键设计
javaCLUSTERED INDEX (ts_ms, modality, scene_hash)
-
聚簇索引天然按前缀排序 ,范围扫描
ts_ms BETWEEN ...
走索引不回表。 -
二级分区:
sqlPARTITION BY RANGE (ts_ms) ( PARTITION p_hot VALUES LESS THAN (unix_timestamp('2024-11-01')), PARTITION p_cold VALUES LESS THAN MAXVALUE )
-
-
优势
- MySQL 语义零成本迁移;支持在线扩缩容;冷热分区自动搬迁。
-
开源 & 工业级
- TiDB 在知乎、美团、Shopee 已跑数百节点;CockroachDB 在 Comcast、Bose 落地。
总结工业界怎么用?
- Elasticsearch →
_id = 1722843000000:txt:c8f9e
- Cassandra →
PRIMARY KEY ((ym), ts, modality, sid)
- TiDB →
CLUSTERED INDEX(ts, modality, sid)
- 搜索/日志/快速 PoC → Elasticsearch/OpenSearch
- 高并发写、去中心化、跨地域 → Cassandra/ScyllaDB
- 在线交易+分析混合、MySQL 生态 → TiDB/CockroachDB
以上三种均已开源,可直接落地。
多模态RAG,如何设计多维主键?
向量存储的条件
- 文本部分用 Sentence-BERT 得到 768 维向量 e_txt。
- 图像部分用 CLIP 得到 768 维向量 e_img(三张图取平均)。
- 表格部分按 markdown
- 文本同样用 Sentence-BERT 得到 e_tbl。
- 音频无独立向量,直接忽略或并入文本。
- 将三向量拼接后降维到 768 维,得到最终向量 e_slot。
- 把 (doc_id, e_slot, 完整 JSON 文档) 插入向量数据库的同一张表。
下面给出一种**面向向量数据库(Milvus / Qdrant / Weaviate / pgvector 均可)**的主键设计,使得 "查 30 秒窗口内所有模态" 仍然只需 一次范围查询 就能召回 e_slot 向量 以及 原始 JSON。
1 一行记录的主键(主键 = 逻辑时间轴)
text
{video_id}:{chunk_start_ms}:{modality_flag}:{uuid}
video_id
------ 8~12 字节定长,保证同一视频连续存储chunk_start_ms
------ 该 30 s 窗口的起始毫秒,左补零 10 位,字典序即时间序modality_flag
------ 1 字符:S
代表"slot",表示这一行是 融合后向量uuid
------ 4~6 字节随机,避免同一毫秒多行冲突
例子:
vid123abc:000000000:S:c8f9e
2 表的 schema(以 Milvus 为例)
字段名 | 类型 | 说明 |
---|---|---|
id | VARCHAR(64) | 主键,格式如上 |
video_id | VARCHAR(12) | 冗余字段,便于过滤 |
chunk_start_ms | INT64 | 起始毫秒 |
e_slot | FLOAT_VECTOR(768) | 降维后的统一向量 |
raw_json | JSON / VARCHAR(MAX) | 原始文档,含文本、图像 url、表格 markdown |
3 插入流程(离线预处理)
python
# 伪代码
for seg in video.chunks(30_000): # 30 s 步长
e_txt = sbert(seg.texts)
e_img = clip(seg.images).mean(axis=0)
e_tbl = sbert(seg.tables_md)
e_slot = pca( np.hstack([e_txt, e_img, e_tbl]) ) # 降到 768 维
row = {
"id" : f"{video_id}:{seg.start:010d}:S:{uuid4().hex[:6]}",
"video_id" : video_id,
"chunk_start_ms" : seg.start,
"e_slot" : e_slot,
"raw_json" : seg.to_json()
}
milvus.insert("video_slots", row)
4 一次查询拉回 30 s 窗口
python
# 想查视频 vid123abc 的 30--60 s 段
start = 30_000
end = 60_000
expr = f'video_id == "vid123abc" and 30000 <= chunk_start_ms < 60000'
results = milvus.query(
collection="video_slots",
filter=expr,
output_fields=["raw_json", "e_slot"]
)
# 结果即包含 e_slot 向量 + 完整 JSON
一句话总结
把 30 秒窗口 编码成 连续字典序前缀 (
vid123abc:000030000:S
→vid123abc:000060000:S
),向量数据库里 一次范围过滤 就能拿回 融合向量与原始多模态 JSON。