Secondary index phân mảnh — local (document) vs global (term)
Index phụ khó phân mảnh vì nó cắt ngang cách chia dữ liệu chính. Hai cách: local (document-partitioned) rẻ ghi đắt đọc, global (term-partitioned) ngược lại.
TL;DR: Secondary index (tra theo trường khác ngoài khoá chính, ví dụ tìm mọi xe màu đỏ) khó phân mảnh vì trường phụ cắt ngang cách chia dữ liệu theo khoá chính. Có hai cách trái ngược: local index (document-partitioned) đẩy chi phí sang đọc — mỗi partition tự index dữ liệu của mình nên ghi rẻ, nhưng một truy vấn phải hỏi mọi partition rồi gộp (scatter/gather). Global index (term-partitioned) đẩy chi phí sang ghi — index phân mảnh theo giá trị trường phụ nên đọc chỉ chạm một partition, nhưng một document phải cập nhật nhiều partition index, thường bất đồng bộ.
Hai bài trước chia dữ liệu theo khoá chính và lo co giãn. Nhưng "tìm theo khoá chính" chỉ là một nửa nhu cầu; nửa kia là "tìm theo thuộc tính" — và đó là lúc secondary index vào cuộc, mang theo một đánh đổi mới đặc trưng cho hệ phân tán.
Bạn đã học inverted index: ánh xạ từ một term (giá trị) tới danh sách document chứa nó — nền của tìm kiếm theo nội dung. Secondary index phân mảnh chính là câu hỏi "đặt inverted index đó ở đâu khi document rải trên nhiều node": cùng chỗ với document (local) hay gom theo term (global)?
1. Analogy — thư viện: mỗi phòng tự làm mục lục vs một mục lục chung
Sách chia ra nhiều phòng theo mã sách (khoá chính). Độc giả muốn "mọi sách chủ đề lịch sử" — một thuộc tính khác. Hai cách làm mục lục theo chủ đề:
- Mỗi phòng tự làm mục lục của phòng mình (local): phòng nào cũng có sổ "sách lịch sử trong phòng này". Thêm một cuốn vào phòng → chỉ sửa mục lục phòng đó (rẻ). Nhưng độc giả hỏi "mọi sách lịch sử toàn thư viện" → phải đi hỏi tất cả các phòng rồi gộp.
- Một mục lục chung, chia theo chủ đề (global): có "quầy Lịch sử" giữ danh sách mọi sách lịch sử ở mọi phòng. Độc giả hỏi → tới đúng quầy Lịch sử, xong (rẻ). Nhưng thêm một cuốn có nhiều chủ đề → phải chạy cập nhật nhiều quầy (quầy Lịch sử, quầy Chiến tranh...), và quầy có thể ở phòng khác.
| Thư viện | Secondary index |
|---|---|
| Mỗi phòng tự làm mục lục | Local (document-partitioned) index |
| Thêm sách chỉ sửa mục lục phòng đó | Ghi rẻ (1 partition) |
| Hỏi "mọi sách LS" = đi mọi phòng | Đọc = scatter/gather |
| Quầy chung theo chủ đề | Global (term-partitioned) index |
| Hỏi tới đúng quầy, xong | Đọc rẻ (1 partition/term) |
| Thêm sách cập nhật nhiều quầy | Ghi đắt (nhiều partition, async) |
Local = mục lục theo phòng (thêm rẻ, tìm phải đi khắp). Global = quầy theo chủ đề (tìm nhanh, thêm phải cập nhật nhiều quầy). Đọc-nhiều → global; ghi-nhiều → local.

2. Local index — vì sao ghi rẻ mà đọc phải scatter?
Mỗi partition giữ secondary index chỉ trên các document của chính partition đó. Index "đồng vị" (co-located) với dữ liệu.
Ghi rẻ. Thêm/sửa một document chỉ đụng partition chứa nó: cập nhật dữ liệu + cập nhật index cục bộ, tất cả trong một partition. Không cần nói chuyện với partition khác.
Local index: mỗi partition chỉ index document của chính nó, mà dữ liệu chia theo khoá chính (car_id), không theo màu. Vậy nếu cụm có 20 partition thay vì 3, một truy vấn "mọi xe đỏ" phải hỏi bao nhiêu trong số 20 partition đó — lần theo lý do "màu không liên quan gì tới cách chia theo car_id" để tự suy ra con số, đừng đoán chung chung "nhiều hay ít".
Đọc theo trường phụ đắt. Truy vấn "mọi xe màu đỏ" — xe đỏ rải khắp mọi partition (vì partition chia theo khoá chính, không theo màu). Nên phải gửi truy vấn tới mọi partition, mỗi partition tra index cục bộ của nó, rồi gộp kết quả (scatter/gather). Chậm và chịu tail latency (chờ partition chậm nhất).
Local index (partition theo car_id):
P1: index color: {red: [c3, c8], blue: [c1]}
P2: index color: {red: [c11], green: [c5]}
P3: index color: {red: [c20, c22]}
Query "red" -> hoi CA P1,P2,P3 -> gop [c3,c8,c11,c20,c22]
Đây là cách phổ biến nhất (Elasticsearch, nhiều store) vì ghi cục bộ đơn giản và bền.
3. Global index — phân mảnh theo term (đọc rẻ, ghi đắt)
Index được coi như dữ liệu riêng và phân mảnh theo giá trị trường phụ (term), độc lập với cách chia dữ liệu chính. Ví dụ: mọi entry index cho color=red nằm trên một partition (chọn theo hash(red)), color=blue trên partition khác.
Với global index phân theo term, truy vấn "mọi xe đỏ" chỉ chạm một partition (đọc rẻ). Bạn thêm một xe mới có 3 trường được index (color=red, brand=toyota, city=hanoi). Đoán con số chính xác: nó phải cập nhật bao nhiêu partition của index (gợi ý: đếm theo số trường được index, không phải trả lời chung chung "nhiều"), và lần theo việc các partition đó nằm ở node khác nhau để đoán vì sao cập nhật thường KHÔNG thể gói gọn trong một transaction đồng bộ.
Đọc rẻ. Truy vấn "mọi xe đỏ" tới đúng partition giữ term red — một chỗ, không scatter. Range query trên term cũng có thể nhanh nếu index sắp theo term.
Ghi đắt và phức tạp. Một document có nhiều trường được index (color=red, brand=toyota), và các term đó nằm trên các partition khác nhau. Nên một lần ghi document phải cập nhật nhiều partition index — thường ở các node khác với node chứa document. Vì cập nhật đồng bộ nhiều partition là 2PC đắt (Module 4), thực tế thường cập nhật bất đồng bộ: index có thể trễ so với dữ liệu (đọc index ngay sau ghi có thể chưa thấy) — một dạng replication lag (Module 2) cho index.
Global index (partition theo term):
P_red: red -> [c3, c8, c11, c20, c22] (moi xe do o day)
P_blue: blue -> [c1]
P_toyota: toyota -> [c8, c22]
Query "red" -> chi hoi P_red (nhanh).
Ghi 1 xe do+toyota -> cap nhat P_red VA P_toyota (nhieu node, async).
4. Pitfall — chọn global index rồi giả định index luôn khớp dữ liệu
Vì global index cập nhật bất đồng bộ, có một khoảng index chưa khớp với dữ liệu.
❌ Ghi xe moi (color=red) -> bao "da luu" -> ngay sau doc index "moi xe do"
-> global index P_red CHUA duoc cap nhat (async) -> xe vua them KHONG hien
-> tuong mat du lieu / bug
❌ Chon LOCAL index cho bang doc-nhieu theo truong phu, tra van nong
-> moi query scatter toi MOI partition -> cham + tai lon vo ich
Hệ quả: hoặc kết quả tìm kiếm thiếu tạm thời (global async) bị hiểu là mất dữ liệu, hoặc đọc chậm triền miên (local cho workload đọc-nhiều).
Hướng đúng: chọn theo tỉ lệ đọc/ghi và yêu cầu độ tươi. Ghi-nhiều, đọc theo trường phụ thưa → local (ghi rẻ, chịu scatter khi đọc). Đọc theo trường phụ rất nhiều, chịu được index trễ chút → global (đọc rẻ, chấp nhận ghi phức tạp + eventual consistency của index). Và với global, thiết kế app biết index có thể trễ — đừng giả định vừa ghi là tìm thấy ngay.
5. 📚 Deep Dive
- Designing Data-Intensive Applications (Kleppmann) — Chương 6, mục "Partitioning and Secondary Indexes" — document-partitioned (local) vs term-partitioned (global) secondary index, và vì sao global thường cập nhật bất đồng bộ.
- Local index ↔ scatter/gather (mọi partition tra index cục bộ) là mô hình của Elasticsearch/Solr; global index ↔ một partition/term là mô hình một số hệ chuyên đọc.
Ghi chú: DDIA Ch6 nhấn: đây là cùng một đánh đổi đọc/ghi lặp lại — chỉ khác lần này áp lên chính index. Không có index phân tán "vừa ghi rẻ vừa đọc rẻ".
6. Liên hệ các bài khác
- Bài 01 — Range vs hash: secondary index khó vì trường phụ cắt ngang cách chia chính (theo khoá chính) — nối thẳng từ cách partition dữ liệu.
- Module 2 — Replication lag: global index async có "index lag" giống replication lag — đọc index ngay sau ghi có thể chưa thấy.
- Module 4 — Transaction phân tán: vì sao global index không cập nhật đồng bộ mọi partition — đồng bộ nhiều partition là 2PC đắt.
- Inverted index (Thuật toán): cấu trúc index bên dưới — bài này quyết đặt nó ở đâu khi phân tán.
7. Tóm tắt
- Giá trị một trường phụ không tôn trọng ranh giới partition vốn chia theo khoá chính — đây là gốc rễ khiến secondary index khó.
- Local (document-partitioned) = mỗi phòng tự làm mục lục riêng: thêm sách rẻ, nhưng tìm theo chủ đề phải lục khắp mọi phòng rồi gộp lại.
- Global (term-partitioned) = một quầy chung theo chủ đề: tìm nhanh (đúng một quầy), nhưng thêm sách phải chạy cập nhật nhiều quầy — thường trễ vài giây tới vài phút.
- Chọn local khi ghi nhiều/đọc theo trường phụ thưa; chọn global khi đọc theo trường phụ dày, chấp nhận độ trễ cập nhật index.
- Anti-pattern: tin global index luôn khớp ngay sau ghi (async → có khoảng trễ tạm thời); hoặc dùng local cho workload đọc-nhiều theo trường phụ (scatter/gather chậm triền miên).
8. Tự kiểm tra
- Q1Vì sao secondary index khó phân mảnh, trong khi partition theo khoá chính thì đơn giản?
- Q2Trong local (document-partitioned) index: vì sao ghi rẻ nhưng đọc theo trường phụ đắt?
- Q3Trong global (term-partitioned) index: giải thích vì sao đọc rẻ nhưng ghi đắt, và vì sao ghi thường bất đồng bộ.
- Q4Một app rao vặt: đăng tin rất nhiều (ghi cao), người xem thỉnh thoảng lọc theo 'khu vực'. Chọn local hay global index cho trường khu vực? Vì sao?
- Q5Vì sao 'index lag' của global index là họ hàng của replication lag (Module 2), và app cần lưu ý gì?
Bài tiếp theo: Request routing — tìm đúng node giữ dữ liệu
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên