19/21 Index làm ghi chậm Các chương

Vì sao index làm INSERT chậm hơn?

Vì sao thêm index lại làm INSERT chậm hơn?

Index giúp PostgreSQL tìm xe nhanh hơn. Nhưng khi có xe mới, PostgreSQL không chỉ thêm xe vào bảng: nó còn phải cập nhật các index để những lần tìm kiếm sau có thể tìm thấy xe đó.

Trong bộ dữ liệu thực hành, nạp 500.000 xe vào bảng chưa có index mất khoảng 0,30 giây. Khi bảng có 6 index, cùng thao tác đó mất khoảng 2,48 giây.

Vì sao thêm index lại làm INSERT chậm hơn?

Thêm một xe, PostgreSQL phải làm gì?

Giả sử bảng vehicles có hai index: một trên id, một trên plate.

Khi thêm một xe:

INSERT INTO vehicles (id, plate)
VALUES (5000001, '47P-600.90');

PostgreSQL cần:

  1. Thêm dòng xe mới vào bảng vehicles.
  2. Thêm 5000001 vào index trên id.
  3. Thêm 47P-600.90 vào index trên plate.

Trong mỗi index, PostgreSQL phải tìm đúng vị trí rồi thêm một mục trỏ tới dòng xe mới.

Một lần INSERT phải cập nhật những gì?

Xe mới có id 5000001, biển số 47P-600.90. PostgreSQL thêm dòng xe vào bảng vehicles, thêm 5000001 vào index id và thêm 47P-600.90 vào index plate.

Một xe mới → thêm vào bảng và cập nhật 2 index.

Càng nhiều index cần cập nhật, mỗi lần thêm xe càng có nhiều việc phải làm.

Giống một cuốn sách có nhiều mục lục: thêm một nội dung mới không chỉ là thêm trang sách, mà còn phải cập nhật từng mục lục liên quan.

Nhiều index hơn, nạp dữ liệu lâu hơn

Thử nạp cùng 500.000 xe vào bảng trống, rồi lặp lại với số index khác nhau:

Nạp 500.000 xe mất bao lâu?

  1. 0 index 0,30 giây
  2. 1 index 0,45 giây
  3. 3 index 1,43 giây
  4. 6 index 2,48 giây

Trong lần thử nghiệm này, bảng có 6 index mất khoảng 8,3 lần thời gian nạp so với bảng không có index.

Kết quả phụ thuộc vào dữ liệu, cấu hình và môi trường chạy.

Xem cách thử nghiệm INSERT INTO vehicles_w SELECT * FROM vehicles WHERE id <= 500000;
  • 1 index: id
  • 3 index: thêm plate, (province, listed_at)
  • 6 index: thêm price_vnd, (brand, model), mileage_km

Bảng vehicles_w trống trước mỗi lần nạp, đã tạo sẵn các index. PostgreSQL 18, mỗi cấu hình chạy 3 lần, lấy trung vị.

Khi không có index, PostgreSQL chỉ cần thêm dữ liệu vào bảng. Khi có 6 index, ngoài bảng, PostgreSQL còn phải cập nhật cả 6 index cho từng dòng mới.

Trong lần thử này, thời gian tăng từ 0,30 giây lên 2,48 giây, tức lâu hơn khoảng 8,3 lần. Con số thực tế sẽ thay đổi theo dữ liệu, loại index và môi trường chạy.

Không phải index nào cũng tốn thời gian như nhau. Một lý do nằm ở vị trí mà giá trị mới được chèn vào cây B-tree.

Khi page của index đã đầy

B-tree lưu các giá trị theo thứ tự trong những page lá. Khi thêm giá trị mới, PostgreSQL tìm page phù hợp và chèn giá trị vào đó.

Ví dụ, index trên plate có một page lá chứa:

47P-600.36   47P-600.55   47P-601.04   47P-601.72

Biển số 47P-600.90 của xe vừa thêm phải nằm giữa 47P-600.55 và 47P-601.04. Nhưng nếu page đó đã đầy thì sao?

PostgreSQL tách page (page split): tạo thêm một page, phân chia các mục giữa hai page, rồi cập nhật cây để tìm được page mới.

Page đã đầy thì thêm biển số mới vào đâu?

Trước: page lá đã đầy chứa 47P-600.36, 47P-600.55, 47P-601.04, 47P-601.72. Biển số mới 47P-600.90 phải nằm giữa 47P-600.55 và 47P-601.04. Sau khi tách: page cũ chứa 47P-600.36, 47P-600.55 và 47P-600.90; page mới chứa 47P-601.04 và 47P-601.72.

Cây B-tree cũng được cập nhật để có thể tìm đến page mới.

Tách page là thêm việc, vì PostgreSQL phải chuyển các mục và cập nhật cấu trúc của index. Không phải lần chèn nào cũng gây tách page: việc này chỉ xảy ra khi page phù hợp không còn đủ chỗ cho mục mới.

Với id tăng dần, giá trị mới thường được thêm vào phía cuối index. Còn biển số đến không theo thứ tự, nên các lần chèn rơi vào nhiều page khác nhau. Thời gian cập nhật vì thế còn phụ thuộc vào cách dữ liệu được đưa vào, không chỉ vào số index.

Còn WAL thì sao?

Khi thêm dữ liệu, PostgreSQL còn ghi thông tin thay đổi vào nhật ký ghi trước (WAL, write-ahead log). Nhật ký này giúp PostgreSQL khôi phục dữ liệu sau sự cố.

Thay đổi trên index cũng được ghi vào WAL. Vì thế, nhiều index không chỉ làm PostgreSQL phải cập nhật nhiều cấu trúc hơn, mà còn làm lượng WAL tăng lên.

Vậy có nên hạn chế tạo index?

Có, nhưng không phải bằng cách xoá hết index. Một index có thể giúp những câu truy vấn thường xuyên chạy nhanh hơn rất nhiều. Đổi lại, nó chiếm thêm dung lượng và làm việc ghi dữ liệu tốn thêm công sức.

Trước khi tạo index, hãy tự hỏi: index này giúp ích cho những câu truy vấn nào, và chúng có chạy đủ thường xuyên không?

Nếu cần nạp một lượng lớn dữ liệu vào bảng mới, nên nạp dữ liệu trước rồi tạo index sau. Khi đó PostgreSQL dựng index một lần từ dữ liệu đã có, thay vì cập nhật index cho từng dòng trong suốt quá trình nạp. Tài liệu PostgreSQL cũng khuyên cách này khi nạp dữ liệu ban đầu.

Index giúp đọc nhanh hơn, nhưng mỗi index cần cập nhật đều làm lần ghi có thêm việc phải làm.

Thêm xe mới là một chuyện. Trên trang rao xe, người bán còn sửa giá, cập nhật số km hay đánh dấu xe đã bán.

Ở bài về cách PostgreSQL lưu dữ liệu trên đĩa, chúng ta đã thấy sửa một dòng có thể tạo ra một phiên bản mới của dòng đó.

Sửa một cột không có index, PostgreSQL có cần cập nhật index không?

Nguồn bài viết (5)

  1. 11.1. Introduction (Chapter 11. Indexes) · PostgreSQL 18 Documentation
  2. 28.3. Write-Ahead Logging (WAL) · PostgreSQL 18 Documentation
  3. 65.1. B-Tree Indexes · PostgreSQL 18 Documentation
  4. CREATE INDEX · PostgreSQL 18 Documentation
  5. 14.4. Populating a Database · PostgreSQL 18 Documentation