Bài trước thấy rollback journal chép trang cũ ra một tệp phụ rồi sửa file chính tại chỗ — an toàn, nhưng có một hệ quả: trong lúc người ghi đang sửa file chính, người đọc không thể thấy một trạng thái nhất quán. WAL (Write-Ahead Log) lật ngược hẳn ý tưởng đó, và đổi lại là một tính chất mà rollback journal không có: người đọc và người ghi chạy song song. Bài này đo cơ chế WAL qua chính các tệp nó tạo, và đo khác biệt đồng thời cùng hiệu năng trong container sqlite 3.45.3.
Lật ngược: ghi trang MỚI ra tệp riêng, để file chính yên
Ý tưởng WAL ngược với rollback journal. Thay vì chép trang cũ ra rồi sửa file chính, WAL ghi các trang MỚI vào một tệp phụ tên -wal và không đụng gì tới file chính cho tới khi checkpoint. Người đọc muốn một trang thì nhìn vào -wal trước (phiên bản mới nhất ở đó); nếu không có thì lấy từ file chính. Một tệp thứ hai, -shm (shared memory), là chỉ mục trỏ nhanh tới vị trí mỗi trang trong -wal.
Đo thật: bật WAL, tắt checkpoint tự động, chèn 50.000 dòng rồi commit — và giữ kết nối mở để xem các tệp trước khi checkpoint:
sau COMMIT 50 000 dòng, TRƯỚC checkpoint (kết nối còn mở):
w.db : 4096 B ← file chính gần như KHÔNG đổi (1 trang)
w.db-wal : 1 104 192 B ← toàn bộ 50 000 dòng MỚI nằm ở đây (~1,1 MB)
w.db-shm : 32 768 B ← chỉ mục bộ nhớ chia sẻ
File chính đứng yên ở 4096 byte trong khi tệp -wal phình lên 1,1 MB ôm trọn dữ liệu mới. Người ghi chỉ nối thêm vào cuối -wal — một thao tác ghi tuần tự, nhanh — chứ không rải rác sửa khắp file chính. Đây là khác biệt cốt lõi với rollback journal.
Checkpoint: dồn WAL trở lại file chính
Dữ liệu không thể nằm mãi trong -wal. Định kỳ, một thao tác checkpoint sao các trang từ -wal trở vào file chính, rồi cắt -wal đi:
sau PRAGMA wal_checkpoint(TRUNCATE):
w.db : 1 089 536 B ← GIỜ mới lớn lên (data đã dồn vào)
w.db-wal : 0 B ← đã cắt về 0
Giờ file chính mới mang dữ liệu, và -wal rỗng, sẵn sàng cho vòng ghi sau. Mặc định SQLite tự checkpoint khi -wal đạt khoảng 1000 trang (wal_autocheckpoint), nên bình thường bạn không phải gọi tay. Nhưng nếu luôn có người đọc giữ kết nối, checkpoint bị hoãn và -wal có thể phình to — một điểm cần canh trong hệ nhiều người đọc.

Hình 1: Rollback journal sửa file chính tại chỗ (trái); WAL ghi trang mới ra tệp -wal và để file chính yên tới checkpoint (phải). Số đo: sau 50.000 dòng, file chính 4096 B còn -wal 1,1 MB.
Đo điều WAL làm được mà rollback không: đọc–ghi song song
Vì người ghi nối vào -wal chứ không sửa file chính, người đọc vẫn thấy file chính (cộng phần -wal tới thời điểm họ bắt đầu) một cách nhất quán — không bị người ghi chặn, và không chặn người ghi. So trực tiếp hai chế độ bằng cách mở một người đọc giữ giao dịch, rồi cho một người ghi thử chèn (đặt busy_timeout=0 để thấy ngay có bị chặn không):
[DELETE] người đọc đang mở → người ghi: BỊ CHẶN → "database is locked" (5)
[WAL] người đọc đang mở → người ghi: THÀNH CÔNG (không bị chặn)
Ở chế độ DELETE (rollback), người ghi cần khóa độc quyền file chính, mà người đọc đang giữ khóa chia sẻ — nên người ghi nhận lỗi database is locked và phải chờ. Ở chế độ WAL, người ghi nối vào -wal nên không xung đột với người đọc: nó thành công ngay. Đây là lý do WAL gần như bắt buộc cho ứng dụng có nhiều luồng/tiến trình vừa đọc vừa ghi — ví dụ một server web nhúng SQLite. (Lưu ý: WAL cho nhiều người đọc đồng thời với một người ghi; vẫn chỉ một người ghi tại một thời điểm.)
Và ghi cũng nhanh hơn
Ngoài đồng thời, WAL thường ghi nhanh hơn vì mỗi commit chỉ cần fsync tệp -wal (một lần, ghi tuần tự nối đuôi) thay vì cặp fsync journal-rồi-db của rollback. Đo 3000 giao dịch nhỏ riêng lẻ:
DELETE (rollback): 3000 giao dịch nhỏ = 2170 ms
WAL : 3000 giao dịch nhỏ = 581 ms ← nhanh hơn ~3,7 lần
Cùng dữ liệu, cùng mức an toàn, khác biệt đến từ cơ chế ghi tuần tự một-fsync của WAL. Với tải nhiều giao dịch nhỏ — đúng kiểu của ứng dụng tương tác — WAL thắng cả về thông lượng lẫn độ trễ.

Hình 2: WAL cho người đọc và người ghi chạy song song trong khi DELETE chặn (trên), và ghi nhanh hơn ~3,7 lần nhờ một-fsync tuần tự (dưới) — đo trong container sqlite 3.45.3.
Cái giá của WAL
WAL không miễn phí. Nó dùng bộ nhớ chia sẻ (-shm) nên không chạy được trên hệ tệp mạng (NFS) nơi nhiều máy mở cùng file — một giới hạn thật nếu đặt file SQLite trên ổ chia sẻ. Tệp -wal có thể phình to nếu checkpoint bị hoãn vì luôn có người đọc. Và CSDL giờ là ba tệp (.db, -wal, -shm) thay vì một, nên sao lưu bằng cách copy mỗi file .db lúc đang mở WAL là sai — phải checkpoint trước hoặc dùng công cụ sao lưu đúng cách. Dù vậy, với phần lớn ứng dụng một máy có đọc–ghi đồng thời, WAL là lựa chọn nên bật, và chỉ cần một lệnh: PRAGMA journal_mode=WAL.
Vì sao điều này quan trọng
WAL là nâng cấp đồng thời quan trọng nhất của SQLite, và ta vừa đo cả hai mặt của nó: cơ chế (trang mới vào -wal, file chính yên, checkpoint dồn lại) và lợi ích (đọc–ghi song song, ghi nhanh hơn ~3,7 lần). Với một server nhúng SQLite phục vụ nhiều yêu cầu, bật WAL thường là khác biệt giữa tắc nghẽn database is locked và chạy mượt. Nhưng WAL chỉ nới lỏng xung đột đọc–ghi, không xóa bỏ chuyện một người ghi tại một thời điểm. Khi hai người cùng muốn ghi, hoặc một giao dịch cần giữ khóa lâu, cơ chế khóa và các loại giao dịch của SQLite quyết định ai chờ ai và khi nào nhận SQLITE_BUSY. Bài sau đo đúng các tình huống tranh chấp đó.