Database (PostgreSQL) Cơ bản
Khi một endpoint chậm, câu hỏi đầu tiên không phải là “nên tạo index nào?”. Cần biết PostgreSQL đang đọc dữ liệu ra sao, đã chọn query plan nào và có đang chờ transaction khác hay không. Ba vấn đề này có thể cho cùng một triệu chứng ở ứng dụng, nhưng cách xử lý khác nhau.
Trang này là điểm bắt đầu để chọn đúng bài cần đọc. Danh sách đầy đủ nằm ở menu bên trái.
Một query đi qua những lớp nào?
Phần tiêu đề “Một query đi qua những lớp nào?”- Dữ liệu được lưu ở đâu? Bảng thông thường dùng heap, gồm các page chứa phiên bản vật lý của row (tuple). Giá trị lớn có thể được nén hoặc chuyển sang TOAST. Đọc page, heap, tuple và TOAST nếu cần hiểu vì sao kích thước bảng, kích thước row và lượng dữ liệu phải đọc không giống nhau.
- PostgreSQL lấy dữ liệu bằng cách nào? Planner ước lượng chi phí của các phương án rồi chọn một plan: quét bảng, đi qua index, kết hợp nhiều bảng bằng join và các bước khác. Có index không đồng nghĩa plan sẽ dùng nó. Bắt đầu bằng B-tree index rồi học cách đọc
EXPLAINvàEXPLAIN ANALYZEđể nhìn thấy lựa chọn thực tế. - Khi nhiều transaction cùng sửa dữ liệu thì sao? Ngoài plan truy vấn, thời gian chờ lock có thể làm request chậm. Đọc row-level locks để phân biệt đọc thông thường với thao tác cần khoá row; nếu các transaction chờ nhau thành vòng, xem deadlock.
Đây là ba góc nhìn, không phải ba bước mà mọi câu SQL đều thực hiện: một câu SELECT thông thường không tự lấy row lock, và một plan dùng index có thể không phải đọc heap cho mọi row.
Triệu chứng → bài cần đọc
Phần tiêu đề “Triệu chứng → bài cần đọc”Đang tải diagram…
| Tình huống đang gặp | Đọc theo thứ tự |
|---|---|
| Mới làm quen PostgreSQL | Cách dữ liệu được lưu → B-tree index → Đọc execution plan |
Đã tạo index nhưng vẫn Seq Scan |
Đọc execution plan → Vì sao index không được dùng → Statistics và ANALYZE |
| Thiết kế index cho query nhiều điều kiện | Thứ tự cột composite → Partial, expression, covering → GIN, GiST, BRIN |
| Join chậm hoặc trang sâu chậm | Join algorithms, OFFSET và keyset |
Request chậm khi nhiều giao dịch cùng ghi, lỗi 40P01 |
Row-level locks → Deadlock → NOWAIT và SKIP LOCKED |
| Dữ liệu đọc ra không nhất quán, nghi anomaly | ACID → Transaction anomalies → Isolation levels |
Bảng phình, VACUUM không kịp, cảnh báo wraparound |
MVCC → VACUUM, autovacuum và bloat → Transaction ID wraparound |
| Bảng rất lớn, cần xoá dữ liệu cũ theo kỳ | Partitioning |
| Migration làm API treo, pool cạn vì chờ lock | Table-level locks → Lock queue → Timeout và quan sát lock |
| Hai người sửa cùng bản ghi, cron chạy trùng trên nhiều pod | Optimistic vs pessimistic, Advisory lock |
too many clients, nhiều pod cạn connection |
Connection pooling và PgBouncer |
| Vừa ghi xong đọc lại thấy dữ liệu cũ | Replication và read replica |
Lỗi 23505 khi hai request cùng thêm một key |
Upsert với ON CONFLICT |
| Báo cáo lệch ngày, tổng tiền lệch vài đồng | Kiểu dữ liệu dễ dùng sai |
| Chọn khoá chính: bigint hay UUID | UUID hay bigint làm khoá |
| Khách hàng SaaS không bao giờ được thấy dữ liệu của nhau | Row-level security và multi-tenancy |
Partitioning mạnh nhất ở retention (DETACH/DROP partition) và bảo trì theo từng phần, chứ không mặc định nhanh hơn một index tốt. Đối chiếu plan và chi phí vận hành trước khi chọn.
Lộ trình học
Phần tiêu đề “Lộ trình học”- Nền tảng: storage layout, B-tree, EXPLAIN.
- Tối ưu query: index không được dùng, statistics, composite index, join, pagination.
- Đồng thời và giao dịch: ACID, MVCC, isolation levels, row-level locks, table-level locks, lock queue, optimistic vs pessimistic, advisory lock, deadlock, timeout và quan sát lock.
- Vận hành: VACUUM và bloat, transaction ID wraparound, partitioning, connection pooling, replication, upsert, kiểu dữ liệu, UUID hay bigint.
Thử đọc một plan trong vài phút
Phần tiêu đề “Thử đọc một plan trong vài phút”Chạy toàn bộ đoạn SQL sau trong cùng một phiên psql trên PostgreSQL 17. Bảng tạm chỉ tồn tại trong phiên đó, không thay đổi bảng ứng dụng:
CREATE TEMP TABLE demo_orders ASSELECT id, id % 100 AS customer_idFROM generate_series(1, 10000) AS g(id);
CREATE INDEX ON demo_orders (customer_id);ANALYZE demo_orders;
EXPLAIN (ANALYZE, BUFFERS)SELECT * FROM demo_orders WHERE customer_id = 42;Nhìn vào node quét dữ liệu: PostgreSQL chọn Seq Scan, Index Scan hay Bitmap Heap Scan? So rows ước lượng với actual rows, rồi xem Buffers để biết các page được truy cập. Plan có thể khác giữa các máy hoặc sau khi dữ liệu thay đổi; mục tiêu của bài thử là đọc điều PostgreSQL đã làm, không phải ép nó phải dùng index. Bài EXPLAIN giải thích từng trường trong output và cách điều tra khi số ước lượng lệch số thực tế.