LSI vs GSI trong DynamoDB: Khi Nào Dùng Index Nào?

Bạn đã thiết kế xong bảng DynamoDB với primary key hoàn hảo, rồi product manager yêu cầu thêm một màn hình tìm kiếm theo thuộc tính hoàn toàn khác — đây là lúc Local Secondary Index và Global Secondary Index xuất hiện, và cũng là lúc nhiều team chọn sai loại index rồi phải migration đau đớn sau này.

TL;DR: LSI vs GSI trong DynamoDB

Tiêu chíLSI (Local Secondary Index)GSI (Global Secondary Index)
Partition keyGiống bảng gốcCó thể khác hoàn toàn
Sort keyKhác bảng gốc (bắt buộc)Tùy chọn
Tạo sau khi bảng tồn tạiKhông — chỉ khi tạo bảngCó — bất kỳ lúc nào
Tính nhất quán đọcStrongly consistent hoặc eventually consistentChỉ eventually consistent
Giới hạn storage per partition10 GB per partition key valueKhông giới hạn theo partition
ThroughputDùng chung với bảng gốcThroughput độc lập (provisioned) hoặc on-demand
Số lượng tối đa5 per table20 per table (soft limit)

Cơ Chế Hoạt Động của Secondary Index trong DynamoDB

DynamoDB lưu trữ dữ liệu theo partition — mỗi item được định vị bởi partition key (hash key) và sort key (range key). Khi bạn query, DynamoDB cần biết chính xác partition nào để đọc. Nếu bạn muốn tìm theo một thuộc tính không phải partition key, DynamoDB phải scan toàn bộ bảng — tốn kém và chậm.

Secondary index giải quyết vấn đề này bằng cách duy trì một cấu trúc dữ liệu song song, được sắp xếp theo key schema khác. DynamoDB tự động đồng bộ index khi item trong bảng gốc thay đổi — bạn không cần quản lý thủ công.

LSI và GSI đều là secondary index, nhưng cơ chế lưu trữ và phạm vi query của chúng khác nhau căn bản.

graph TD Write["Item Write"] --> Table["Bảng Gốc
PK: userId / SK: createdAt"] Table --> LSI["LSI: UserStatusIndex
PK: userId / SK: status
Cùng partition storage"] Table --> GSI["GSI: StatusCreatedAtIndex
PK: status / SK: createdAt
Partition storage độc lập"] LSI --> LSINote["Chỉ query được
khi biết userId"] GSI --> GSINote["Query được theo
status bất kỳ"]
  1. Bảng gốc lưu trữ item với primary key (PK + SK). Mọi write đều đi qua đây trước.
  2. LSI được lưu cùng partition với bảng gốc — partition key giống hệt, chỉ sort key khác. Query LSI vẫn phải chỉ định đúng partition key.
  3. GSI là một bảng phân tán độc lập bên trong DynamoDB. Nó có partition key riêng, có thể phân tán dữ liệu hoàn toàn khác so với bảng gốc.
  4. Khi bạn write một item, DynamoDB tự động propagate thay đổi sang tất cả index liên quan — đây là lý do GSI chỉ hỗ trợ eventual consistency.

LSI — Local Secondary Index: Truy Vấn Linh Hoạt Trong Cùng Partition

LSI cho phép bạn query các item trong cùng một partition key với sort key khác. Nếu bảng của bạn có PK là userId và SK là createdAt, bạn có thể tạo LSI với SK là status để query tất cả orders của một user theo trạng thái — nhưng vẫn phải biết userId.

LSI giống như việc có nhiều ngăn kéo khác nhau trong cùng một tủ hồ sơ — bạn vẫn phải mở đúng tủ (partition key), nhưng có thể tìm theo nhiều cách sắp xếp khác nhau bên trong.

Điểm quan trọng nhất về LSI: chỉ tạo được khi tạo bảng. Không có cách nào thêm LSI vào bảng đang tồn tại. Nếu bạn quên điều này và cần LSI sau khi bảng đã có dữ liệu, bạn phải tạo bảng mới và migrate toàn bộ data.

LSI cũng chia sẻ giới hạn 10 GB storage per partition key value với bảng gốc. Nếu một partition key value có hơn 10 GB data (tính cả tất cả LSI), DynamoDB sẽ từ chối write mới vào partition đó.

Tạo Bảng với LSI

🔽 Click để xem lệnh tạo bảng với LSI
aws dynamodb create-table \
  --table-name Orders \
  --attribute-definitions \
    AttributeName=userId,AttributeType=S \
    AttributeName=createdAt,AttributeType=S \
    AttributeName=status,AttributeType=S \
  --key-schema \
    AttributeName=userId,KeyType=HASH \
    AttributeName=createdAt,KeyType=RANGE \
  --local-secondary-indexes '[{
    "IndexName": "UserStatusIndex",
    "KeySchema": [
      {"AttributeName": "userId", "KeyType": "HASH"},
      {"AttributeName": "status", "KeyType": "RANGE"}
    ],
    "Projection": {
      "ProjectionType": "INCLUDE",
      "NonKeyAttributes": ["orderId", "totalAmount"]
    }
  }]' \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1

Query sử dụng LSI — vẫn phải cung cấp partition key:

aws dynamodb query \
  --table-name Orders \
  --index-name UserStatusIndex \
  --key-condition-expression 'userId = :uid AND #s = :status' \
  --expression-attribute-names '{"#s": "status"}' \
  --expression-attribute-values '{
    ":uid": {"S": "user-123"},
    ":status": {"S": "PENDING"}
  }' \
  --region us-east-1

GSI — Global Secondary Index: Truy Vấn Không Giới Hạn Partition

GSI cho phép bạn query theo bất kỳ thuộc tính nào làm partition key mới. Không cần biết partition key của bảng gốc. Đây là công cụ chính để hỗ trợ các access pattern đa dạng trong DynamoDB.

Không giống LSI, GSI có thể thêm vào bảng đang tồn tại bất kỳ lúc nào. Tuy nhiên, sau khi tạo, DynamoDB cần thời gian backfill index từ dữ liệu hiện có — bảng vẫn hoạt động bình thường trong quá trình này.

Với provisioned capacity, GSI có throughput độc lập với bảng gốc. Đây vừa là ưu điểm (có thể scale riêng) vừa là nguồn gốc của một lỗi phổ biến: throttling xảy ra ở GSI trong khi bảng gốc vẫn còn capacity dư thừa.

Thêm GSI vào Bảng Đang Tồn Tại

aws dynamodb update-table \
  --table-name Orders \
  --attribute-definitions \
    AttributeName=status,AttributeType=S \
    AttributeName=createdAt,AttributeType=S \
  --global-secondary-index-updates '[{
    "Create": {
      "IndexName": "StatusCreatedAtIndex",
      "KeySchema": [
        {"AttributeName": "status", "KeyType": "HASH"},
        {"AttributeName": "createdAt", "KeyType": "RANGE"}
      ],
      "Projection": {
        "ProjectionType": "ALL"
      }
    }
  }]' \
  --region us-east-1

Lưu ý: Lệnh trên không bao gồm ProvisionedThroughput vì bảng Orders đã được tạo với PAY_PER_REQUEST. Nếu bảng của bạn dùng PROVISIONED mode, bạn cần thêm block ProvisionedThroughput bên trong phần Create của GSI.

Kiểm tra trạng thái backfill của GSI:

aws dynamodb describe-table \
  --table-name Orders \
  --query 'Table.GlobalSecondaryIndexes[*].{Name:IndexName,Status:IndexStatus,Backfilling:Backfilling}' \
  --region us-east-1

Query sử dụng GSI — không cần biết userId của bảng gốc:

aws dynamodb query \
  --table-name Orders \
  --index-name StatusCreatedAtIndex \
  --key-condition-expression '#s = :status AND createdAt BETWEEN :start AND :end' \
  --expression-attribute-names '{"#s": "status"}' \
  --expression-attribute-values '{
    ":status": {"S": "PENDING"},
    ":start": {"S": "2024-01-01T00:00:00Z"},
    ":end": {"S": "2024-01-31T23:59:59Z"}
  }' \
  --region us-east-1

Khi Nào Dùng LSI, Khi Nào Dùng GSI

graph TD Start(["Cần query theo
thuộc tính mới?"]) Start --> Q1{"Access pattern có
luôn bao gồm
partition key gốc?"} Q1 -->|Không| UseGSI["Dùng GSI"] Q1 -->|Có| Q2{"Bảng đã tồn tại?"} Q2 -->|Có| UseGSI2["Dùng GSI
(LSI không thể thêm sau)"] Q2 -->|Chưa| Q3{"Cần strongly
consistent reads?"} Q3 -->|Có| UseLSI["Cân nhắc LSI"] Q3 -->|Không| Q4{"Partition có thể
vượt 10 GB?"} Q4 -->|Có| UseGSI3["Dùng GSI"] Q4 -->|Không| Default["GSI là lựa chọn
mặc định an toàn hơn"]
  1. Câu hỏi đầu tiên: access pattern có luôn luôn bao gồm partition key của bảng gốc không? Nếu không — chỉ GSI mới giải quyết được.
  2. Nếu có, câu hỏi tiếp theo: bảng đã tồn tại chưa? LSI không thể thêm sau khi tạo bảng.
  3. Nếu bảng chưa tồn tại và cần strongly consistent reads — LSI là lựa chọn duy nhất hỗ trợ điều này.
  4. Nếu một partition key value có thể vượt 10 GB — tránh LSI, dùng GSI.
  5. Trong hầu hết các trường hợp còn lại, GSI linh hoạt hơn và là lựa chọn mặc định an toàn hơn.

Projection: Đừng Bỏ Qua Chi Tiết Này

Cả LSI và GSI đều yêu cầu bạn chỉ định ProjectionType — quyết định thuộc tính nào được copy sang index:

  • KEYS_ONLY: Chỉ lưu primary key của bảng gốc và key của index. Query trả về ít data nhất, nhưng nếu cần thêm thuộc tính, DynamoDB phải fetch lại từ bảng gốc (tốn thêm RCU).
  • INCLUDE: Lưu key + các thuộc tính bạn chỉ định. Cân bằng giữa storage và fetch cost.
  • ALL: Copy toàn bộ item sang index. Tiện nhất nhưng tốn storage và WCU nhất — mỗi write vào bảng gốc cũng tốn WCU để cập nhật index.

Chọn ALL cho mọi GSI là một trong những nguyên nhân phổ biến khiến chi phí DynamoDB tăng đột biến mà không rõ lý do.

Bẫy Thực Tế: Khi GSI Throttle Nhưng Bảng Gốc Vẫn Ổn

Đây là tình huống gây nhầm lẫn nhất khi vận hành DynamoDB với provisioned capacity. Alarm báo throttle, bạn vào console kiểm tra bảng — consumed capacity vẫn thấp hơn provisioned. Nhưng lỗi ProvisionedThroughputExceededException vẫn xuất hiện trong logs.

Nguyên nhân: throttle đang xảy ra ở GSI, không phải bảng gốc. GSI có throughput riêng biệt. Nếu bạn set GSI với 5 WCU nhưng write pattern tạo ra hot partition trên GSI — ví dụ tất cả orders mới đều có status = PENDING — GSI partition đó bị throttle trong khi bảng gốc vẫn còn capacity.

Kiểm tra consumed capacity của từng GSI riêng lẻ:

aws cloudwatch get-metric-statistics \
  --namespace AWS/DynamoDB \
  --metric-name ConsumedWriteCapacityUnits \
  --dimensions \
    Name=TableName,Value=Orders \
    Name=GlobalSecondaryIndexName,Value=StatusCreatedAtIndex \
  --start-time 2024-01-15T00:00:00Z \
  --end-time 2024-01-15T01:00:00Z \
  --period 60 \
  --statistics Sum \
  --region us-east-1

Nếu GSI đang bị throttle do hot partition (ví dụ: quá nhiều item có cùng GSI partition key value), tăng provisioned capacity không giải quyết được gốc rễ — bạn cần xem lại key design để phân tán data đều hơn.

IAM: Kiểm Soát Truy Cập Index

Query trên GSI hoặc LSI yêu cầu permission dynamodb:Query trên resource của index, không phải chỉ bảng gốc. Nhiều policy thiếu điều này và gây lỗi AccessDeniedException khó debug.

🔽 Click để xem IAM policy mẫu
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "QueryOrdersTable",
      "Effect": "Allow",
      "Action": [
        "dynamodb:Query",
        "dynamodb:GetItem"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:123456789012:table/Orders",
        "arn:aws:dynamodb:us-east-1:123456789012:table/Orders/index/StatusCreatedAtIndex",
        "arn:aws:dynamodb:us-east-1:123456789012:table/Orders/index/UserStatusIndex"
      ]
    }
  ]
}

Wrap-Up: LSI vs GSI trong DynamoDB — Chọn Đúng Từ Đầu

Quyết định giữa LSI và GSI thực ra không phức tạp nếu bạn hiểu rõ hai ràng buộc cốt lõi: LSI không thể thêm sau khi tạo bảng, và LSI luôn yêu cầu partition key của bảng gốc. Ngoài hai điểm đó, GSI linh hoạt hơn trong hầu hết mọi chiều.

Nếu bạn đang thiết kế bảng mới, hãy lập danh sách tất cả access pattern trước khi tạo bảng — đây là cách duy nhất để không bỏ lỡ LSI cần thiết. Với GSI, bạn có thể thêm sau, nhưng key design kém vẫn sẽ gây throttle và chi phí cao.

Tài liệu tham khảo chính thức: AWS DynamoDB Secondary Indexes.

Glossary

Thuật ngữGiải thích
Partition Key (Hash Key)Thuộc tính xác định partition vật lý nơi item được lưu. DynamoDB hash giá trị này để phân tán data.
Sort Key (Range Key)Thuộc tính thứ hai trong composite primary key, dùng để sắp xếp và range query trong cùng partition.
ProjectionTập hợp thuộc tính được copy từ bảng gốc sang index. Ảnh hưởng trực tiếp đến storage cost và RCU khi query.
Hot PartitionPartition nhận lượng traffic không cân đối so với các partition khác, dẫn đến throttling dù tổng capacity vẫn còn.
Eventual ConsistencyMô hình đọc trong đó dữ liệu có thể chưa phản ánh write gần nhất — GSI luôn hoạt động theo mô hình này.

Related Posts

Nhận xét

Bài đăng phổ biến từ blog này

EC2 Không Có Internet Trong Custom VPC: Cách Gắn Internet Gateway và Cập Nhật Route Table

RDS Multi-AZ: Lợi Ích Thực Sự Là Gì và Khi Nào Nên Bật?

Lỗi CORS trên API Gateway: Cách bật CORS và Lambda phải trả về header gì