# Manticore 中的 UUID：主数据库与 Manticore 统一 ID

从 Manticore Search 28.5.0 开始，UUID 可以作为文档 ID 使用。我们来看看如何摆脱与数字 ID 的映射、谁来生成 UUID，以及还保留哪些限制。

假设你在主数据库中的商品已经有了 ID `550e8400-e29b-41d4-a716-446655440000`。它会出现在事件、日志和 API 响应里。但当同一个商品写入 Manticore 时，应用还得再给它分配一个新的数字 ID。

在 Manticore Search 28.5.0 之前，文档 ID 是一个无符号 64 位整数。UUID 可以保存在单独的字符串属性里，但这并不会让它变成文档标识。对于 `UPDATE`、`REPLACE` 和 `DELETE`，仍然需要数字 `id`。

结果就是，必须在主数据库里的 UUID 和 Manticore 表中的数字 ID 之间维护对应关系。现在可以不再需要这层映射：Manticore 的 RT 表可以直接使用 UUID 作为文档 ID。

## 为什么第二个 ID 会添麻烦

ID 对应表当然不算特别复杂，但也只是在应用加载数据和执行搜索时如此。真正的麻烦会在文档变更时出现。

worker 收到带 UUID 的事件，先找到对应的数字 ID，然后才把更新发送到 Manticore。删除也按同样流程进行。如果对应关系记录丢失或过期，就可能更新错文档，或者变更根本不会进入 Manticore。

另一种方案是把 UUID 变成 64 位哈希。那样应用就必须自己考虑碰撞的可能性。也可以单独维护一个顺序计数器，但这又需要所有创建文档的进程之间协调一致。

数字 ID 还有一个细节问题，已经到了 API 层面。Manticore 内部把它当作 `uint64`，而 SQL 显示时把它当作有符号 `BIGINT`。因此，SQL 可能会把大于 `2^63-1` 的值以负数形式返回，客户端就得小心转换。UUID 以字符串传入和返回，所以不用担心有符号范围和溢出问题。

`id uuid` 从根上解决了这个问题：对象标识不再需要转换。同一个 UUID 会在主数据库、队列、Manticore、日志和外部 API 中统一使用。

## 什么是 UUID

[UUID](https://www.rfc-editor.org/rfc/rfc9562.html) 本质上是一个 128 位标识符，生成它不需要中央注册中心。文本形式通常由 36 个字符组成：32 个十六进制数字和 4 个连字符。

```text
550e8400-e29b-41d4-a716-446655440000
```

UUID 内置了版本和 `variant`。版本决定其余比特的组织方式。UUIDv4 基于随机或伪随机数据构造。UUIDv7 包含时间信息，并保持标识符的时间顺序。UUIDv8 中其余比特的布局由具体实现决定。

没有中央注册，就可以在写入共享数据库之前分配 ID。比如，两个互不依赖的服务可以并行创建对象，而某个连不上 LTE 的移动客户端也能在离线状态下准备数据。同步后，对象仍会保留同一个标识符。

不过，UUID 不能被视为绝对唯一的保证。实际唯一性取决于所选生成器是否正确。还要注意，UUID 不是密钥，不能替代密码、token 或权限校验。

## Manticore 28.5.0 新增了什么

现在可以为 RT 表显式指定文档 ID 类型：

```sql
CREATE TABLE products_uuid (
    id uuid,
    title text,
    sku string,
    price int
);

INSERT INTO products_uuid (id, title, sku, price)
VALUES (
    '550e8400-e29b-41d4-a716-446655440000',
    'Mechanical keyboard',
    'KB-001',
    149
);
```

插入后，这个 UUID 也可以用于等值和 `IN` 过滤，以及 `UPDATE`、`REPLACE` 和 `DELETE`。SQL 会把 ID 作为字符串返回。通过 JSON API 也可以传入显式 UUID 或让 Manticore 自动生成 ID；更详细的请求和响应我们会在下一篇文章里讲。

如果再次 `INSERT` 一个已经存在的 UUID，Manticore 会像以前对数字 id 一样拒绝它。要按 ID 覆盖文档，请使用 `REPLACE`。

需要注意的是，`uuid` 类型只适用于文档 ID。普通用户属性不能声明为 `uuid` 类型。

## 谁来生成 UUID

有两种方式：

| 文档写入方式 | 谁生成 ID | Manticore 的行为 |
|---|---|---|
| 已传入 `id` 字段 | 主数据库、客户端库或系统中的其他组件 | 检查格式、版本和 `variant`，然后将 UUID 以小写保存 |
| 未提供 `id` 字段 | Manticore | 生成 UUIDv8，并在其中编码内部数字 auto-ID |

如果 UUID 已经由应用生成，就不需要为 Manticore 特别调整。格式保持常规的五段式 `8-4-4-4-12`。版本可以是 v1 到 v8 中的任意一种，而 `variant` 位置必须是 `8`、`9`、`a` 或 `b`。大小写无关紧要：UUID 可以大写传入，但 Manticore 会以小写保存。

到这里校验就结束了：Manticore 会检查 UUID 格式，但不会检查生成质量。UUIDv4 的随机性和 UUIDv7 的时间部分是否正确，由客户端库负责。

服务端生成则不同。如果没有传入 `id` 字段，Manticore 会生成自有结构的 UUIDv8，并在其中编码内部数字 auto-ID。这既不是随机 UUIDv4，也不是秘密值。

应用生成的 UUIDv8，Manticore 会按原样校验并保存；服务端不会按自己的结构重新构造它。

> 技术细节。对外暴露的仍然是完整 UUID：Manticore 不会把它替换成 64 位哈希，也不要求应用保存对应表。UUID 在引擎内部的具体结构只是实现细节，不影响对外契约。

## 适用场景

UUID 可以作为普通 RT 表、带 `engine='columnar'` 的 RT 表，以及复制集群中的表的文档 ID。使用这类 ID 时，可以进行精确搜索、`IN` 过滤，以及常规文档操作：`INSERT`、`REPLACE`、`UPDATE` 和 `DELETE`。

需要注意这些限制：

- `uuid` 类型不适用于普通属性：Manticore 会拒绝类似 `guid uuid` 的声明；
- 在 plain、percolate/PQ 和 shard 表中，UUID 不能作为文档 ID 使用；
- 现有表不能通过 `ALTER TABLE` 从数字 ID 切换到 UUID，或反过来；
- 不支持范围条件 `<`、`<=`、`>` 和 `>=`，以及这类 ID 的算术运算；
- 文档 ID 不能通过 `UPDATE` 修改，这对 UUID 和数字 ID 都一样；
- 要自动生成，必须省略 `id` 字段：这里的值 `0` 不会被当作特殊标记。

迁移旧代码时很容易忘记这一点。对于数字 RT 表，`0` 可能表示“自动创建 ID”。在 UUID 表里，`id` 字段只需不传即可。

UUIDv7 也不会让 `id` 适合按时间过滤。它可以作为外部标识符，但 Manticore 目前还不支持 `id > ...` 这类查询。

## 总结

如果 UUID 已经存在于主数据库中，现在可以随文档一起传给 Manticore。如果文档先出现在 Manticore 中，就不要指定 `id`，然后从响应里取回生成的 UUID。

之后在 `UPDATE`、`REPLACE` 和 `DELETE` 中继续使用同一个 UUID 就行了。无需再给文档分配单独的数字 ID，也不必维护两个标识符之间的对应关系。

完整契约和当前限制都收录在文档中：[UUID document IDs](https://manual.manticoresearch.com/dev/Creating_a_table/Data_types#UUID-document-IDs)。UUID 作为文档 ID 的支持已在 [Manticore Search 28.5.0](/blog/manticore-search-28-5-0/) 中加入。

下一篇文章，也就是 [UUID 作为文档 ID 的实战指南](/blog/uuid-document-ids-cookbook/)，会一步一步演示如何通过 SQL 和 JSON 指定并生成 ID、搜索、更新、替换和删除文档，以及如何处理错误。
