Chọn SQL hay NoSQL cho app .NET
Cách chọn database cho service .NET: khi nào Postgres scale xa hơn bạn tưởng, khi nào cần document store, khi nào shard hợp lý, và read replica giúp được ở đâu.
Mục lục
Chọn database là quyết định thiết kế đắt nhất trong bất kỳ service .NET nào - không phải vì database sai chậm, mà vì chuyển khỏi nó về sau nghĩa là viết lại lớp truy xuất, công cụ migration, setup test, và thường cả mô hình nhất quán. Chương này cho bạn cây quyết định rút gọn thành "PostgreSQL + EF Core" cho 80% service và giải thích chính xác khi nào nên đi chệch.
Khi nào mặc định - PostgreSQL + EF Core - hết đất?
Ba tín hiệu cụ thể.
Write QPS một node chạm trần máy. Postgres hiện đại xử lý
5K-20K write/s thoải mái; với synchronous_commit = off và SSD chạm
50K. Khi đỉnh write QPS từ
chương 2 gần con số đó, database
là điểm nghẽn.
Storage vượt ~1 TB data nóng mỗi node. Postgres xử lý nhiều hơn, nhưng query plan trên bảng khổng lồ khó hơn, vacuum chậm hơn, backup phồng to. Sharding hay columnar storage bắt đầu quan trọng.
Working set vượt RAM. Câu chuyện scale "buồn tẻ" của Postgres dựa vào page cache OS giữ page nóng. Khi working set bằng 10x RAM, mọi query đập đĩa và latency sập.
Nếu không cái nào đúng, bạn không hết Postgres - bạn hết kiên nhẫn hoặc hết marketing.
Ngân sách số nào cho tier database?
Thao tác Latency Chi phí/QPS
Postgres SELECT theo PK ~1-3 ms rẻ
Postgres SELECT 1 join ~5-10 ms vừa
Postgres SELECT có aggregate ~20-200 ms đắt
Postgres INSERT (1 row) ~1-3 ms rẻ
Postgres INSERT (batch 100) ~5-10 ms rẻ hơn theo row
EF Core SaveChanges (batch nhỏ) +~1-2 ms overhead
DynamoDB GetItem ~5-10 ms phẳng
MongoDB findOne theo index ~2-5 ms rẻ
Trực giác chính: engine rẻ, overhead .NET nhỏ, chi phí ở hình dạng query. Một join chưa index trên triệu row đắt hơn lookup cross-collection có index trong MongoDB. Thiết kế schema quan trọng hơn chọn database cho phần lớn service .NET.
Kiến trúc tối thiểu trông thế nào?
flowchart LR
App1[ASP.NET Core 1] --> PG[(Postgres primary<br/>write)]
App2[ASP.NET Core 2] --> PG
App1 -.chỉ đọc.-> RR[(Read replica)]
App2 -.chỉ đọc.-> RR
PG -. replication async .-> RR
Hai replica web tier sau load balancer; một Postgres primary lo write; một read replica phục vụ dashboard và đọc phân tích. Kiến trúc này phủ ~80% service trong ba năm tăng trưởng đầu. Sharding, document store, multi-region đến sau khi bạn đã vượt cái này.
EF Core nối primary + read replica ra sao?
Hai instance DbContext chỉ vào connection string khác nhau:
// Primary - write và read-after-write
builder.Services.AddDbContextPool<AppDbContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("Primary")));
// Context chỉ đọc cho replica
builder.Services.AddDbContextPool<AppReadDbContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("Replica"))
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
// Sử dụng:
public class ProductController(AppDbContext write, AppReadDbContext read) : Controller
{
[HttpGet]
public Task<List<Product>> ListAsync()
=> read.Products.AsNoTracking().ToListAsync(); // đập replica
[HttpPost]
public async Task CreateAsync(Product p)
{
write.Products.Add(p);
await write.SaveChangesAsync(); // đập primary
}
}
Hai quy tắc thiết kế. Một, không bao giờ write qua context đọc - sẽ chạy thầm lặng trên dev và nổ trên prod khi replica chỉ đọc. Hai, ngay sau write, đọc từ primary cho user đó ( cookie stickiness ở chương 3) - replication lag có thật và thấy rõ.
Khi nào nên shard, và tốn gì?
Sharding chia một bảng logic ra nhiều database vật lý theo một key. Đánh đổi rất gắt.
Lợi: throughput write scale tuyến tính theo số shard; storage scale tuyến tính; lỗi cô lập trong một shard.
Hại: mọi query phải có shard key; cross-shard join thành code tầng app; transaction chỉ local; migration schema nhân lên.
Hệ sinh thái .NET tới sharding qua Citus (extension Postgres biến nó thành hệ thống phân tán) hoặc shard thủ công với một DbContext mỗi shard cộng tầng routing. Cả hai nặng. Mặc định trì hoãn shard cho đến khi metric từ chương 13 đòi.
Tier database tạo failure mode nào?
Bốn cái xuất hiện trước:
- Cạn connection pool - request đồng thời nhiều hơn pool size,
tất cả chờ DB. Sửa: tăng
Maximum Pool Sizetrong connection string và xem counterNpgsql; tốt hơn là sửa query chậm. - Đỉnh replication lag - read replica tụt sau primary trong bùng phát write. Sửa: route read-after-write về primary (mẹo cookie); alert khi lag > 5 s.
- Transaction dài chặn vacuum - transaction để mở vài giờ ngăn
Postgres dọn tuple chết; bảng phình, query chậm. Sửa: kill
transaction dài, đừng dùng
IsolationLevel.Serializablecho read-only. - Lock migration -
ALTER TABLEtrên bảng nóng có thể giữ lock exclusive đủ lâu để gây outage. Sửa: pattern migration không downtime (thêm column nullable, backfill async, rồi enforce NOT NULL).
Khi nào NoSQL thật sự là câu trả lời?
Hai trường hợp rõ ràng.
Trường hợp 1: throughput write key nóng vượt sức một node Postgres. Workload key-value phân tán toàn cầu (session cache, counter realtime, IoT telemetry) nơi mọi write độc lập hợp với Cassandra hay DynamoDB hoàn hảo. Postgres cần shard để theo kịp; NoSQL được thiết kế cho điều đó từ ngày một.
Trường hợp 2: schema biến đổi giữa tenant. Một SaaS cho phép
mỗi khách thêm field tuỳ chỉnh kết thúc với cột JSONB trong
Postgres - hoặc trung thực hơn - document trong MongoDB. Schema
chính là data; ép vào row là chèo ngược dòng. Tầng
caching phủ nhu cầu KV nhỏ
ngay cả trước điểm này.
Đi tiếp đâu từ đây?
Chương kế tiếp: message queue cho .NET - khi write database đồng bộ là hình dạng sai và bạn cần đệm qua queue. Sau đó, các khối xây dựng còn lại lắp với tier database này như nhà bền lưu giữ state.
Câu hỏi thường gặp
Vì sao Postgres mặc định mà không phải SQL Server?
Khi nào document store như MongoDB hay Cosmos DB thắng?
Nên thêm read replica trước khi shard không?
Làm sao biết EF Core là điểm nghẽn?
dotnet-counters với Microsoft.EntityFrameworkCore và xem total-commands-executed-rate, total-queries-rate, thời gian SaveChanges. Nếu query latency p99 có thể chấp nhận nhưng wall-clock bị serialisation phía .NET chiếm lĩnh, đổi sang compiled query hoặc AsNoTracking. Nếu query latency cao thì đó là vấn đề DB và cần cache hoặc sửa schema - không phải sửa EF Core.