自增主键
什么是自增主键
自增主键是数据领域中一种常见的机制,是指用户在写入数据时无需指定该行的自增主键的值,由数据库内部按照自增的方式自动赋值,同时也保证了该值的全表唯一性。自增主键一般仅适用于数据类型为整型的主键。自增主键机制,能够降低合适场景下业务对主键的管理负担,简化业务程序。
VectorDB也支持自增主键机制,限制于UINT64
类型的主键。当通过API或者SDK调用写入操作时,客户无需指定自增主键的值,VectorDB内部会自动赋值,且保证全表唯一。
自增主键的实现原理
在传统的单机数据库中,对自增主键赋予一个全表唯一的值是非常容易实现的,但当该特性引入到分布式数据库时,会变得比较复杂,大体上业界有三种基本的实现方式,现简介如下:
1. 中心节点赋值
通过引入一个中心节点,由中心节点统一管理自增序列,每当客户端需要写入一行时,从中心节点申请,中心节点按照单调递增的方式来分配一个全表唯一的值。这种实现方式的好处在于简单,且确实可以按照单调递增的方式在全表范围内进行赋值。其劣势也是非常明显的,即客户端每次写入时都需要联系中心节点,可能会大大影响整体的写入效率。另外,引入这类新的中心节点角色可能会导致数据库架构变得更加复杂。
2. 中心节点不赋值但管理值的范围
这类方式可以认为是第1种方式的优化,在第1种方式中,中心节点每次只分配1个值,导致效率很低,在本方式中,优化为每次分配1批或者一个范围,比如从某个值开始的连续的1000个等。客户端或者分片的主节点可以提前向中心节点申请一个值范围,然后本地直接赋值使用,当使用完这批之后再次向中心节点申请一批。这种方式优化了分配效率,但是中心节点的值管理开销和架构复杂度并没有得到消除。这类方式还有一个特点是,分配出来的值的顺序,不再保证与数据写入的顺序完全一致了,也会出现值序列的空洞。
3. 不依赖中心节点
在分布式数据库中,一个表划分为多个分片,不论是哈希分片,还是全局有序分片,都能保证每个分片存在表内唯一的ID。那么可以基于这个唯一的分片ID来实现自增主键的分配。具体而言,将自增主键的分片任务下放到每个分片内部,分片内部能够保证严格递增地从一个整型序列中分配出一个分片内唯一的值,然后再将分片ID与这个整型值进行拼接,构建出一个全表唯一的值。这个方式的好处是整个自增值的分配工作被大大简化,效率极高,同时不再依赖一个中心化的自增值管理节点,数据库架构上不会引入额外的复杂度。这种方式的不足之处在于,不同分片之间的自增值不再能够构成一个完整的序列,也无法支持由用户来指定分配序列的起始值。
VectorDB采用了第三种实现方式。在莫愁内核层,会将全表唯一的分片ID,打到自增值(8字节或64位bits)的高24位bits,剩下低40位bits留给分片主副本做内部从1开始单调递增分配。在这种划分方式下,能够保证每个分片可以分配1万亿次,对于绝大部分业务而言,这应该足够了。若业务需要感知某个分片内部的自增序列值,可以将自增值的高24位掩码掉来获得。
自增主键的使用限制
自增主键在使用上存在一些限制,说明如下:
- 仅支持64位无符号整型值:即仅支持
UINT64
类型的主键。 - 不保证表内所有分区之间的自增值的连续性:根据上一小节的原理可以推理出这个限制。
- 一个批次写入的数据只会分配到同一个分片中:这样做的目的是优化自增主键场景的写入性能,如果一个批次写入1000条,若我们将其分散到所有的分片,整体性能不如作为一个完整的批处理请求写到同一个分片。
- 不支持由用户来指定分配序列的起始值。
使用示例
我们通过Python SDK展示一下自增主键的使用。
- 创建一个莫愁客户端对象。
import time
import json
import random
import pymochow
import logging
from pymochow.configuration import Configuration
from pymochow.auth.bce_credentials import BceCredentials
from pymochow.exception import ClientError, ServerError
from pymochow.model.schema import Schema, Field, SecondaryIndex, VectorIndex, HNSWParams
from pymochow.model.enum import FieldType, IndexType, MetricType, ServerErrCode
from pymochow.model.enum import TableState, IndexState
from pymochow.model.table import Partition, Row, AnnSearch, HNSWSearchParams
logging.basicConfig(filename='documentinsight.log', level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
if __name__ == "__main__":
account = '$您的账户名称'
api_key = '$您的账户API密钥'
endpoint = '$您的实例访问端点' # 例如:'http://127.0.0.1:5287'
db_name = 'DocumentInsight'
table_name = 'AutoIncrementTestTable'
# 根据配置创建一个MochowClient对象
config = Configuration(credentials=BceCredentials(account, api_key), endpoint=endpoint)
mochow_client = pymochow.MochowClient(config)
- 创建一个带自增主键的表,该表的DocId字段作为主键,类型为UINT64。
# 建库
db = mochow_client.create_database(db_name);
# 若库已存在,则直接使用下述代码获取一个库对象
# db = mochow_client.database(db_name)
# 建表,该表有2个分区
fields = []
fields.append(Field("DocId", FieldType.UINT64, primary_key=True, partition_key=True, auto_increment=True, not_null=True))
fields.append(Field("Title", FieldType.STRING, not_null=True))
table = db.create_table(
table_name=table_name,
replication=1,
partition=Partition(partition_num=2), # 2个分区
schema=Schema(fields=fields, indexes=[])
)
while True:
time.sleep(1)
table = db.describe_table(table_name)
if table.state == TableState.NORMAL:
break
- 插入几条数据,由于这几条数据是作为一个批次插入的,故会统一插入到某个分区中。由于DocId是自增主键,我们不需要为主键赋值,只需要给其它的字段赋值。
# 插入几条数据,无需指定主键,即DocId的值,该字段的值由数据库自动分配
# 也因为没有指定主键,这些数据作为一个批次会被随机写入该表的某个分区中
rows = [
Row(Title="LLM技术详解"),
Row(Title="深入浅出大语言模型技术"),
Row(Title="基于大模型的RAG应用开发实践"),
Row(Title="AI原生应用开发"),
Row(Title="向量数据库在RAG开发中的应用与实践")
]
table.insert(rows=rows)
- 我们分别查询两个分区的第一条数据(自增序列值为1),这两次查询的结果中预期会包含1条数据,代码如下:
# 标量查询,分别查询两个分区的第1条数据,两个查询会返回1条数据
primaryKey = {}
primaryKey['DocId'] = 1 # 查询第0个分区(分区ID=0)的第一条数据
results = table.query(primary_key=primaryKey)
print("Query result from tablet 0: {}".format(results))
primaryKey = {}
primaryKey['DocId'] = (1 << 40) + 1 # 查询第1个分区(分区ID=1)的第一条数据
results = table.query(primary_key=primaryKey)
print("Query result from tablet 1: {}".format(results))
执行完上述代码之后,我们会从控制台中观察到2行输出,如下所示:
Query result from tablet 0: {metadata:{content__length:u'35',content__type:u'application/json',request_id:u'0fd95c45-00df-4060-9b18-ce231f7a6883'},row:{},code:0,msg:u'Success'}
Query result from tablet 1: {metadata:{content__length:u'82',content__type:u'application/json',request_id:u'bb72d085-da96-437c-8f54-92c9c5c5e6b6'},row:{'DocId': 1099511627777, 'Title': 'LLM技术详解'},code:0,msg:u'Success'}
我们可以看到,从ID为1的分区中查询到了1条数据,其DocId值为1099511627777,该值的高24位是1,即分区ID,低40位也是1,是该分区的分配的第一个自增值。