pg_stat_statements ở phần trước trả lời "truy vấn nào tốn nhất". Các khung nhìn pg_stat_* trả lời câu khác: "bảng nào, chỉ mục nào, và bộ đệm đang làm gì". Phần này đo chúng nói gì, và ba chỗ chúng im lặng.
Bốn khung nhìn
Sau một tải giả lập trên bảng một triệu dòng:
pg_stat_user_tables — bảng được truy cập thế nào:
seq_scan = 3 seq_tup_read = 3.000.000 idx_scan = 2.003
n_tup_upd = 100.000 n_tup_del = 10.000
n_tup_hot_upd = 0 n_dead_tup = 110.000
pg_stat_user_indexes — chỉ mục nào được dùng:
| Chỉ mục | Số lần quét | Kích thước |
|---|---|---|
d_kh |
2.000 | 13 MB |
d_pkey |
3 | 24 MB |
d_khong_dung |
0 | 27 MB |
pg_statio_user_tables — bộ đệm:
heap_blks_hit = 457.412 heap_blks_read = 953 (99,8% trúng)
pg_stat_activity — trạng thái ngay lúc này, không có lịch sử.
n_tup_hot_upd = 0 nói ra một chuyện
Con số này đáng dừng lại.
update d set v = v + 1 where id <= 100000;
100.000 lần cập nhật, và không lần nào đi được đường HOT. Phần 19 và 21 đã đo cơ chế: cập nhật HOT chỉ xảy ra khi cột được sửa không nằm trong chỉ mục nào.
Ở đây cột v có chỉ mục — chính là d_khong_dung. Nên mỗi lần cập nhật phải ghi thêm vào chỉ mục đó.
Và cột "số lần quét" cho biết chỉ mục ấy chưa từng được dùng để tra cứu lần nào. Nó tốn 27 MB đĩa, làm mọi câu UPDATE chậm đi, chặn cập nhật HOT — và không mang lại gì.
Đây là ví dụ hoàn hảo cho việc đọc hai khung nhìn cùng lúc thay vì từng cái một. n_tup_hot_upd = 0 một mình chỉ là một con số; ghép với idx_scan = 0 thì nó thành một việc cần làm.
Điều không nói 1: seq_scan cao không tự động là vấn đề
Bảng 50 dòng, tổng cộng 8.192 byte — đúng một trang:
seq_scan = 5.000
seq_tup_read = 250.000
Năm nghìn lần quét tuần tự, và hoàn toàn bình thường. Quét một trang rẻ hơn tra chỉ mục — bộ lập lịch chọn đúng.
Nếu bạn có cảnh báo kiểu "seq_scan vượt ngưỡng thì báo động", nó sẽ kêu suốt về những bảng nhỏ mà không có gì để sửa.
Con số đáng nhìn không phải seq_scan mà là seq_tup_read chia cho seq_scan — số dòng đọc trung bình mỗi lần quét. Vài chục thì không sao; vài triệu thì đáng xem.
select relname,
seq_scan,
seq_tup_read / nullif(seq_scan, 0) as dong_moi_lan_quet,
pg_size_pretty(pg_relation_size(relid)) as kich_thuoc
from pg_stat_user_tables
where seq_scan > 0
order by seq_tup_read / nullif(seq_scan, 0) desc limit 10;
Điều không nói 2: sự cố xoá sạch mọi bộ đếm
Tôi kiểm hai kiểu dừng.
Khởi động lại sạch:
seq_scan trước: 5.001
seq_scan sau : 5.001
Giữ nguyên. PostgreSQL ghi thống kê xuống đĩa khi tắt đàng hoàng.
Sự cố thật — giết cả tiến trình, log ghi redo done:
seq_scan trước: 5.001
seq_scan sau : 0
Về 0. Và stats_reset trở thành rỗng.
Điều này quan trọng vì các quyết định vận hành thường dựa vào số tích luỹ. Phần 19 đã cảnh báo rằng idx_scan = 0 có thể chỉ có nghĩa là bộ đếm mới được đặt lại — và một lần mất điện là đủ để đặt lại.
Luôn kiểm cùng lúc:
select stats_reset, now() - stats_reset as da_bao_lau
from pg_stat_database where datname = current_database();
Nếu da_bao_lau là hai giờ, đừng kết luận gì về chỉ mục nào không được dùng.
Điều không nói 3: không có chiều thời gian
Mọi bộ đếm là tổng cộng kể từ stats_reset. Không có cách nào hỏi "giờ vừa rồi thế nào".
Bảng có 500 triệu lần quét tuần tự tích luỹ trong sáu tháng nói rất ít; bảng có 500 triệu lần trong hai giờ vừa rồi thì nói rất nhiều. Khung nhìn không phân biệt được hai trường hợp.
Cách chữa là tự chụp ảnh:
create table anh_chup_stat as
select now() as luc, * from pg_stat_user_tables where false;
-- chạy mỗi 5 phút
insert into anh_chup_stat select now(), * from pg_stat_user_tables;
Rồi lấy hiệu giữa hai ảnh liên tiếp:
select relname,
seq_scan - lag(seq_scan) over (partition by relname order by luc) as quet_trong_ky,
luc
from anh_chup_stat
where relname = 'd' order by luc desc limit 10;
Đây chính xác là việc mà mọi công cụ giám sát làm sẵn, và là lý do chúng tồn tại.
Một dự đoán của tôi bị bác bỏ
Tôi vào phép đo này tin rằng n_live_tup là con số ước lượng lấy từ ANALYZE, nên có thể lệch nhiều so với thực tế.
n_live_tup báo : 990.000 đếm thật : 990.000
sau khi chèn 50.000 dòng (chưa ANALYZE):
n_live_tup báo : 1.040.000 đếm thật : 1.040.000
Khớp chính xác, kể cả trước khi chạy ANALYZE.
PostgreSQL duy trì con số này tăng dần theo từng thao tác chèn, sửa, xoá — không chỉ lấy từ lần lấy mẫu gần nhất. Nó có thể lệch (khi giao dịch bị huỷ, hoặc sau sự cố), nhưng trên hệ thống bình thường thì nó chính xác hơn tôi tưởng.
Điều đó cũng có nghĩa là nó dùng được thay cho count(*) khi bạn chỉ cần con số gần đúng — và phần 33 đã đo count(*) mất 106,83 ms trên bảng 5 triệu dòng.
Năm câu truy vấn đáng để sẵn
Bảng bị quét tuần tự nặng nhất:
select relname, seq_scan, seq_tup_read / nullif(seq_scan,0) as dong_moi_lan,
pg_size_pretty(pg_relation_size(relid))
from pg_stat_user_tables where seq_scan > 0
order by seq_tup_read desc limit 10;
Chỉ mục chưa dùng lần nào (nhớ ba cái bẫy ở phần 19):
select s.relname, s.indexrelname, pg_size_pretty(pg_relation_size(s.indexrelid))
from pg_stat_user_indexes s join pg_index i on i.indexrelid = s.indexrelid
where s.idx_scan = 0 and not i.indisunique and not i.indisprimary
order by pg_relation_size(s.indexrelid) desc limit 10;
Bảng có nhiều dòng chết nhất:
select relname, n_dead_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) as pt_chet,
last_autovacuum
from pg_stat_user_tables where n_dead_tup > 10000
order by n_dead_tup desc limit 10;
Tỉ lệ cập nhật HOT — thấp nghĩa là có chỉ mục đang cản:
select relname, n_tup_upd, n_tup_hot_upd,
round(100.0 * n_tup_hot_upd / nullif(n_tup_upd, 0), 1) as pt_hot
from pg_stat_user_tables where n_tup_upd > 10000
order by n_tup_upd desc limit 10;
Kết nối đang làm gì ngay lúc này:
select pid, state,
round(extract(epoch from now() - xact_start)) as giao_dich_mo_giay,
wait_event_type, wait_event,
left(query, 60)
from pg_stat_activity
where backend_type = 'client backend' and state <> 'idle'
order by xact_start;
Cột wait_event_type là cột hữu ích nhất trong bảng cuối và hay bị bỏ qua: nó cho biết tiến trình đang đợi cái gì — khoá, I/O, hay client.
Thử ba mươi giây
select stats_reset, now() - stats_reset as thong_ke_tich_luy_bao_lau
from pg_stat_database where datname = current_database();
Nếu con số đó nhỏ hơn một tuần, mọi kết luận về "chỉ mục này không ai dùng" đều chưa đủ căn cứ.
Và chạy câu tỉ lệ HOT ở trên. Bảng nào có pt_hot gần 0 với n_tup_upd lớn là bảng đang trả giá cho một chỉ mục — và câu truy vấn chỉ-mục-chưa-dùng sẽ cho biết đó có phải chỉ mục đáng giữ hay không.
Phần sau khép lại nhóm giám sát bằng một quy trình gỡ lỗi hoàn chỉnh, chạy trên một sự cố dựng sẵn.