“SQL hay NoSQL?” nghe giống câu hỏi so sánh sản phẩm, nhưng quyết định đầu tiên là cách ứng dụng đọc và sửa dữ liệu. Checkout phải giữ chỗ tồn kho và ghi thanh toán nguyên tử; session cache thì không. Dashboard quét số liệu một tenant theo thời gian cũng khác màn hình quản trị cần kết hợp khách hàng, hóa đơn và hoàn tiền bằng bộ lọc chưa biết trước.
Vì thế, hãy hỏi: mô hình nào giúp mẫu truy cập quan trọng đúng, ổn định và có chi phí chấp nhận được? SQL tổ chức dữ liệu theo quan hệ và kết hợp truy vấn linh hoạt. NoSQL bao gồm document, key-value và wide-column, mỗi loại có cách đánh đổi riêng.
Lựa chọn tốt bắt đầu từ thao tác, tần suất và yêu cầu đúng đắn, rồi tính thêm index, tăng trưởng, schema, backup, năng lực đội ngũ và chi phí vận hành.
Bắt đầu từ workload và mẫu truy cập
Mẫu truy cập phải cụ thể hơn “đọc người dùng”. Nó mô tả khóa tra cứu, điều kiện lọc, thứ tự, kích thước kết quả, độ mới và hành vi ghi. Với dịch vụ đơn hàng, danh sách có thể gồm:
- Lấy một đơn hàng theo ID duy nhất toàn cục, kèm các dòng hàng và trạng thái thanh toán hiện tại.
- Liệt kê 50 đơn mới nhất của một khách hàng, theo thứ tự mới trước, dùng continuation cursor.
- Thay đổi tồn kho, thanh toán và trạng thái đơn hàng cùng lúc, hoặc không thay đổi gì.
- Tìm các đơn quá hạn chưa thanh toán của mọi khách hàng để lập báo cáo vận hành.
- Tổng hợp doanh thu theo ngày, khu vực và nhóm sản phẩm.
Với từng mẫu, ghi request mỗi giây, kích thước item, retention, tỷ lệ tải đỉnh và tăng trưởng. Đánh dấu dữ liệu cũ có chấp nhận được không, truy vấn là online, phân tích hay background job. Phép scan ổn ở 5 request mỗi giây có thể sụp ở 50.000; báo cáo tháng có thể nên chạy trong warehouse.
Tách đường truy cập đã biết khỏi truy vấn bất định. Key-value và wide-column mạnh khi request được biết trước và bắt đầu bằng partition key. SQL linh hoạt hơn khi bộ lọc, câu hỏi quản trị hay quan hệ tiếp tục thay đổi. Document nằm giữa: truy vấn nội bộ khá phong phú nhưng quan hệ xuyên document kém tự nhiên hơn join.
Tip
Trước khi chọn database, hãy viết mười thao tác quan trọng nhất cùng khóa, bộ lọc, sort, cardinality, latency, consistency và tần suất. Danh sách này hữu ích hơn bảng điểm chung chung.
Workload còn có độ lệch. “Mười triệu tài khoản” chưa đủ nếu một tài khoản nhận nửa lưu lượng. Hash partition vẫn có hot key, hot tenant hoặc điểm nóng do khóa tăng đơn điệu. Hãy kiểm thử phân phối khóa và mức đồng thời giờ cao điểm.
Mô hình hóa cùng một miền theo bốn cách
Giả sử ứng dụng thương mại lưu khách hàng, đơn hàng và các dòng hàng. Schema SQL chuẩn hóa giữ dữ kiện dùng chung ở một nơi và biểu diễn quan hệ rõ ràng:
CREATE TABLE customers (
customer_id UUID PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL
);
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
customer_id UUID NOT NULL REFERENCES customers(customer_id),
status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
total_cents BIGINT NOT NULL CHECK (total_cents >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_lines (
order_id UUID NOT NULL REFERENCES orders(order_id),
line_number INTEGER NOT NULL,
sku TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price_cents BIGINT NOT NULL,
PRIMARY KEY (order_id, line_number)
);
CREATE INDEX orders_by_customer_time
ON orders (customer_id, created_at DESC, order_id DESC);
Index khách hàng-thời gian phục vụ lịch sử có phân trang; join dựng đơn cùng dòng hàng và khách. Foreign key, constraint và transaction bảo vệ invariant. Tên xuất hóa đơn bất biến vẫn có thể được snapshot vì thuộc lịch sử đơn.
Document có thể lưu aggregate theo hình dạng màn hình sử dụng:
{
"_id": "ord_01JZ8K...",
"customerId": "cus_2048",
"customerSnapshot": {
"displayName": "Amina Yusuf",
"billingCountry": "NG"
},
"status": "paid",
"createdAt": "2026-01-26T09:15:00Z",
"lines": [
{ "sku": "KB-75", "quantity": 1, "unitPriceCents": 12900 },
{ "sku": "CAP-BLK", "quantity": 2, "unitPriceCents": 2500 }
],
"totalCents": 17900,
"schemaVersion": 3
}
Một lookup trả toàn bộ đơn, và một update có thể sửa nguyên tử aggregate. Đổi lại, dữ liệu bị lặp và kích thước phải có giới hạn: snapshot không phải dữ liệu khách hiện tại; mảng tăng vô hạn tốn chi phí ghi lại hoặc vượt giới hạn document. Reference, join phía ứng dụng và transaction nhiều document làm giảm lợi thế này.
Key-value tối giản contract: khóa chính là query plan.
SET session:7f31c2 '{"userId":"cus_2048","roles":["buyer"]}' EX 1800
GET session:7f31c2
SET idempotency:checkout:req_91 '{"status":"completed","orderId":"ord_01JZ8K"}' NX EX 86400
GET idempotency:checkout:req_91
Mô hình này hợp session, idempotency, feature flag, counter và cache khi biết đủ khóa. Nó không hợp câu hỏi “tìm đơn đã thanh toán trên 100 đô ở ba khu vực”, trừ khi ứng dụng tự duy trì index phụ và đồng bộ nó ở mỗi lần ghi.
Thiết kế wide-column bắt đầu từ partition và thứ tự clustering. Với lịch sử đơn hàng trong hệ giống Cassandra:
CREATE TABLE orders_by_customer (
customer_id text,
order_day date,
created_at timestamp,
order_id text,
status text,
total_cents bigint,
PRIMARY KEY ((customer_id, order_day), created_at, order_id)
) WITH CLUSTERING ORDER BY (created_at DESC);
Partition key đưa request tới lát dữ liệu có giới hạn; clustering column hỗ trợ range mới trước. Tra cứu bằng order ID cần bảng khác như orders_by_id: mỗi bảng là một access path đã materialize. Pipeline phải cập nhật cả hai, chọn bucket tránh partition khổng lồ; predicate tùy ý không phải thế mạnh.
Tính nhất quán, transaction, join và phi chuẩn hóa
Khi một hành động chạm nhiều bản ghi, hãy hỏi invariant phải đúng ở đâu và lỗi một phần có ý nghĩa gì. Tổng đơn có thể nằm trong một document hoặc transaction SQL. Chống bán trùng ghế cần phối hợp tuần tự quanh ghế; ảnh đại diện chậm một giây thường chấp nhận lan truyền bất đồng bộ.
SQL thường có ACID nhiều dòng, constraint và isolation trưởng thành, phù hợp sổ cái, tồn kho và quyền truy cập. Nhưng replica có thể trễ, isolation level có anomaly khác nhau và transaction xuyên shard có thể đắt. Hãy kiểm tra topology thật.
Document database có thể hỗ trợ transaction; NoSQL phân tán có tunable consistency hoặc conditional write. Quan trọng là ranh giới transaction tự nhiên, ít tốn kém: một document, một key compare-and-set, hoặc một partition. Nếu request thường xuyên phối hợp xuyên partition, mô hình đang chống lại workload.
Join trả chi phí lúc đọc để kết hợp dữ kiện chuẩn hóa; denormalization trả lúc ghi và vận hành để kết hợp trước. Mỗi bản sao cần owner, cơ chế cập nhật, chính sách độ cũ, backfill và đường sửa chữa.
Warning
Đừng diễn giải “NoSQL mở rộng tốt” thành “transaction và join bị cấm”. Các hệ hiện đại có nhiều tính năng giao nhau. Hãy đánh giá bảo đảm cần thiết có cục bộ và rẻ trong mô hình đề xuất hay không, rồi benchmark đúng topology. Tính năng có tồn tại nhưng đòi hỏi phối hợp phân tán ở mọi request có thể là nền tảng sai.
So sánh toàn bộ vòng đời, không chỉ benchmark ngày ra mắt
Bảng sau mô tả thế mạnh điển hình, không phải quy luật tuyệt đối.
| Khía cạnh | SQL quan hệ | Document | Key-value | Wide-column |
|---|---|---|---|---|
| Đơn vị tự nhiên | Dòng và bảng có quan hệ | Document aggregate khép kín | Giá trị opaque theo khóa chính xác | Partition chứa các dòng thưa có thứ tự |
| Mẫu truy cập tốt nhất | Bộ lọc linh hoạt, join, aggregate, range query | Lấy/cập nhật aggregate; index field bên trong | Get/set đúng khóa, TTL, counter, trạng thái kiểu cache | Đọc/ghi lớn theo partition key và khoảng có thứ tự |
| Độ linh hoạt truy vấn | Cao; optimizer kết hợp index và join | Trung bình đến cao trong collection; yếu hơn xuyên aggregate | Thấp nếu không tự duy trì index phụ | Thấp; bảng được thiết kế theo từng truy vấn |
| Vùng transaction thuận lợi | Nhiều dòng/bảng trên một primary hoặc cluster | Field trong một document; xuyên document tùy engine | Một khóa hoặc phạm vi nhỏ tùy engine | Một partition; phối hợp có điều kiện đắt hơn |
| Xử lý quan hệ | Foreign key và join là thành phần hạng nhất | Nhúng phần sở hữu có giới hạn; reference entity dùng chung | Ứng dụng tự quản lý | Sao chép vào bảng riêng cho truy vấn |
| Index | Primary, composite, partial, expression, extension full-text | Primary và secondary, thường hỗ trợ nested/array | Thường là primary key; đôi khi có secondary chuyên biệt | Partition và clustering key; secondary bị giới hạn |
| Mở rộng ngang | Read replica và partition/shard; việc xuyên shard tăng độ phức tạp | Thường có sharding tích hợp; chất lượng shard key quyết định | Hash thường đơn giản; hot key vẫn tồn tại | Thiết kế cho nhiều node và tải ghi duy trì cao |
| Tiến hóa schema | Migration rõ ràng; constraint phơi bày trạng thái sai | Nhiều version document cùng tồn tại, migrate lazy/eager | Ứng dụng quản lý version format của value | Thêm cột dễ; đổi key cần bảng mới |
| Bề mặt vận hành | Backup, vacuum/compaction, replica, failover, query plan | Replica set, cân bằng shard, tăng trưởng document/index | Chính sách RAM/disk, persistence, eviction, clustering | Compaction, repair, tombstone, kích thước partition, consistency |
| Bẫy chi phí phổ biến | Quá nhiều index, join kém, primary quá lớn, replica lag | Dữ liệu lặp, index rộng, document không giới hạn | Lưu dữ liệu lạnh lớn trên RAM đắt; hot key | Read amplification, tombstone, quá nhiều replica, thiếu chuyên môn |
| Lỗi cần diễn tập | Primary failover và rollback migration | Di chuyển shard và workflow nhiều document dở dang | Eviction, mất node và cache stampede | Mất node, repair tồn đọng và projection lệch nhau |
Index đổi chi phí đọc thành ghi và lưu trữ. Composite index SQL phải theo predicate và sort thật; nhiều index một cột không tương đương. Document index trên field lồng hoặc mảng có thể rất lớn. Secondary index phân tán có thể fan-out qua nhiều node. Mỗi index cần access pattern, selectivity đã đo và owner.
Schema linh hoạt chỉ chuyển việc. SQL migration bảng lớn cần add nullable, deploy tương thích, backfill rồi constrain. Document cho nhiều version cùng tồn tại nên reader phải hiểu shape cũ. Key-value cần version; đổi partition key wide-column cần bảng mới.
Managed service giảm vá lỗi; tự quản lý cần trực và khôi phục.
Dùng khung quyết định và di chuyển dữ liệu
Bắt đầu từ invariant và access pattern chủ đạo, rồi loại mô hình khiến chúng gượng ép:
Với hệ mới, chọn mô hình ít chuyên biệt nhất vẫn đáp ứng yêu cầu. SQL thường là mặc định thực dụng vì yêu cầu thay đổi. Chọn mô hình chuyên biệt khi số đo cho thấy lợi thế rõ: exact-key ổn định, throughput ghi rất lớn theo partition hoặc đồng bộ document offline-first.
Với hệ đang chạy, migrate cần bằng chứng: p50/p95/p99, throughput, lỗi, storage, I/O, thời gian kỹ sư, RPO/RTO và chi phí. Thiếu index, ORM chatty, truy vấn không giới hạn, pool kém hoặc transaction lớn thường rẻ hơn thay database.
Khi migrate có căn cứ, hãy đi theo từng access pattern:
- Xác định mô hình đích, invariant, quyền sở hữu và ngưỡng thành công đo được.
- Tạo load test đại diện với độ lệch khóa, kích thước item và ghi đồng thời thực tế.
- Backfill sang đích bằng pipeline idempotent, có thể tiếp tục, kèm checksum và số đếm.
- Stream thay đổi tiếp theo qua outbox hoặc change-data-capture; theo dõi độ trễ.
- Chạy shadow read và so sánh kết quả theo ngữ nghĩa, không chỉ số lượng dòng.
- Chuyển một đường đọc có giới hạn, rồi một đường ghi, sau cơ chế định tuyến có thể đảo ngược.
- Dừng dual write, giữ dữ liệu rollback trong thời hạn thống nhất, chỉ tháo hệ cũ sau khi diễn tập restore thành công.
Dual write trong request nguy hiểm vì hai nơi có thể cho kết quả khác nhau. Transactional outbox gắn thay đổi nguồn và event; consumer replay được sẽ cập nhật đích. Trong giai đoạn cùng tồn tại, phải chỉ rõ system of record để giải quyết xung đột.
Polyglot persistence có thể đúng: SQL cho đơn, key-value cho session, search store cho văn bản, warehouse cho phân tích. Mỗi hệ thêm credential, driver, monitoring, backup, xóa dữ liệu riêng tư, kiểu sự cố và đồng bộ. Chỉ thêm store cho năng lực có ranh giới; ưu tiên projection dựng lại được và một nguồn có thẩm quyền cho mỗi dữ kiện.
Điểm chính cần nhớ
- Chọn từ mẫu truy cập và invariant, không chọn từ nhãn SQL hay NoSQL.
- SQL mạnh khi quan hệ, transaction và truy vấn bất định quan trọng; mở rộng vẫn phụ thuộc topology.
- Document phù hợp aggregate có giới hạn, key-value phù hợp thao tác exact-key, wide-column phù hợp tải theo partition có thứ tự.
- Join trả chi phí lúc đọc; denormalization trả chi phí lúc ghi và vận hành. Mọi bản sao đều cần chính sách nhất quán.
- Index, schema, backup, repair, nhân lực và giá dịch vụ đều thuộc quyết định.
- Benchmark phân phối, hot key, transaction và lỗi thực tế.
- Migrate từng bước với nguồn có thẩm quyền, lan truyền thay đổi replay được, xác minh ngữ nghĩa và rollback.
- Chỉ dùng polyglot khi lợi thế có ranh giới lớn hơn gánh nặng vận hành.