Chương 29. Database Quan Hệ Vẫn Là Nền Tảng
Từ phần này, ta đi vào dữ liệu.
Đây là nơi rất nhiều hệ thống bắt đầu đau.
Không phải vì database là thứ cũ kỹ.
Mà vì dữ liệu là nơi sự thật của hệ thống nằm lại.
Code có thể deploy lại.
Server có thể thay.
Queue có thể chạy lại.
Cache có thể xóa.
Nhưng dữ liệu sai thì rất mệt.
Ví dụ:
- Đơn hàng mất item.
- Thanh toán bị ghi hai lần.
- Điểm chấm bài sai học viên.
- User bị cấp nhầm quyền.
- Số dư ví lệch.
- Báo cáo doanh thu sai.
Trong rất nhiều hệ thống thực tế, database quan hệ như PostgreSQL hoặc MySQL vẫn là nền tảng cực kỳ mạnh.
Không phải vì chúng mới nhất.
Mà vì chúng giải quyết rất tốt những việc cốt lõi:
- Lưu dữ liệu có cấu trúc.
- Ràng buộc tính đúng.
- Transaction.
- Query linh hoạt.
- Index.
- Join.
- Backup/restore.
- Replication.
- Công cụ vận hành trưởng thành.
Thông điệp chính của chương:
> Đừng xem database quan hệ là lựa chọn tầm thường. Với phần lớn hệ thống nghiệp vụ, PostgreSQL/MySQL vẫn là điểm bắt đầu rất mạnh và rất thực dụng.
---
29.1. Ví dụ quán bánh: dữ liệu là sổ cái
Quán bánh có thể có nhiều phần mềm:
- Website bán hàng.
- App bếp.
- App giao hàng.
- Trang admin.
- Báo cáo doanh thu.
- Email notification.
Nhưng cuối cùng vẫn phải biết sự thật:
Khách nào đặt đơn nào?
Đơn có những bánh nào?
Đã thanh toán chưa?
Bếp đã làm xong chưa?
Đã giao chưa?
Có hoàn tiền không?
Doanh thu là bao nhiêu?
Những sự thật này thường nằm trong database.
Nếu database ghi sai, mọi thứ phía trên sẽ sai theo.
Ví dụ:
Nếu order item bị mất, bếp làm thiếu bánh.
Nếu payment bị ghi thành công nhầm, kế toán sai.
Nếu delivery status sai, chăm sóc khách hàng xử lý nhầm.
Vì vậy database không chỉ là "chỗ lưu data".
Nó là nơi giữ sự thật vận hành.
---
29.2. Database quan hệ là gì?
Database quan hệ lưu dữ liệu thành bảng.
Ví dụ:
customers
orders
order_items
payments
deliveries
Các bảng có quan hệ với nhau.
Ví dụ:
orders.customer_id -> customers.id
order_items.order_id -> orders.id
payments.order_id -> orders.id
Điểm mạnh của database quan hệ là:
- Dữ liệu rõ cấu trúc.
- Có khóa chính.
- Có khóa ngoại nếu dùng.
- Có constraint.
- Có transaction.
- Có SQL để query linh hoạt.
Với hệ thống nghiệp vụ như bán hàng, học online, booking, payment, AI grading, database quan hệ thường rất hợp.
Vì các hệ thống này có nhiều thực thể liên quan nhau và cần tính đúng.
---
29.3. PostgreSQL/MySQL vì sao vẫn mạnh?
PostgreSQL và MySQL đã tồn tại lâu, nhưng đó là lợi thế.
Chúng có:
- Công cụ vận hành trưởng thành.
- Tài liệu nhiều.
- Người biết dùng nhiều.
- Backup/restore tốt.
- Replication tốt.
- Index tốt.
- Transaction ổn định.
- Hệ sinh thái mạnh.
PostgreSQL đặc biệt mạnh ở:
- SQL phong phú.
- Constraint.
- JSONB nếu cần dữ liệu linh hoạt.
- Index đa dạng.
- Full-text search mức cơ bản.
- Extension.
- Transaction và consistency.
MySQL cũng rất phổ biến, dễ vận hành, hiệu năng tốt trong nhiều workload web.
Điểm chính:
> Nếu chưa có lý do rõ để chọn thứ khác, database quan hệ là lựa chọn mặc định rất tốt.
---
29.4. Database không chỉ lưu, nó còn bảo vệ
Một sai lầm là xem database chỉ như file lưu trữ.
Thực ra database có thể giúp bảo vệ hệ thống khỏi dữ liệu sai.
Ví dụ:
email unique
order_id not null
quantity > 0
payment amount >= 0
foreign key order_items.order_id -> orders.id
unique provider_event_id
Những ràng buộc này gọi là constraint.
Nếu code có bug, database vẫn có thể chặn dữ liệu sai.
Ví dụ:
Hai request cùng tạo cùng một payment với provider_payment_id.
Nếu có unique constraint, database chặn bản ghi trùng.
Nếu không có, dữ liệu trùng lọt vào.
Code tốt là cần thiết.
Nhưng database constraint là lớp bảo vệ cuối rất đáng giá.
---
29.5. Transaction giúp gì?
Transaction giúp nhiều thay đổi cùng thành công hoặc cùng hủy.
Ví dụ tạo order:
Tạo orders
Tạo order_items
Tính total
Ghi outbox event OrderPlaced
Nếu tạo order thành công nhưng order_items fail, dữ liệu sai.
Transaction giúp:
Tất cả thành công -> commit
Có lỗi -> rollback
Với nghiệp vụ quan trọng, transaction là nền tảng.
Ví dụ:
- Tạo đơn hàng.
- Trừ tồn kho.
- Ghi ledger.
- Tạo payment record.
- Cập nhật trạng thái grading job.
Không phải mọi thứ cần một transaction khổng lồ.
Nhưng phần dữ liệu phải đúng cùng nhau nên được bảo vệ bằng transaction.
Chương sau sẽ nói kỹ hơn.
---
29.6. Constraint giúp gì?
Constraint là luật dữ liệu ở tầng database.
Ví dụ:
NOT NULL
UNIQUE
CHECK
FOREIGN KEY
Chúng giúp database từ chối dữ liệu không hợp lệ.
Ví dụ:
quantity > 0
Không cho order item có số lượng âm.
Ví dụ:
unique(user_id, assignment_id, version)
Không cho tạo trùng submission version nếu nghiệp vụ không cho phép.
Ví dụ:
unique(provider_event_id)
Webhook gửi trùng không tạo event trùng.
Constraint không thay thế domain logic.
Nhưng nó làm hệ thống bền hơn nhiều.
---
29.7. Index giúp gì?
Index giúp database tìm dữ liệu nhanh hơn.
Nếu không có index, database có thể phải quét nhiều dòng.
Ví dụ bảng grading_jobs có 10 triệu dòng.
Query:
Tìm job theo student_id và status.
Nếu không có index phù hợp, query có thể rất chậm.
Index giống mục lục sách.
Không có mục lục, muốn tìm một chủ đề phải lật từng trang.
Có mục lục, ta nhảy đến đúng chỗ nhanh hơn.
Nhưng index không miễn phí.
Index làm:
- Đọc nhanh hơn.
- Ghi chậm hơn một chút.
- Tốn dung lượng.
- Cần bảo trì.
Vì vậy index nên dựa trên query thật, không tạo bừa.
---
29.8. Query linh hoạt là lợi thế lớn
SQL cho phép hỏi dữ liệu linh hoạt.
Ví dụ:
Có bao nhiêu bài đang chờ chấm?
Học viên nào nộp bài nhưng chưa có điểm?
Đơn nào đã thanh toán nhưng chưa giao?
Payment nào thành công nhưng order chưa cập nhật?
Job nào retry quá 3 lần?
Trong hệ thống nghiệp vụ, những câu hỏi này xuất hiện liên tục.
Database quan hệ rất mạnh ở việc trả lời các câu hỏi có điều kiện, join, group, sort.
Nếu dùng một database không hỗ trợ query linh hoạt, nhiều câu hỏi vận hành sẽ khó hơn.
Đây là lý do không nên vội bỏ SQL.
---
29.9. Join có xấu không?
Join hay bị hiểu nhầm là chậm hoặc xấu.
Join không xấu.
Join sai hoặc join trên dữ liệu quá lớn không có index mới là vấn đề.
Ví dụ:
orders join customers
order_items join products
grading_jobs join submissions
Đây là nhu cầu bình thường.
Database quan hệ sinh ra để làm việc này.
Nếu query chậm, đừng kết luận ngay:
Join xấu, phải dùng NoSQL.
Hãy kiểm tra:
- Có index đúng không?
- Query plan thế nào?
- Có join quá nhiều bảng không?
- Có lọc dữ liệu trước khi join không?
- Có cần read model riêng không?
- Có đang lấy quá nhiều cột không?
Join là công cụ.
Dùng đúng thì rất hữu ích.
---
29.10. Khi nào database bắt đầu nghẽn?
Database có thể nghẽn vì nhiều lý do.
Ví dụ:
- Query chậm.
- Thiếu index.
- Index sai.
- Ghi quá nhiều.
- Lock chờ nhau.
- Connection quá nhiều.
- Transaction giữ quá lâu.
- Disk I/O cao.
- CPU database cao.
- Bảng quá lớn nhưng query không lọc tốt.
- Report nặng chạy trên database chính.
Khi database nghẽn, cả hệ thống đau.
Vì nhiều phần phụ thuộc vào database.
Nhưng cách xử lý đầu tiên thường không phải đổi database.
Thường là:
- Tìm query chậm.
- Thêm/sửa index.
- Tối ưu query.
- Giảm dữ liệu đọc.
- Pagination.
- Batch job.
- Tách read workload.
- Cache đúng chỗ.
- Giảm transaction dài.
Đổi database là bước lớn, không phải phản xạ đầu tiên.
---
29.11. Tối ưu query trước khi đổi database
Nhiều hệ thống chậm vì query rất bình thường nhưng chưa tối ưu.
Ví dụ:
SELECT * FROM grading_jobs WHERE status = 'PENDING';
Bảng có hàng triệu dòng.
Không có index trên status.
Query chậm.
Giải pháp có thể là index:
index(status, created_at)
hoặc query theo batch:
WHERE status = 'PENDING'
ORDER BY created_at
LIMIT 100
Ví dụ khác:
API danh sách không pagination.
GET /orders
trả 500.000 dòng.
Đổi sang NoSQL không cứu được thiết kế API này.
Phải pagination.
Rất nhiều vấn đề database được giải bằng thiết kế query tốt hơn.
---
29.12. N+1 query là gì?
N+1 là lỗi rất phổ biến.
Ví dụ cần hiển thị 100 orders.
Code làm:
Query 1 lần lấy 100 orders.
Sau đó mỗi order query thêm customer.
Tổng:
1 + 100 = 101 queries.
Nếu thêm items, payments, delivery, số query còn tăng nữa.
N+1 làm API chậm dù mỗi query nhỏ.
Cách xử lý:
- Eager loading.
- Join đúng.
- Batch query.
- Preload.
- Read model.
N+1 là ví dụ rất hay cho việc:
> Vấn đề không nằm ở loại database, mà nằm ở cách dùng database.
---
29.13. Connection pool là gì?
Mỗi request/worker muốn nói chuyện với database thường cần connection.
Connection không vô hạn.
Nếu có quá nhiều web worker/job worker mở connection, database có thể quá tải.
Connection pool quản lý số connection được dùng.
Ví dụ:
web pool: 50 connections
worker-ai pool: 20 connections
worker-report pool: 5 connections
Với job gọi external API, đừng giữ database connection trong lúc chờ API 90 giây.
Ví dụ không tốt:
begin transaction
call Gemini 90 giây
commit
Ví dụ tốt hơn:
load data
release connection
call Gemini
open connection
save result
Database connection là tài nguyên quý.
Đừng giữ nó khi không cần.
---
29.14. NoSQL có xấu không?
Không.
NoSQL có nhiều loại và nhiều trường hợp dùng tốt.
Ví dụ:
- Document database.
- Key-value store.
- Wide-column store.
- Graph database.
- Time-series database.
- Search engine.
Chúng giải quyết các bài toán khác nhau.
Ví dụ:
- Redis rất tốt cho cache.
- Elasticsearch/OpenSearch tốt cho search.
- DynamoDB/Cassandra tốt cho một số workload scale lớn theo key.
- MongoDB có thể hợp với document linh hoạt.
- Neo4j hợp với graph query.
- Time-series DB hợp metrics.
Vấn đề không phải NoSQL xấu.
Vấn đề là dùng NoSQL để né thiết kế dữ liệu khi thật ra dữ liệu cần transaction, relation, constraint.
---
29.15. Sai lầm khi dùng NoSQL để né schema
Một câu hay nghe:
Dữ liệu còn thay đổi, dùng NoSQL cho linh hoạt.
Đôi khi đúng.
Nhưng nhiều khi "linh hoạt" biến thành:
- Không biết field nào tồn tại.
- Dữ liệu mỗi bản ghi một kiểu.
- Không có constraint.
- Query khó.
- Migration khó.
- Bug chỉ lộ khi đọc dữ liệu cũ.
- Logic kiểm tra dồn hết vào application code.
Schema không phải kẻ thù.
Schema là cách ta nói:
> Dữ liệu này có hình dạng và luật rõ ràng.
Với nghiệp vụ như order, payment, grading result, wallet, booking, schema rõ thường là lợi thế.
---
29.16. Sai lầm khi dùng NoSQL để né join
Một lý do khác:
Join chậm, dùng NoSQL.
Nhưng nếu dữ liệu thật sự có quan hệ phức tạp, NoSQL không làm quan hệ biến mất.
Nó chỉ bắt bạn xử lý quan hệ ở application code hoặc denormalize dữ liệu.
Denormalize có thể đúng, nhưng phải chấp nhận:
- Dữ liệu trùng.
- Cập nhật nhiều nơi.
- Eventual consistency.
- Cần sync.
- Cần rebuild read model.
Không sai.
Nhưng đó là trade-off, không phải miễn phí.
Trước khi bỏ join, hãy chắc bạn hiểu query, index, và read model.
---
29.17. Khi nào NoSQL hoặc công cụ khác hợp lý?
Dùng công cụ khác khi có nhu cầu rõ.
Ví dụ:
Cache
Redis/Memcached.
Cần đọc nhanh dữ liệu tạm.
Search
Elasticsearch/OpenSearch/Meilisearch.
Cần full-text search, ranking, autocomplete.
Event streaming
Kafka/Redpanda/Pub/Sub.
Cần log event, replay, data pipeline.
Object storage
S3/GCS/R2.
Cần lưu file lớn.
Time-series
Prometheus/ClickHouse/Timescale tùy trường hợp.
Cần metrics/log/analytics theo thời gian.
Điểm chính:
> Dùng thêm công cụ vì workload cần, không phải vì database quan hệ chưa được tối ưu.
---
29.18. Database quan hệ và JSON
PostgreSQL có JSONB.
MySQL cũng hỗ trợ JSON ở mức nhất định.
Điều này hữu ích khi một phần dữ liệu linh hoạt.
Ví dụ:
grading_jobs
- id
- status
- score
- feedback
- metadata JSONB
Metadata có thể chứa:
model_name
token_usage
provider_request_id
rubric_version
Nhưng đừng nhét toàn bộ domain vào JSON chỉ để khỏi thiết kế bảng.
Dữ liệu quan trọng cần query, constraint, join thường nên có cột/bảng rõ.
JSON tốt cho phần phụ, linh hoạt, ít cần ràng buộc chặt.
Không nên biến database quan hệ thành document store lộn xộn nếu nghiệp vụ cần cấu trúc.
---
29.19. Source of truth là gì?
Source of truth là nơi hệ thống coi là sự thật chính.
Ví dụ:
Order truth: orders database.
Payment truth: payment database/provider + payment records.
Grading truth: grading_jobs/results database.
Search index: không phải truth, chỉ là read model.
Cache: không phải truth, chỉ là bản nhớ tạm.
Analytics warehouse: thường không phải truth vận hành.
Cần biết rõ dữ liệu nào thật sự nằm ở đâu.
Nếu cache khác database, tin ai?
Nếu search index thiếu record, tin ai?
Nếu dashboard analytics chậm, nghiệp vụ chính có sai không?
Không rõ source of truth là nguồn gốc của rất nhiều bug.
---
29.20. Database trong microservices
Trong microservices, một nguyên tắc hay dùng:
> Service nào sở hữu dữ liệu thì service đó quản lý database của nó.
Service khác không nên sửa database trực tiếp.
Ví dụ:
Payment service sở hữu payments/refunds.
Ordering service sở hữu orders/order_items.
Grading service sở hữu grading_jobs/results.
Service khác cần thông tin thì dùng:
- API.
- Event.
- Read model.
- Data pipeline.
Không nên:
Notification service update thẳng orders table.
Analytics service sửa payments table.
Trong monolith, các module có thể chung database vật lý.
Nhưng vẫn nên có ownership rõ.
Chung database không có nghĩa là ai cũng được sửa mọi bảng.
---
29.21. Một database hay nhiều database?
Hệ thống nhỏ thường nên bắt đầu với một database chính.
Lý do:
- Đơn giản.
- Transaction dễ.
- Query dễ.
- Vận hành dễ.
- Backup dễ.
Tách nhiều database khi có lý do:
- Bounded context đã rõ.
- Team/service cần deploy độc lập.
- Workload rất khác.
- Scale độc lập.
- Bảo mật dữ liệu.
- Cô lập lỗi.
Tách database quá sớm làm nhiều thứ khó hơn:
- Không join trực tiếp.
- Transaction phân tán khó.
- Debug khó.
- Dữ liệu đồng bộ trễ.
- Cần event/API/read model.
Một câu nhớ:
> Tách database là quyết định vận hành lớn, không phải cách dọn code cho đẹp.
---
29.22. Database không thay thế domain model
Database giữ dữ liệu.
Domain model giữ luật nghiệp vụ.
Không nên để database schema là nơi duy nhất diễn đạt nghiệp vụ.
Ví dụ:
Database có cột:
order.status
Nhưng luật:
DELIVERED không được chuyển về DRAFT.
PAID hủy thì cần refund.
PREPARING có thể hủy hay không tùy bếp.
Những luật này nên nằm trong domain/use case, và database hỗ trợ bằng constraint phù hợp.
Database mạnh, nhưng không thay thế việc hiểu domain.
---
29.23. Database không thay thế observability
Khi hệ thống chậm, nhiều người nhìn database đầu tiên.
Đúng, database rất quan trọng.
Nhưng phải có quan sát:
- Query nào chậm?
- Index nào được dùng?
- Lock nào đang chờ?
- Connection pool có hết không?
- Transaction nào giữ lâu?
- Table nào tăng nhanh?
- Disk/CPU/I/O thế nào?
Không có observability, tối ưu database trở thành đoán mò.
Giống như worker, database cần dashboard riêng.
---
29.24. Bảng chọn nhanh
| Nhu cầu | Công cụ thường phù hợp | |---|---| | Nghiệp vụ có transaction, quan hệ, constraint | PostgreSQL/MySQL | | Cache tạm để đọc nhanh | Redis/Memcached | | Full-text search/autocomplete/ranking | Search engine | | File lớn | Object storage | | Event log/replay | Event streaming | | Metrics/time-series | Time-series/metrics DB | | Analytics khối lượng lớn | Warehouse/OLAP | | Graph traversal sâu | Graph database |
Database quan hệ vẫn có thể là trung tâm.
Các công cụ khác bổ sung theo workload.
Không phải thay thế toàn bộ.
---
29.25. Những lỗi phổ biến
Lỗi 1: Không dùng constraint
Mọi luật dựa vào code, bug lọt là dữ liệu sai.
Lỗi 2: Thiếu index
Query chậm rồi kết luận database không scale.
Lỗi 3: Không pagination
API trả quá nhiều dữ liệu.
Lỗi 4: N+1 query
Một màn hình tạo hàng trăm query nhỏ.
Lỗi 5: Giữ transaction quá lâu
Gọi API ngoài trong transaction, lock kéo dài.
Lỗi 6: Dùng NoSQL để né schema
Sau này dữ liệu lộn xộn, migration khó.
Lỗi 7: Không rõ source of truth
Cache, search, database, analytics mỗi nơi một kiểu.
Lỗi 8: Tách database quá sớm
Tự tạo distributed system trước khi cần.
---
29.26. Checklist thiết kế database ban đầu
Khi thiết kế dữ liệu, hãy hỏi:
- Dữ liệu chính của domain là gì?
- Bảng nào là source of truth?
- Quan hệ giữa các bảng là gì?
- Field nào bắt buộc
NOT NULL? - Field nào cần unique?
- Có cần foreign key không?
- Có check constraint nào rõ không?
- Query chính là gì?
- Cần index nào cho query chính?
- Có API danh sách nào cần pagination không?
- Có nguy cơ N+1 không?
- Transaction quan trọng nằm ở đâu?
- Có dữ liệu nào chỉ là read model/cache/search không?
- Có cần JSON không, hay nên cột/bảng rõ?
- Có backup/restore chưa?
Trả lời được những câu này, database sẽ chắc hơn rất nhiều.
---
29.27. Tóm tắt bằng AI Judge
Trong AI Judge, database quan hệ có thể lưu:
users
assignments
submissions
grading_jobs
grading_attempts
grading_results
rubrics
classes
enrollments
Những luật cần giữ:
- Submission thuộc đúng student và assignment.
- GradingJob có trạng thái rõ.
- Không tạo trùng job nếu chính sách không cho phép.
- Result thuộc đúng job/submission.
- Attempt count không âm.
- Job đã succeeded không bị ghi kết quả bừa.
Các index có thể cần:
grading_jobs(status, created_at)
grading_jobs(submission_id)
submissions(student_id, assignment_id)
grading_results(submission_id)
Tùy query thật.
Search feedback toàn văn có thể dùng search engine sau.
Analytics chi phí AI có thể sang warehouse sau.
Nhưng source of truth cho bài nộp, job chấm, kết quả chấm vẫn rất hợp với database quan hệ.
---
29.28. Kết luận của chương
Database quan hệ vẫn là nền tảng vì nó giải quyết rất tốt những việc cốt lõi của hệ thống nghiệp vụ:
- Lưu dữ liệu có cấu trúc.
- Giữ tính đúng bằng transaction và constraint.
- Query linh hoạt bằng SQL.
- Tối ưu đọc bằng index.
- Hỗ trợ vận hành bằng backup, replication, tooling trưởng thành.
PostgreSQL/MySQL không phải lựa chọn "cũ" theo nghĩa yếu.
Chúng là lựa chọn thực dụng, mạnh, dễ tuyển người, dễ vận hành, và phù hợp với rất nhiều sản phẩm.
NoSQL, search engine, cache, event streaming, object storage đều có chỗ đứng.
Nhưng chúng nên được đưa vào vì workload cần, không phải vì ta muốn né thiết kế dữ liệu.
Thông điệp quan trọng nhất:
> Trước khi đổi database, hãy chắc rằng bạn đã thiết kế schema, constraint, index, query, transaction và ownership dữ liệu đủ tốt.
Ở chương tiếp theo, ta sẽ đi sâu vào transaction và tính đúng: khi nào cần mọi thứ cùng thành công hoặc cùng hủy, lock là gì, race condition là gì, và vì sao một số nghiệp vụ phải ưu tiên đúng hơn nhanh.