Bạn có thực sự cần một Database cho Ứng dụng của mình?
Nhiều lập trình viên vội vã thiết lập database phức tạp ngay từ đầu, nhưng thực tế các tệp tin đơn giản vẫn hoạt động cực tốt ở quy mô nhỏ. Các bài kiểm tra hiệu năng cho thấy việc sử dụng bản đồ trong bộ nhớ (in-memory map) hoặc tìm kiếm nhị phân trên tệp tin có thể vượt qua cả SQLite về tốc độ xử lý, hỗ trợ hàng triệu người dùng mà không cần hạ tầng nặng nề.

Bạn có thực sự cần một Database cho Ứng dụng của mình?
Cơ sở dữ liệu (Database) về bản chất chỉ là những tập tin. SQLite là một tập tin duy nhất trên đĩa cứng, còn PostgreSQL là một thư mục các tập tin được quản lý bởi một tiến trình. Mọi database bạn từng sử dụng đều đọc và ghi vào hệ thống tệp tin (filesystem), giống hệt như cách code của bạn thực hiện lệnh open().
Vậy câu hỏi không phải là có nên dùng tập tin hay không, vì bạn luôn đang dùng chúng. Câu hỏi thực sự là: liệu bạn nên dùng các tập tin của database hay tự xây dựng cách lưu trữ riêng? Với nhiều ứng dụng, đặc biệt là ở giai đoạn đầu, câu trả lời có thể là: tự xây dựng.
Dashboard minh họa hiệu năng hệ thống
Thiết lập bài kiểm tra hiệu năng
Để chứng minh giả thuyết này, chúng tôi đã xây dựng cùng một HTTP server bằng ba ngôn ngữ: Go, Bun và Rust. Chúng tôi sử dụng hai chiến lược lưu trữ và thử nghiệm áp lực với wrk để xem kết quả thực tế.
Dữ liệu được lưu trong ba tệp phẳng: users.jsonl, products.jsonl, orders.jsonl. Định dạng sử dụng là JSONL (Newline-delimited JSON), nơi mỗi bản ghi nằm trên một dòng riêng biệt. Chúng tôi tập trung vào endpoint GET /users/:id để đo lường tốc độ đọc dữ liệu.
Các chiến lược lưu trữ
Chúng tôi đã so sánh ba cách tiếp cận khác nhau để xử lý việc đọc dữ liệu:
1. Đọc tệp mỗi lần (Linear Scan)
Đây là cách đơn giản nhất: khi có yêu cầu tìm user, mở tệp, quét từng dòng, phân tích cú pháp JSON và kiểm tra ID.
- Độ phức tạp: O(n). Mỗi yêu cầu đọc toàn bộ tệp từ đầu đến cuối.
- Kết quả: Tệp càng lớn, tốc độ càng chậm. Với 1 triệu bản ghi, Go chỉ xử lý được 23 yêu cầu/giây, trong khi Bun mất hơn 1 giây cho mỗi yêu cầu. Rõ ràng, phương pháp này không khả thi cho quy mô lớn.
2. Tải vào bộ nhớ (In-Memory Map)
Khi khởi động, đọc toàn bộ tệp một lần và lưu trữ mọi bản ghi trong một Hash Map với khóa là ID. Các thao tác ghi sẽ cập nhật cả Map và tệp, còn thao tác đọc chỉ đơn thuần là tra cứu Map.
- Độ phức tạp: O(1) cho bất kỳ quy mô nào.
- Kết quả: Đây là "trần hiệu năng". Với Go, tốc độ đạt khoảng 97.000 req/s. Bun thậm chí còn vượt qua Go khi đạt hơn 106.000 req/s nhờ vào HTTP server được viết bằng Zig và engine JavaScriptCore. Rust dẫn đầu với khoảng 169.000 req/s. Độ trễ trung bình luôn dưới 1 mili-giây.
3. Tìm kiếm nhị phân trên đĩa (Binary Search)
Giải pháp trung gian: sắp xếp tệp dữ liệu theo ID, xây dựng một chỉ mục (index) có độ rộng cố định bên cạnh, và thực hiện tìm kiếm nhị phân.
- Cơ chế: Mỗi lượt tìm kiếm thực hiện khoảng O(log n) lần đọc nhảy, sau đó đọc đúng một bản ghi từ tệp dữ liệu.
- Kết quả: Đáng ngạc nhiên, phương pháp này đã đánh bại SQLite. Với 1 triệu bản ghi, Go sử dụng binary search đạt ~38.866 req/s, cao hơn 1.7 lần so với SQLite (~25.085 req/s). Hiệu năng rất ổn định, chỉ giảm khoảng 15% khi dữ liệu tăng gấp 100 lần.
Bài học từ số liệu
Dưới đây là bảng so sánh tóm tắt về hiệu năng:
| Phương pháp | 10k bản ghi (req/s) | 1M bản ghi (req/s) | Độ trễ trung bình |
|---|---|---|---|
| Go: Linear Scan | 783 | 23 | 1.010ms |
| Go: Binary Search | 45.742 | 38.866 | 1.4ms |
| SQLite (Go) | 26.000 | 25.085 | 2.1ms |
| Go: In-Memory Map | 97.040 | 97.829 | 584µs |
| Rust: In-Memory Map | 163.687 | 169.106 | 221µs |
- SQLite ổn định: Cung cấp hiệu năng cố định (~25.000 req/s) nhờ cấu trúc B-tree, dù dữ liệu có tăng lên. Đây là sự đánh đổi chấp nhận được cho các tính năng phong phú của SQL.
- Rust vượt trội trong quét tuyến tính: Nhanh hơn Go và Bun từ 3-6 lần nhờ chi phí I/O thấp và thư viện
serdetối ưu. - Tìm kiếm nhị phân nhanh và phẳng: Phụ thuộc nhiều vào bộ nhớ đệm (cache) của hệ điều hành. Chỉ mục nhỏ (55MB cho 1M bản ghi) giúp các mức cao của cây tìm kiếm luôn nằm trong RAM.
Đặt con số vào bối cảnh thực tế
Con số 25.000 yêu cầu mỗi giây nghe có vẻ trừu tượng, nhưng hãy xem xét nó tương đương với bao nhiêu người dùng thực.
Giả sử một người dùng kích hoạt khoảng 10 tra cứu database mỗi giờ và tỷ lệ người dùng trực tuyến cùng lúc là 10%. Để đạt mức đỉnh 25.000 req/s, bạn cần khoảng 90 triệu người dùng hoạt động hàng ngày (DAU).
Một ví dụ thực tế hơn: Một ứng dụng SaaS với 10.000 khách hàng trả phí, mỗi người dùng app một lần/ngày, chỉ tạo ra khoảng 3 req/s vào giờ cao điểm. Ngay cả một ứng dụng tiêu dùng với 100.000 DAU cũng chỉ đạt khoảng 30 req/s. Cả hai đều chưa đến gần giới hạn của bất kỳ phương pháp nào được thử nghiệm.
Câu trả lời trung thực cho câu hỏi "Bạn có cần database không?" là: Có thể chưa. Khi nào thực sự cần, một SQLite chạy từ tệp phẳng có thể xử lý 90 triệu DAU trên một máy chủ duy nhất.
Khi nào bạn thực sự cần một Database?
Bạn sẽ vượt quá khả năng của các tệp phẳng (flat files) trong các trường hợp sau:
- Dữ liệu quá lớn cho RAM: Phương pháp
in-memory mapyêu cầu tải mọi thứ vào bộ nhớ. Khi đạt hàng chục triệu bản ghi, bạn cần hàng gigabyte RAM chỉ cho việc đánh index. Database giúp bạn phân trang dữ liệu hiệu quả. - Cần truy vấn theo nhiều trường: Chỉ có tìm kiếm theo ID là nhanh. Nếu cần "tìm tất cả đơn hàng của user X" hoặc "sản phẩm giá dưới 50$", bạn sẽ phải quét toàn bộ tệp hoặc xây dựng nhiều map phức tạp.
- Cần kết nối (Joins): Kết hợp dữ liệu từ users, products và orders trong một phản hồi sẽ rất phức tạp khi dùng tệp tin riêng lẻ, trong khi SQL xử lý việc này cực kỳ hiệu quả.
- Nhiều tiến trình ghi cùng lúc: Cơ chế
RwLockchỉ bảo vệ truy cập đồng thời trong một tiến trình. Khi bạn chạy nhiều instances server, chúng sẽ lệch pha nhau. Bạn cần một nguồn sự thật trung tâm. - Cần ghi nguyên tử (Atomic Writes): Tạo đơn hàng trong khi giảm tồn kho cần thành công cùng lúc hoặc thất bại cùng lúc (ACID). Tự triển khai log giao dịch trên các tệp riêng lẻ là rất rủi ro.
Nhiều công cụ nội bộ, dự án phụ (side projects) và sản phẩm giai đoạn đầu sẽ không bao giờ chạm đến các giới hạn này. Với những ứng dụng đó, cách tiếp cận sử dụng tệp tin hoạt động hoàn hảo. JSONL có thể dễ dàng nhập vào bất kỳ database nào sau này, nên bạn không bị khóa chặt vào một công nghệ.
Đôi khi, sự đơn giản chính là chìa khóa cho sự khởi đầu nhanh chóng.
Bài viết liên quan

Phần mềm
Ra mắt Rail: Ngôn ngữ lập trình tự hosting tích hợp HTTPS thuần túy
18 tháng 4, 2026

Phần mềm
Tương lai "Headless" cho AI cá nhân: Khi giao diện dòng lệnh lên ngôi
18 tháng 4, 2026

Công nghệ
Cursor đàm phán huy động hơn 2 tỷ USD với định giá 50 tỷ USD khi tăng trưởng doanh nghiệp bùng nổ
17 tháng 4, 2026
