Ai đó có thể giải thích hành vi kỳ quái thực hiện hàng triệu CẬP NHẬT?


8

Ai đó có thể giải thích hành vi này cho tôi? Tôi đã chạy truy vấn sau đây trên Postgres 9.3 chạy trên OS X. Tôi đã cố gắng mô phỏng một số hành vi trong đó kích thước chỉ mục có thể lớn hơn nhiều so với kích thước bảng và thay vào đó tìm thấy thứ gì đó kỳ quái hơn.

CREATE TABLE test(id int);
CREATE INDEX test_idx ON test(id);

CREATE FUNCTION test_index(batch_size integer, total_batches integer) RETURNS void AS $$
DECLARE
  current_id integer := 1;
BEGIN
FOR i IN 1..total_batches LOOP
  INSERT INTO test VALUES (current_id);
  FOR j IN 1..batch_size LOOP
    UPDATE test SET id = current_id + 1 WHERE id = current_id;
    current_id := current_id + 1;
  END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT test_index(500, 10000);

Tôi đã để nó chạy khoảng một giờ trên máy cục bộ của mình, trước khi tôi bắt đầu nhận được các cảnh báo về vấn đề đĩa từ OS X. Tôi nhận thấy rằng Postgres đã hút khoảng 10 MB / s từ đĩa cục bộ của tôi và cơ sở dữ liệu Postgres đã tiêu tốn tổng cộng 30GB từ máy của tôi. Tôi đã kết thúc việc hủy bỏ truy vấn. Bất kể, Postgres đã không trả lại dung lượng đĩa cho tôi và tôi đã truy vấn cơ sở dữ liệu để thống kê sử dụng với kết quả sau:

test=# SELECT nspname || '.' || relname AS "relation",
    pg_size_pretty(pg_relation_size(C.oid)) AS "size"
  FROM pg_class C
  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
  ORDER BY pg_relation_size(C.oid) DESC
  LIMIT 20;

           relation            |    size
-------------------------------+------------
 public.test                   | 17 GB
 public.test_idx               | 14 GB

Tuy nhiên, lựa chọn từ bảng mang lại không có kết quả.

test=# select * from test limit 1;
 id
----
(0 rows)

Chạy 10000 lô 500 là 5.000.000 hàng, sẽ mang lại kích thước bảng / chỉ mục khá nhỏ (theo tỷ lệ MB). Tôi nghi ngờ rằng Postgres đang tạo một phiên bản mới của bảng / chỉ mục cho mỗi CHERTN / CẬP NHẬT đang xảy ra với chức năng, nhưng điều này có vẻ lạ. Toàn bộ chức năng được chạy giao dịch và bảng trống để bắt đầu.

Bất kỳ suy nghĩ về lý do tại sao tôi nhìn thấy hành vi này?

Cụ thể, hai câu hỏi tôi có là: tại sao không gian này chưa được cơ sở dữ liệu thu hồi và thứ hai là tại sao cơ sở dữ liệu yêu cầu nhiều không gian này ở nơi đầu tiên? 30 GB có vẻ như rất nhiều ngay cả khi chiếm MVCC

Câu trả lời:


7

Phiên bản ngắn

Thuật toán của bạn trông O (n * m) trong cái nhìn đầu tiên, nhưng phát triển O (n * m ^ 2) một cách hiệu quả, bởi vì tất cả các hàng có cùng một ID. Thay vì 5M hàng, bạn đang nhận> 1,25G hàng

Phiên bản dài

Chức năng của bạn là trong một giao dịch ngầm. Đó là lý do tại sao bạn không thấy dữ liệu sau khi hủy truy vấn của mình và cũng là lý do tại sao nó cần duy trì các phiên bản khác nhau của các bộ dữ liệu được cập nhật / chèn cho cả hai vòng.

Ngoài ra, tôi nghi ngờ bạn có lỗi trong logic của mình hoặc đánh giá thấp số lượng cập nhật được thực hiện.

Lặp lại đầu tiên của vòng lặp bên ngoài - current_id bắt đầu từ 1, chèn 1 hàng, sau đó vòng lặp bên trong thực hiện cập nhật 10000 lần cho cùng một hàng, hoàn thành với hàng duy nhất hiển thị ID 10001 và current_id với giá trị 10001. 10001 các phiên bản của hàng vẫn được giữ, vì giao dịch chưa kết thúc.

Lặp lại lần thứ hai của vòng lặp bên ngoài - vì current_id là 10001, một hàng mới được chèn với ID 10001. Bây giờ bạn có 2 hàng có cùng "ID" và 10003 phiên bản trong tổng số cả hai hàng (10002 của hàng đầu tiên, 1 của cái thứ hai). Sau đó, vòng lặp bên trong cập nhật 10000 lần CẢ HAI hàng, tạo ra 20000 phiên bản mới, đạt 30003 bộ dữ liệu cho đến nay ...

Lặp lại lần thứ ba của vòng lặp bên ngoài: ID hiện tại là 20001, một hàng mới được chèn với ID 20001. Bạn có 3 hàng, tất cả đều có cùng phiên bản "ID" 20001, 30006 hàng / tuples. Sau đó, bạn thực hiện 10000 cập nhật của 3 hàng, tạo 30000 phiên bản mới, giờ là 60006 ...

...

(Nếu không gian của bạn đã cho phép) - Lặp lại lần thứ 500 của vòng lặp bên ngoài, tạo 5M cập nhật 500 hàng, chỉ trong lần lặp này

Như bạn thấy, thay vì các bản cập nhật 5 triệu dự kiến ​​của bạn, bạn đã nhận được 1000 + 2000 + 3000 + ... + 4990000 + 5000000 cập nhật (cộng với thay đổi), sẽ là 10000 * (1 + 2 + 3 + ... + 499+ 500), cập nhật hơn 1.25G. Và tất nhiên, một hàng không chỉ là kích thước của int của bạn, nó cần một số cấu trúc bổ sung, vì vậy bảng và chỉ mục của bạn có kích thước hơn mười gigabyte.

Hỏi đáp liên quan:


5

PostgreSQL chỉ trả về dung lượng ổ đĩa sau VACUUM FULL, không phải sau một DELETEhoặc ROLLBACK(kết quả của việc hủy bỏ)

Hình thức tiêu chuẩn của VACUUM loại bỏ các phiên bản hàng chết trong bảng và chỉ mục và đánh dấu khoảng trống có sẵn để tái sử dụng trong tương lai. Tuy nhiên, nó sẽ không trả lại dung lượng cho hệ điều hành, trừ trường hợp đặc biệt khi một hoặc nhiều trang ở cuối bảng trở nên hoàn toàn miễn phí và có thể dễ dàng lấy được khóa bảng độc quyền. Ngược lại, VACUUM FULL chủ động tính toán các bảng bằng cách viết một phiên bản hoàn toàn mới của tệp bảng không có không gian chết. Điều này giảm thiểu kích thước của bảng, nhưng có thể mất nhiều thời gian. Nó cũng đòi hỏi thêm không gian đĩa cho bản sao mới của bảng, cho đến khi hoàn thành thao tác.

Như một lưu ý phụ toàn bộ chức năng của bạn có vẻ nghi vấn. Tôi không chắc chắn những gì bạn đang cố gắng kiểm tra, nhưng nếu bạn muốn tạo dữ liệu, bạn có thể sử dụnggenerate_series

INSERT INTO test
SELECT x FROM generate_series(1, batch_size*total_batches) AS t(x);

Thật tuyệt, điều đó giải thích tại sao bảng vẫn được đánh dấu là tiêu thụ quá nhiều dữ liệu, nhưng tại sao nó lại cần tất cả không gian đó ở nơi đầu tiên? Theo hiểu biết của tôi về MVCC, nó cần duy trì các phiên bản khác nhau của các bộ dữ liệu được cập nhật / chèn cho giao dịch, nhưng không cần duy trì các phiên bản riêng biệt cho mỗi lần lặp của vòng lặp.
Nikhil N

1
Mỗi lần lặp của vòng lặp sẽ tạo ra các bộ dữ liệu mới.
Evan Carroll

2
Đúng, nhưng ấn tượng của tôi là MVCC không nên tạo các bộ dữ liệu cho tất cả các bộ dữ liệu mà nó đã được sửa đổi trong suốt quá trình giao dịch. Điều đó có nghĩa là, khi INSERT đầu tiên chạy Postgres tạo một bộ dữ liệu duy nhất và nó thêm một bộ dữ liệu mới duy nhất cho mỗi CẬP NHẬT. Vì CẬP NHẬT được chạy cho mỗi hàng 500 lần và có 10000 CHERTN, nên số tiền này lên tới 500 * 10000 hàng = 5 triệu tup tại thời điểm giao dịch được thực hiện. Bây giờ đây chỉ là ước tính, nhưng bất kể 5M * nói 50 byte để theo dõi mỗi tuple ~ = 250MB, đó là RẤT NHIỀU dưới 30 GB. Tất cả đến từ đâu?
Nikhil N

Ngoài ra, chức năng có thể nghi ngờ, tôi đang cố kiểm tra hành vi của một chỉ mục khi các trường được lập chỉ mục đang cập nhật nhiều lần nhưng theo cách tăng đơn điệu, do đó mang lại một chỉ mục rất thưa thớt, nhưng một chỉ số luôn được gắn vào đĩa.
Nikhil N

Tôi bối rối như những gì bạn nghĩ. Bạn có nghĩ rằng một hàng được cập nhật 18e lần trong một vòng lặp là một tuple hoặc 1e8 tuple không?
Evan Carroll

3

Các số thực tế sau khi phân tích hàm lớn hơn nhiều vì tất cả các hàng của bảng có cùng giá trị được cập nhật nhiều lần trong mỗi lần lặp.

Khi chúng tôi chạy nó với các tham số nm:

SELECT test_index(n, m);

mchèn hàng và n * (m^2 + m) / 2cập nhật. Vì vậy, cho n = 500m = 10000, Postgres sẽ chỉ cần chèn 10 nghìn hàng nhưng thực hiện cập nhật tuple ~ 25G (25 tỷ).

Xem xét rằng một hàng trong Postgres có khoảng 24 byte trên không, một bảng chỉ có một intcột sẽ cần 28 byte mỗi hàng cộng với chi phí trên trang. Vì vậy, để hoạt động kết thúc, chúng tôi cần khoảng 700 GB cộng với dung lượng cho chỉ mục (cũng sẽ là vài trăm GB).


Kiểm tra

Để kiểm tra lý thuyết, chúng tôi đã tạo một bảng khác test_testvới một hàng duy nhất.

CREATE TABLE test_test (i int not null) ;
INSERT INTO test_test (i) VALUES (0);

Sau đó, chúng tôi thêm một kích hoạt testđể mỗi bản cập nhật sẽ tăng bộ đếm lên 1. (Mã bị bỏ qua). Sau đó, chúng tôi chạy hàm, với các giá trị nhỏ hơn, n = 50m = 100.

Lý thuyết của chúng tôi dự đoán :

  • 100 hàng chèn,
  • Cập nhật tuple 250K (252500 = 50 * 100 * 101/2)
  • ít nhất 7 MB cho bảng trên đĩa
  • (+ không gian cho chỉ mục)

Kiểm tra 1 ( testbảng gốc , có chỉ mục)

    SELECT test_index(50, 100) ;

Sau khi hoàn thành, chúng tôi kiểm tra nội dung bảng:

x=# SELECT COUNT(*) FROM test ;
 count 
-------
   100
(1 row)

x=# SELECT i FROM test_test ;
   i    
--------
 252500
(1 row)

Và sử dụng đĩa (truy vấn theo Chỉ số kích thước / thống kê sử dụng trong Bảo trì chỉ mục ):

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
test      | test_idx  |      100 | 8944 kB    | 5440 kB    | N      |           10001 |      505003
test_test |           |        1 | 8944 kB    |            | N      |                 |            

Các testbảng đã sử dụng gần như 9MB cho bảng và 5MB cho chỉ số. Lưu ý rằng test_testbảng đã sử dụng 9MB khác! Điều đó được mong đợi vì nó cũng đã trải qua 250K cập nhật (trình kích hoạt thứ hai của chúng tôi đã cập nhật một hàng duy nhất test_testcho mỗi bản cập nhật của một hàng test.)

Cũng lưu ý số lần quét trên bảng test(10K) và các bộ dữ liệu đọc (500K).

Kiểm tra 2 ( testbảng không có chỉ mục)

Chính xác như trên, ngoại trừ bảng không có chỉ mục.

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
 test        |        |      100 | 8944 kB    |            | N      |                 |            
 test_test   |        |        1 | 8944 kB    |            | N      |                 |            

Chúng tôi có cùng kích thước cho việc sử dụng đĩa của bảng và tất nhiên không sử dụng đĩa cho các chỉ mục. Số lần quét trên bàn testlà 0 và các bộ dữ liệu cũng đọc.

Kiểm tra 3 (với fillfactor thấp hơn)

Đã thử với fillfactor 50 và thấp nhất có thể, 10. Không cải thiện chút nào. Việc sử dụng đĩa gần như giống hệt với các thử nghiệm trước đó (đã sử dụng fillfactor mặc định, 100 phần trăm)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.