DynamoDB本地二级索引 #

一、LSI概述 #

1.1 什么是LSI #

本地二级索引(Local Secondary Index)是一种与表共享分区键,但使用不同排序键的索引。

text
LSI特点:
├── 与表共享分区键
├── 使用不同的排序键
├── 创建表时定义
├── 支持强一致性读取
├── 共享表的容量
└── 最多5个/表

1.2 LSI vs GSI #

特性 LSI GSI
分区键 必须与表相同 可不同
排序键 必须不同 可选
创建时机 创建表时 任何时候
数量限制 5个/表 20个/表
一致性 强一致/最终一致 最终一致
容量 共享表容量 独立配置
项目大小 400KB限制 无限制

二、创建LSI #

2.1 创建表时定义LSI #

使用CLI:

bash
aws dynamodb create-table \
  --table-name Orders \
  --attribute-definitions \
    AttributeName=CustomerId,AttributeType=S \
    AttributeName=OrderId,AttributeType=S \
    AttributeName=OrderDate,AttributeType=S \
  --key-schema \
    AttributeName=CustomerId,KeyType=HASH \
    AttributeName=OrderId,KeyType=RANGE \
  --local-secondary-indexes \
    '[
      {
        "IndexName": "OrderDateIndex",
        "KeySchema": [
          {"AttributeName": "CustomerId", "KeyType": "HASH"},
          {"AttributeName": "OrderDate", "KeyType": "RANGE"}
        ],
        "Projection": {"ProjectionType": "ALL"}
      }
    ]' \
  --billing-mode PAY_PER_REQUEST

使用JavaScript SDK:

javascript
const { CreateTableCommand } = require('@aws-sdk/client-dynamodb');

const command = new CreateTableCommand({
  TableName: 'Orders',
  AttributeDefinitions: [
    { AttributeName: 'CustomerId', AttributeType: 'S' },
    { AttributeName: 'OrderId', AttributeType: 'S' },
    { AttributeName: 'OrderDate', AttributeType: 'S' },
    { AttributeName: 'TotalAmount', AttributeType: 'N' }
  ],
  KeySchema: [
    { AttributeName: 'CustomerId', KeyType: 'HASH' },
    { AttributeName: 'OrderId', KeyType: 'RANGE' }
  ],
  LocalSecondaryIndexes: [
    {
      IndexName: 'OrderDateIndex',
      KeySchema: [
        { AttributeName: 'CustomerId', KeyType: 'HASH' },
        { AttributeName: 'OrderDate', KeyType: 'RANGE' }
      ],
      Projection: {
        ProjectionType: 'ALL'
      }
    },
    {
      IndexName: 'TotalAmountIndex',
      KeySchema: [
        { AttributeName: 'CustomerId', KeyType: 'HASH' },
        { AttributeName: 'TotalAmount', KeyType: 'RANGE' }
      ],
      Projection: {
        ProjectionType: 'INCLUDE',
        NonKeyAttributes: ['OrderId', 'OrderDate', 'Status']
      }
    }
  ],
  BillingMode: 'PAY_PER_REQUEST'
});

2.2 LSI必须条件 #

text
创建LSI必须满足:
├── 表必须有复合主键(分区键+排序键)
├── LSI分区键必须与表相同
├── LSI排序键必须与表不同
├── 只能在创建表时定义
└── 排序键属性必须定义在AttributeDefinitions中

三、投影类型 #

3.1 KEYS_ONLY #

javascript
{
  IndexName: 'OrderDateIndex',
  KeySchema: [
    { AttributeName: 'CustomerId', KeyType: 'HASH' },
    { AttributeName: 'OrderDate', KeyType: 'RANGE' }
  ],
  Projection: {
    ProjectionType: 'KEYS_ONLY'
  }
}

3.2 INCLUDE #

javascript
{
  IndexName: 'OrderDateIndex',
  KeySchema: [
    { AttributeName: 'CustomerId', KeyType: 'HASH' },
    { AttributeName: 'OrderDate', KeyType: 'RANGE' }
  ],
  Projection: {
    ProjectionType: 'INCLUDE',
    NonKeyAttributes: ['OrderId', 'TotalAmount', 'Status']
  }
}

3.3 ALL #

javascript
{
  IndexName: 'OrderDateIndex',
  KeySchema: [
    { AttributeName: 'CustomerId', KeyType: 'HASH' },
    { AttributeName: 'OrderDate', KeyType: 'RANGE' }
  ],
  Projection: {
    ProjectionType: 'ALL'
  }
}

四、查询LSI #

4.1 基本查询 #

javascript
const { QueryCommand } = require('@aws-sdk/lib-dynamodb');

const response = await docClient.send(new QueryCommand({
  TableName: 'Orders',
  IndexName: 'OrderDateIndex',
  KeyConditionExpression: 'CustomerId = :cid AND OrderDate BETWEEN :start AND :end',
  ExpressionAttributeValues: {
    ':cid': 'customer001',
    ':start': '2024-01-01',
    ':end': '2024-12-31'
  }
}));

4.2 强一致性读取 #

javascript
const response = await docClient.send(new QueryCommand({
  TableName: 'Orders',
  IndexName: 'OrderDateIndex',
  KeyConditionExpression: 'CustomerId = :cid',
  ExpressionAttributeValues: {
    ':cid': 'customer001'
  },
  ConsistentRead: true  // LSI支持强一致性
}));

4.3 排序 #

javascript
// 按OrderDate升序
const response = await docClient.send(new QueryCommand({
  TableName: 'Orders',
  IndexName: 'OrderDateIndex',
  KeyConditionExpression: 'CustomerId = :cid',
  ExpressionAttributeValues: {
    ':cid': 'customer001'
  },
  ScanIndexForward: true  // 升序
}));

// 按TotalAmount降序
const response = await docClient.send(new QueryCommand({
  TableName: 'Orders',
  IndexName: 'TotalAmountIndex',
  KeyConditionExpression: 'CustomerId = :cid',
  ExpressionAttributeValues: {
    ':cid': 'customer001'
  },
  ScanIndexForward: false  // 降序
}));

五、LSI使用场景 #

5.1 多种排序需求 #

javascript
// 表设计
// PK: CustomerId
// SK: OrderId
// LSI1: OrderDate
// LSI2: TotalAmount

// 按订单ID查询(表主键)
await docClient.send(new QueryCommand({
  TableName: 'Orders',
  KeyConditionExpression: 'CustomerId = :cid AND OrderId = :oid'
}));

// 按日期排序查询(LSI1)
await docClient.send(new QueryCommand({
  TableName: 'Orders',
  IndexName: 'OrderDateIndex',
  KeyConditionExpression: 'CustomerId = :cid',
  ScanIndexForward: false  // 最新订单在前
}));

// 按金额排序查询(LSI2)
await docClient.send(new QueryCommand({
  TableName: 'Orders',
  IndexName: 'TotalAmountIndex',
  KeyConditionExpression: 'CustomerId = :cid',
  ScanIndexForward: false  // 最高金额在前
}));

5.2 时间序列数据 #

javascript
// 表设计
// PK: DeviceId
// SK: EventId
// LSI: Timestamp

// 按时间范围查询
const response = await docClient.send(new QueryCommand({
  TableName: 'Events',
  IndexName: 'TimestampIndex',
  KeyConditionExpression: 'DeviceId = :did AND Timestamp BETWEEN :start AND :end',
  ExpressionAttributeValues: {
    ':did': 'device001',
    ':start': '2024-01-01T00:00:00Z',
    ':end': '2024-01-01T23:59:59Z'
  }
}));

5.3 强一致性需求 #

javascript
// 需要强一致性的场景
const response = await docClient.send(new QueryCommand({
  TableName: 'Inventory',
  IndexName: 'ProductCodeIndex',
  KeyConditionExpression: 'WarehouseId = :wid AND ProductCode = :code',
  ExpressionAttributeValues: {
    ':wid': 'warehouse001',
    ':code': 'PROD001'
  },
  ConsistentRead: true  // 强一致性
}));

六、LSI限制 #

6.1 数量限制 #

text
限制:
├── 每表最多5个LSI
├── 只能在创建表时定义
├── 无法删除或修改
└── 排序键属性必须定义

6.2 大小限制 #

text
大小限制:
├── 索引键大小:最大2048字节
├── 每个分区键值对应的项目总大小:最大10GB
└── 单个项目大小:最大400KB(与表相同)

6.3 项目集合大小限制 #

text
项目集合:
├── 同一分区键下的所有项目
├── 包括表项目和所有LSI项目
├── 总大小不超过10GB
└── 超过限制会导致写入失败

七、LSI vs GSI选择 #

7.1 选择LSI的场景 #

text
选择LSI:
├── 需要强一致性读取
├── 同一分区键下需要多种排序
├── 项目大小可控(<400KB)
├── 分区键下数据量可控(<10GB)
└── 查询模式固定

7.2 选择GSI的场景 #

text
选择GSI:
├── 需要不同的分区键
├── 项目可能超过400KB
├── 分区键下数据量大
├── 需要灵活添加索引
└── 可以接受最终一致性

7.3 对比总结 #

场景 推荐
需要强一致性 LSI
需要不同分区键 GSI
项目可能超过400KB GSI
同分区键多种排序 LSI
需要灵活添加索引 GSI
数据量可控 LSI

八、最佳实践 #

8.1 LSI设计建议 #

text
设计建议:
├── 仅创建必要的LSI
├── 评估项目集合大小
├── 选择合适的投影类型
├── 考虑未来扩展需求
└── 监控索引使用情况

8.2 避免项目集合过大 #

javascript
// 不好的设计:可能导致项目集合过大
{
  PK: 'STATUS#ACTIVE',  // 低基数分区键
  SK: 'ORDER#001'
}

// 好的设计:分散数据
{
  PK: 'CUSTOMER#001',  // 高基数分区键
  SK: 'ORDER#001'
}

8.3 投影类型选择 #

text
投影类型选择:
├── KEYS_ONLY
│   ├── 仅需要回表查询
│   └── 存储成本敏感
├── INCLUDE
│   ├── 需要部分属性
│   └── 平衡存储和性能
└── ALL
    ├── 需要所有属性
    └── 避免回表查询

九、实际示例 #

9.1 订单系统LSI设计 #

javascript
// 创建表
const command = new CreateTableCommand({
  TableName: 'Orders',
  AttributeDefinitions: [
    { AttributeName: 'CustomerId', AttributeType: 'S' },
    { AttributeName: 'OrderId', AttributeType: 'S' },
    { AttributeName: 'OrderDate', AttributeType: 'S' },
    { AttributeName: 'TotalAmount', AttributeType: 'N' },
    { AttributeName: 'Status', AttributeType: 'S' }
  ],
  KeySchema: [
    { AttributeName: 'CustomerId', KeyType: 'HASH' },
    { AttributeName: 'OrderId', KeyType: 'RANGE' }
  ],
  LocalSecondaryIndexes: [
    {
      IndexName: 'OrderDateIndex',
      KeySchema: [
        { AttributeName: 'CustomerId', KeyType: 'HASH' },
        { AttributeName: 'OrderDate', KeyType: 'RANGE' }
      ],
      Projection: { ProjectionType: 'ALL' }
    },
    {
      IndexName: 'TotalAmountIndex',
      KeySchema: [
        { AttributeName: 'CustomerId', KeyType: 'HASH' },
        { AttributeName: 'TotalAmount', KeyType: 'RANGE' }
      ],
      Projection: { ProjectionType: 'INCLUDE', NonKeyAttributes: ['OrderId', 'OrderDate', 'Status'] }
    }
  ],
  BillingMode: 'PAY_PER_REQUEST'
});

// 查询客户订单(按日期)
async function getCustomerOrdersByDate(customerId, limit = 10) {
  return await docClient.send(new QueryCommand({
    TableName: 'Orders',
    IndexName: 'OrderDateIndex',
    KeyConditionExpression: 'CustomerId = :cid',
    ExpressionAttributeValues: { ':cid': customerId },
    ScanIndexForward: false,
    Limit: limit
  }));
}

// 查询客户订单(按金额)
async function getCustomerOrdersByAmount(customerId, minAmount) {
  return await docClient.send(new QueryCommand({
    TableName: 'Orders',
    IndexName: 'TotalAmountIndex',
    KeyConditionExpression: 'CustomerId = :cid AND TotalAmount >= :min',
    ExpressionAttributeValues: {
      ':cid': customerId,
      ':min': minAmount
    },
    ScanIndexForward: false
  }));
}

十、总结 #

LSI要点:

特性 说明
分区键 必须与表相同
排序键 必须与表不同
创建时机 创建表时
一致性 强一致/最终一致
容量 共享表容量
数量限制 5个/表
项目大小 400KB限制

下一步,让我们学习高级特性!

最后更新:2026-03-27