一、引言
在开发过程中,我们经常会遇到需要对数据库进行复杂查询的情况。DynamoDB 是亚马逊提供的一种 NoSQL 数据库,它有一些索引限制,不过通过使用全局二级索引(GSI)和本地二级索引(LSI),我们可以突破这些限制,满足复杂查询需求。接下来,我们就详细聊聊怎么用这两种索引来解决问题。
二、DynamoDB 索引基础
2.1 主键和排序键
在 DynamoDB 里,每个表都得有一个主键。主键可以是简单主键(只包含一个分区键),也可以是复合主键(包含分区键和排序键)。分区键就像是一个大箱子,数据会根据分区键的值被分到不同的箱子里;排序键呢,就是箱子里的数据排序的依据。
比如,我们有一个用户表,用用户 ID 作为分区键,注册时间作为排序键。这样,相同用户 ID 的记录会被放在一起,并且按照注册时间排序。
2.2 索引的作用
索引就像是书的目录,能让我们更快地找到想要的数据。在 DynamoDB 中,索引可以提高查询效率,让我们不用扫描整个表就能找到需要的数据。
三、全局二级索引(GSI)
3.1 什么是 GSI
全局二级索引(GSI)是一种独立于主表的索引,它有自己的主键和排序键。GSI 可以包含主表中的部分或全部属性,并且可以根据不同的查询需求来设计。
3.2 GSI 的使用场景
假设我们有一个商品表,主表的主键是商品 ID,排序键是商品名称。现在我们想要根据商品的价格进行查询,这时候就可以创建一个 GSI,把商品价格作为分区键,商品销量作为排序键。
下面是使用 Python 和 Boto3 库创建 GSI 的示例:
# 技术栈:Python + Boto3
import boto3
# 创建 DynamoDB 客户端
dynamodb = boto3.resource('dynamodb')
# 获取商品表
table = dynamodb.Table('ProductTable')
# 创建 GSI
response = table.update(
AttributeDefinitions=[
{
'AttributeName': 'Price',
'AttributeType': 'N'
},
{
'AttributeName': 'Sales',
'AttributeType': 'N'
}
],
GlobalSecondaryIndexUpdates=[
{
'Create': {
'IndexName': 'PriceSalesIndex',
'KeySchema': [
{
'AttributeName': 'Price',
'KeyType': 'HASH'
},
{
'AttributeName': 'Sales',
'KeyType': 'RANGE'
}
],
'Projection': {
'ProjectionType': 'ALL'
},
'ProvisionedThroughput': {
'ReadCapacityUnits': 10,
'WriteCapacityUnits': 10
}
}
}
]
)
print(response)
3.3 GSI 的优缺点
优点
- 灵活性高:可以根据不同的查询需求创建多个 GSI,而且 GSI 的主键和排序键可以和主表不同。
- 独立于主表:GSI 的读写操作不会影响主表的性能。
缺点
- 成本高:创建和维护 GSI 需要额外的存储和读写容量,会增加成本。
- 数据一致性问题:GSI 的更新是异步的,可能会出现数据不一致的情况。
3.4 使用 GSI 的注意事项
- 合理设计 GSI:根据实际的查询需求来设计 GSI 的主键和排序键,避免创建过多不必要的 GSI。
- 注意成本:在创建 GSI 时,要考虑存储和读写容量的成本,避免浪费资源。
四、本地二级索引(LSI)
4.1 什么是 LSI
本地二级索引(LSI)和主表共享分区键,但是有自己的排序键。LSI 只能在创建表的时候创建,不能在表创建后再添加。
4.2 LSI 的使用场景
还是以商品表为例,主表的主键是商品 ID,排序键是商品名称。现在我们想要根据商品的上架时间进行查询,就可以创建一个 LSI,把商品 ID 作为分区键,上架时间作为排序键。
下面是使用 Python 和 Boto3 库创建 LSI 的示例:
# 技术栈:Python + Boto3
import boto3
# 创建 DynamoDB 客户端
dynamodb = boto3.resource('dynamodb')
# 创建商品表并添加 LSI
table = dynamodb.create_table(
TableName='ProductTable',
KeySchema=[
{
'AttributeName': 'ProductID',
'KeyType': 'HASH'
},
{
'AttributeName': 'ProductName',
'KeyType': 'RANGE'
}
],
AttributeDefinitions=[
{
'AttributeName': 'ProductID',
'AttributeType': 'S'
},
{
'AttributeName': 'ProductName',
'AttributeType': 'S'
},
{
'AttributeName': '上架时间',
'AttributeType': 'S'
}
],
LocalSecondaryIndexes=[
{
'IndexName': '上架时间Index',
'KeySchema': [
{
'AttributeName': 'ProductID',
'KeyType': 'HASH'
},
{
'AttributeName': '上架时间',
'KeyType': 'RANGE'
}
],
'Projection': {
'ProjectionType': 'ALL'
}
}
],
ProvisionedThroughput={
'ReadCapacityUnits': 10,
'WriteCapacityUnits': 10
}
)
print(table.table_status)
3.3 LSI 的优缺点
优点
- 数据一致性好:LSI 和主表的更新是同步的,不会出现数据不一致的情况。
- 成本相对较低:LSI 和主表共享分区键,不需要额外的存储和读写容量。
缺点
- 灵活性低:LSI 只能在创建表的时候创建,不能在表创建后再添加。
- 排序键限制:LSI 的排序键必须是主表的属性。
3.4 使用 LSI 的注意事项
- 提前规划:由于 LSI 只能在创建表时创建,所以要提前规划好需要的 LSI。
- 排序键选择:选择合适的排序键,以满足查询需求。
五、复杂查询示例
5.1 结合 GSI 和 LSI 进行复杂查询
假设我们有一个订单表,主表的主键是订单 ID,排序键是订单日期。我们创建了一个 GSI,以客户 ID 作为分区键,订单金额作为排序键;同时创建了一个 LSI,以订单 ID 作为分区键,商品数量作为排序键。
现在我们想要查询某个客户的所有订单,并且按照订单金额从高到低排序,同时筛选出商品数量大于 10 的订单。
下面是使用 Python 和 Boto3 库进行查询的示例:
# 技术栈:Python + Boto3
import boto3
# 创建 DynamoDB 客户端
dynamodb = boto3.resource('dynamodb')
# 获取订单表
table = dynamodb.Table('OrderTable')
# 查询某个客户的所有订单,按照订单金额从高到低排序,筛选商品数量大于 10 的订单
response = table.query(
IndexName='CustomerOrderIndex', # GSI 名称
KeyConditionExpression='#customer_id = :customer_id',
ExpressionAttributeNames={
'#customer_id': 'CustomerID'
},
ExpressionAttributeValues={
':customer_id': '123'
},
ScanIndexForward=False, # 降序排序
FilterExpression='#商品数量 > :商品数量',
ExpressionAttributeNames={
'#商品数量': '商品数量'
},
ExpressionAttributeValues={
':商品数量': 10
}
)
for item in response['Items']:
print(item)
六、总结
通过使用全局二级索引(GSI)和本地二级索引(LSI),我们可以突破 DynamoDB 的索引限制,满足复杂查询需求。GSI 灵活性高,但成本也高,可能存在数据一致性问题;LSI 数据一致性好,成本相对较低,但灵活性低。在实际应用中,我们要根据具体的查询需求和业务场景,合理选择使用 GSI 和 LSI,并且注意它们的优缺点和使用注意事项。
评论