Ở bài phần 1 tôi cảnh báo đừng đọc cost như thời gian, và bài phần 3 tách rows ước lượng khỏi actual rows. Nhưng cost thực ra là gì, tính từ đâu ra? Bài này mổ vào chính con số đó: tôi sẽ tự tính lại cost của một truy vấn bằng tay từ vài tham số cấu hình, xem nó có khớp con số PostgreSQL in ra không — rồi phát hiện một điều về bản chất của cost khiến tôi phải sửa lại cách nghĩ.
Cost là một mô hình, không phải thời gian
Con số cost mà planner gán cho mỗi kế hoạch không phải mili giây, cũng không phải đo được — nó là kết quả của một mô hình chi phí: một công thức cộng dồn từ vài tham số cấu hình. Bốn tham số chính:
seq_page_cost= 1.0 — chi phí đọc một trang tuần tự (liền kề trên đĩa). Đây là đơn vị gốc: mọi cost khác đo tương đối so với "một lần đọc trang tuần tự".random_page_cost= 4.0 (mặc định) — đọc một trang ngẫu nhiên (nhảy vị trí). Đắt gấp 4 lần đọc tuần tự, vì mô hình giả định ổ đĩa quay phải quay đầu đọc tới đúng chỗ.cpu_tuple_cost= 0.01 — chi phí CPU xử lý một hàng.cpu_operator_cost= 0.0025 — chi phí một phép toán/so sánh.
Với một quét tuần tự toàn bảng, công thức rất gọn: cost = số_trang × seq_page_cost + số_hàng × cpu_tuple_cost. Planner tính cost như vậy cho mọi kế hoạch khả dĩ (seq scan, index scan, bitmap...) rồi chọn cái có cost thấp nhất. Cost không phải để nói cho bạn biết truy vấn mất bao lâu; nó là thước đo tương đối để planner so sánh và chọn.
Đo: tự tính lại cost, khớp đến từng số lẻ
Tôi tạo một bảng một triệu hàng và đọc thống kê thật của nó từ pg_class: relpages = 5406 (bảng chiếm 5406 trang), reltuples = 1000000. Rồi EXPLAIN SELECT * FROM t (quét toàn bảng):
Seq Scan on t (cost=0.00..15406.00 rows=1000000)
PostgreSQL nói cost = 15406.00. Giờ tôi tự tính lại bằng tay từ công thức:
đọc trang : 5406 × seq_page_cost(1.0) = 5406.00
xử lý hàng: 1.000.000 × cpu_tuple(0.01) = 10000.00
tổng = 15406.00
Khớp chính xác đến từng số lẻ. Cost không phải con số huyền bí; nó là số học đơn giản trên số trang và số hàng, với các hệ số bạn có thể đọc bằng SHOW seq_page_cost. Việc tự tính lại và khớp được xác nhận rằng ta hiểu đúng planner đang cân nhắc gì: nó đếm trang phải đọc và hàng phải xử lý, nhân với chi phí đơn vị, rồi cộng.
Một lần tôi đo hớ: cost không cố định theo dữ liệu
Tính khớp được 15406.00, tôi khoái chí, tưởng đã "giải mã" xong cost và kế hoạch. Trong đầu tôi khi đó, cost là một thuộc tính cố định của cặp (truy vấn, dữ liệu): cùng truy vấn trên cùng bảng thì phải ra cùng kế hoạch. Nghe hiển nhiên.
Để kiểm, tôi lấy một truy vấn lọc khoảng 25% số hàng và chạy EXPLAIN hai lần, chỉ đổi một tham số cấu hình giữa hai lần — random_page_cost từ 4.0 (mặc định) xuống 1.1:
random_page_cost = 4.0 -> Bitmap Heap Scan
random_page_cost = 1.1 -> Index Scan
Kế hoạch đổi hẳn — từ Bitmap Heap Scan sang Index Scan — dù tôi không sửa một hàng dữ liệu nào. Cùng bảng, cùng truy vấn, cùng thống kê, mà planner chọn khác. Theo kỷ luật đo lường, hai kết quả khác nhau trên cùng đầu vào nghĩa là có một biến ẩn — và biến ẩn ở đây chính là tham số cấu hình.
Đó là chỗ tôi đo hớ: tôi tưởng cost (và kế hoạch) là hàm của dữ liệu, nhưng nó là hàm của dữ liệu cộng với mô hình chi phí. Cost là mô hình, và các tham số của mô hình — random_page_cost và bạn bè — là những giả định về phần cứng mà bạn có thể chỉnh. random_page_cost = 4.0 giả định một ổ đĩa quay, nơi đọc ngẫu nhiên (như index scan phải làm) đắt gấp 4 lần đọc tuần tự. Trên SSD, đọc ngẫu nhiên gần như rẻ ngang tuần tự, nên giá trị hợp lý là ~1.1 — và khi tôi đặt vậy, index scan (vốn đọc nhiều trang ngẫu nhiên) đột nhiên rẻ hơn trong mắt planner, nên nó chọn index. Chỉnh tham số là đổi thế giới quan của planner, và thế giới quan đổi thì lựa chọn đổi, dù dữ liệu đứng yên.
Bài học đo lường: cost là đầu ra của một mô hình với các tham số cấu hình, không phải một đại lượng đo được của dữ liệu. Khi cùng một truy vấn cho hai kế hoạch khác nhau, đừng ngạc nhiên — hãy tìm biến ẩn: một SET nào đó, một tham số khác giữa hai môi trường. Rất nhiều chuyện "kế hoạch tốt trên máy dev, tệ trên máy chủ" gốc rễ là cost parameter khác nhau, không phải dữ liệu.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên, rất thực tế: random_page_cost mặc định 4.0 là dành cho ổ đĩa quay; nếu bạn chạy SSD, nên hạ xuống ~1.1. Với mặc định 4.0, planner định giá index scan (đọc ngẫu nhiên) đắt hơn thực tế trên SSD, nên nó ngại dùng index và hay chọn seq scan — dẫn tới truy vấn chậm mà EXPLAIN trông vẫn "hợp lý" theo cost. Đây là một trong những chỉnh cấu hình có tác động lớn nhất trên PostgreSQL hiện đại, và nó thuần túy là chuyện điều chỉnh mô hình cho khớp phần cứng thật.
Hệ quả thứ hai: hiểu cost giúp đọc vì sao planner chọn kế hoạch này thay kế hoạch kia. Khi bạn thấy planner "cố chấp" quét tuần tự dù có index, EXPLAIN cho bạn hai con số cost để so; thường thủ phạm là ước lượng số hàng sai (bài phần 3) hoặc cost parameter không khớp phần cứng. Cost là ngôn ngữ planner dùng để ra quyết định — đọc được nó là hiểu được quyết định, thay vì chỉ bực vì "sao nó không dùng index của tôi".
Hệ quả thứ ba là bài học đo lường bao trùm. Con số mang theo: cost là đầu ra của một mô hình — cost seq scan = số_trang × 1.0 + số_hàng × 0.01, khớp EXPLAIN đến từng số lẻ (15406.00) — và các tham số của mô hình (như random_page_cost) là giả định về phần cứng có thể chỉnh, nên đổi tham số làm planner đổi kế hoạch dù dữ liệu y nguyên. Cost là đơn vị tương đối (1.0 = một lần đọc trang tuần tự), không phải mili giây, và không cố định theo dữ liệu — nó phụ thuộc cả vào mô hình mà bạn cấu hình. Biết điều đó là biết cost nói gì và không nói gì.
Thử ba mươi giây
Trong psql, chạy SHOW random_page_cost; và SHOW seq_page_cost; để xem mô hình chi phí của cơ sở dữ liệu bạn đang giả định gì về phần cứng. Nếu random_page_cost là 4.0 mà máy chủ chạy SSD, đó là một cơ hội chỉnh: thử SET random_page_cost = 1.1; (chỉ trong phiên hiện tại) rồi chạy lại EXPLAIN một truy vấn hay bị quét tuần tự — bạn có thể thấy planner chuyển sang index scan. Và để tự kiểm tra công thức: đọc SELECT relpages, reltuples FROM pg_class WHERE relname = 'ten_bang';, nhân relpages × 1.0 + reltuples × 0.01, rồi so với cost của EXPLAIN SELECT * FROM ten_bang; — nó sẽ khớp, và bạn vừa tự tay xác nhận cost chỉ là số học đơn giản.