一、引言

在开发过程中,我们经常会遇到需要对数据库进行复杂查询的情况。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,并且注意它们的优缺点和使用注意事项。