Câu hỏi "query nào chậm" chỉ trả lời được một nửa. Nửa còn lại, thường quan trọng hơn, là vì sao nó chậm — nó đang chờ cái gì? Chờ một khoá? Chờ đĩa trả dữ liệu? Chờ ghi WAL? PostgreSQL trả lời câu này qua cột wait_event trong pg_stat_activity. Bài trước ta dùng nó để tìm phiên bị chặn; bài này khai thác nó thành một công cụ mạnh hơn: lấy mẫu wait event nhiều lần để thấy toàn hệ thống đang chờ ở đâu — một profiler nghèo nhưng cực hữu dụng.
Ý tưởng: lấy mẫu để biết thời gian đi đâu
Một ảnh chụp wait_event chỉ cho biết một khoảnh khắc. Nhưng nếu bạn lấy mẫu liên tục (mỗi 50–100 ms) và đếm phân bố, bức tranh hiện ra: nhóm wait_event nào chiếm nhiều mẫu nhất chính là nơi hệ thống tiêu tốn thời gian chờ — tức nút thắt.
-- Lấy mẫu này nhiều lần rồi đếm:
SELECT coalesce(wait_event_type || '/' || wait_event, 'CPU')
FROM pg_stat_activity
WHERE state = 'active' AND backend_type = 'client backend';
-- wait_event NULL nghĩa là đang thật sự chạy trên CPU, không chờ gì

Hình 1: Lấy mẫu wait_event liên tục để thấy thời gian đi đâu. Các nhóm chính: Lock (khoá nặng), LWLock (khoá nhẹ nội bộ), IO (đĩa), Client (chờ client). Nhóm chiếm nhiều mẫu = nút thắt; bỏ qua ClientRead.
Đo thật: một tải ghi bị nghẽn ở WAL
Tôi chạy pgbench ghi nặng (16 client) rồi lấy mẫu wait_event khoảng 40 lần trong vài giây. Phân bố thật:

Hình 2: Phân bố wait_event dưới tải ghi 16 client: sau khi bỏ Client/ClientRead (273, chỉ là chờ client), nút thắt là WAL — LWLock/WALWrite 117 và IO/WALSync 31. Cùng IO/DataFileRead khi quét bảng lớn và Lock/transactionid khi hai phiên tranh khoá.
Con số thật, sau khi loại Client/ClientRead (273 mẫu — chỉ là chờ ứng dụng gửi lệnh, không phải nút thắt):
- LWLock/WALWrite (117) + IO/WALSync (31): đây là nút thắt thật — hệ thống dành phần lớn thời gian chờ ghi WAL. Đúng bản chất của một tải ghi nặng.
- CPU (62): đang thật sự làm việc.
- Lock/transactionid (30): tranh chấp khoá hàng giữa các giao dịch đồng thời.
- IO/DataFileRead (2): đọc heap từ đĩa (ít, vì dữ liệu phần lớn đã trong cache).
Bức tranh rõ ngay: tải này nghẽn ở WAL. Muốn tăng tốc, hướng đúng là chỉnh cấu hình WAL, gộp commit, hoặc dùng đĩa WAL nhanh hơn — chứ không phải tối ưu từng query hay thêm index. Wait events cho bạn hướng tối ưu, thay vì đoán mò.
Đọc từng nhóm wait event
IO/DataFileRead — đọc trang dữ liệu từ đĩa. Tôi quét bảng pgbench_accounts 925 MB (lớn hơn shared_buffers 128 MB nhiều), và IO/DataFileRead xuất hiện — vì cache không chứa nổi cả bảng, PostgreSQL phải đọc đĩa. Nhóm này chiếm nhiều nghĩa là thiếu RAM/cache, hướng tối ưu là tăng shared_buffers hoặc RAM.
Lock/transactionid — chờ khoá hàng nặng. Tôi dựng hai phiên cùng UPDATE hàng id=1; phiên thứ hai hiện wait_event = Lock/transactionid, đang chờ giao dịch kia nhả khoá. Nhóm này nhiều nghĩa là tranh chấp khoá — hướng tối ưu là giảm giao dịch dài, đổi thứ tự truy cập, hoặc thiết kế lại để bớt đụng cùng hàng.
LWLock — khoá nhẹ nội bộ (buffer WAL, ánh xạ buffer). Nhiều nghĩa là tranh chấp cấu trúc nội bộ, thường do tải rất cao trên một điểm nóng.
Đánh đổi cần cân nhắc
Lấy mẫu là gần đúng, không chính xác tuyệt đối. Đây là phương pháp thống kê: mẫu càng nhiều càng đáng tin, nhưng vẫn có nhiễu. Với chẩn đoán nghiêm túc trên production, dùng extension pg_wait_sampling (lấy mẫu tự động ở tần số cao, lưu lịch sử) thay vì tự vòng lặp psql — nó cho phân bố chính xác hơn nhiều và không tốn công thủ công.
Bỏ Client/ClientRead khỏi phân tích. Đây là bẫy phổ biến: ClientRead thường chiếm nhiều nhất, nhưng nó chỉ nghĩa là phiên đang chờ ứng dụng gửi câu lệnh tiếp theo — kết nối rảnh, không phải database chậm. Đưa nó vào làm lệch hẳn kết luận. Luôn loại nó (và các trạng thái idle) khi tìm nút thắt của database.
Wait event chỉ ra hướng, không phải giải pháp cụ thể. Biết "nghẽn WAL" là bước đầu; giải pháp còn tuỳ (đĩa nhanh hơn, wal_compression, gộp commit, synchronous_commit phù hợp — các bài trước đã đo). Wait events thu hẹp không gian tìm kiếm, giúp bạn không tối ưu nhầm chỗ, nhưng vẫn cần hiểu cơ chế để chọn cách chữa.
Ba ý mang về
- Wait events cho biết mỗi phiên đang CHỜ gì, và lấy mẫu nhiều lần rồi đếm phân bố biến chúng thành một profiler cho database: nhóm chiếm nhiều mẫu nhất chính là nút thắt — mạnh hơn câu hỏi "query nào chậm" vì nó chỉ ra chờ ở đâu.
- Đo thật một tải ghi 16 client cho thấy nút thắt là WAL (LWLock/WALWrite 117 + IO/WALSync 31 sau khi loại Client/ClientRead) — hướng tối ưu đúng là chỉnh WAL, không phải thêm index; cùng đó IO/DataFileRead xuất hiện khi quét bảng lớn hơn cache, và Lock/transactionid khi hai phiên tranh khoá hàng.
- Luôn bỏ Client/ClientRead khi tìm nút thắt (nó chỉ là kết nối rảnh chờ ứng dụng), và với chẩn đoán nghiêm túc dùng
pg_wait_samplingthay vì tự vòng lặp — wait events chỉ ra hướng, còn giải pháp cụ thể vẫn cần hiểu cơ chế bên dưới.
Phần sau ta chuyển sang một view thống kê giúp phát hiện thiếu index: Phần sau mổ xẻ pg_stat_user_tables — cách đọc tỷ lệ seq scan so với index scan để tìm bảng đang bị quét tuần tự quá nhiều và cần thêm index.