引言 🌟

在 AWS 云服务的众多产品中,DynamoDB 作为一款全托管的 NoSQL 数据库服务,以其高性能、可扩展性和灵活性赢得了众多开发者的青睐。然而,对于初学者来说,DynamoDB 的一些概念可能不太容易理解,尤其是二级索引这一重要特性。本文将深入浅出地讲解 DynamoDB 的二级索引,帮助你更好地设计和优化你的数据库结构。

DynamoDB 主键回顾 📝

在讨论二级索引之前,我们需要先回顾一下 DynamoDB 的主键概念。DynamoDB 表的主键可以是:

  1. 简单主键:仅包含一个分区键(Partition Key)

  2. 复合主键:包含分区键和排序键(Sort Key)

默认情况下,DynamoDB 只能通过主键高效地查询数据。例如,如果你的表使用 userId 作为分区键,orderId 作为排序键,你可以轻松地:

  • 查询特定用户的所有订单

  • 查询特定用户的特定订单

但如果你想按订单日期、金额或状态查询,就会面临挑战。这时,二级索引就派上用场了!🎯

什么是二级索引?🤔

二级索引是 DynamoDB 表的一个强大功能,它允许你使用主键之外的属性来查询数据。简单来说,二级索引就像是为你的数据创建了另一种视角,让你可以从不同的角度高效地访问数据。

DynamoDB 提供了两种类型的二级索引:

  1. 全局二级索引 (GSI) 🌍

  2. 本地二级索引 (LSI) 🏠

全局二级索引 (GSI) 详解 🌍

特点

  • 可以有与主表完全不同的分区键和排序键

  • 可以在表创建添加或删除

  • 有自己独立的预置吞吐量设置

  • 只支持最终一致性读取

  • 每个表最多可以有 20 个 GSI

实际案例

假设我们有一个电子商务应用的订单表,主键结构如下:

{
  "userId": "user123",
  "orderId": "order456",
  "orderDate": "2023-07-15",
  "orderAmount": 99.99,
  "orderStatus": "SHIPPED",
  "productId": "prod789",
  "shippingAddress": "123 Main St"
}

如果我们想按订单状态和日期查询,可以创建一个 GSI:

{
  "IndexName": "OrderStatus-OrderDate-index",
  "KeySchema": [
    { "AttributeName": "orderStatus", "KeyType": "HASH" },
    { "AttributeName": "orderDate", "KeyType": "RANGE" }
  ],
  "Projection": {
    "ProjectionType": "ALL"
  }
}

现在,我们可以轻松查询所有"已发货"的订单,并按日期排序:

response = table.query(
    IndexName='OrderStatus-OrderDate-index',
    KeyConditionExpression='orderStatus = :status AND orderDate BETWEEN :start AND :end',
    ExpressionAttributeValues={
        ':status': 'SHIPPED',
        ':start': '2023-07-01',
        ':end': '2023-07-31'
    }
)

这样,我们就能快速找到七月份所有已发货的订单!🚚

本地二级索引 (LSI) 详解 🏠

特点

  • 必须使用与主表相同的分区键,但可以有不同的排序键

  • 只能在表创建时定义,之后不能修改或删除

  • 与主表共享预置吞吐量

  • 支持强一致性读取和最终一致性读取

  • 每个表最多可以有 5 个 LSI

实际案例

继续使用上面的订单表例子,如果我们想查询特定用户的订单,并按金额排序,可以创建一个 LSI:

{
  "IndexName": "UserId-OrderAmount-index",
  "KeySchema": [
    { "AttributeName": "userId", "KeyType": "HASH" },
    { "AttributeName": "orderAmount", "KeyType": "RANGE" }
  ],
  "Projection": {
    "ProjectionType": "INCLUDE",
    "NonKeyAttributes": ["orderId", "orderDate", "orderStatus"]
  }
}

现在,我们可以查询用户的所有订单,按金额从高到低排序:

response = table.query(
    IndexName='UserId-OrderAmount-index',
    KeyConditionExpression='userId = :uid',
    ExpressionAttributeValues={
        ':uid': 'user123'
    },
    ScanIndexForward=False  # 降序排列
)

这样,我们就能快速找到用户的高价值订单!💰

索引投影 - 优化成本和性能的关键 📊

创建索引时,你需要决定将哪些属性"投影"到索引中。DynamoDB 提供了三种投影类型:

  1. KEYS_ONLY:只包含表和索引的键属性

  2. INCLUDE:包含键属性和你指定的其他非键属性

  3. ALL:包含表中的所有属性

投影类型会影响:

  • 存储成本:投影的属性越多,存储成本越高

  • 查询效率:如果需要的属性没有被投影,DynamoDB 需要额外查询主表

选择合适的投影类型是平衡成本和性能的关键!⚖️

# 创建一个只投影特定属性的 GSI
gsi = {
    'IndexName': 'ProductId-index',
    'KeySchema': [
        {'AttributeName': 'productId', 'KeyType': 'HASH'},
    ],
    'Projection': {
        'ProjectionType': 'INCLUDE',
        'NonKeyAttributes': ['orderDate', 'orderStatus']
    }
}

GSI vs LSI:如何选择?🤷‍♂️

选择使用 GSI 还是 LSI 取决于你的具体需求:

特性

GSI 🌍

LSI 🏠

分区键

可以不同

必须相同

创建时间

任何时候

仅表创建时

一致性

最终一致性

强/最终一致性

查询范围

全表

单个分区

数量限制

每表20个

每表5个

何时选择 GSI 🌍

  • 需要在表创建后灵活添加索引

  • 需要使用与主表不同的分区键

  • 需要查询跨多个分区的数据

  • 可以接受最终一致性

何时选择 LSI 🏠

  • 需要强一致性读取

  • 查询限制在单个分区内

  • 已经在表创建时规划好了所有查询模式

实际应用场景 💼

1. 社交媒体应用 👥

假设你正在构建一个社交媒体应用,用户可以发布帖子:

{
  "userId": "user123",
  "postId": "post456",
  "postTimestamp": "2023-07-15T14:30:00Z",
  "content": "Hello world!",
  "likes": 42,
  "tags": ["tech", "aws", "dynamodb"]
}

你可能需要的索引:

  • 用户时间线 GSI:按 userId(分区键) 和 postTimestamp(排序键) 查询用户的所有帖子

  • 热门帖子 GSI:按 likes(分区键) 查询点赞数最多的帖子

  • 标签搜索 GSI:按 tags(分区键) 查询带有特定标签的帖子

2. 游戏排行榜 🎮

对于一个在线游戏的排行榜系统:

{
  "gameId": "game123",
  "userId": "player456",
  "score": 9500,
  "level": 42,
  "lastPlayed": "2023-07-15T20:45:00Z"
}

可能的索引:

  • 游戏排行榜 GSI:按 gameId(分区键) 和 score(排序键) 查询特定游戏的排行榜

  • 活跃玩家 GSI:按 lastPlayed(分区键) 查询最近活跃的玩家

性能和成本优化技巧 💡

  1. 选择合适的投影类型:只投影你需要的属性,减少存储成本

  2. 使用稀疏索引:创建只包含部分项目的索引,减少索引大小

  3. 监控和调整:使用 CloudWatch 监控索引使用情况,及时调整

  4. 考虑写入放大:每次更新主表中的索引项时,也需要更新所有包含该项的索引

# 创建稀疏索引的例子
sparse_index = {
    'IndexName': 'PremiumCustomer-index',
    'KeySchema': [
        {'AttributeName': 'isPremium', 'KeyType': 'HASH'},
        {'AttributeName': 'userId', 'KeyType': 'RANGE'}
    ],
    'Projection': {
        'ProjectionType': 'ALL'
    }
}

在这个例子中,只有包含 isPremium 属性的项目才会出现在索引中,从而减少索引大小。

常见陷阱和解决方案 ⚠️

  1. GSI 吞吐量不足:GSI 有自己的吞吐量设置,确保为高流量索引分配足够的容量

  2. 项目大小限制:DynamoDB 项目大小限制为 400KB,包括所有索引

  3. 属性类型不匹配:确保索引键的数据类型与表中定义的一致

  4. 索引数量限制:每个表最多 20 个 GSI 和 5 个 LSI,提前规划好查询模式

结论 🎯

DynamoDB 的二级索引是一个强大的功能,它极大地扩展了 DynamoDB 的查询能力。通过合理设计和使用二级索引,你可以构建高性能、可扩展的应用程序,同时保持 NoSQL 数据库的灵活性。

记住,索引设计是数据建模的关键部分,应该在应用程序设计的早期就考虑进去。通过理解 GSI 和 LSI 的特点和限制,你可以为你的应用选择最合适的索引类型,优化查询性能,同时控制成本。

希望这篇文章能帮助你更好地理解和使用 DynamoDB 的二级索引!如果你有任何问题或想法,欢迎在评论区留言讨论。🙌


参考资源 📚