Partitioning & Sharding: Suy luận về vị trí đặt dữ liệu
Suy luận về table partitioning và distributed sharding qua data placement, khóa phân vùng và khóa shard, pruning, hotspot, fan-out, công việc liên shard, ranh giới uniqueness và resharding.
Bản đồ học tập phát triển phần mềm bởi Tran Trong Thuc · Về dự án Atlas · Cập nhật lần cuối: 22 thg 9, 2026
Partitioning & Sharding: Suy luận về vị trí đặt dữ liệu
Tóm tắt
Hãy tưởng tượng đêm hội mua sắm Black Friday: lưu lượng truy cập tăng vọt gấp 50 lần chỉ trong vài giây. Cụm cơ sở dữ liệu gồm 10 node sharded được mở rộng tự tin với khóa phân vùng theo ngày tạo created_at. Bất thình lình, CPU của node 10 chạm ngưỡng 100%, hàng đợi ghi nghẽn nghẹt, connection pool cạn kiệt và toàn bộ hệ thống thanh toán sập hoàn toàn—trong khi 9 node còn lại ngồi chơi xơi nước ở mức 2% tải. Việc chọn mốc thời gian tăng dần làm khóa shard (shard key) đã dồn toàn bộ lưu lượng ghi của ngày hôm đó vào đúng 1 node, biến cụm máy chủ đắt đỏ thành một điểm nóng (hotspot) thắt cổ chai. Tệ hơn nữa, màn hình lịch sử đơn hàng của người dùng lọc theo customer_id mà không kèm mốc thời gian, kích hoạt truy vấn rải rác đa phân vùng (cross-partition scatter-gather query) quét qua toàn bộ 10 shard (fan-out / quét nhiều shard), thổi bùng tail latency và làm tê liệt cụm cơ sở dữ liệu.
💡 Quy tắc bỏ túi: Partitioning trong nội bộ một database instance dùng để loại bỏ partition không liên quan (partition pruning) và quản lý vòng đời dữ liệu; sharding qua nhiều node vật lý dùng để mở rộng năng lực ghi và bộ nhớ. Đừng bao giờ chọn placement key chỉ vì cảm giác "chia đều"—hãy chọn dựa trên vị từ lọc (predicate) và ranh giới giao dịch (transaction) chiếm ưu thế.
- Ranh giới giữa partitioning và sharding: Partitioning chia một bảng logic thành nhiều partition vật lý trên cùng một database engine để tận dụng partition pruning (loại bỏ partition thừa khi quét); còn sharding phân tán quyền sở hữu dữ liệu qua nhiều node máy chủ vật lý độc lập.
- Khóa phân vùng quyết định tính cục bộ (locality): Dù dùng phân vùng theo range, phân vùng theo list hay phân vùng theo hash, khóa phân vùng (partition key) và khóa shard (shard key) quyết định query chỉ chạm đúng một lát cắt vật lý hay phải quét nhiều shard (fan-out / scatter-gather) tốn kém.
- Ranh giới tính duy nhất (uniqueness): Ràng buộc duy nhất (
UNIQUE/PRIMARY KEY) trong bảng phân vùng bắt buộc phải chứa toàn bộ khóa phân vùng; trên cụm sharding độc lập, index duy nhất cục bộ không thể bảo đảm tính duy nhất toàn cục nếu thiếu bộ điều phối trung tâm. - Tái phân shard (resharding) phải được thiết kế từ ngày đầu: Chiến lược tái phân shard khi dữ liệu phình to đòi hỏi quy trình chuyển giao quyền sở hữu (authority cutover), đồng bộ dữ liệu nền và cơ chế định tuyến không gây downtime.
- Cạm bẫy chết người (Điểm nóng đơn node & quét rải rác): Chọn khóa shard theo cột tăng dần (timestamp, sequence) dồn 100% lưu lượng ghi vào một shard duy nhất gây sập node (hotspot), trong khi các truy vấn thiếu khóa định tuyến buộc hệ thống phải quét toàn bộ các node (cross-shard scatter-gather fan-out), đẩy độ trễ lên mức không thể kiểm soát.
partitioning (phân vùng bảng)
-> chia một bảng logic thành nhiều phân vùng vật lý
-> thường nằm trong cùng một hệ quản trị / cụm cơ sở dữ liệu
-> bộ tối ưu hóa có thể loại bỏ partition không liên quan khi điều kiện lọc khớp với ranh giới phân vùng
sharding (phân mảnh ngang)
-> phân chia quyền sở hữu dữ liệu qua nhiều node cơ sở dữ liệu độc lập
-> định tuyến trở thành trách nhiệm của ứng dụng / middleware / hệ CSDL phân tán
-> thao tác đọc và ghi liên shard biến thành tác vụ phân tán tốn kém1. Partitioning là tổ chức vật lý phía sau một logical table
Phân vùng khai báo (declarative partitioning) của PostgreSQL cho phép một bảng logic tự động định tuyến các hàng vào các bảng con (child partition) dựa theo ranh giới phân vùng (partition bounds).
Bảng cha được phân vùng thực chất chỉ là cấu trúc ảo: dữ liệu thực tế được lưu trữ hoàn toàn bên trong các partition con.
Ví dụ về một bảng sự kiện được phân vùng theo thời gian:
CREATE TABLE events (
tenant_id bigint NOT NULL,
event_id bigint NOT NULL,
occurred_at timestamptz NOT NULL,
payload jsonb NOT NULL
) PARTITION BY RANGE (occurred_at);
CREATE TABLE events_2026_09
PARTITION OF events
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');Thao tác INSERT vào bảng cha sẽ được tự động định tuyến theo khóa phân vùng. Nếu không có partition nào tiếp nhận giá trị đó và cũng không có partition mặc định (DEFAULT), lệnh INSERT sẽ thất bại.
Partitioning tổ chức lại dữ liệu vật lý mà không bắt các truy vấn thông thường phải chỉ định trực tiếp tên của từng bảng con.
2. Range, list và hash partitioning mã hóa các giả định locality khác nhau
PostgreSQL hỗ trợ ba chiến lược phân vùng khai báo chính:
Phân vùng theo range
Phân vùng theo range gán các khoảng giá trị không chồng lấn của khóa phân vùng vào từng partition cụ thể.
2026-07 -> partition A
2026-08 -> partition B
2026-09 -> partition CChiến lược này rất phù hợp khi dữ liệu có trật tự tự nhiên và ranh giới vòng đời rõ ràng, chẳng hạn như mốc thời gian (timestamp) hoặc dải số tăng dần.
Các lý do áp dụng phổ biến:
- Truy vấn thường xuyên lọc theo khoảng thời gian (time window);
- Dữ liệu cũ được thu hồi hoặc lưu trữ lạnh theo từng khối định kỳ;
- Dữ liệu mới phát sinh có cường độ truy cập cao hơn hẳn dữ liệu lịch sử;
- Công tác bảo trì và dọn dẹp hệ thống hưởng lợi từ các phân vùng có giới hạn thời gian rõ ràng.
Phân vùng theo list
Phân vùng theo list ánh xạ các giá trị cụ thể, tường minh của khóa vào từng partition.
region = 'apac' -> partition APAC
region = 'emea' -> partition EMEA
region = 'amer' -> partition AMERChiến lược này đặc biệt hữu ích khi các nhóm giá trị đã được xác định trước và mang ý nghĩa nghiệp vụ cố định. Tuy nhiên, độ phức tạp vận hành sẽ tăng vọt nếu danh mục phân loại thay đổi liên tục.
Phân vùng theo hash
Phân vùng theo hash ánh xạ các hàng dữ liệu dựa trên phép chia lấy dư (modulus/remainder) từ giá trị băm của khóa phân vùng.
Cách này giúp dàn đều các hàng khi không thể tận dụng khoảng giá trị có thứ tự, nhưng lại đánh đổi ranh giới vòng đời trực quan vốn có của range partitioning.
Tên gọi chiến lược không quan trọng bằng câu hỏi then chốt về khối lượng công việc:
Điều kiện lọc (predicate) và tác vụ bảo trì nào có thể giúp hệ thống chỉ cần xử lý một tập hợp con nhỏ của dữ liệu vật lý?
3. Partition pruning là nơi placement có thể biến thành query saving
Giả sử bảng events được phân vùng theo trường occurred_at.
Truy vấn sau đây ăn khớp hoàn toàn với khóa phân vùng:
SELECT count(*)
FROM events
WHERE occurred_at >= '2026-09-01'
AND occurred_at < '2026-09-08';PostgreSQL có thể dùng partition bound để prune partition ngoài time window đó.
Nhưng query này không có time predicate:
SELECT *
FROM events
WHERE tenant_id = 42;Nếu mọi time partition đều có thể chứa tenant 42, partitioning theo time không giúp planner chứng minh partition nào không liên quan. Query vẫn có thể phải chạm nhiều partition.
Dùng EXPLAIN / EXPLAIN ANALYZE để xác nhận pruning thay vì giả định nó xảy ra.
4. Partition key tốt thường đi theo predicate quan trọng hoặc lifecycle boundary
Guidance của PostgreSQL nhấn mạnh việc chọn column thường xuất hiện trong WHERE clause khi điều đó cho phép prune partition.
Nhưng query filtering không phải lý do duy nhất.
Một partition key tốt còn có thể align với:
- deletion hoặc archival boundary;
- data-retention policy;
- maintenance window;
- tenant hoặc geography ownership;
- write distribution;
- operational isolation.
Time key mạnh khi workload có hình dạng theo thời gian. Tenant key mạnh khi phần lớn work tenant-scoped. Compound strategy có thể hữu ích khi cả hai dimension quan trọng, nhưng sub-partitioning thêm metadata và operational complexity.
Đừng chọn key chỉ vì “trông phân phối đều” nếu important query không route hoặc prune được bằng key đó.
5. Partitioning và indexing giải quyết hai tầng khác nhau của access problem
Partition pruning quyết định partition nào cần được xét.
Index quyết định cách tìm row hiệu quả bên trong partition còn lại.
Hai cơ chế bổ sung nhau.
query predicate
-> prune partition bằng partition bound
-> trên partition còn lại, chọn index/scan access pathPostgreSQL ghi rõ partition pruning dựa trên partition bound, không dựa vào việc partition key có index hay không.
Mỗi partition vẫn có thể cần index cho local query workload của nó.
6. Quá nhiều partition không miễn phí
Partitioning chia data structure thành phần nhỏ hơn, nhưng mỗi partition cũng là database object có cost về planning, locking, metadata, maintenance, index và vận hành.
Phản xạ sai với table lớn là:
càng nhiều partition càng tốtThay vào đó, hãy suy luận về:
- số partition trung bình mỗi important query chạm tới;
- tổng partition count;
- planner overhead;
- DDL/maintenance automation;
- index trên mỗi partition;
- retention job;
- connection/query concurrency;
- observability theo partition.
Partition theo ngày có thể hợp lý với high-volume event store nhưng vô lý với table volume vừa phải và retention mười năm nếu nó tạo hàng nghìn partition gần như không cần thiết.
7. Partition maintenance phải được thiết kế trước khi table phình lớn
Time-based partitioning thường tồn tại một phần vì old data có thể detach hoặc drop theo cả unit.
PostgreSQL hỗ trợ ATTACH PARTITION và DETACH PARTITION; DETACH PARTITION ... CONCURRENTLY có thể dùng lock level thấp hơn non-concurrent detach, tùy các restriction được document.
Operational pattern hữu ích:
tạo future partition sớm
-> ingest vào bound đã biết
-> quan sát size / query behavior
-> detach old partition
-> archive / transform / drop ngoài hot pathĐừng để lần đầu thiết kế maintenance xảy ra trong sự cố khi insert fail vì partition của ngày mai chưa tồn tại.
8. Constraint cho thấy một ranh giới quan trọng của partitioning
Một hiểu lầm phổ biến là unique index trên từng child partition tự động chứng minh global uniqueness trên toàn logical table.
Điều đó nhìn chung không đúng.
Trong PostgreSQL, UNIQUE hoặc PRIMARY KEY constraint trên partitioned table phải chứa toàn bộ partition-key column; partition key cũng không được dùng expression/function cho constraint loại này. Lý do mang tính cấu trúc: child index trực tiếp enforce uniqueness chỉ trong partition của nó, nên partition layout phải làm duplicate xuyên partition trở nên bất khả thi.
Ví dụ:
CREATE TABLE orders (
tenant_id bigint NOT NULL,
order_id bigint NOT NULL,
created_at timestamptz NOT NULL,
PRIMARY KEY (tenant_id, order_id, created_at)
) PARTITION BY RANGE (created_at);Nếu business invariant thật sự là “order_id một mình phải duy nhất toàn cục vĩnh viễn,” time partition key tạo ra mismatch cần solution tường minh chứ không thể dựa vào local unique index.
Đây là design signal quan trọng:
Placement boundary thay đổi nơi integrity có thể được enforce rẻ và local.
9. Sharding đẩy placement boundary qua nhiều database node
Sharding giống partitioning ở mặt khái niệm nhưng có hậu quả vận hành lớn hơn.
Mỗi shard có thể có storage, index, transaction log, replica, failover state và operational limit riêng.
PostgreSQL core có partitioning và foreign-data mechanism, nhưng application không nên giả định ordinary declarative partitioning tự nhiên biến thành complete distributed-sharding control plane. Routing, rebalance, cross-node semantics và failure handling phụ thuộc kiến trúc hoặc distributed database layer được chọn.
10. Shard key tốt tối đa locality cho dominant transaction
Giả sử SaaS product lưu project, task, comment và permission.
Nếu phần lớn operation scoped theo tenant, tenant_id có thể là shard key mạnh vì co-locate working set của một tenant:
tenant 42 -> shard B
projects
tasks
comments
membershipsCommon request path khi đó có thể ở trong một shard.
Một random project_id shard key có thể dàn row đều, nhưng request load tenant dashboard có thể phải scatter qua nhiều shard.
Đánh giá candidate shard key theo:
- read locality;
- write locality;
- transaction locality;
- join locality;
- expected cardinality;
- skew / hotspot risk;
- tốc độ tăng của tenant hoặc entity;
- khả năng move khi reshard;
- regulatory/geographic constraint.
Even distribution hữu ích, nhưng locality thường là leverage lớn hơn về correctness và cost.
11. Hotspot là failure mode ẩn của “natural” shard key
Shard key có thể cực kỳ dễ route nhưng vẫn phân phối load rất tệ.
Ví dụ:
shard theo country
-> một country tạo 70% traffic
shard theo tenant_id
-> một enterprise tenant tạo 40% write
shard theo date range
-> toàn bộ write mới đổ vào newest shardĐây là hotspot: một placement slice chạm giới hạn CPU, I/O, connection, lock hoặc storage trong khi shard khác gần như nhàn rỗi.
Đừng chỉ đo row-count balance. Hãy đo load balance.
Chiến lược cho “whale tenant” có thể cần dedicated placement, sub-sharding, bucket indirection hoặc migration path cho phép một logical tenant dùng nhiều physical bucket mà không phá application identity.
12. Fan-out biến logical query đơn giản thành distributed work
Khi router không xác định được một shard từ request, hệ thống có thể phải dùng scatter/gather:
query
-> shard A
-> shard B
-> shard C
-> merge / sort / aggregate resultĐây là fan-out hay quét nhiều shard.
Fan-out nhân lên:
- network call;
- tail-latency exposure;
- partial-failure case;
- connection use;
- result merging;
- retry complexity;
- ordering/pagination difficulty.
Query rẻ trên một shard có thể trở nên đắt khi chạy qua 100 shard.
Vì vậy “có route được bằng shard key không?” phải là câu hỏi first-class trong API/data-access review.
13. Transaction liên shard là design smell cần đo, không phải khái niệm bị cấm
Transaction chạm hai shard không tự động sai. Nhưng nó đã vượt local atomicity boundary.
Hệ thống bây giờ phải nói rõ ai coordinate write.
Một số hướng:
- redesign ownership để invariant local trong một shard;
- chấp nhận asynchronous workflow với idempotency và compensation;
- dùng distributed transaction mechanism khi latency/availability/operational đánh đổi hợp lý;
- centralize một global invariant nhỏ vào dedicated authority.
Thiết kế nguy hiểm là cross-shard work xảy ra ngầm phía sau repository method vẫn trông như một local transaction.
Viết invariant trước, rồi quyết định nó có thể co-locate hay không.
14. Global uniqueness đắt hơn qua các shard độc lập
Trong một PostgreSQL partitioned table, uniqueness có partition-key restriction tường minh như phần trước.
Qua các independent shard, local unique index chỉ chứng minh duy nhất bên trong shard đó.
Nếu product cần globally unique human-readable username, slug hoặc external identifier, pattern thường gặp gồm:
- route identifier đó deterministically tới một owning shard;
- reserve nó tại global directory/authority;
- sinh identifier có collision property không cần central lookup;
- scope uniqueness theo tenant/shard nếu product semantics cho phép.
Đừng suy ra global uniqueness từ câu “mỗi shard đều có unique index.”
15. Resharding là một phần của initial design, không phải cleanup về sau
Growth làm thay đổi placement assumption.
Shard từng chứa 10 triệu row có thể về sau quá lớn hoặc quá nóng. Tenant distribution có thể skew. Hardware shape có thể đổi. Region mới có thể được thêm.
Kế hoạch resharding phải trả lời:
ownership mới được biểu diễn thế nào?
read route ra sao trong lúc move?
new write đi đâu trong lúc move?
copied data được verify thế nào?
dual write được tránh hoặc reconcile ra sao?
khi nào old ownership bị fence?
rollback bằng cách nào?Routing directory hoặc virtual-bucket layer có thể làm movement dễ hơn vì logical ownership không cần đồng nhất với physical node identity mãi mãi.
Exact migration protocol phụ thuộc datastore, nhưng bài toán authority transfer tồn tại trong mọi resharding system.
16. Partitioning và sharding có thể kết hợp
Một shard vẫn có thể chứa partitioned table.
Ví dụ:
route theo tenant bucket -> chọn shard
trong shard -> partition events theo tháng
trong monthly partition -> dùng index cho local predicateCách này mạnh vì mỗi layer giải quyết một placement problem khác nhau:
- sharding phân ownership qua nhiều node;
- partitioning tổ chức logical table lớn trong một shard;
- indexing thu hẹp access trong physical slice còn lại.
Nhưng mỗi layer thêm operational state. Dùng ít placement dimension nhất có thể để giải quyết measured constraint.
17. Tình huống production: time partitioning giúp retention nhưng phá tenant locality
Một multi-tenant audit platform lưu hàng tỷ event. Team partition theo tháng vì retention là 13 tháng và drop old monthly partition rất tiện vận hành.
Sau đó gần như mọi interactive request thành:
SELECT ...
FROM events
WHERE tenant_id = $1
ORDER BY occurred_at DESC
LIMIT 100;Nhiều request không có bounded time predicate vì UI hỏi “latest events của tenant này”. Đồng thời một enterprise tenant tạo traffic cao hơn rất nhiều tenant khác.
Team tiếp tục đề xuất shard chỉ theo tháng, với giả định “đã partition theo time thì dùng luôn key đó khi scale out.”
Hậu quả: tenant read fan-out qua nhiều monthly slice, boundary giữa old/current partition làm pagination phức tạp, newest shard trở thành write hotspot, và tenant lớn nhất vẫn có thể thống trị active month. Key tốt cho retention không tạo tenant locality.
Nguyên nhân cốt lõi: một placement dimension bị kỳ vọng giải quyết hai problem khác nhau. Time range rất tốt cho lifecycle management, nhưng dominant online workload lại tenant-scoped và skewed. Thiết kế tối ưu partition maintenance trước khi model request routing và future shard ownership.
Cách khắc phục chuẩn: giữ time partitioning khi nó thật sự đơn giản hóa retention, nhưng thiết kế shard ownership theo dominant transaction/read locality như tenant hoặc virtual tenant bucket. Cho tenant cực lớn một escape hatch, giữ time như secondary partitioning dimension bên trong shard khi hữu ích, và verify bằng EXPLAIN cộng routing telemetry rằng ordinary request chạm đúng số partition/shard dự kiến.
18. Review data placement bằng routing worksheet
Với mỗi important operation, ghi lại:
operation biết placement key? slice chạm tới invariant scope
create tenant task có: tenant_id 1 một tenant
load recent tenant events có: tenant_id 1 shard một tenant
search all tenants không nhiều read-only fan-out
reserve global username global owner 1 authority global uniqueness
monthly retention time boundary partition set lifecycleCách này biến câu “chúng ta dùng sharding” thành concrete evidence.
Cũng nên monitor:
- row/byte trên mỗi partition và shard;
- QPS/write rate mỗi shard;
- top key theo load;
- số partition/shard mỗi request chạm;
- pruning effectiveness;
- fan-out tail latency;
- tần suất cross-shard transaction;
- migration/resharding progress;
- routing error và unknown-key fallback.
Tự kiểm tra: partition theo time có làm tenant query selective không?
Table events partition theo tháng bằng occurred_at.
Query chạy:
SELECT *
FROM events
WHERE tenant_id = 42;Mỗi monthly partition đều có index trên tenant_id.
Application có thể kết luận PostgreSQL sẽ loại bỏ partition và chỉ scan một monthly partition không?
Xem giải thích chi tiết
Không. Partition pruning dựa trên partition bound. Vì query không constrain occurred_at, mọi monthly partition đều có thể chứa row của tenant 42, nên khóa phân vùng không chứng minh được rằng phần lớn partition có thể bỏ qua.
Index tenant_id trên từng partition vẫn có thể làm local scan trong mỗi partition được chạm rẻ hơn, nhưng indexing và partition pruning là hai mechanism khác nhau.
Nếu tenant-scoped query là dominant workload, design cần xem lại time một mình có phải placement dimension phù hợp không, request có thể cung cấp time bound hữu ích không, hoặc tenant-based sharding/partitioning nên được thêm ở layer khác.
Checklist production
- Mục tiêu: ghi rõ partitioning phục vụ query pruning, retention, maintenance, isolation hay measured reason nào khác.
- Khóa phân vùng: chọn key align với important predicate và/hoặc lifecycle boundary.
- Bằng chứng pruning: dùng
EXPLAIN/EXPLAIN ANALYZEđể verify partition không liên quan thật sự được prune. - Index: thiết kế index cho partition còn lại; không nhầm indexing với pruning.
- Partition count: giới hạn planning/metadata/maintenance cost thay vì mặc định càng nhiều partition càng tốt.
- Future partition: automate creation/attachment trước khi incoming row chạm new bound.
- Retention: biến detach/drop/archive thành lifecycle procedure tường minh.
- Uniqueness: verify required unique/primary-key invariant có enforce được với partition key đã chọn không.
- Khóa shard: chọn routing key tối đa locality cho dominant read, write, join và transaction.
- Skew: đo load theo key và shard, không chỉ row count.
- Hotspot escape hatch: định nghĩa cách unusually large key/tenant có thể move hoặc split.
- Fan-out: quan sát số shard mỗi request chạm và budget cho tail latency/partial failure.
- Invariant liên shard: làm distributed coordination tường minh thay vì accidental.
- Global uniqueness: xác định authority chứng minh uniqueness qua các shard.
- Resharding: document authority transfer, routing cutover, verification, rollback và fencing trước khi khẩn cấp.
- Layering: dùng sharding, partitioning và indexing cho các measured problem khác nhau thay vì stack mặc định.
Quy tắc cho agent
Khi đề xuất partitioning hoặc sharding, đừng bắt đầu bằng số partition hay shard. Hãy bắt đầu bằng dominant operation và invariant. Gọi tên placement key, cho thấy mỗi important request route thế nào, nói rõ nó chạm bao nhiêu slice, nhận diện hotspot và global-integrity risk, và giải thích ownership có thể move sau này bằng cách nào. Chỉ xem placement scheme là thành công khi routing/pruning evidence cho thấy nó giảm đúng loại work dự kiến.
Nguồn tham khảo
Database Replication: Suy luận về bản sao, độ trễ và failoverNew
Suy luận về database replication qua WAL shipping, contract commit bất đồng bộ và đồng bộ, replica lag, read routing, replication slot, xung đột hot standby, failover và fencing.
In-Memory Data Stores: Suy luận về độ trễ, bộ nhớ và độ bền dữ liệuNew
Suy luận về in-memory data store qua working set, source-of-truth boundary, eviction, expiration, persistence, replication, hot key, sharding và failure contract.