---
title: "Thiết kế database nhiều shard - shard key, hot key, lookup index và reshard"
description: "Khóa học về chia database ra nhiều shard cho app mà Postgres của nó không còn ghi kịp, xoay quanh câu hỏi: mỗi lệnh đọc và lệnh ghi phải tới database nào. Bạn sẽ biết khi nào chưa cần chia, chọn được shard key, xử lý hot key, trả lời truy vấn không có shard key và chuyển dữ liệu sang database khác khi app vẫn ghi."
canonical: "https://200lab.io/courses/thiet-ke-database-nhieu-shard-shard-key-hot-key-lookup-index-va-reshard"
type: "course"
course_type: "course"
learning_type: "free_form"
published: "2026-10-10T05:00:11Z"
students: 0
---

## Khóa học này mang lại gì cho bạn

App bạn xây cùng agent chạy trên một Postgres, và database nhận mọi lệnh ghi của app được gọi là primary. Khi app lớn dần, team thêm replica, tức các bản sao nhận bớt lệnh đọc, và partition các bảng lớn, tức chia mỗi bảng thành nhiều phần ngay trên cùng máy. Rồi tới lúc primary bận gần như suốt giờ cao điểm, hoặc disk sắp đầy, và agent đề xuất sharding: chia các dòng của một bảng ra nhiều database, mỗi database giữ một phần gọi là shard. Bảy kỹ năng dưới đây giúp bạn quyết định đã nên chia hay chưa, chia theo cột nào, và hỏi agent số liệu nào trước khi đồng ý với đề xuất của nó.

- **Biết khi nào chưa cần chia database** — Bạn nói được vì sao replica và partition không thêm máy nhận lệnh ghi, rồi từ bảng tỉ lệ lệnh ghi theo nhóm bảng, bạn nhận ra lúc chỉ cần tách theo chức năng, tức chuyển một nhóm bảng ít join với phần còn lại sang database riêng mà không bảng nào phải chia theo dòng.
- **Chọn shard key theo truy vấn chính** — Shard key là cột quyết định một dòng nằm ở shard nào. Bạn chọn cột mà truy vấn chính lọc theo và làm lệnh ghi trải đều giữa các shard, tính được bao nhiêu phần trăm truy vấn sẽ phải hỏi mọi shard, và chia các bảng được ghi chung trong một transaction theo cùng shard key, để đơn, dòng hàng và thanh toán của một shop nằm chung một shard.
- **Xử lý hot key** — Khi CPU của database thấp mà p99 (thời gian phản hồi mà 99% request không vượt quá) vẫn cao, bạn nhận ra một hot key, tức một key như một sản phẩm hay một shop nhận phần lớn lệnh ghi. Rồi bạn chọn cách xử lý hợp với loại dữ liệu, vì cách dùng được cho lượt xem chưa chắc dùng được cho tồn kho.
- **Tìm đúng database cho từng key** — Bạn so sánh các cách app tìm ra database cho một key theo lượng dữ liệu phải chép khi thêm một database, và hiểu vì sao bảng thư mục, tức bảng ghi mỗi nhóm key đang nằm ở database nào, cho bạn chuyển từng nhóm key sang database mới thay vì chép lại gần hết dữ liệu. Bạn cũng chọn được cách sinh ID để hai shard không cấp trùng một mã đơn.
- **Trả lời truy vấn không có shard key** — Với màn hình tìm đơn theo số điện thoại, hay đường dẫn của shop không được trùng giữa các shard, bạn chọn giữa hỏi mọi shard, lookup index (bảng phụ cho biết dữ liệu của một giá trị nằm ở shard nào) và một bảng toàn cục, và nói được mỗi cách được gì, mất gì.
- **Chuyển dữ liệu khi app vẫn ghi** — Bạn giải thích được vì sao chép một lần rồi đổi nơi ghi có thể làm mất dữ liệu mà không để lại dòng lỗi nào, và vì sao app vẫn có thể ghi vào database cũ sau khi dữ liệu đã chuyển đi nếu không có gì chặn. Khi một database đầy nhanh hơn các database khác, bạn chọn được phần dữ liệu nào chuyển trước.
- **Hỏi agent đúng số liệu** — Trước khi đồng ý với một đề xuất sharding, bạn hỏi agent nhóm bảng nào chiếm bao nhiêu phần trăm lệnh ghi, bao nhiêu phần trăm truy vấn sẽ phải hỏi mọi shard, thêm một database thì phải chép bao nhiêu dữ liệu, và lúc chuyển nơi ghi phải dừng ghi bao nhiêu giây.

Khóa học dùng Postgres làm ví dụ và không phụ thuộc vào công cụ sharding nào: code minh họa là vài đoạn TypeScript và SQL ngắn, còn lệnh, cấu hình và script nằm trong demo kit để agent của bạn chạy.

## Khóa học cung cấp những nền tảng nào

Cả khóa học xoay quanh một câu hỏi: khi dữ liệu đã nằm trên nhiều database, mỗi lệnh đọc và lệnh ghi phải tới database nào, và hệ thống có biết chắc chắn điều đó không? Một trang phải hỏi cả tám database mới có kết quả, một shard nhận gần hết lệnh ghi mới, hay một đơn đã thanh toán bị ghi vào database cũ sau khi dữ liệu đã chuyển đi đều là những lần hệ thống trả lời sai câu hỏi đó.

Mỗi bài bắt đầu từ một tín hiệu bạn tự thấy được, như một biểu đồ trên dashboard, một dòng log, kết quả load test (chạy tải giả lập để đo hệ thống chịu được bao nhiêu) hay một đề xuất của agent. Sau đó bài chỉ ra lệnh đọc và lệnh ghi đi tới những database nào, bao nhiêu truy vấn phải hỏi mọi shard hay bao nhiêu dữ liệu phải chép, cách sửa có tradeoff gì, và khi hệ thống còn ở quy mô nào thì bạn chưa cần tới cơ chế của bài.

Bài viết [Herding elephants: Lessons learned from sharding Postgres at Notion](https://www.notion.com/blog/sharding-postgres-at-notion) kể lại cách Notion chia Postgres theo workspace thành **480** nhóm key cố định trên **32** database, đọc kèm được với phần chọn shard key và bảng thư mục. Bài [The growing pains of database architecture](https://www.figma.com/blog/how-figma-scaled-to-multiple-databases/) cho thấy Figma dời từng nhóm bảng sang database riêng trước khi chia bảng theo dòng. Bài [Shard Balancing: Moving Shops Confidently with Zero-Downtime at Terabyte-scale](https://shopify.engineering/mysql-database-shard-balancing-terabyte-scale) mô tả cách Shopify dời từng shop sang database khác trong lúc app vẫn chạy.

Năm nhóm bài dưới đây đi theo bậc thang scale, tức các nấc mà database của một app đi qua khi lớn dần: từ một Postgres có replica và partition, qua tách theo chức năng, tới nhiều shard phải chuyển dữ liệu định kỳ. Bài nền về partition trên một máy dùng chung với một khóa System Design khác và đứng ngay trước bài chọn shard key. Ba bài tình huống ghép nhiều cơ chế vào một sự cố trọn vẹn, có số đo trước và sau khi sửa.

### 1. Khi nào chưa cần chia database, và partition trên một máy giúp được tới đâu

Disk của primary bận khoảng **90%** thời gian trong giờ cao điểm, nên agent đề xuất chia cả database **1,6 TB** của sàn thương mại điện tử ra **8 shard** theo `shop_id`. Bảng tỉ lệ lệnh ghi theo nhóm bảng lại cho thấy nhóm chat giữa người mua và người bán nhận **62%** lệnh ghi và giữ **1,1 TB**, trong khi chỉ nối với phần còn lại qua một join và một transaction. Chỉ cần chuyển riêng nhóm chat sang database `chat` thì database dùng chung, nơi mọi chức năng còn lại vẫn ghi vào, còn nhận khoảng **38%** lệnh ghi và disk bận khoảng **35%**, mà chưa phải chọn shard key cho bảng nào. Bài khép lại bằng một phép so sánh: tự shard trong app với dùng một database có sẵn sharding như Citus, Vitess, CockroachDB hay DynamoDB, xét theo việc team phải tự làm, mức tương thích SQL và chi phí.

Ngay sau đó là bài nền về partition, tức chia một bảng thành nhiều phần theo một cột ngay trong cùng database, để dọn dữ liệu cũ bằng một câu lệnh và để truy vấn lọc theo cột đó chỉ phải đọc những phần cần thiết. Bài này cũng chỉ ra vì sao partition không giúp máy ghi được nhiều hơn. Khi một bảng vượt trần ghi, tức mức ghi tối đa của một máy đo bằng load test, bạn mới phải chọn shard key.

### 2. Chọn shard key và xử lý hot key

Agent chia bảng đơn hàng ra **4 database** theo tháng tạo đơn, và ngay ngày đầu tháng, database của tháng hiện tại lên **95%** CPU trong khi ba database kia dưới **10%**. Xếp thử mười hai đơn hàng vào bốn shard, bạn thấy vì sao chia theo một cột tăng dần thì mọi lệnh ghi mới dồn vào một shard. Ví dụ đó cũng cho thấy vì sao chia theo người mua trải đều lệnh ghi mà vẫn để **80%** lượt đọc phải hỏi mọi shard. Bài chọn `shop_id` vì trang đơn của shop chiếm **70%** lượt đọc. Ba bảng mà transaction tạo đơn cùng ghi vào được chia theo cùng shard key, nên đơn, dòng hàng và thanh toán của một shop nằm chung một shard; cách sắp xếp này gọi là co-location, và nhờ nó transaction tạo đơn chỉ ghi vào một shard.

Hot key có thể xuất hiện ngay cả khi app chỉ có một database. Một KOL chia sẻ link một mẫu áo khoác, dòng đếm lượt xem của sản phẩm đó nhận khoảng **4.000** lệnh ghi mỗi giây, và p99 của API trang sản phẩm tăng từ **60 ms** lên **2,4 giây**, kể cả với những sản phẩm không liên quan. CPU của Postgres lúc đó chỉ khoảng **30%**, vì các request không đợi CPU mà xếp hàng chờ lock của một dòng, tức dấu Postgres đặt lên dòng đang được sửa để không request nào khác sửa được dòng đó cùng lúc. Trong lúc xếp hàng, các request vẫn giữ gần hết connection mà app mở sẵn tới database. Bài so sánh ba cách xử lý hot key theo loại dữ liệu, chọn cách đếm lượt xem trong bộ nhớ của app rồi ghi gộp mỗi giây, và chỉ ra dấu hiệu cho thấy một shop lớn đang làm cả shard chứa nó bận hơn hẳn.

Bài tình huống khép nhóm này bằng đợt flash sale **5.000** suất của một mẫu nồi chiên. Khoảng **9 trên 10** request checkout lỗi timeout và số suất đó mất hơn **3 phút** mới bán hết, dù lượng người bấm mua đủ để bán hết trong khoảng **13 giây**. Mỗi lượt checkout giữ lock của dòng tồn kho khoảng **40 ms**, và chỉ từ số đo đó, bạn tính ra dòng tồn kho này chỉ cho bán được **25** đơn mỗi giây. Bài đổi thứ tự các câu lệnh SQL trong transaction, chia tồn kho thành **10** dòng và xử lý những suất cuối cùng còn trong kho để app không báo hết hàng khi kho vẫn còn, rồi đo lại để chắc rằng số suất bán vượt vẫn bằng **0**.

### 3. Tìm đúng database cho một key và sinh ID không trùng giữa các shard

Bảng đơn hàng đã chia ra bốn database bằng phép chia lấy dư `shop_id % 4`. Script ước lượng do agent viết báo rằng thêm database thứ năm với `shop_id % 5` thì khoảng **80%** số dòng phải đổi database, tức **2,4 TB** trên **3 TB**. Bài thu nhỏ bài toán xuống **20** key để đếm từng key đổi chỗ, rồi so sánh phép chia lấy dư với consistent hashing. Với consistent hashing, app tính hash của mỗi key, tức một số tính ra từ giá trị của key, rồi đặt cả database và key lên một vòng hash, nên thêm một database chỉ làm một phần nhỏ key đổi chỗ.

Phần lớn bài dành cho cách thứ ba, trong đó key được chia thành nhiều logical shard (những nhóm key cố định được chuyển nguyên khối giữa các database) và vị trí của từng nhóm được ghi vào bảng thư mục. App tính hash của `shop_id` để biết shop thuộc logical shard nào, rồi tra bảng thư mục để biết logical shard đó đang nằm ở database nào. Nếu dữ liệu đã được chia theo **256** logical shard từ trước, thêm database thứ năm chỉ phải chép **51** logical shard, khoảng **20%** dữ liệu, và bạn chọn được logical shard nào chuyển trước.

Bảng vừa chia xong thì một shop báo đơn 1048213 hiện “đã thanh toán” dù khách chưa trả tiền. Cột ID vẫn để kiểu `bigserial`, tức Postgres tự đánh số tăng dần cho mỗi dòng mới, nên mỗi database tự đếm bằng bộ đếm riêng, cùng bắt đầu từ mã đơn lớn nhất lúc chia, và hai shop ở hai database nhận cùng một mã đơn. Callback của cổng thanh toán, tức request cổng thanh toán gọi vào app để báo kết quả, lại chỉ kèm mã đơn, nên tuần đầu có **37** đơn bị đánh dấu thanh toán nhầm. Bài so sánh năm cách sinh ID, chọn ID **64 bit** mang sẵn số logical shard vì cổng thanh toán chỉ gửi lại mã đơn, và nói rõ khi nào nên mặc định dùng UUIDv7, tức UUID có phần đầu là thời điểm tạo nên vẫn sắp xếp được theo thời gian. Bài cũng chỉ ra vì sao ID **64 bit** nên gửi dạng chuỗi trong JSON tới app TypeScript.

### 4. Truy vấn không có shard key và giá trị phải unique trên mọi shard

Bảng đơn hàng giờ nằm trên **8 database** theo `shop_id`, nên màn hình tìm đơn theo số điện thoại phải gửi cùng một truy vấn tới cả tám database rồi gộp kết quả, cách làm gọi là scatter-gather. p99 của màn hình tăng từ **120 ms** lên **1,6 giây**, và lúc một database đang chạy backup thì gần như lượt tìm nào cũng chậm theo, vì lượt tìm phải chờ database chậm nhất trả lời mới kết thúc. Bài dựng bảng `lookup` chia theo số điện thoại, một lookup index cho biết đơn của mỗi số nằm ở shard nào, nên mỗi lượt tìm chỉ hỏi một hoặc hai shard và p99 về khoảng **140 ms**. Đổi lại, `lookup` cập nhật trễ hơn shard vài giây, và bài so sánh thêm cách denormalize, tức chép thêm vài field vào `lookup` để khỏi đọc bảng gốc.

Bảng shop cũng nằm trên tám database, và database nào cũng đặt unique constraint, tức quy tắc không cho hai dòng trùng giá trị, cho cột handle chứa phần cuối đường dẫn của shop, như `minh-anh` trong `/shop/minh-anh`. Vậy mà tháng trước có **17** cặp shop trùng đường dẫn, vì unique constraint chỉ kiểm tra dữ liệu trong một database. Bài đặt bảng toàn cục `shop_handles` trong một database nhỏ làm nơi duy nhất quyết định handle thuộc shop nào, và sắp thứ tự ghi để một bước lỗi giữa chừng chỉ để lại dữ liệu thừa có thể dọn sau. Khi một lệnh ghi phải sửa dữ liệu ở hai shard, bài nói rõ 2PC và saga làm được gì; 2PC (two-phase commit) là cách các database cùng chuẩn bị commit rồi cùng commit theo lệnh của một coordinator, tức thành phần đứng ngoài điều phối các database đó.

Bài tình huống của nhóm này bắt đầu từ load test trước đợt sale 11/11. Ở mức **1.500** đơn mỗi giây, p99 của API tạo đơn lên **4,2 giây** và khoảng **40%** request lỗi timeout. Tăng tải dần cho thấy database đơn hàng chỉ ghi kịp khoảng **900** đơn mỗi giây. Bạn chia database đơn hàng thành hai shard theo **256** logical shard tính từ `shop_id`, cho mã đơn mang số logical shard, đưa lượt tìm theo số điện thoại và job báo cáo theo ngày, hai loại truy vấn không có `shop_id`, sang đường đi riêng, rồi chạy lại đúng load test đó. Trần ghi lên khoảng **1.700** đơn mỗi giây, không phải gấp đôi, p99 ở mức tải dự kiến không quá **0,42 giây**, và bài chỉ ra vì sao tải của đợt sale đã dùng khoảng **88%** trần mới.

### 5. Chuyển dữ liệu sang database khác khi app vẫn ghi

Lần đầu chuyển bảng đánh giá sản phẩm **400 GB** sang database riêng, agent dump rồi restore cả bảng trong **6 giờ**, sau đó đổi app sang database mới. Sáng hôm sau hai database lệch khoảng **38.000** đánh giá, toàn là những đánh giá được viết hoặc sửa trong lúc chép.

Bài tách việc chuyển thành bốn bước. Bước một là backfill, tức chép dữ liệu đang có theo từng lô, và bước hai là bắt lại mọi lệnh ghi mới từ trước lô đầu tiên. Bước ba so sánh hai database ở cùng một mốc, và chỉ khi không còn dòng nào lệch mới tới bước bốn là cutover, tức bước chuyển nơi ghi từ database cũ sang database mới. Lần chuyển thứ hai không mất lệnh ghi nào, app chỉ dừng ghi khoảng **4 giây**, đổi lại job chép mất khoảng **9 giờ** vì tự giới hạn tốc độ để database cũ vẫn phục vụ app.

Cutover xong chưa có nghĩa là mọi lệnh ghi đã đi đúng chỗ. Lúc **10:00**, một logical shard được chuyển từ `orders-2` sang `orders-5` và bảng thư mục đã đổi theo. Nhưng mỗi instance, tức một tiến trình đang chạy của service đơn hàng, giữ bản cache của bảng thư mục tới **60 giây**, nên **27** đơn đã thanh toán vẫn được ghi vào `orders-2`, nơi trang “Đơn của shop” không còn đọc tới. Bài gắn cho mỗi dòng của bảng thư mục một epoch, tức số tăng thêm một mỗi lần dòng đó đổi database. Database dùng epoch để từ chối lệnh ghi gửi theo bản cache cũ bằng lỗi “moved”, và instance tra lại bảng thư mục rồi ghi vào đúng database.

Có lúc dữ liệu đã chia theo hash mà một database vẫn đầy nhanh hơn hẳn. Disk của `orders-3` đã dùng **85%** và tăng nhanh gấp đôi so với disk của ba database còn lại, vì nhiều shop lớn rơi vào các logical shard của nó. Bài phân biệt repartition, reshard và đổi shard key theo lượng dữ liệu mỗi cách phải chuyển sang máy khác. Repartition là đổi cách chia partition ngay trong một database nên không dòng nào phải sang máy khác, còn reshard là đổi số database hoặc đổi logical shard nào nằm ở database nào, trong khi shard key giữ nguyên. Dựa trên bảng tải theo logical shard, bạn chuyển **20** logical shard lớn nhất sang database mới trong ba đêm, đưa tỉ lệ dung lượng giữa database đầy nhất và mức trung bình từ **1,6** xuống khoảng **1,1**.

Bài tình huống cuối khóa ghép cả nhóm này. Mỗi tối từ **20:00** tới **22:00**, kênh chat trong buổi livestream của một shop lớn nhận khoảng **45%** lệnh ghi của `chat-2` và đẩy CPU của database này lên **92%**. Người dùng ở các kênh khác trên `chat-2` gửi tin chậm hẳn, với p99 cao gấp **3** lần p99 trên các database chat còn lại. Bạn cấp cho kênh một dòng riêng trong bảng thư mục, chuyển riêng kênh sang `chat-5` bằng bốn bước trong lúc app vẫn nhận tin nhắn, và để `chat-2` từ chối mọi lệnh ghi của kênh mà app còn gửi nhầm tới nó. Kênh dừng nhận tin không quá **5 giây** lúc cutover, không tin nhắn nào bị mất, và CPU của `chat-2` trong giờ livestream về khoảng **50%**.

## Vì sao 200Lab tạo ra khóa học này

Ngày nay, chỉ cần hỏi agent vì sao database chậm, bạn có thể nhận về ngay đề xuất chia database ra tám shard, kèm luôn phần code chọn database cho mỗi lệnh đọc và lệnh ghi. Code đó chạy được trên staging chỉ sau vài giờ. Phần khó nằm trước dòng code đầu tiên: hệ thống của bạn đã thật sự cần chia chưa, và chia theo cột nào.

Chọn sai shard key thường không gây lỗi nào ngay. Lệnh ghi vẫn trải đều, biểu đồ CPU trông rất ổn, rồi trang được mở nhiều nhất phải hỏi mọi database, một shop lớn khiến cả shard bận hơn hẳn, hay vài chục đơn đã thanh toán nằm ở database mà trang của shop không còn đọc tới. Lúc đó, muốn đổi cách chia thì phải chép lại cả bảng trong lúc app vẫn nhận đơn. Người ngồi đối chiếu từng đơn và giải thích với shop vì sao đơn biến mất chính là bạn.

Những hệ thống lớn cũng từng phải trả lời hai câu hỏi đó, và cũng từng gặp những sự cố như trên. Figma dời từng nhóm bảng sang database riêng trước khi chia bảng theo dòng, và Notion chia Postgres theo workspace thành **480** logical shard trên **32** database. Slack từng có những shard chứa khách hàng enterprise lớn bận hơn hẳn phần còn lại, còn ở Buildkite, bước chép dữ liệu lịch sử sang database mới đã làm database đó quá tải. Ở hệ thống lớn cũng như ở app vừa chia database lần đầu, gốc của sự cố thường giống nhau: cột chia được chọn khi chưa có số liệu về cách app đọc và ghi, và không thành phần nào chặn lệnh ghi đi nhầm database.

Khóa học giúp bạn có số liệu trong tay trước khi quyết định. Mỗi sự cố trong khóa được truy ngược về một lựa chọn cụ thể, như tách nhóm bảng nào, chia theo cột nào, sinh ID thế nào hay database nào được phép nhận lệnh ghi. Học xong, bạn trình bày được với team vì sao nên chia theo cột mình chọn, và bảo vệ phương án bằng tradeoff cụ thể của nó. Bạn cũng có cơ sở để nói rằng ở quy mô hiện tại, tách một nhóm bảng hay partition trên một máy là đủ, để team chưa phải vận hành thêm nhiều database khi hệ thống chưa cần.

## Những vấn đề thường gặp khi chia database ra nhiều shard

Database không còn ghi kịp thì hai cách làm nhanh nhất là nâng primary lên máy lớn hơn, hoặc đồng ý luôn với đề xuất chia database mà agent vừa viết. Nhưng trong các triệu chứng dưới đây, cách sửa đúng phụ thuộc vào việc lệnh ghi đang dồn vào đâu, truy vấn chính lọc theo cột nào, và app có biết chắc chắn mỗi dòng đang nằm ở database nào hay không.

- Agent đề xuất chia cả database ra **8 shard** vì disk của primary bận khoảng **90%** trong giờ cao điểm — một nhóm bảng ít join với phần còn lại, ở đây là chat, đang nhận phần lớn lệnh ghi, nên tách riêng nhóm đó sang database khác đã đủ mà chưa bảng nào phải chọn shard key.
- Đã chia theo tháng mà database của tháng hiện tại lên **95%** CPU, ba database kia dưới **10%** — chọn một cột tăng dần làm shard key thì mọi lệnh ghi mới dồn vào một shard, dù dữ liệu cũ vẫn nằm đều trên các shard.
- Lệnh ghi đã trải đều nhưng trang người bán mở nhiều nhất chậm từ **150 ms** lên **1,4 giây** — trang đó không lọc theo shard key nên phải hỏi mọi shard, và chờ shard trả lời chậm nhất mới có kết quả.
- CPU của Postgres chỉ khoảng **30%** mà p99 của API vẫn ở mức **2,4 giây** — request đang chờ lock của một dòng nóng và giữ gần hết connection tới database, nên thêm CPU hay thêm database đều không giúp được.
- Thêm một database mà script ước lượng báo phải chép **80%** dữ liệu — app chọn database bằng phép chia lấy dư cho số database, nên đổi số database làm gần như mọi key đổi chỗ.
- Callback thanh toán đánh dấu nhầm đơn của một shop khác thành “đã thanh toán” — mỗi database vẫn tự cấp mã đơn bằng bộ đếm riêng, nên hai đơn ở hai shard mang cùng một mã.
- Dump và restore cả bảng trong một đêm, không bước nào báo lỗi, sáng ra hai database lệch **38.000** đánh giá — chép một lần chỉ mang theo dữ liệu có sẵn khi bắt đầu chép, còn mọi lệnh ghi trong lúc chép phải được bắt lại và so sánh trước khi đổi nơi ghi.
- Đã chuyển dữ liệu và đổi bảng thư mục, vậy mà cuối ngày **27** đơn đã thanh toán không hiện trên trang của shop — instance còn giữ bản cache cũ của bảng thư mục, và database cũ không có cách nào nhận ra lệnh ghi gửi nhầm để từ chối.

Mỗi triệu chứng trên được phân tích trong một bài của khóa, kèm phép tính và những câu bạn hỏi lại agent trước khi chọn cách sửa.

## Bạn sẽ học theo cách nào

- **Bắt đầu từ tín hiệu thật** — Mỗi bài mở bằng thứ bạn tự thấy được trên hệ thống của mình, như disk của primary bận **90%**, p99 của một trang tăng vọt, một database đầy nhanh hơn các database khác hay một đề xuất chia database của agent, kèm câu hỏi lệnh đọc và lệnh ghi đang đi tới đâu.
- **Ví dụ thu nhỏ trước, nghiệp vụ sau** — Với cơ chế khó hình dung, bài bắt đầu bằng ví dụ chỉ có vài shard và vài key, như mười hai đơn trên bốn shard hay hai mươi key khi thêm database thứ năm, rồi mới quay lại sàn thương mại; sơ đồ gọn và bảng cho thấy mỗi lệnh đi tới database nào.
- **Chọn phương án bằng số liệu** — Cột chia, cách tìm database cho một key, cách sinh ID hay phần dữ liệu chuyển trước đều được chọn từ số liệu của ví dụ, kèm tradeoff, và cả nấc của bậc thang scale mà ở đó bạn chưa cần tới phương án vừa chọn.
- **Quiz yêu cầu bạn tự tính** — Nhiều bài cơ chế có quiz ngắn để bạn tính trước khi xem đáp án, như một lượt tìm phải chờ bao lâu khi một trong tám shard đang chậm, hay thêm database thứ năm thì bao nhiêu trong hai mươi key phải đổi chỗ.
- **Demo kit chạy bằng Docker** — Ba bài tình huống và hai bài cơ chế có demo kit, mỗi kit là một file Markdown để coding agent của bạn dựng hệ thống trên máy, chạy cùng một kịch bản trước và sau khi sửa rồi so sánh số đo; lệnh và cấu hình Postgres nằm trong kit, và kit không bắt buộc.

## Khóa học dành cho ai

1. **Junior developer hoặc vibe coder** — Bạn có app chạy trên Postgres, phần lớn code do AI agent viết, đã có replica hoặc partition mà database vẫn không ghi kịp hay sắp hết chỗ chứa dữ liệu; bạn cần biết nên chia hay chưa, chia theo cột nào và hỏi agent câu gì.
2. **Developer vừa nhận đề xuất sharding** — Agent hoặc team vừa đề xuất chia database, thêm một database vào cụm đã chia hay chuyển sang một database có sẵn sharding, và bạn cần số liệu để quyết định trước khi đồng ý.
3. **Developer đang vận hành nhiều shard** — Hệ thống của bạn đã chia, và bạn đang gặp một shard bận hơn hẳn hay một màn hình phải hỏi mọi database, hoặc sắp phải chuyển dữ liệu sang database khác; bạn cần thứ tự xử lý để không mất lệnh ghi nào.

## Những điều cần lưu ý

- **Kiến thức nền cần có** — Bạn chỉ cần từng làm app nhận request và ghi vào Postgres trong transaction, kể cả khi code do agent viết, và đọc được TypeScript, SQL cùng dòng lỗi trong log; không cần từng vận hành nhiều database.
- **Nên học thêm các khóa System Design liên quan** — Bài nền về partition trên một máy, nằm ngay trước bài chọn shard key, lấy từ khóa “Thiết kế hệ thống — ước lượng tải, load balancer, cache, queue, replication và partitioning”. Hai khóa “Xử lý timeout, retry và crash — idempotency, outbox, lease và reconciliation” và “Thiết kế dữ liệu phân tán — consistency, quorum, leader election và failover” không phải điều kiện để học khóa này, nhưng giúp bạn nắm đầy đủ hơn về outbox, fencing token, replication và failover của từng shard, những phần ở đây chỉ được nhắc một câu.
- **Không hướng dẫn cấu hình một công cụ sharding cụ thể** — Citus, Vitess, CockroachDB hay DynamoDB chỉ được so sánh theo những tiêu chí giúp bạn chọn, còn lệnh, cấu hình Postgres và script chép dữ liệu nằm trong demo kit cho agent; đổi lại, bạn hiểu cơ chế đủ rõ để biết công cụ và agent đang quyết định thay bạn những gì, và hiểu vì sao không công cụ nào chọn shard key thay bạn.
- **Số liệu trong ví dụ do bài tự đặt** — Số đơn, dung lượng hay p99 trong bài được chọn để các phép tính khớp nhau, không phải số đo từ production của một công ty nào; khi áp dụng, bạn nhờ agent đo tỉ lệ lệnh ghi theo nhóm bảng, tỉ lệ truy vấn phải hỏi mọi shard và lượng dữ liệu phải chép trên hệ thống của mình rồi đưa vào cùng cách tính.

## Bắt đầu từ đâu

Bài đầu tiên mở bằng một đề xuất của agent: chia bảng đơn hàng ra **8 database** theo hash của `order_id`, tức một số app tính ra từ mã đơn để chọn database. Lệnh ghi được chia đều, nhưng trang “Đơn của shop” chậm từ **150 ms** lên **1,4 giây**. Bài chỉ ra vì sao trang đó phải hỏi cả tám database, ba câu hỏi bạn cần có số liệu để trả lời trước khi chia, và sáu nấc mà database của một app đi qua khi lớn dần. Khi ước chừng được app của mình đang ở nấc nào, bạn sẽ biết phần nào của khóa nên đọc kỹ trước và phần nào hệ thống của bạn chưa cần tới.

## Chương trình

- Database không còn ghi kịp thì chia dữ liệu thế nào (miễn phí)

### Shard key, hot key và khi nào cần chia database

- Database sắp quá tải nhưng chưa chắc đã cần sharding (miễn phí)
- Partition khi một bảng quá lớn và sharding khi một máy không ghi kịp · quiz
- Chia database theo tháng làm mọi lệnh ghi dồn vào một shard · quiz · kit cho agent
- Vì sao một dòng đếm lượt xem làm chậm mọi trang sản phẩm? · quiz
- Tình huống: checkout treo khi hàng nghìn người cùng mua một SKU · kit cho agent

### Tìm đúng shard cho mỗi lệnh đọc và lệnh ghi

- Thêm database thứ năm có cần chép lại 80% dữ liệu không? · quiz
- Mỗi shard tự cấp mã đơn nên hai đơn hàng mang cùng một mã · quiz
- Tìm đơn theo số điện thoại nhưng phải hỏi mọi shard · quiz · kit cho agent
- Hai shop trên hai shard mà cùng đăng ký được một đường dẫn · quiz
- Tình huống: chia database đơn hàng ra hai shard trước đợt sale · kit cho agent

### Hot shard hoặc shard đầy thì chuyển dữ liệu thế nào

- Chuyển một bảng sang database mới khi app vẫn đang ghi · quiz
- Đã chuyển shard xong mà app vẫn ghi vào database cũ · quiz
- Vì sao chia theo hash mà một database vẫn đầy disk nhanh gấp đôi? · quiz
- Tình huống: chuyển kênh chat livestream sang database riêng · kit cho agent
