Tại sao cần database — 4 bài toán file/CSV không giải được
Concurrent access, durability, query performance, data integrity. Hiểu vì sao database tồn tại trước khi học SELECT đầu tiên.
Bạn lưu dữ liệu user vào một file CSV. 1.000 user — mọi thứ vẫn ổn. Khi tăng lên 100.000 user, mỗi lần tìm kiếm mất hàng giây — đặc biệt khi đọc line-by-line không streaming, hoặc trên ổ đĩa chậm/cache lạnh. Khi deploy thêm server thứ hai để scale, cả hai process cùng ghi vào file một lúc — race condition xảy ra, một số record bị ghi đè hoặc mất hoàn toàn. Đây không phải lỗi lập trình — đây là giới hạn cơ bản của file.
Bài này giải thích 4 bài toán cốt lõi mà file và CSV không giải được — và cơ chế database dùng để giải từng cái. Hiểu điều này trước khi viết SELECT đầu tiên sẽ giúp bạn ra quyết định đúng khi thiết kế hệ thống.
1. Analogy — Tủ hồ sơ giấy vs thư viện chuyên nghiệp
Hãy tưởng tượng văn phòng có một tủ hồ sơ giấy dùng chung. Bất kỳ ai cũng tự vào lấy file, không có ai kiểm soát. Hai nhân viên cùng lấy hồ sơ khách hàng A ra sửa — một người ghi lại địa chỉ mới, người kia ghi lại số điện thoại mới, ai lưu sau thì thắng, ai lưu trước thì mất thay đổi. Muốn tìm tất cả khách ở Hà Nội? Phải lật từng tờ một. Không có khóa, không có index, không có ai đảm bảo dữ liệu nhất quán.
Database giống thư viện chuyên nghiệp: có thủ thư (query engine), có mục lục (index), có quy trình mượn sách (transaction), có két sắt lưu trữ (WAL + fsync).
| Đời thường | Concept database |
|---|---|
| Tủ hồ sơ không khóa | File/CSV trên disk |
| Hai người cùng sửa 1 hồ sơ | Race condition (concurrent write) |
| Lật từng tờ tìm địa chỉ | Full table scan O(n) |
| Mục lục thư viện | Index — O(log n) lookup |
| Thủ thư kiểm soát ai được lấy gì | Row-level lock |
| Ghi vào sổ trước khi lưu hồ sơ | Write-Ahead Log (WAL) |
| Thủ tục mượn/trả nguyên bộ | Transaction atomicity |
| Thủ thư photo từng bản cho khách đọc đồng thời | MVCC — mỗi transaction thấy snapshot riêng, reader không block writer |
Database không phải chỉ là nơi lưu dữ liệu — nó là hệ thống quản lý truy cập đồng thời, đảm bảo bền vững, và tìm kiếm hiệu quả. CSV chỉ làm được phần đầu tiên.
2. Concurrent access — vì sao file lock không scale
Khi hai process cùng mở một file để ghi, hệ điều hành không tự động ngăn xung đột. Bạn có thể dùng file lock (flock trên Linux), nhưng lock ở mức file nghĩa là toàn bộ file bị block — chỉ một writer tại một thời điểm. Với 100 request/giây, 99 request phải chờ.
Database dùng row-level locking: chỉ lock đúng hàng đang được sửa, các hàng khác vẫn đọc và ghi bình thường. PostgreSQL còn tiến xa hơn với MVCC (Multi-Version Concurrency Control) — reader không cần lock gì cả, vì mỗi transaction thấy một snapshot nhất quán tại thời điểm nó bắt đầu.
-- Hai session chay dong thoi:
-- Session A update user 1, Session B update user 2 -> khong block nhau.
-- Session A update user 1, Session B cung update user 1 -> B doi A xong.
BEGIN;
UPDATE users SET email = '[email protected]' WHERE id = 1;
-- Hang id=1 bi lock. Hang id=2, 3, ... van free.
COMMIT;
3. Durability — dữ liệu không mất khi server crash
Khi bạn ghi vào file và gọi write(), dữ liệu thường chỉ nằm trong buffer của kernel — chưa thực sự xuống disk. Nếu server mất điện lúc này, dữ liệu mất.
Database giải quyết bằng Write-Ahead Log (WAL): trước khi thay đổi dữ liệu thực, database ghi log vào một file tuần tự trước và gọi fsync() để đảm bảo log đã xuống disk. Nếu crash xảy ra, database replay log để phục hồi đúng trạng thái. Đây là lý do PostgreSQL có thể đảm bảo "committed transaction sẽ không bao giờ bị mất" — gọi là durability trong ACID.
Chúng ta sẽ đi sâu vào cơ chế WAL và MVCC ở Module 11 khi học về observability và operations. Ở đây chỉ cần nhớ: COMMIT thành công nghĩa là dữ liệu đã an toàn trên disk.
4. Query performance — O(n) vs O(log n)
File CSV không có cấu trúc tìm kiếm. Muốn tìm user có email = '[email protected]', bạn phải đọc từ dòng 1 đến dòng cuối — full scan O(n). Với 1 triệu user, đó là 1 triệu phép so sánh.
Database dùng index — thường là B-tree — để tổ chức dữ liệu như cây nhiều nhánh cân bằng (mỗi node chứa hàng trăm key, lưu trong 1 page 8KB của Postgres). Tìm kiếm trên indexed column chỉ cần O(log n) bước. PostgreSQL B-tree với 1 triệu row chỉ cần 3-4 cấp (branching factor ~400 per page) — tìm O(log n) thực tế chỉ vài lần đọc disk.
-- Khong co index: full scan qua toan bo bang.
SELECT * FROM users WHERE email = '[email protected]';
-- Tao index: B-tree tren cot email.
CREATE INDEX idx_users_email ON users (email);
-- Query tuong tu, gio dung index: 3-4 cap B-tree thay vi 1 trieu buoc.
SELECT * FROM users WHERE email = '[email protected]';
Khi bảng có 10 hàng, index không tạo ra sự khác biệt đáng kể. Khi có 10 triệu hàng, index là sự khác biệt giữa 10ms và 30 giây.
5. Data integrity — ràng buộc và tính nguyên tử
File CSV không có cơ chế nào ngăn bạn lưu age = -5, để trống email, hoặc tham chiếu đến một user_id không tồn tại. Dữ liệu bẩn tích lũy dần, và bug xuất hiện ở nơi bạn không ngờ tới.
Database cung cấp hai lớp bảo vệ:
Constraints — quy tắc ràng buộc tại mức schema, database từ chối ghi nếu vi phạm:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id), -- foreign key
total_cents INT CHECK (total_cents > 0), -- no negative total
status TEXT NOT NULL DEFAULT 'pending'
);
-- INSERT nay se bi tu choi vi user_id 999 khong ton tai:
INSERT INTO orders (user_id, total_cents) VALUES (999, 5000);
-- ERROR: insert or update on table "orders" violates foreign key constraint
Transaction atomicity — nhiều thao tác gộp lại thành một đơn vị "tất cả hoặc không có gì":
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;
-- Neu server crash giua chung: ca hai UPDATE deu bi rollback.
-- Khong bao gio co trang thai "tru roi nhung chua cong".
6. Pitfall — "JSON file thay database"
Dùng JSON file như database là pattern phổ biến trong script nhỏ và prototype. Nó hoạt động tốt với dưới 1.000 record và một writer duy nhất. Nó thất bại ở quy mô lớn hơn hoặc khi có nhiều process cùng ghi.
Pattern sai phổ biến trong Node.js:
// WRONG: race condition khi nhieu request den cung luc.
// Request A doc file -> chinh sua -> ghi lai.
// Request B doc file (truoc khi A ghi) -> chinh sua -> ghi lai.
// Ket qua: thay doi cua A bi mat hoan toan.
const data = JSON.parse(fs.readFileSync('db.json', 'utf8'));
data.users.push(newUser);
fs.writeFileSync('db.json', JSON.stringify(data));
Fix đúng là dùng database transaction:
-- Atomic insert: khong co race condition.
-- Database dam bao chi mot writer thay doi hang tai mot thoi diem.
INSERT INTO users (name, email, created_at)
VALUES ('Alice', '[email protected]', NOW());
JSON file vẫn hợp lý cho: config đọc-once lúc khởi động, cache tĩnh không cập nhật runtime, hoặc dữ liệu do một process duy nhất ghi theo lịch trình. Khi có hai process trở lên ghi đồng thời, hoặc khi dữ liệu cần query phức tạp, đã đến lúc chuyển sang database.
7. Applied — Khi nào chọn PostgreSQL, khi nào SQLite vẫn đủ
Hãy lấy một ví dụ thực tế: một e-commerce listing API với 50.000 sản phẩm. Mỗi giây có hàng chục request tìm kiếm theo category, lọc theo giá, và ghi log view — đồng thời seller cập nhật tồn kho. Đây là workload multi-writer điển hình: cần row-level locking để các seller không ghi đè nhau, cần index B-tree để query lọc giá chạy nhanh, cần WAL để không mất đơn hàng khi deploy. PostgreSQL giải được cả 4 bài toán đồng thời.
Ví dụ gần nhất bạn đang dùng là chính nền tảng OLHub này — chạy trên PostgreSQL vì có nhiều user đồng thời đọc bài học, ghi progress, và post comment, kết nối qua Prisma để tách biệt app và database layer.
SQLite vẫn sống khỏe vì nó giải quyết đúng bài toán của mình: single-writer embedded. Discourse forum dùng SQLite cho development và small-scale deployment. Nhiều ứng dụng desktop Linux (Firefox, Thunderbird, nhiều app GNOME) dùng SQLite làm storage nội bộ. Nếu ứng dụng của bạn chỉ có một process ghi vào storage local — SQLite là lựa chọn xuất sắc, không cần deploy database server riêng.
Quyết định không phải "PostgreSQL vs SQLite" mà là: bao nhiêu writer đồng thời, dữ liệu ở đâu, query phức tạp đến đâu?
8. Deep Dive — Database fundamentals
Spec / reference chính thức:
- Architecture of a Database System — Hellerstein et al. (2007) — Paper 116 trang tổng quan kiến trúc DBMS từ Berkeley. Đọc Section 1-2 trước để có mental model về process model, storage, query execution. Phần còn lại đọc khi cần đi sâu vào từng component.
- Designing Data-Intensive Applications — Ch.3 Storage and Retrieval — Kleppmann giải thích vì sao database dùng cấu trúc đặc biệt (B-tree, LSM-tree, SSTables) thay vì ghi thẳng. Đây là nền tảng để hiểu DDIA Chapter 5-6 (Replication & Partitioning trong cuốn sách) — và liên quan trực tiếp khi bạn học Module 10 (Scaling & Replication) của khoá này.
- PostgreSQL Documentation — What is PostgreSQL? — Tuyên bố chính thức ngắn gọn về mục đích thiết kế của PostgreSQL. Đọc trong 5 phút để biết PostgreSQL tự định nghĩa mình như thế nào.
Ghi chú: Bắt đầu với PostgreSQL docs để nắm quick overview, sau đó đọc Hellerstein Section 1-2 để hiểu tại sao database được thiết kế theo cách đó. DDIA Ch.3 đọc song song hoặc sau khi học Module 5 (Indexing Internals) sẽ hợp lý nhất.
9. Tóm tắt
- Concurrent access: File lock block toàn bộ file — database dùng row-level lock và MVCC để nhiều writer hoạt động đồng thời mà không xung đột.
- Durability: Write-Ahead Log (WAL) +
fsyncđảm bảo committed transaction không bao giờ mất dù server crash — file thường chỉ ghi vào kernel buffer. - Query performance: Không có index thì tìm kiếm là O(n) full scan — B-tree index giảm xuống O(log n), tức từ hàng triệu bước còn 3-4 lần đọc disk.
- Data integrity: Constraints (NOT NULL, CHECK, FOREIGN KEY) và transaction atomicity ngăn dữ liệu bẩn ngay tại mức lưu trữ, không cần validate lại ở application layer.
- JSON/CSV file vẫn hợp lý cho single-writer, small-scale, hoặc config đọc-once — không phải mọi thứ đều cần database.
- SQLite giải quyết tốt bài toán single-writer embedded (desktop app, local tool) — không cần PostgreSQL cho mọi use case.
- Database tồn tại vì file không thể giải đồng thời cả 4 bài toán trên ở quy mô production.
10. Tự kiểm tra
Q1Vì sao mỗi trong 4 bài toán (concurrent access, durability, query performance, data integrity) không thể giải bằng cách thêm code ở application layer thay vì dùng database?▸
- Concurrent access: Application code có thể đọc-sửa-ghi, nhưng giữa bước đọc và ghi vẫn có race condition window. Nhiều process/server không chia sẻ lock — file lock chỉ hoạt động trong cùng một máy và block toàn bộ file. Không có cơ chế app-level nào đạt row-level granularity an toàn trên multi-server.
- Durability: Application gọi
write()nhưng không kiểm soát được khi nào kernel flush xuống disk. Để đảm bảo fsync đúng lúc, app phải tự implement WAL — đó chính là reinvent the wheel. Database đã làm điều này đúng và đã được battle-tested. - Query performance: App có thể tự build index trong memory, nhưng index đó không persist, không shared giữa các process, và phải rebuild mỗi lần restart. Một B-tree index trong database persist trên disk, shared cho mọi connection, và tự cập nhật khi data thay đổi.
- Data integrity: Validate ở application layer có thể bị bypass (direct DB access, migration script, bug). Constraint ở database là last line of defense — không có code path nào tránh được. Transaction atomicity cũng không thể tái tạo đáng tin cậy ở app layer khi có nhiều writer.
Q2Vì sao file CSV không scale khi nhiều process ghi đồng thời? Cơ chế nào database dùng để tránh xung đột?▸
File CSV không có cơ chế locking nội tại. Khi dùng file lock (flock), toàn bộ file bị block — chỉ một writer tại một thời điểm. Với nhiều request đồng thời, các request phải xếp hàng chờ, throughput sụt mạnh. Tệ hơn, nếu không có lock, hai process cùng đọc-sửa-ghi sẽ gây race condition: thay đổi của một process bị ghi đè bởi process kia.
Database dùng row-level locking: chỉ lock đúng hàng đang sửa, các hàng khác vẫn đọc/ghi tự do. PostgreSQL còn dùng MVCC — reader không cần lock, thấy snapshot nhất quán tại thời điểm transaction bắt đầu, không bị block bởi writer.
Q3Phân biệt durability vs persistence — mỗi khái niệm một dòng.▸
Persistence: dữ liệu được lưu lại giữa các lần chạy chương trình — file CSV, JSON, và database đều có persistence.
Durability: dữ liệu đã commit không bao giờ mất dù crash xảy ra bất kỳ lúc nào — chỉ database với WAL và fsync mới đảm bảo được điều này; file thông thường có thể mất dữ liệu đang trong kernel buffer.
Q4Khi nào nên KHÔNG dùng database đầy đủ — và JSON file hoặc SQLite vẫn là lựa chọn đúng?▸
Không cần database đầy đủ khi: chỉ có một process ghi duy nhất, dữ liệu nhỏ (dưới vài nghìn record), query đơn giản (chỉ đọc toàn bộ hoặc lọc theo key), hoặc ứng dụng chạy embedded không thể deploy server riêng.
- JSON file: phù hợp cho config đọc-once lúc khởi động, feature flags tĩnh, hoặc cache không cập nhật runtime.
- SQLite: phù hợp cho desktop app, CLI tool, test fixture, hoặc small-scale single-user app — có đủ ACID nhưng không cần network layer.
Dấu hiệu cần chuyển lên PostgreSQL: xuất hiện writer thứ hai, dữ liệu vượt vài chục MB, hoặc cần query JOIN/aggregation phức tạp.
Q5Bạn đang viết script analytics chạy một lần, đọc file CSV 50 MB và tính tổng theo nhóm. Có cần database không? Vì sao?▸
Không cần. Script chạy một lần, single process, chỉ đọc — không có concurrent access, không cần durability (kết quả tính xong thì xong), không cần persist state phức tạp.
50 MB CSV hoàn toàn đọc được trong memory (dưới 1 giây trên máy hiện đại). Công cụ phù hợp hơn là pandas, DuckDB (in-process analytics), hoặc đơn giản là awk nếu logic đơn giản. Đưa vào PostgreSQL chỉ tốn thêm thời gian import và không mang lại lợi ích gì cho use case này.
Database phù hợp khi cần: lưu trữ lâu dài, nhiều user truy vấn cùng lúc, hoặc dữ liệu thay đổi liên tục. One-shot analytics script không có bất kỳ yêu cầu nào trong số này.
Bài tiếp theo: Relational model — Codd's 12 rules
Bài này có giúp bạn hiểu bản chất không?