Làm thế nào để xử lý kế hoạch truy vấn xấu gây ra bởi sự bình đẳng chính xác trên loại phạm vi?


28

Tôi đang thực hiện một bản cập nhật trong đó tôi yêu cầu một đẳng thức chính xác trên một tstzrangebiến. ~ 1M hàng được sửa đổi và truy vấn mất ~ 13 phút. Kết quả EXPLAIN ANALYZEcó thể được nhìn thấy ở đây và kết quả thực tế rất khác so với kết quả được ước tính bởi trình hoạch định truy vấn. Vấn đề là quét chỉ mục trên t_rangemong đợi một hàng duy nhất được trả về.

Điều này dường như có liên quan đến thực tế là số liệu thống kê về các loại phạm vi được lưu trữ khác với các loại khác. Nhìn vào pg_statsxem cho cột, n_distinctlà -1 và các lĩnh vực khác (ví dụ most_common_vals, most_common_freqs) là rỗng.

Tuy nhiên, phải có số liệu thống kê được lưu trữ ở t_rangeđâu đó. Một bản cập nhật cực kỳ giống nhau khi tôi sử dụng 'trong' trên t_range thay vì một đẳng thức chính xác mất khoảng 4 phút để thực hiện và sử dụng một kế hoạch truy vấn khác biệt đáng kể (xem tại đây ). Kế hoạch truy vấn thứ hai có ý nghĩa với tôi bởi vì mỗi hàng trong bảng tạm thời và một phần đáng kể của bảng lịch sử sẽ được sử dụng. Quan trọng hơn, trình hoạch định truy vấn dự đoán số lượng hàng chính xác cho bộ lọc trên t_range.

Sự phân phối của t_rangemột chút khác thường. Tôi đang sử dụng bảng này để lưu trữ trạng thái lịch sử của một bảng khác và các thay đổi đối với bảng khác xảy ra cùng một lúc trong các bãi lớn, do đó không có nhiều giá trị khác biệt t_range. Dưới đây là số lượng tương ứng với từng giá trị duy nhất của t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

Tổng số cho các khác biệt t_rangeở trên đã hoàn tất, do đó, số lượng thẻ là ~ 3M (trong đó ~ 1M sẽ bị ảnh hưởng bởi một trong hai truy vấn cập nhật).

Tại sao truy vấn 1 thực hiện kém hơn nhiều so với truy vấn 2? Trong trường hợp của tôi, truy vấn 2 là một thay thế tốt, nhưng nếu một sự bình đẳng phạm vi chính xác thực sự được yêu cầu, làm thế nào tôi có thể khiến Postgres sử dụng một kế hoạch truy vấn thông minh hơn?

Định nghĩa bảng với các chỉ mục (bỏ các cột không liên quan):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Truy vấn 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Truy vấn 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1 cập nhật 999753 hàng và cập nhật Q2 999753 + 36791 = 1036544 (nghĩa là bảng tạm thời sao cho mọi hàng khớp với điều kiện phạm vi thời gian được cập nhật).

Tôi đã thử truy vấn này để phản hồi bình luận của @ ypercube :

Truy vấn 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

Kế hoạch truy vấn và kết quả (xem tại đây ) là trung gian giữa hai trường hợp trước (~ 6 phút).

2016/02/05 CHỈNH SỬA

Không còn có quyền truy cập vào dữ liệu sau 1,5 năm, tôi đã tạo một bảng thử nghiệm có cùng cấu trúc (không có chỉ mục) và tính chính xác tương tự. Câu trả lời của jjanes đề xuất rằng nguyên nhân có thể là thứ tự của bảng tạm thời được sử dụng để cập nhật. Tôi không thể kiểm tra giả thuyết trực tiếp vì tôi không có quyền truy cập track_io_timing(sử dụng Amazon RDS).

  1. Kết quả tổng thể nhanh hơn nhiều (theo hệ số vài). Tôi đoán rằng điều này là do việc loại bỏ các chỉ số, phù hợp với câu trả lời của Erwin .

  2. Trong trường hợp thử nghiệm này, Truy vấn 1 và 2 về cơ bản mất cùng một lượng thời gian, vì cả hai đều sử dụng phép nối hợp nhất. Đó là, tôi không thể kích hoạt bất cứ điều gì khiến Postgres chọn tham gia băm, vì vậy tôi không rõ tại sao Postgres lại chọn tham gia băm hoạt động kém ở nơi đầu tiên.


1
Điều gì xảy ra nếu bạn chuyển đổi điều kiện đẳng thức (a = b)thành hai điều kiện "chứa" : (a @> b AND b @> a)? Liệu kế hoạch thay đổi?
ypercubeᵀᴹ

@ypercube: kế hoạch thay đổi đáng kể, mặc dù nó vẫn không hoàn toàn tối ưu - xem phần chỉnh sửa # 2 của tôi.
abeboparebop

1
Một ý tưởng khác là thêm một chỉ số btree thông thường (lower(t_range),upper(t_range))kể từ khi bạn kiểm tra sự bình đẳng.
ypercubeᵀᴹ

Câu trả lời:


9

Sự khác biệt lớn nhất về thời gian trong các kế hoạch thực hiện của bạn là ở nút trên cùng, chính CẬP NHẬT. Điều này cho thấy rằng hầu hết thời gian của bạn sẽ dành cho IO trong quá trình cập nhật. Bạn có thể xác minh điều này bằng cách bật track_io_timingvà chạy các truy vấn vớiEXPLAIN (ANALYZE, BUFFERS)

Các kế hoạch khác nhau đang trình bày các hàng sẽ được cập nhật theo các thứ tự khác nhau. Một là theo trip_idthứ tự, và thứ hai là theo bất kỳ thứ tự nào chúng xảy ra để hiện diện trong bảng tạm thời.

Bảng đang được cập nhật dường như có thứ tự vật lý tương quan với cột trip_id và việc cập nhật các hàng theo thứ tự này dẫn đến các mẫu IO hiệu quả với các lần đọc trước / tuần tự. Trong khi thứ tự vật lý của bảng tạm thời dường như dẫn đến rất nhiều lần đọc ngẫu nhiên.

Nếu bạn có thể thêm một order by trip_idcâu lệnh tạo bảng tạm thời, điều đó có thể giải quyết vấn đề cho bạn.

PostgreSQL không tính đến các ảnh hưởng của việc đặt hàng IO khi lập kế hoạch cho hoạt động CẬP NHẬT. (Không giống như các hoạt động CHỌN, nơi nó đưa chúng vào tài khoản). Nếu PostgreSQL thông minh hơn, nó sẽ nhận ra rằng một kế hoạch tạo ra một trật tự hiệu quả hơn hoặc nó sẽ xen vào một nút sắp xếp rõ ràng giữa bản cập nhật và nút con của nó để bản cập nhật sẽ nhận được các hàng được cung cấp theo thứ tự ctid.

Bạn đúng rằng PostgreSQL thực hiện công việc kém khi ước tính tính chọn lọc của đẳng thức tham gia trên các phạm vi. Tuy nhiên, điều này chỉ liên quan đến vấn đề cơ bản của bạn. Một truy vấn hiệu quả hơn trên phần được chọn trong bản cập nhật của bạn có thể vô tình xảy ra để đưa các hàng vào bản cập nhật phù hợp theo thứ tự tốt hơn, nhưng nếu vậy thì điều đó chủ yếu là do may mắn.


Thật không may, tôi không thể sửa đổi track_io_timing, và (vì đã được một năm rưỡi!) Tôi không còn có quyền truy cập vào dữ liệu gốc. Tuy nhiên, tôi đã kiểm tra lý thuyết của bạn bằng cách tạo các bảng có cùng lược đồ và kích thước tương tự (hàng triệu hàng) và chạy hai bản cập nhật khác nhau - một bảng trong đó bảng cập nhật tạm thời được sắp xếp như bảng gốc và một bảng khác được sắp xếp bán ngẫu nhiên. Thật không may, hai bản cập nhật mất khoảng thời gian như nhau, ngụ ý rằng thứ tự của bảng cập nhật không ảnh hưởng đến truy vấn này.
abeboparebop

7

Tôi không chắc chắn chính xác tại sao độ chọn lọc của một vị từ đẳng thức lại được ước tính quá mức bởi chỉ số GiST trên tstzrangecột. Mặc dù điều đó vẫn còn thú vị, nhưng nó có vẻ không liên quan đến trường hợp cụ thể của bạn.

Vì bạn UPDATEsửa đổi một phần ba (!) Của tất cả các hàng 3M hiện có, nên một chỉ mục sẽ không giúp ích gì cả . Ngược lại, việc tăng dần chỉ mục ngoài bảng sẽ tăng thêm chi phí đáng kể cho bạn UPDATE.

Chỉ cần giữ truy vấn đơn giản 1 của bạn . Các đơn giản, triệt để giải pháp là để thả các chỉ số trước UPDATE. Nếu bạn cần nó cho các mục đích khác, tạo lại nó sau UPDATE. Điều này vẫn sẽ nhanh hơn so với việc duy trì chỉ số trong thời gian lớn UPDATE.

Đối với UPDATEmột phần ba của tất cả các hàng, có thể sẽ trả tiền để loại bỏ tất cả các chỉ mục khác - và tạo lại chúng sau UPDATE. Nhược điểm duy nhất: bạn cần các đặc quyền bổ sung và khóa độc quyền trên bàn (chỉ trong một khoảnh khắc ngắn nếu bạn sử dụng CREATE INDEX CONCURRENTLY).

Ý tưởng của @ ypercube là sử dụng btree thay vì chỉ số GiST có vẻ tốt về hiệu trưởng. Nhưng không phải cho một phần ba của tất cả các hàng (nơi không có chỉ số nào là tốt để bắt đầu), và không chỉ (lower(t_range),upper(t_range)), vì tstzrangekhông phải là một loại phạm vi rời rạc.

Hầu hết các loại phạm vi riêng biệt có dạng chính tắc, điều này làm cho khái niệm "đẳng thức" đơn giản hơn: giới hạn dưới và giới hạn trên của giá trị ở dạng chính tắc xác định nó. Các tài liệu:

Một loại phạm vi riêng biệt nên có chức năng chuẩn hóa nhận biết kích thước bước mong muốn cho loại phần tử. Hàm chuẩn hóa được tính với việc chuyển đổi các giá trị tương đương của loại phạm vi để có các biểu diễn giống hệt nhau, đặc biệt là các giới hạn bao gồm nhất quán hoặc độc quyền. Nếu một chức năng chuẩn hóa không được chỉ định, thì các phạm vi với định dạng khác nhau sẽ luôn được coi là không bằng nhau, mặc dù chúng có thể biểu thị cùng một bộ giá trị trong thực tế.

Việc xây dựng trong các loại dải int4range, int8rangedaterangetất cả sử dụng một hình thức kinh điển bao gồm các ràng buộc thấp hơn và không bao gồm phía trên ràng buộc; có nghĩa là, [). Tuy nhiên, các loại phạm vi do người dùng xác định có thể sử dụng các quy ước khác.

Đây không phải là trường hợp tstzrange, trong đó tính bao gồm của giới hạn trên và dưới cần phải được xem xét cho bình đẳng. Một chỉ số btree có thể sẽ phải được bật:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

Và các truy vấn sẽ phải sử dụng các biểu thức tương tự trong WHEREmệnh đề.

Người ta có thể chỉ muốn lập chỉ mục cho toàn bộ giá trị được đúc thành text: (cast(t_range AS text))- nhưng biểu thức này không phải IMMUTABLEdo biểu diễn văn bản của các timestamptzgiá trị phụ thuộc vào timezonecài đặt hiện tại . Bạn sẽ cần đặt các bước bổ sung vào một IMMUTABLEhàm bao bọc tạo ra một biểu mẫu chính tắc và tạo một chỉ mục chức năng trên đó ...

Biện pháp bổ sung / ý tưởng thay thế

Nếu shape_dist_traveledcó thể có cùng giá trị tt.shape_dist_traveledvới hơn một vài hàng được cập nhật của bạn (và bạn không dựa vào bất kỳ tác dụng phụ nào của các UPDATEkích hoạt tương tự của mình ...), bạn có thể thực hiện truy vấn của mình nhanh hơn bằng cách loại trừ các cập nhật trống:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Tất nhiên, tất cả các lời khuyên chung để tối ưu hóa hiệu suất được áp dụng. Wiki Postgres là một điểm khởi đầu tốt.

VACUUM FULLsẽ là độc dược đối với bạn, vì một số bộ dữ liệu chết (hoặc không gian dành riêng FILLFACTOR) có lợi cho UPDATEhiệu suất.

Với nhiều hàng được cập nhật và nếu bạn có đủ khả năng (không có quyền truy cập đồng thời hoặc các phụ thuộc khác), có thể còn nhanh hơn để viết một bảng hoàn toàn mới thay vì cập nhật tại chỗ. Hướng dẫn trong câu trả lời liên quan này:

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.