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 key | Giống bảng gốc | Có thể khác hoàn toàn |
| Sort key | Khác bảng gốc (bắt buộc) | Tùy chọn |
| Tạo sau khi bảng tồn tại | Không — chỉ khi tạo bảng | Có — bất kỳ lúc nào |
| Tính nhất quán đọc | Strongly consistent hoặc eventually consistent | Chỉ eventually consistent |
| Giới hạn storage per partition | 10 GB per partition key value | Không giới hạn theo partition |
| Throughput | Dùng chung với bảng gốc | Throughput độc lập (provisioned) hoặc on-demand |
| Số lượng tối đa | 5 per table | 20 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.
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ỳ"]
- Bảng gốc lưu trữ item với primary key (PK + SK). Mọi write đều đi qua đây trước.
- 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.
- 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.
- 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
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"]
- 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.
- 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.
- 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.
- Nếu một partition key value có thể vượt 10 GB — tránh LSI, dùng GSI.
- 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. |
| Projection | Tậ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 Partition | Partition 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 Consistency | Mô 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. |
Nhận xét
Đăng nhận xét